From f6d685392af183171bdfd4da99c6547f1656cc03 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Sun, 29 Mar 2026 14:06:43 +0200 Subject: [PATCH] request telemetry, fixed usage logging bug --- .../AutopilotMonitor.Agent/buildcounter.txt | 2 +- .../Middleware/AuthenticationMiddleware.cs | 8 +- .../Middleware/RequestTelemetryMiddleware.cs | 98 +++++++++++++++++++ .../AutopilotMonitor.Functions/Program.cs | 3 +- 4 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 src/Backend/AutopilotMonitor.Functions/Middleware/RequestTelemetryMiddleware.cs diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index d4d7ff29..a3888f7a 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -685 +687 diff --git a/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs b/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs index 4c051c3c..042bcfec 100644 --- a/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs +++ b/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs @@ -10,6 +10,7 @@ using System.IdentityModel.Tokens.Jwt; using Microsoft.AspNetCore.Http; using System.Net; using System.Security.Claims; +using AutopilotMonitor.Functions.Extensions; using AutopilotMonitor.Functions.Helpers; using AutopilotMonitor.Shared.DataAccess; @@ -232,10 +233,9 @@ public class AuthenticationMiddleware : IFunctionsWorkerMiddleware authenticated = true; // Track per-user usage (fire-and-forget via Task.Run to ensure execution in isolated worker) - var oid = principal.FindFirst("oid")?.Value; - var upn = principal.FindFirst("upn")?.Value - ?? principal.FindFirst("preferred_username")?.Value; - var tid = principal.FindFirst("tid")?.Value; + var oid = principal.GetObjectId(); + var upn = principal.GetUserPrincipalName(); + var tid = principal.GetTenantId(); if (!string.IsNullOrEmpty(oid)) { var normalizedEndpoint = EndpointNormalizer.Normalize(requestPath); diff --git a/src/Backend/AutopilotMonitor.Functions/Middleware/RequestTelemetryMiddleware.cs b/src/Backend/AutopilotMonitor.Functions/Middleware/RequestTelemetryMiddleware.cs new file mode 100644 index 00000000..de1304f0 --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/Middleware/RequestTelemetryMiddleware.cs @@ -0,0 +1,98 @@ +using System.Diagnostics; +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Middleware; +using AutopilotMonitor.Functions.Helpers; + +namespace AutopilotMonitor.Functions.Middleware; + +/// +/// Emits worker-side with business context (TenantId, UserId, +/// CorrelationId, UserRole) so the Application Insights requests table is queryable +/// by tenant, user, and correlation ID. Runs first in the pipeline to capture accurate +/// duration including auth and policy evaluation. Non-HTTP triggers are skipped. +/// +public class RequestTelemetryMiddleware : IFunctionsWorkerMiddleware +{ + private readonly TelemetryClient _telemetryClient; + + public RequestTelemetryMiddleware(TelemetryClient telemetryClient) + { + _telemetryClient = telemetryClient; + } + + public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next) + { + var httpContext = context.GetHttpContext(); + if (httpContext == null) + { + // Non-HTTP trigger (timer, queue) — nothing to track + await next(context); + return; + } + + var sw = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; + Exception? caughtException = null; + + try + { + await next(context); + } + catch (Exception ex) + { + caughtException = ex; + throw; + } + finally + { + sw.Stop(); + + var statusCode = caughtException != null ? 500 : httpContext.Response.StatusCode; + var functionName = context.FunctionDefinition.Name; + var method = httpContext.Request.Method; + var url = $"{httpContext.Request.Scheme}://{httpContext.Request.Host}{httpContext.Request.Path}{httpContext.Request.QueryString}"; + + var requestTelemetry = new RequestTelemetry + { + Name = $"{method} {functionName}", + Timestamp = startTime, + Duration = sw.Elapsed, + ResponseCode = statusCode.ToString(), + Success = statusCode < 500, + Url = new Uri(url), + }; + + // Distributed trace correlation + var activity = Activity.Current; + if (activity != null) + { + requestTelemetry.Context.Operation.Id = activity.RootId; + requestTelemetry.Context.Operation.ParentId = activity.Id; + } + + // Business context from downstream middleware + requestTelemetry.Properties["Source"] = "WorkerMiddleware"; + requestTelemetry.Properties["FunctionName"] = functionName; + requestTelemetry.Properties["HttpMethod"] = method; + requestTelemetry.Properties["HttpPath"] = httpContext.Request.Path.Value ?? ""; + + if (context.Items.TryGetValue("CorrelationId", out var corrId) && corrId is string correlationId) + requestTelemetry.Properties["CorrelationId"] = correlationId; + + var reqCtx = context.GetRequestContext(); + if (!string.IsNullOrEmpty(reqCtx.TenantId)) + requestTelemetry.Properties["TenantId"] = reqCtx.TenantId; + if (!string.IsNullOrEmpty(reqCtx.UserPrincipalName)) + requestTelemetry.Properties["UserId"] = reqCtx.UserPrincipalName; + if (!string.IsNullOrEmpty(reqCtx.UserRole)) + requestTelemetry.Properties["UserRole"] = reqCtx.UserRole; + + if (caughtException != null) + requestTelemetry.Properties["ExceptionType"] = caughtException.GetType().Name; + + _telemetryClient.TrackRequest(requestTelemetry); + } + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/Program.cs b/src/Backend/AutopilotMonitor.Functions/Program.cs index 356a19fd..1c9062a7 100644 --- a/src/Backend/AutopilotMonitor.Functions/Program.cs +++ b/src/Backend/AutopilotMonitor.Functions/Program.cs @@ -21,7 +21,8 @@ var builder = FunctionsApplication.CreateBuilder(args); builder.ConfigureFunctionsWebApplication(); // Register middleware pipeline (Azure Functions .NET 8 isolated worker pattern) -// Order matters: correlation ID (all requests) → JWT authentication (401) → policy enforcement (403) +// Order matters: request telemetry (wraps all) → correlation ID → JWT authentication (401) → policy enforcement (403) +builder.UseMiddleware(); builder.UseMiddleware(); builder.UseMiddleware(); builder.UseMiddleware();