From aab39ecb80b583ff5f79983d30d99093c14dc3ed Mon Sep 17 00:00:00 2001 From: Knut Haug Date: Fri, 25 Oct 2024 09:42:14 +0200 Subject: [PATCH] removed Application Insights SDK, added OpenTelemetry --- .../ApplicationInsightEventListener.cs | 114 ------------------ .../OpenTelemetryEventListener.cs | 112 +++++++++++++++++ .../Program.cs | 19 ++- .../ServiceCollectionExtensions.cs | 2 +- 4 files changed, 126 insertions(+), 121 deletions(-) delete mode 100644 src/Digdir.Domain.Dialogporten.GraphQL/ApplicationInsightEventListener.cs create mode 100644 src/Digdir.Domain.Dialogporten.GraphQL/OpenTelemetryEventListener.cs diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/ApplicationInsightEventListener.cs b/src/Digdir.Domain.Dialogporten.GraphQL/ApplicationInsightEventListener.cs deleted file mode 100644 index f2ac29838..000000000 --- a/src/Digdir.Domain.Dialogporten.GraphQL/ApplicationInsightEventListener.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Globalization; -using HotChocolate.Execution; -using HotChocolate.Execution.Instrumentation; -using Microsoft.ApplicationInsights; -using Microsoft.ApplicationInsights.AspNetCore.Extensions; -using Microsoft.ApplicationInsights.DataContracts; -using Microsoft.ApplicationInsights.Extensibility; - -namespace Digdir.Domain.Dialogporten.GraphQL; - -public sealed class ApplicationInsightEventListener : ExecutionDiagnosticEventListener -{ - private readonly TelemetryClient _telemetryClient; - - public ApplicationInsightEventListener( - TelemetryClient telemetryClient) - { - _telemetryClient = telemetryClient; - } - - public override IDisposable ExecuteRequest(IRequestContext context) - { - var httpContext = GetHttpContextFrom(context); - if (httpContext == null) - return EmptyScope; - - //During debugging every playground action will come here, so we want this while debugging -#if DEBUG - if (context.Request.OperationName == "IntrospectionQuery") - return EmptyScope; -#endif - - //Create a new telemetry request - var operationPath = $"{context.Request.OperationName ?? "UnknownOperation"} - {context.Request.QueryHash}"; - var requestTelemetry = new RequestTelemetry - { - Name = $"/graphql{operationPath}", - Url = new Uri(httpContext.Request.GetUri().AbsoluteUri + operationPath) - }; - - requestTelemetry.Context.Operation.Name = $"POST /graphql/{operationPath}"; - requestTelemetry.Context.Operation.Id = GetOperationIdFrom(httpContext); - requestTelemetry.Context.Operation.ParentId = GetOperationIdFrom(httpContext); - requestTelemetry.Context.User.AuthenticatedUserId = httpContext.User.Identity?.Name ?? "Not authenticated"; - - var operation = _telemetryClient.StartOperation(requestTelemetry); - return new ScopeWithEndAction(() => OnEndRequest(context, operation)); - } - - public override void RequestError(IRequestContext context, Exception exception) - { - _telemetryClient.TrackException(exception); - base.RequestError(context, exception); - } - - public override void ValidationErrors(IRequestContext context, IReadOnlyList errors) - { - foreach (var error in errors) - { - _telemetryClient.TrackTrace("GraphQL validation error: " + error.Message, SeverityLevel.Warning); - } - - base.ValidationErrors(context, errors); - } - - private static HttpContext? GetHttpContextFrom(IRequestContext context) => - // This method is used to enable start/stop events for query. - !context.ContextData.TryGetValue("HttpContext", out var value) ? null : value as HttpContext; - - private static string GetOperationIdFrom(HttpContext context) => context.TraceIdentifier; - - private void OnEndRequest(IRequestContext context, IOperationHolder operation) - { - var httpContext = GetHttpContextFrom(context); - operation.Telemetry.Success = httpContext is { Response.StatusCode: >= 200 and <= 299 }; - if (httpContext != null) - operation.Telemetry.ResponseCode = httpContext.Response.StatusCode.ToString(CultureInfo.InvariantCulture); - - if (context.Exception != null) - { - operation.Telemetry.Success = false; - operation.Telemetry.ResponseCode = "500"; - _telemetryClient.TrackException(context.Exception); - } - - if (context.Result is QueryResult { Errors: not null } queryResult) - { - foreach (var error in queryResult.Errors) - { - if (error.Exception is null) - { - continue; - } - - operation.Telemetry.Success = false; - _telemetryClient.TrackException(error.Exception); - } - } - - _telemetryClient.StopOperation(operation); - } -} - -internal sealed class ScopeWithEndAction : IDisposable -{ - private readonly Action _disposeAction; - - public ScopeWithEndAction(Action disposeAction) - { - _disposeAction = disposeAction; - } - - public void Dispose() => _disposeAction.Invoke(); -} diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/OpenTelemetryEventListener.cs b/src/Digdir.Domain.Dialogporten.GraphQL/OpenTelemetryEventListener.cs new file mode 100644 index 000000000..fe40d30e9 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.GraphQL/OpenTelemetryEventListener.cs @@ -0,0 +1,112 @@ +using System.Diagnostics; +using HotChocolate.Execution; +using HotChocolate.Execution.Instrumentation; +using Microsoft.AspNetCore.Http.Extensions; +using OpenTelemetry.Trace; + +namespace Digdir.Domain.Dialogporten.GraphQL; + +public sealed class OpenTelemetryEventListener : ExecutionDiagnosticEventListener +{ + private static readonly ActivitySource ActivitySource = new("Dialogporten.GraphQL"); + + public override IDisposable ExecuteRequest(IRequestContext context) + { + var httpContext = GetHttpContextFrom(context); + if (httpContext == null) + return EmptyScope; + +#if DEBUG + if (context.Request.OperationName == "IntrospectionQuery") + return EmptyScope; +#endif + + var operationName = context.Request.OperationName ?? "UnknownOperation"; + var operationPath = $"{operationName} - {context.Request.QueryHash}"; + + var activity = ActivitySource.StartActivity($"GraphQL {operationPath}", ActivityKind.Server); + + if (activity == null) + return EmptyScope; + + activity.SetTag("graphql.operation_name", operationName); + activity.SetTag("graphql.query_hash", context.Request.QueryHash); + activity.SetTag("http.url", httpContext.Request.GetDisplayUrl()); + activity.SetTag("user.id", httpContext.User.Identity?.Name ?? "Not authenticated"); + activity.SetTag("http.method", httpContext.Request.Method); + activity.SetTag("http.route", httpContext.Request.Path); + + return new ScopeWithEndAction(() => OnEndRequest(context, activity)); + } + + public override void RequestError(IRequestContext context, Exception exception) + { + var currentActivity = Activity.Current; + if (currentActivity != null) + { + currentActivity.RecordException(exception); + currentActivity.SetStatus(ActivityStatusCode.Error, exception.Message); + } + base.RequestError(context, exception); + } + + public override void ValidationErrors(IRequestContext context, IReadOnlyList errors) + { + foreach (var error in errors) + { + var currentActivity = Activity.Current; + currentActivity?.AddEvent(new ActivityEvent("ValidationError", default, new ActivityTagsCollection + { + { "message", error.Message } + })); + } + + base.ValidationErrors(context, errors); + } + + private static HttpContext? GetHttpContextFrom(IRequestContext context) => + context.ContextData.TryGetValue("HttpContext", out var value) ? value as HttpContext : null; + + private static void OnEndRequest(IRequestContext context, Activity activity) + { + var httpContext = GetHttpContextFrom(context); + if (context.Exception != null) + { + activity.RecordException(context.Exception); + activity.SetStatus(ActivityStatusCode.Error, context.Exception.Message); + } + + if (context.Result is QueryResult { Errors: not null } queryResult) + { + foreach (var error in queryResult.Errors) + { + if (error.Exception is null) + { + continue; + } + + activity.RecordException(error.Exception); + activity.SetStatus(ActivityStatusCode.Error, error.Exception.Message); + } + } + + if (httpContext != null) + { + activity.SetTag("http.status_code", httpContext.Response.StatusCode); + } + + activity.Dispose(); + } +} + +internal sealed class ScopeWithEndAction : IDisposable +{ + private readonly Action _disposeAction; + + public ScopeWithEndAction(Action disposeAction) + { + _disposeAction = disposeAction; + } + + public void Dispose() => _disposeAction.Invoke(); +} diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Program.cs b/src/Digdir.Domain.Dialogporten.GraphQL/Program.cs index 10040bcad..68a417bb3 100644 --- a/src/Digdir.Domain.Dialogporten.GraphQL/Program.cs +++ b/src/Digdir.Domain.Dialogporten.GraphQL/Program.cs @@ -6,7 +6,6 @@ using Digdir.Domain.Dialogporten.GraphQL.Common; using Digdir.Domain.Dialogporten.GraphQL.Common.Authentication; using Digdir.Domain.Dialogporten.GraphQL.Common.Authorization; -using Digdir.Domain.Dialogporten.GraphQL.Common.Extensions; using Digdir.Domain.Dialogporten.Infrastructure; using Digdir.Domain.Dialogporten.Application.Common.Extensions.OptionExtensions; using Digdir.Domain.Dialogporten.GraphQL; @@ -18,17 +17,20 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Options; +const string DialogportenGraphQLSource = "Dialogporten.GraphQL"; + +var telemetryConfiguration = TelemetryConfiguration.CreateDefault(); // Using two-stage initialization to catch startup errors. Log.Logger = new LoggerConfiguration() .MinimumLevel.Warning() .Enrich.FromLogContext() .WriteTo.Console(formatProvider: CultureInfo.InvariantCulture) - .WriteTo.ApplicationInsights(TelemetryConfiguration.CreateDefault(), TelemetryConverter.Traces) + .WriteTo.ApplicationInsights(telemetryConfiguration, TelemetryConverter.Traces) .CreateBootstrapLogger(); try { - BuildAndRun(args); + BuildAndRun(args, telemetryConfiguration); } catch (Exception ex) when (ex is not OperationCanceledException) { @@ -40,7 +42,7 @@ Log.CloseAndFlush(); } -static void BuildAndRun(string[] args) +static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfiguration) { var builder = WebApplication.CreateBuilder(args); @@ -50,7 +52,7 @@ static void BuildAndRun(string[] args) .ReadFrom.Services(services) .Enrich.FromLogContext() .WriteTo.Console(formatProvider: CultureInfo.InvariantCulture) - .WriteTo.ApplicationInsights(services.GetRequiredService(), TelemetryConverter.Traces)); + .WriteTo.ApplicationInsights(telemetryConfiguration, TelemetryConverter.Traces)); builder.Configuration .AddAzureConfiguration(builder.Environment.EnvironmentName) @@ -65,6 +67,11 @@ static void BuildAndRun(string[] args) var thisAssembly = Assembly.GetExecutingAssembly(); builder.ConfigureTelemetry(); + builder.Services.AddOpenTelemetry() + .WithTracing(tracerProviderBuilder => + { + tracerProviderBuilder.AddSource(DialogportenGraphQLSource); + }); builder.Services // Options setup @@ -76,7 +83,7 @@ static void BuildAndRun(string[] args) .WithPubCapabilities() .Build() .AddAutoMapper(Assembly.GetExecutingAssembly()) - .AddApplicationInsightsTelemetry() + .AddHttpContextAccessor() .AddScoped() .AddValidatorsFromAssembly(thisAssembly, ServiceLifetime.Transient, includeInternalTypes: true) .AddAzureAppConfiguration() diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/ServiceCollectionExtensions.cs b/src/Digdir.Domain.Dialogporten.GraphQL/ServiceCollectionExtensions.cs index 14b44b80f..0877a9618 100644 --- a/src/Digdir.Domain.Dialogporten.GraphQL/ServiceCollectionExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.GraphQL/ServiceCollectionExtensions.cs @@ -16,7 +16,7 @@ public static IServiceCollection AddDialogportenGraphQl(this IServiceCollection .AddSubscriptionType() .AddAuthorization() .RegisterDbContext() - .AddDiagnosticEventListener() + .AddDiagnosticEventListener() .AddQueryType() .AddMutationType() .AddType()