From 8ea0c9fe21db69fdbef606035d00c582f460576b Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Wed, 17 May 2023 13:46:05 +0100 Subject: [PATCH 1/8] Add config for SingleWebSpanEnabled --- .../Datadog.Trace/Configuration/ConfigurationKeys.cs | 8 ++++++++ .../Configuration/ImmutableTracerSettings.cs | 7 +++++++ .../src/Datadog.Trace/Configuration/TracerSettings.cs | 11 +++++++++++ 3 files changed, 26 insertions(+) diff --git a/tracer/src/Datadog.Trace/Configuration/ConfigurationKeys.cs b/tracer/src/Datadog.Trace/Configuration/ConfigurationKeys.cs index 9ae09408aef9..064ff709b997 100644 --- a/tracer/src/Datadog.Trace/Configuration/ConfigurationKeys.cs +++ b/tracer/src/Datadog.Trace/Configuration/ConfigurationKeys.cs @@ -578,6 +578,14 @@ internal static class FeatureFlags /// public const string RouteTemplateResourceNamesEnabled = "DD_TRACE_ROUTE_TEMPLATE_RESOURCE_NAMES_ENABLED"; + /// + /// When enabled, aspnet_core_mvc.request, aspnet-webapi.request, and aspnet-mvc.request + /// spans will not be created. Instead, tags typically associated with these spans will be associated with + /// the aspnet.request and aspnet_core.request span instead. + /// + /// + public const string SingleWebSpanEnabled = "DD_TRACE_SINGLE_WEB_SPAN_ENABLED"; + /// /// Configuration key to enable or disable the updated WCF instrumentation that delays execution /// until later in the WCF pipeline when the WCF server exception handling is established. diff --git a/tracer/src/Datadog.Trace/Configuration/ImmutableTracerSettings.cs b/tracer/src/Datadog.Trace/Configuration/ImmutableTracerSettings.cs index 997a56e4ead7..4f87ea1d687a 100644 --- a/tracer/src/Datadog.Trace/Configuration/ImmutableTracerSettings.cs +++ b/tracer/src/Datadog.Trace/Configuration/ImmutableTracerSettings.cs @@ -122,6 +122,7 @@ internal ImmutableTracerSettings(TracerSettings settings, bool unusedParamNotToU TraceBufferSize = settings.TraceBufferSize; TraceBatchInterval = settings.TraceBatchInterval; RouteTemplateResourceNamesEnabled = settings.RouteTemplateResourceNamesEnabled; + SingleWebSpanEnabled = settings.SingleWebSpanEnabled; DelayWcfInstrumentationEnabled = settings.DelayWcfInstrumentationEnabled; WcfWebHttpResourceNamesEnabled = settings.WcfWebHttpResourceNamesEnabled; WcfObfuscationEnabled = settings.WcfObfuscationEnabled; @@ -426,6 +427,12 @@ internal ImmutableTracerSettings(TracerSettings settings, bool unusedParamNotToU /// internal bool RouteTemplateResourceNamesEnabled { get; } + /// + /// Gets a value indicating whether the feature flag to enable single-web spans is enabled + /// + /// + internal bool SingleWebSpanEnabled { get; } + /// /// Gets a value indicating whether route parameters in ASP.NET and ASP.NET Core resource names /// should be expanded with their values. Only applies when diff --git a/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs b/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs index 811b76a294dc..8d470dcef597 100644 --- a/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs +++ b/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs @@ -236,6 +236,11 @@ internal TracerSettings(IConfigurationSource? source, IConfigurationTelemetry te .WithKeys(ConfigurationKeys.FeatureFlags.RouteTemplateResourceNamesEnabled) .AsBool(defaultValue: true); + SingleWebSpanEnabled = RouteTemplateResourceNamesEnabled // always disabled if route template resources names disabled + && config + .WithKeys(ConfigurationKeys.FeatureFlags.SingleWebSpanEnabled) + .AsBool(defaultValue: false); + ExpandRouteTemplatesEnabled = config .WithKeys(ConfigurationKeys.ExpandRouteTemplatesEnabled) .AsBool(defaultValue: !RouteTemplateResourceNamesEnabled); // disabled by default if route template resource names enabled @@ -792,6 +797,12 @@ public bool DiagnosticSourceEnabled /// internal bool RouteTemplateResourceNamesEnabled { get; } + /// + /// Gets a value indicating whether the feature flag to enable single-web spans is enabled + /// + /// + internal bool SingleWebSpanEnabled { get; } + /// /// Gets a value indicating whether resource names for ASP.NET and ASP.NET Core spans should be expanded. Only applies /// when is true. From fdc2754cac4e587024ce0702cbf8eff224423b25 Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Mon, 4 Dec 2023 13:34:45 +0000 Subject: [PATCH 2/8] Create a duplicate of the existing AspNetCoreDiagnosticObserver --- .../ClrProfiler/Instrumentation.cs | 8 +- .../SingleSpanAspNetCoreDiagnosticObserver.cs | 790 ++++++++++++++++++ 2 files changed, 796 insertions(+), 2 deletions(-) create mode 100644 tracer/src/Datadog.Trace/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserver.cs diff --git a/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs b/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs index a20e313f0dad..8e9671b3e20a 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs @@ -432,12 +432,16 @@ private static void StartDiagnosticManager() { var observers = new List(); - if (Tracer.Instance.Settings.AzureAppServiceMetadata?.IsFunctionsApp is not true) + var settings = Tracer.Instance.Settings; + if (settings.AzureAppServiceMetadata?.IsFunctionsApp is not true) { // Not adding the `AspNetCoreDiagnosticObserver` is particularly important for Azure Functions. // The AspNetCoreDiagnosticObserver will be loaded in a separate Assembly Load Context, breaking the connection of AsyncLocal // This is because user code is loaded within the functions host in a separate context - observers.Add(new AspNetCoreDiagnosticObserver()); + DiagnosticObserver observer = settings.SingleWebSpanEnabled + ? new SingleSpanAspNetCoreDiagnosticObserver() + : new AspNetCoreDiagnosticObserver(); + observers.Add(observer); } var diagnosticManager = new DiagnosticManager(observers); diff --git a/tracer/src/Datadog.Trace/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserver.cs b/tracer/src/Datadog.Trace/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserver.cs new file mode 100644 index 000000000000..bef1a15444d5 --- /dev/null +++ b/tracer/src/Datadog.Trace/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserver.cs @@ -0,0 +1,790 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#if !NETFRAMEWORK + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Datadog.Trace.AppSec; +using Datadog.Trace.AppSec.Coordinator; +using Datadog.Trace.Configuration; +using Datadog.Trace.DuckTyping; +using Datadog.Trace.Logging; +using Datadog.Trace.PlatformHelpers; +using Datadog.Trace.Tagging; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Template; + +namespace Datadog.Trace.DiagnosticListeners +{ + /// + /// Instruments ASP.NET Core. + /// + /// Unfortunately, ASP.NET Core only uses one instance + /// for everything so we also only create one observer to ensure best performance. + /// + /// Hosting events: https://github.com/dotnet/aspnetcore/blob/master/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs + /// + internal sealed class SingleSpanAspNetCoreDiagnosticObserver : DiagnosticObserver + { + public const IntegrationId IntegrationId = Configuration.IntegrationId.AspNetCore; + + private const string DiagnosticListenerName = "Microsoft.AspNetCore"; + private const string HttpRequestInOperationName = "aspnet_core.request"; + private const string MvcOperationName = "aspnet_core_mvc.request"; + + private static readonly int PrefixLength = "Microsoft.AspNetCore.".Length; + + private static readonly Type EndpointFeatureType = + Assembly.GetAssembly(typeof(RouteValueDictionary)) + ?.GetType("Microsoft.AspNetCore.Http.Features.IEndpointFeature", throwOnError: false); + + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(); + private static readonly AspNetCoreHttpRequestHandler AspNetCoreRequestHandler = new AspNetCoreHttpRequestHandler(Log, HttpRequestInOperationName, IntegrationId); + private readonly Tracer _tracer; + private readonly Security _security; + private string _hostingHttpRequestInStartEventKey; + private string _mvcBeforeActionEventKey; + private string _mvcAfterActionEventKey; + private string _hostingUnhandledExceptionEventKey; + private string _diagnosticsUnhandledExceptionEventKey; + private string _hostingHttpRequestInStopEventKey; + private string _routingEndpointMatchedKey; + + public SingleSpanAspNetCoreDiagnosticObserver() + : this(null, null) + { + } + + public SingleSpanAspNetCoreDiagnosticObserver(Tracer tracer, Security security) + { + _tracer = tracer; + _security = security; + } + + protected override string ListenerName => DiagnosticListenerName; + + private Tracer CurrentTracer => _tracer ?? Tracer.Instance; + + private Security CurrentSecurity => _security ?? Security.Instance; + +#if NETCOREAPP + protected override void OnNext(string eventName, object arg) + { + var lastChar = eventName[^1]; + + if (lastChar == 't') + { + if (ReferenceEquals(eventName, _hostingHttpRequestInStartEventKey)) + { + OnHostingHttpRequestInStart(arg); + } + else if (eventName.AsSpan().Slice(PrefixLength).SequenceEqual("Hosting.HttpRequestIn.Start")) + { + _hostingHttpRequestInStartEventKey = eventName; + OnHostingHttpRequestInStart(arg); + } + + return; + } + + if (lastChar == 'n') + { + if (ReferenceEquals(eventName, _mvcBeforeActionEventKey)) + { + OnMvcBeforeAction(arg); + return; + } + else if (ReferenceEquals(eventName, _mvcAfterActionEventKey)) + { + OnMvcAfterAction(arg); + return; + } + else if (ReferenceEquals(eventName, _hostingUnhandledExceptionEventKey) || + ReferenceEquals(eventName, _diagnosticsUnhandledExceptionEventKey)) + { + OnHostingUnhandledException(arg); + return; + } + + var suffix = eventName.AsSpan().Slice(PrefixLength); + + if (suffix.SequenceEqual("Mvc.BeforeAction")) + { + _mvcBeforeActionEventKey = eventName; + OnMvcBeforeAction(arg); + } + else if (suffix.SequenceEqual("Mvc.AfterAction")) + { + _mvcAfterActionEventKey = eventName; + OnMvcAfterAction(arg); + } + else if (suffix.SequenceEqual("Hosting.UnhandledException")) + { + _hostingUnhandledExceptionEventKey = eventName; + OnHostingUnhandledException(arg); + } + else if (suffix.SequenceEqual("Diagnostics.UnhandledException")) + { + _diagnosticsUnhandledExceptionEventKey = eventName; + OnHostingUnhandledException(arg); + } + + return; + } + + if (lastChar == 'p') + { + if (ReferenceEquals(eventName, _hostingHttpRequestInStopEventKey)) + { + OnHostingHttpRequestInStop(arg); + } + else if (eventName.AsSpan().Slice(PrefixLength).SequenceEqual("Hosting.HttpRequestIn.Stop")) + { + _hostingHttpRequestInStopEventKey = eventName; + OnHostingHttpRequestInStop(arg); + } + + return; + } + + if (lastChar == 'd') + { + if (ReferenceEquals(eventName, _routingEndpointMatchedKey)) + { + OnRoutingEndpointMatched(arg); + } + else if (eventName.AsSpan().Slice(PrefixLength).SequenceEqual("Routing.EndpointMatched")) + { + _routingEndpointMatchedKey = eventName; + OnRoutingEndpointMatched(arg); + } + + return; + } + } + +#else + protected override void OnNext(string eventName, object arg) + { + var lastChar = eventName[eventName.Length - 1]; + + if (lastChar == 't') + { + if (ReferenceEquals(eventName, _hostingHttpRequestInStartEventKey)) + { + OnHostingHttpRequestInStart(arg); + } + else if (eventName == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start") + { + _hostingHttpRequestInStartEventKey = eventName; + OnHostingHttpRequestInStart(arg); + } + + return; + } + + if (lastChar == 'n') + { + if (ReferenceEquals(eventName, _mvcBeforeActionEventKey)) + { + OnMvcBeforeAction(arg); + return; + } + else if (ReferenceEquals(eventName, _mvcAfterActionEventKey)) + { + OnMvcAfterAction(arg); + return; + } + else if (ReferenceEquals(eventName, _hostingUnhandledExceptionEventKey) || + ReferenceEquals(eventName, _diagnosticsUnhandledExceptionEventKey)) + { + OnHostingUnhandledException(arg); + return; + } + + switch (eventName) + { + case "Microsoft.AspNetCore.Mvc.BeforeAction": + _mvcBeforeActionEventKey = eventName; + OnMvcBeforeAction(arg); + break; + + case "Microsoft.AspNetCore.Mvc.AfterAction": + _mvcAfterActionEventKey = eventName; + OnMvcAfterAction(arg); + break; + + case "Microsoft.AspNetCore.Hosting.UnhandledException": + _hostingUnhandledExceptionEventKey = eventName; + OnHostingUnhandledException(arg); + break; + case "Microsoft.AspNetCore.Diagnostics.UnhandledException": + _diagnosticsUnhandledExceptionEventKey = eventName; + OnHostingUnhandledException(arg); + break; + } + + return; + } + + if (lastChar == 'p') + { + if (ReferenceEquals(eventName, _hostingHttpRequestInStopEventKey)) + { + OnHostingHttpRequestInStop(arg); + } + else if (eventName == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop") + { + _hostingHttpRequestInStopEventKey = eventName; + OnHostingHttpRequestInStop(arg); + } + + return; + } + + if (lastChar == 'd') + { + if (ReferenceEquals(eventName, _routingEndpointMatchedKey)) + { + OnRoutingEndpointMatched(arg); + } + else if (eventName == "Microsoft.AspNetCore.Routing.EndpointMatched") + { + _routingEndpointMatchedKey = eventName; + OnRoutingEndpointMatched(arg); + } + + return; + } + } +#endif + + private static string GetLegacyResourceName(BeforeActionStruct typedArg) + { + ActionDescriptor actionDescriptor = typedArg.ActionDescriptor; + HttpRequest request = typedArg.HttpContext.Request; + + string httpMethod = request.Method?.ToUpperInvariant() ?? "UNKNOWN"; + string routeTemplate = actionDescriptor.AttributeRouteInfo?.Template; + if (routeTemplate is null) + { + string controllerName = actionDescriptor.RouteValues["controller"]; + string actionName = actionDescriptor.RouteValues["action"]; + + routeTemplate = $"{controllerName}/{actionName}"; + } + + return $"{httpMethod} {routeTemplate}"; + } + + private static Span StartMvcCoreSpan( + Tracer tracer, + AspNetCoreHttpRequestHandler.RequestTrackingFeature trackingFeature, + BeforeActionStruct typedArg, + HttpContext httpContext, + HttpRequest request) + { + // Create a child span for the MVC action + var mvcSpanTags = new AspNetCoreMvcTags(); + var mvcScope = tracer.StartActiveInternal(MvcOperationName, tags: mvcSpanTags); + tracer.TracerManager.Telemetry.IntegrationGeneratedSpan(IntegrationId); + var span = mvcScope.Span; + span.Type = SpanTypes.Web; + + // StartMvcCoreSpan is only called with new route names, so parent tags are always AspNetCoreEndpointTags + var rootSpan = trackingFeature.RootScope.Span; + var rootSpanTags = (AspNetCoreEndpointTags)rootSpan.Tags; + + var isUsingEndpointRouting = trackingFeature.IsUsingEndpointRouting; + + var isFirstExecution = trackingFeature.IsFirstPipelineExecution; + if (isFirstExecution) + { + trackingFeature.IsFirstPipelineExecution = false; + if (!trackingFeature.MatchesOriginalPath(httpContext.Request)) + { + // URL has changed from original, so treat this execution as a "subsequent" request + // Typically occurs for 404s for example + isFirstExecution = false; + } + } + + ActionDescriptor actionDescriptor = typedArg.ActionDescriptor; + IDictionary routeValues = actionDescriptor.RouteValues; + + string controllerName = routeValues.TryGetValue("controller", out controllerName) + ? controllerName?.ToLowerInvariant() + : null; + string actionName = routeValues.TryGetValue("action", out actionName) + ? actionName?.ToLowerInvariant() + : null; + string areaName = routeValues.TryGetValue("area", out areaName) + ? areaName?.ToLowerInvariant() + : null; + string pagePath = routeValues.TryGetValue("page", out pagePath) + ? pagePath?.ToLowerInvariant() + : null; + string aspNetRoute = trackingFeature.Route; + string resourceName = trackingFeature.ResourceName; + + if (aspNetRoute is null || resourceName is null) + { + // Not using endpoint routing + string rawRouteTemplate = actionDescriptor.AttributeRouteInfo?.Template; + RouteTemplate routeTemplate = null; + if (rawRouteTemplate is not null) + { + try + { + routeTemplate = TemplateParser.Parse(rawRouteTemplate); + } + catch { } + } + + if (routeTemplate is null) + { + var routeData = httpContext.Features.Get()?.RouteData; + if (routeData is not null) + { + var route = routeData.Routers.OfType().FirstOrDefault(); + routeTemplate = route?.ParsedTemplate; + } + } + + if (routeTemplate is not null) + { + // If we have a route, overwrite the existing resource name + var resourcePathName = AspNetCoreResourceNameHelper.SimplifyRouteTemplate( + routeTemplate, + typedArg.RouteData.Values, + areaName: areaName, + controllerName: controllerName, + actionName: actionName, + expandRouteParameters: tracer.Settings.ExpandRouteTemplatesEnabled); + + resourceName = $"{rootSpanTags.HttpMethod} {request.PathBase.ToUriComponent()}{resourcePathName}"; + + aspNetRoute = routeTemplate?.TemplateText.ToLowerInvariant(); + } + } + + // mirror the parent if we couldn't extract a route for some reason + // (and the parent is not using the placeholder resource name) + span.ResourceName = resourceName + ?? (string.IsNullOrEmpty(rootSpan.ResourceName) + ? AspNetCoreRequestHandler.GetDefaultResourceName(httpContext.Request) + : rootSpan.ResourceName); + + mvcSpanTags.AspNetCoreAction = actionName; + mvcSpanTags.AspNetCoreController = controllerName; + mvcSpanTags.AspNetCoreArea = areaName; + mvcSpanTags.AspNetCorePage = pagePath; + mvcSpanTags.AspNetCoreRoute = aspNetRoute; + + if (!isUsingEndpointRouting && isFirstExecution) + { + // If we're using endpoint routing or this is a pipeline re-execution, + // these will already be set correctly + rootSpanTags.AspNetCoreRoute = aspNetRoute; + rootSpan.ResourceName = span.ResourceName; + rootSpanTags.HttpRoute = aspNetRoute; + } + + return span; + } + + private void OnHostingHttpRequestInStart(object arg) + { + var tracer = CurrentTracer; + var security = CurrentSecurity; + + var shouldTrace = tracer.Settings.IsIntegrationEnabled(IntegrationId); + var shouldSecure = security.Enabled; + + if (!shouldTrace && !shouldSecure) + { + return; + } + + if (arg.TryDuckCast(out var requestStruct)) + { + HttpContext httpContext = requestStruct.HttpContext; + if (shouldTrace) + { + // Use an empty resource name here, as we will likely replace it as part of the request + // If we don't, update it in OnHostingHttpRequestInStop or OnHostingUnhandledException + var scope = AspNetCoreRequestHandler.StartAspNetCorePipelineScope(tracer, CurrentSecurity, httpContext, resourceName: string.Empty); + if (shouldSecure) + { + CoreHttpContextStore.Instance.Set(httpContext); + SecurityCoordinator.ReportWafInitInfoOnce(security, scope.Span); + } + } + } + } + + private void OnRoutingEndpointMatched(object arg) + { + var tracer = CurrentTracer; + + if (!tracer.Settings.IsIntegrationEnabled(IntegrationId) || + !tracer.Settings.RouteTemplateResourceNamesEnabled) + { + return; + } + + if (arg.TryDuckCast(out var typedArg) + && typedArg.HttpContext is { } httpContext + && httpContext.Features.Get() is { RootScope.Span: { } rootSpan } trackingFeature) + { + if (rootSpan.Tags is not AspNetCoreEndpointTags tags) + { + // customer is using legacy resource names + return; + } + + var isFirstExecution = trackingFeature.IsFirstPipelineExecution; + if (isFirstExecution) + { + trackingFeature.IsUsingEndpointRouting = true; + trackingFeature.IsFirstPipelineExecution = false; + + if (!trackingFeature.MatchesOriginalPath(httpContext.Request)) + { + // URL has changed from original, so treat this execution as a "subsequent" request + // Typically occurs for 404s for example + isFirstExecution = false; + } + } + + // NOTE: This event is when the routing middleware selects an endpoint. Additional middleware (e.g + // Authorization/CORS) may still run, and the endpoint itself has not started executing. + + if (EndpointFeatureType is null) + { + return; + } + + var rawEndpointFeature = httpContext.Features[EndpointFeatureType]; + if (rawEndpointFeature is null) + { + return; + } + + RouteEndpoint? routeEndpoint = null; + + if (rawEndpointFeature.TryDuckCast(out var endpointFeatureInterface)) + { + if (endpointFeatureInterface.GetEndpoint().TryDuckCast(out var routeEndpointObj)) + { + routeEndpoint = routeEndpointObj; + } + } + + if (routeEndpoint is null && rawEndpointFeature.TryDuckCast(out var endpointFeatureStruct)) + { + if (endpointFeatureStruct.Endpoint.TryDuckCast(out var routeEndpointObj)) + { + routeEndpoint = routeEndpointObj; + } + } + + if (routeEndpoint is null) + { + // Unable to cast to either type + return; + } + + if (isFirstExecution) + { + tags.AspNetCoreEndpoint = routeEndpoint.Value.DisplayName; + } + + var routePattern = routeEndpoint.Value.RoutePattern; + + // Have to pass this value through to the MVC span, as not available there + var normalizedRoute = routePattern.RawText?.ToLowerInvariant(); + trackingFeature.Route = normalizedRoute; + + var request = httpContext.Request.DuckCast(); + RouteValueDictionary routeValues = request.RouteValues; + // No need to ToLowerInvariant() these strings, as we lower case + // the whole route later + object raw; + string controllerName = routeValues.TryGetValue("controller", out raw) + ? raw as string + : null; + string actionName = routeValues.TryGetValue("action", out raw) + ? raw as string + : null; + string areaName = routeValues.TryGetValue("area", out raw) + ? raw as string + : null; + + var resourcePathName = AspNetCoreResourceNameHelper.SimplifyRoutePattern( + routePattern, + routeValues, + areaName: areaName, + controllerName: controllerName, + actionName: actionName, + tracer.Settings.ExpandRouteTemplatesEnabled); + + var resourceName = $"{tags.HttpMethod} {request.PathBase.ToUriComponent()}{resourcePathName}"; + + // NOTE: We could set the controller/action/area tags on the parent span + // But instead we re-extract them in the MVC endpoint as these are MVC + // constructs. this is likely marginally less efficient, but simplifies the + // already complex logic in the MVC handler + // Overwrite the route in the parent span + trackingFeature.ResourceName = resourceName; + if (isFirstExecution) + { + rootSpan.ResourceName = resourceName; + tags.AspNetCoreRoute = normalizedRoute; + tags.HttpRoute = normalizedRoute; + } + + CurrentSecurity.CheckPathParams(httpContext, rootSpan, routeValues); + + if (Iast.Iast.Instance.Settings.Enabled) + { + rootSpan.Context?.TraceContext?.IastRequestContext?.AddRequestData(httpContext.Request, routeValues); + } + } + } + + private void OnMvcBeforeAction(object arg) + { + var tracer = CurrentTracer; + var security = CurrentSecurity; + + var shouldTrace = tracer.Settings.IsIntegrationEnabled(IntegrationId); + var shouldSecure = security.Enabled; + var shouldUseIast = Iast.Iast.Instance.Settings.Enabled; + + if (!shouldTrace && !shouldSecure && !shouldUseIast) + { + return; + } + + if (arg.TryDuckCast(out var typedArg) + && typedArg.HttpContext is { } httpContext + && httpContext.Features.Get() is { RootScope.Span: { } rootSpan } trackingFeature) + { + HttpRequest request = httpContext.Request; + + // NOTE: This event is the start of the action pipeline. The action has been selected, the route + // has been selected but no filters have run and model binding hasn't occurred. + Span span = null; + if (shouldTrace) + { + if (!tracer.Settings.RouteTemplateResourceNamesEnabled) + { + // override the parent's resource name with the simplified MVC route template + rootSpan.ResourceName = GetLegacyResourceName(typedArg); + } + else + { + span = StartMvcCoreSpan(tracer, trackingFeature, typedArg, httpContext, request); + } + } + + if (span is not null) + { + CurrentSecurity.CheckPathParamsFromAction(httpContext, span, typedArg.ActionDescriptor?.Parameters, typedArg.RouteData.Values); + } + + if (shouldUseIast) + { + rootSpan.Context?.TraceContext?.IastRequestContext?.AddRequestData(request, typedArg.RouteData?.Values); + } + } + } + + private void OnMvcAfterAction(object arg) + { + var tracer = CurrentTracer; + + if (!tracer.Settings.IsIntegrationEnabled(IntegrationId) || + !tracer.Settings.RouteTemplateResourceNamesEnabled) + { + return; + } + + var scope = tracer.InternalActiveScope; + + if (scope is not null && ReferenceEquals(scope.Span.OperationName, MvcOperationName)) + { + try + { + // Extract data from the Activity + var activity = Activity.ActivityListener.GetCurrentActivity(); + if (activity is not null) + { + foreach (var activityTag in activity.Tags) + { + scope.Span.SetTag(activityTag.Key, activityTag.Value); + } + + foreach (var activityBag in activity.Baggage) + { + scope.Span.SetTag(activityBag.Key, activityBag.Value); + } + } + } + catch (Exception ex) + { + Log.Error(ex, "Error extracting activity data."); + } + + scope.Dispose(); + } + } + + private void OnHostingHttpRequestInStop(object arg) + { + var tracer = CurrentTracer; + + if (!tracer.Settings.IsIntegrationEnabled(IntegrationId)) + { + return; + } + + if (arg.DuckCast().HttpContext is { } httpContext + && httpContext.Features.Get() is { RootScope: { } rootScope }) + { + AspNetCoreRequestHandler.StopAspNetCorePipelineScope(tracer, CurrentSecurity, rootScope, httpContext); + } + + // If we don't have a scope, no need to call Stop pipeline + } + + private void OnHostingUnhandledException(object arg) + { + var tracer = CurrentTracer; + + if (!tracer.Settings.IsIntegrationEnabled(IntegrationId)) + { + return; + } + + if (arg.TryDuckCast(out var unhandledStruct) + && unhandledStruct.HttpContext is { } httpContext + && httpContext.Features.Get() is { RootScope.Span: { } rootSpan }) + { + AspNetCoreRequestHandler.HandleAspNetCoreException(tracer, CurrentSecurity, rootSpan, httpContext, unhandledStruct.Exception); + } + + // If we don't have a span, no need to call Handle exception + } + + [DuckCopy] + internal struct HttpRequestInStartStruct + { + [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase)] + public HttpContext HttpContext; + } + + [DuckCopy] + internal struct HttpRequestInStopStruct + { + [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase)] + public HttpContext HttpContext; + } + + [DuckCopy] + internal struct UnhandledExceptionStruct + { + [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase)] + public HttpContext HttpContext; + + [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase)] + public Exception Exception; + } + + [DuckCopy] + internal struct BeforeActionStruct + { + [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase)] + public HttpContext HttpContext; + + [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase)] + public ActionDescriptor ActionDescriptor; + + [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase)] + public RouteData RouteData; + } + + [DuckCopy] + internal struct BadHttpRequestExceptionStruct + { + [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase | BindingFlags.NonPublic)] + public int StatusCode; + } + + [DuckCopy] + internal struct HttpRequestInEndpointMatchedStruct + { + [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase)] + public HttpContext HttpContext; + } + + /// + /// Proxy for ducktyping IEndpointFeature when the interface is not implemented explicitly + /// + /// + [DuckCopy] + internal struct EndpointFeatureStruct + { + public object Endpoint; + } + + [DuckCopy] + internal struct HttpRequestStruct + { + public string Method; + public RouteValueDictionary RouteValues; + public PathString PathBase; + } + + /// + /// Proxy for https://github1s.com/dotnet/aspnetcore/blob/v3.0.3/src/Http/Routing/src/Patterns/RoutePatternPathSegment.cs + /// + [DuckCopy] + internal struct RoutePatternPathSegmentStruct + { + public IEnumerable Parts; + } + + /// + /// Proxy for https://github1s.com/dotnet/aspnetcore/blob/v3.0.3/src/Http/Routing/src/Patterns/RoutePatternLiteralPart.cs + /// and https://github1s.com/dotnet/aspnetcore/blob/v3.0.3/src/Http/Routing/src/Patterns/RoutePatternSeparatorPart.cs + /// + [DuckCopy] + internal struct RoutePatternContentPartStruct + { + public string Content; + } + + /// + /// Proxy for https://github1s.com/dotnet/aspnetcore/blob/v3.0.3/src/Http/Routing/src/Patterns/RoutePatternParameterPart.cs + /// + [DuckCopy] + internal struct RoutePatternParameterPartStruct + { + public string Name; + public bool IsOptional; + public bool IsCatchAll; + public bool EncodeSlashes; + } + } +} +#endif From 64d4252d9407dc0ee10bd51752773faffc38660c Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Mon, 4 Dec 2023 13:46:35 +0000 Subject: [PATCH 3/8] switch to inline #if in OnNext to make it easier to follow --- .../SingleSpanAspNetCoreDiagnosticObserver.cs | 96 ++++--------------- 1 file changed, 20 insertions(+), 76 deletions(-) diff --git a/tracer/src/Datadog.Trace/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserver.cs b/tracer/src/Datadog.Trace/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserver.cs index bef1a15444d5..b056ade94e14 100644 --- a/tracer/src/Datadog.Trace/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserver.cs +++ b/tracer/src/Datadog.Trace/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserver.cs @@ -40,8 +40,9 @@ internal sealed class SingleSpanAspNetCoreDiagnosticObserver : DiagnosticObserve private const string HttpRequestInOperationName = "aspnet_core.request"; private const string MvcOperationName = "aspnet_core_mvc.request"; +#if NETCOREAPP private static readonly int PrefixLength = "Microsoft.AspNetCore.".Length; - +#endif private static readonly Type EndpointFeatureType = Assembly.GetAssembly(typeof(RouteValueDictionary)) ?.GetType("Microsoft.AspNetCore.Http.Features.IEndpointFeature", throwOnError: false); @@ -75,10 +76,13 @@ public SingleSpanAspNetCoreDiagnosticObserver(Tracer tracer, Security security) private Security CurrentSecurity => _security ?? Security.Instance; -#if NETCOREAPP protected override void OnNext(string eventName, object arg) { +#if NETCOREAPP var lastChar = eventName[^1]; +#else + var lastChar = eventName[eventName.Length - 1]; +#endif if (lastChar == 't') { @@ -86,7 +90,11 @@ protected override void OnNext(string eventName, object arg) { OnHostingHttpRequestInStart(arg); } +#if NETCOREAPP else if (eventName.AsSpan().Slice(PrefixLength).SequenceEqual("Hosting.HttpRequestIn.Start")) +#else + else if (eventName == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start") +#endif { _hostingHttpRequestInStartEventKey = eventName; OnHostingHttpRequestInStart(arg); @@ -113,7 +121,7 @@ protected override void OnNext(string eventName, object arg) OnHostingUnhandledException(arg); return; } - +#if NETCOREAPP var suffix = eventName.AsSpan().Slice(PrefixLength); if (suffix.SequenceEqual("Mvc.BeforeAction")) @@ -137,79 +145,7 @@ protected override void OnNext(string eventName, object arg) OnHostingUnhandledException(arg); } - return; - } - - if (lastChar == 'p') - { - if (ReferenceEquals(eventName, _hostingHttpRequestInStopEventKey)) - { - OnHostingHttpRequestInStop(arg); - } - else if (eventName.AsSpan().Slice(PrefixLength).SequenceEqual("Hosting.HttpRequestIn.Stop")) - { - _hostingHttpRequestInStopEventKey = eventName; - OnHostingHttpRequestInStop(arg); - } - - return; - } - - if (lastChar == 'd') - { - if (ReferenceEquals(eventName, _routingEndpointMatchedKey)) - { - OnRoutingEndpointMatched(arg); - } - else if (eventName.AsSpan().Slice(PrefixLength).SequenceEqual("Routing.EndpointMatched")) - { - _routingEndpointMatchedKey = eventName; - OnRoutingEndpointMatched(arg); - } - - return; - } - } - #else - protected override void OnNext(string eventName, object arg) - { - var lastChar = eventName[eventName.Length - 1]; - - if (lastChar == 't') - { - if (ReferenceEquals(eventName, _hostingHttpRequestInStartEventKey)) - { - OnHostingHttpRequestInStart(arg); - } - else if (eventName == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start") - { - _hostingHttpRequestInStartEventKey = eventName; - OnHostingHttpRequestInStart(arg); - } - - return; - } - - if (lastChar == 'n') - { - if (ReferenceEquals(eventName, _mvcBeforeActionEventKey)) - { - OnMvcBeforeAction(arg); - return; - } - else if (ReferenceEquals(eventName, _mvcAfterActionEventKey)) - { - OnMvcAfterAction(arg); - return; - } - else if (ReferenceEquals(eventName, _hostingUnhandledExceptionEventKey) || - ReferenceEquals(eventName, _diagnosticsUnhandledExceptionEventKey)) - { - OnHostingUnhandledException(arg); - return; - } - switch (eventName) { case "Microsoft.AspNetCore.Mvc.BeforeAction": @@ -231,6 +167,7 @@ protected override void OnNext(string eventName, object arg) OnHostingUnhandledException(arg); break; } +#endif return; } @@ -241,7 +178,11 @@ protected override void OnNext(string eventName, object arg) { OnHostingHttpRequestInStop(arg); } +#if NETCOREAPP + else if (eventName.AsSpan().Slice(PrefixLength).SequenceEqual("Hosting.HttpRequestIn.Stop")) +#else else if (eventName == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop") +#endif { _hostingHttpRequestInStopEventKey = eventName; OnHostingHttpRequestInStop(arg); @@ -256,7 +197,11 @@ protected override void OnNext(string eventName, object arg) { OnRoutingEndpointMatched(arg); } +#if NETCOREAPP + else if (eventName.AsSpan().Slice(PrefixLength).SequenceEqual("Routing.EndpointMatched")) +#else else if (eventName == "Microsoft.AspNetCore.Routing.EndpointMatched") +#endif { _routingEndpointMatchedKey = eventName; OnRoutingEndpointMatched(arg); @@ -265,7 +210,6 @@ protected override void OnNext(string eventName, object arg) return; } } -#endif private static string GetLegacyResourceName(BeforeActionStruct typedArg) { From cf03a709c6658908493df3eab170a8635ec0050c Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Mon, 4 Dec 2023 13:51:41 +0000 Subject: [PATCH 4/8] Add AspNetCoreSingleSpanTags --- .../AspNetCoreSingleSpanTags.g.cs | 222 ++++++++++++++++++ .../AspNetCoreSingleSpanTags.g.cs | 222 ++++++++++++++++++ .../AspNetCoreSingleSpanTags.g.cs | 222 ++++++++++++++++++ .../AspNetCoreSingleSpanTags.g.cs | 222 ++++++++++++++++++ .../Tagging/AspNetCoreSingleSpanTags.cs | 29 +++ 5 files changed, 917 insertions(+) create mode 100644 tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/TagListGenerator/AspNetCoreSingleSpanTags.g.cs create mode 100644 tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/TagListGenerator/AspNetCoreSingleSpanTags.g.cs create mode 100644 tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/TagListGenerator/AspNetCoreSingleSpanTags.g.cs create mode 100644 tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/TagListGenerator/AspNetCoreSingleSpanTags.g.cs create mode 100644 tracer/src/Datadog.Trace/Tagging/AspNetCoreSingleSpanTags.cs diff --git a/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/TagListGenerator/AspNetCoreSingleSpanTags.g.cs b/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/TagListGenerator/AspNetCoreSingleSpanTags.g.cs new file mode 100644 index 000000000000..245dc8a55cd2 --- /dev/null +++ b/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/TagListGenerator/AspNetCoreSingleSpanTags.g.cs @@ -0,0 +1,222 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// +// + +#nullable enable + +using Datadog.Trace.Processors; +using Datadog.Trace.Tagging; +using System; + +namespace Datadog.Trace.Tagging +{ + partial class AspNetCoreSingleSpanTags + { + // InstrumentationNameBytes = MessagePack.Serialize("component"); +#if NETCOREAPP + private static ReadOnlySpan InstrumentationNameBytes => new byte[] { 169, 99, 111, 109, 112, 111, 110, 101, 110, 116 }; +#else + private static readonly byte[] InstrumentationNameBytes = new byte[] { 169, 99, 111, 109, 112, 111, 110, 101, 110, 116 }; +#endif + // AspNetCoreRouteBytes = MessagePack.Serialize("aspnet_core.route"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreRouteBytes => new byte[] { 177, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 114, 111, 117, 116, 101 }; +#else + private static readonly byte[] AspNetCoreRouteBytes = new byte[] { 177, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 114, 111, 117, 116, 101 }; +#endif + // HttpRouteBytes = MessagePack.Serialize("http.route"); +#if NETCOREAPP + private static ReadOnlySpan HttpRouteBytes => new byte[] { 170, 104, 116, 116, 112, 46, 114, 111, 117, 116, 101 }; +#else + private static readonly byte[] HttpRouteBytes = new byte[] { 170, 104, 116, 116, 112, 46, 114, 111, 117, 116, 101 }; +#endif + // AspNetCoreEndpointBytes = MessagePack.Serialize("aspnet_core.endpoint"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreEndpointBytes => new byte[] { 180, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 101, 110, 100, 112, 111, 105, 110, 116 }; +#else + private static readonly byte[] AspNetCoreEndpointBytes = new byte[] { 180, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 101, 110, 100, 112, 111, 105, 110, 116 }; +#endif + // AspNetCoreControllerBytes = MessagePack.Serialize("aspnet_core.controller"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreControllerBytes => new byte[] { 182, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 99, 111, 110, 116, 114, 111, 108, 108, 101, 114 }; +#else + private static readonly byte[] AspNetCoreControllerBytes = new byte[] { 182, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 99, 111, 110, 116, 114, 111, 108, 108, 101, 114 }; +#endif + // AspNetCoreActionBytes = MessagePack.Serialize("aspnet_core.action"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreActionBytes => new byte[] { 178, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 97, 99, 116, 105, 111, 110 }; +#else + private static readonly byte[] AspNetCoreActionBytes = new byte[] { 178, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 97, 99, 116, 105, 111, 110 }; +#endif + // AspNetCoreAreaBytes = MessagePack.Serialize("aspnet_core.area"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreAreaBytes => new byte[] { 176, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 97, 114, 101, 97 }; +#else + private static readonly byte[] AspNetCoreAreaBytes = new byte[] { 176, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 97, 114, 101, 97 }; +#endif + // AspNetCorePageBytes = MessagePack.Serialize("aspnet_core.page"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCorePageBytes => new byte[] { 176, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 112, 97, 103, 101 }; +#else + private static readonly byte[] AspNetCorePageBytes = new byte[] { 176, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 112, 97, 103, 101 }; +#endif + + public override string? GetTag(string key) + { + return key switch + { + "component" => InstrumentationName, + "aspnet_core.route" => AspNetCoreRoute, + "http.route" => HttpRoute, + "aspnet_core.endpoint" => AspNetCoreEndpoint, + "aspnet_core.controller" => AspNetCoreController, + "aspnet_core.action" => AspNetCoreAction, + "aspnet_core.area" => AspNetCoreArea, + "aspnet_core.page" => AspNetCorePage, + _ => base.GetTag(key), + }; + } + + public override void SetTag(string key, string value) + { + switch(key) + { + case "aspnet_core.route": + AspNetCoreRoute = value; + break; + case "http.route": + HttpRoute = value; + break; + case "aspnet_core.endpoint": + AspNetCoreEndpoint = value; + break; + case "aspnet_core.controller": + AspNetCoreController = value; + break; + case "aspnet_core.action": + AspNetCoreAction = value; + break; + case "aspnet_core.area": + AspNetCoreArea = value; + break; + case "aspnet_core.page": + AspNetCorePage = value; + break; + case "component": + Logger.Value.Warning("Attempted to set readonly tag {TagName} on {TagType}. Ignoring.", key, nameof(AspNetCoreSingleSpanTags)); + break; + default: + base.SetTag(key, value); + break; + } + } + + public override void EnumerateTags(ref TProcessor processor) + { + if (InstrumentationName is not null) + { + processor.Process(new TagItem("component", InstrumentationName, InstrumentationNameBytes)); + } + + if (AspNetCoreRoute is not null) + { + processor.Process(new TagItem("aspnet_core.route", AspNetCoreRoute, AspNetCoreRouteBytes)); + } + + if (HttpRoute is not null) + { + processor.Process(new TagItem("http.route", HttpRoute, HttpRouteBytes)); + } + + if (AspNetCoreEndpoint is not null) + { + processor.Process(new TagItem("aspnet_core.endpoint", AspNetCoreEndpoint, AspNetCoreEndpointBytes)); + } + + if (AspNetCoreController is not null) + { + processor.Process(new TagItem("aspnet_core.controller", AspNetCoreController, AspNetCoreControllerBytes)); + } + + if (AspNetCoreAction is not null) + { + processor.Process(new TagItem("aspnet_core.action", AspNetCoreAction, AspNetCoreActionBytes)); + } + + if (AspNetCoreArea is not null) + { + processor.Process(new TagItem("aspnet_core.area", AspNetCoreArea, AspNetCoreAreaBytes)); + } + + if (AspNetCorePage is not null) + { + processor.Process(new TagItem("aspnet_core.page", AspNetCorePage, AspNetCorePageBytes)); + } + + base.EnumerateTags(ref processor); + } + + protected override void WriteAdditionalTags(System.Text.StringBuilder sb) + { + if (InstrumentationName is not null) + { + sb.Append("component (tag):") + .Append(InstrumentationName) + .Append(','); + } + + if (AspNetCoreRoute is not null) + { + sb.Append("aspnet_core.route (tag):") + .Append(AspNetCoreRoute) + .Append(','); + } + + if (HttpRoute is not null) + { + sb.Append("http.route (tag):") + .Append(HttpRoute) + .Append(','); + } + + if (AspNetCoreEndpoint is not null) + { + sb.Append("aspnet_core.endpoint (tag):") + .Append(AspNetCoreEndpoint) + .Append(','); + } + + if (AspNetCoreController is not null) + { + sb.Append("aspnet_core.controller (tag):") + .Append(AspNetCoreController) + .Append(','); + } + + if (AspNetCoreAction is not null) + { + sb.Append("aspnet_core.action (tag):") + .Append(AspNetCoreAction) + .Append(','); + } + + if (AspNetCoreArea is not null) + { + sb.Append("aspnet_core.area (tag):") + .Append(AspNetCoreArea) + .Append(','); + } + + if (AspNetCorePage is not null) + { + sb.Append("aspnet_core.page (tag):") + .Append(AspNetCorePage) + .Append(','); + } + + base.WriteAdditionalTags(sb); + } + } +} diff --git a/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/TagListGenerator/AspNetCoreSingleSpanTags.g.cs b/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/TagListGenerator/AspNetCoreSingleSpanTags.g.cs new file mode 100644 index 000000000000..245dc8a55cd2 --- /dev/null +++ b/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/TagListGenerator/AspNetCoreSingleSpanTags.g.cs @@ -0,0 +1,222 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// +// + +#nullable enable + +using Datadog.Trace.Processors; +using Datadog.Trace.Tagging; +using System; + +namespace Datadog.Trace.Tagging +{ + partial class AspNetCoreSingleSpanTags + { + // InstrumentationNameBytes = MessagePack.Serialize("component"); +#if NETCOREAPP + private static ReadOnlySpan InstrumentationNameBytes => new byte[] { 169, 99, 111, 109, 112, 111, 110, 101, 110, 116 }; +#else + private static readonly byte[] InstrumentationNameBytes = new byte[] { 169, 99, 111, 109, 112, 111, 110, 101, 110, 116 }; +#endif + // AspNetCoreRouteBytes = MessagePack.Serialize("aspnet_core.route"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreRouteBytes => new byte[] { 177, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 114, 111, 117, 116, 101 }; +#else + private static readonly byte[] AspNetCoreRouteBytes = new byte[] { 177, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 114, 111, 117, 116, 101 }; +#endif + // HttpRouteBytes = MessagePack.Serialize("http.route"); +#if NETCOREAPP + private static ReadOnlySpan HttpRouteBytes => new byte[] { 170, 104, 116, 116, 112, 46, 114, 111, 117, 116, 101 }; +#else + private static readonly byte[] HttpRouteBytes = new byte[] { 170, 104, 116, 116, 112, 46, 114, 111, 117, 116, 101 }; +#endif + // AspNetCoreEndpointBytes = MessagePack.Serialize("aspnet_core.endpoint"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreEndpointBytes => new byte[] { 180, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 101, 110, 100, 112, 111, 105, 110, 116 }; +#else + private static readonly byte[] AspNetCoreEndpointBytes = new byte[] { 180, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 101, 110, 100, 112, 111, 105, 110, 116 }; +#endif + // AspNetCoreControllerBytes = MessagePack.Serialize("aspnet_core.controller"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreControllerBytes => new byte[] { 182, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 99, 111, 110, 116, 114, 111, 108, 108, 101, 114 }; +#else + private static readonly byte[] AspNetCoreControllerBytes = new byte[] { 182, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 99, 111, 110, 116, 114, 111, 108, 108, 101, 114 }; +#endif + // AspNetCoreActionBytes = MessagePack.Serialize("aspnet_core.action"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreActionBytes => new byte[] { 178, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 97, 99, 116, 105, 111, 110 }; +#else + private static readonly byte[] AspNetCoreActionBytes = new byte[] { 178, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 97, 99, 116, 105, 111, 110 }; +#endif + // AspNetCoreAreaBytes = MessagePack.Serialize("aspnet_core.area"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreAreaBytes => new byte[] { 176, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 97, 114, 101, 97 }; +#else + private static readonly byte[] AspNetCoreAreaBytes = new byte[] { 176, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 97, 114, 101, 97 }; +#endif + // AspNetCorePageBytes = MessagePack.Serialize("aspnet_core.page"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCorePageBytes => new byte[] { 176, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 112, 97, 103, 101 }; +#else + private static readonly byte[] AspNetCorePageBytes = new byte[] { 176, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 112, 97, 103, 101 }; +#endif + + public override string? GetTag(string key) + { + return key switch + { + "component" => InstrumentationName, + "aspnet_core.route" => AspNetCoreRoute, + "http.route" => HttpRoute, + "aspnet_core.endpoint" => AspNetCoreEndpoint, + "aspnet_core.controller" => AspNetCoreController, + "aspnet_core.action" => AspNetCoreAction, + "aspnet_core.area" => AspNetCoreArea, + "aspnet_core.page" => AspNetCorePage, + _ => base.GetTag(key), + }; + } + + public override void SetTag(string key, string value) + { + switch(key) + { + case "aspnet_core.route": + AspNetCoreRoute = value; + break; + case "http.route": + HttpRoute = value; + break; + case "aspnet_core.endpoint": + AspNetCoreEndpoint = value; + break; + case "aspnet_core.controller": + AspNetCoreController = value; + break; + case "aspnet_core.action": + AspNetCoreAction = value; + break; + case "aspnet_core.area": + AspNetCoreArea = value; + break; + case "aspnet_core.page": + AspNetCorePage = value; + break; + case "component": + Logger.Value.Warning("Attempted to set readonly tag {TagName} on {TagType}. Ignoring.", key, nameof(AspNetCoreSingleSpanTags)); + break; + default: + base.SetTag(key, value); + break; + } + } + + public override void EnumerateTags(ref TProcessor processor) + { + if (InstrumentationName is not null) + { + processor.Process(new TagItem("component", InstrumentationName, InstrumentationNameBytes)); + } + + if (AspNetCoreRoute is not null) + { + processor.Process(new TagItem("aspnet_core.route", AspNetCoreRoute, AspNetCoreRouteBytes)); + } + + if (HttpRoute is not null) + { + processor.Process(new TagItem("http.route", HttpRoute, HttpRouteBytes)); + } + + if (AspNetCoreEndpoint is not null) + { + processor.Process(new TagItem("aspnet_core.endpoint", AspNetCoreEndpoint, AspNetCoreEndpointBytes)); + } + + if (AspNetCoreController is not null) + { + processor.Process(new TagItem("aspnet_core.controller", AspNetCoreController, AspNetCoreControllerBytes)); + } + + if (AspNetCoreAction is not null) + { + processor.Process(new TagItem("aspnet_core.action", AspNetCoreAction, AspNetCoreActionBytes)); + } + + if (AspNetCoreArea is not null) + { + processor.Process(new TagItem("aspnet_core.area", AspNetCoreArea, AspNetCoreAreaBytes)); + } + + if (AspNetCorePage is not null) + { + processor.Process(new TagItem("aspnet_core.page", AspNetCorePage, AspNetCorePageBytes)); + } + + base.EnumerateTags(ref processor); + } + + protected override void WriteAdditionalTags(System.Text.StringBuilder sb) + { + if (InstrumentationName is not null) + { + sb.Append("component (tag):") + .Append(InstrumentationName) + .Append(','); + } + + if (AspNetCoreRoute is not null) + { + sb.Append("aspnet_core.route (tag):") + .Append(AspNetCoreRoute) + .Append(','); + } + + if (HttpRoute is not null) + { + sb.Append("http.route (tag):") + .Append(HttpRoute) + .Append(','); + } + + if (AspNetCoreEndpoint is not null) + { + sb.Append("aspnet_core.endpoint (tag):") + .Append(AspNetCoreEndpoint) + .Append(','); + } + + if (AspNetCoreController is not null) + { + sb.Append("aspnet_core.controller (tag):") + .Append(AspNetCoreController) + .Append(','); + } + + if (AspNetCoreAction is not null) + { + sb.Append("aspnet_core.action (tag):") + .Append(AspNetCoreAction) + .Append(','); + } + + if (AspNetCoreArea is not null) + { + sb.Append("aspnet_core.area (tag):") + .Append(AspNetCoreArea) + .Append(','); + } + + if (AspNetCorePage is not null) + { + sb.Append("aspnet_core.page (tag):") + .Append(AspNetCorePage) + .Append(','); + } + + base.WriteAdditionalTags(sb); + } + } +} diff --git a/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/TagListGenerator/AspNetCoreSingleSpanTags.g.cs b/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/TagListGenerator/AspNetCoreSingleSpanTags.g.cs new file mode 100644 index 000000000000..245dc8a55cd2 --- /dev/null +++ b/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/TagListGenerator/AspNetCoreSingleSpanTags.g.cs @@ -0,0 +1,222 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// +// + +#nullable enable + +using Datadog.Trace.Processors; +using Datadog.Trace.Tagging; +using System; + +namespace Datadog.Trace.Tagging +{ + partial class AspNetCoreSingleSpanTags + { + // InstrumentationNameBytes = MessagePack.Serialize("component"); +#if NETCOREAPP + private static ReadOnlySpan InstrumentationNameBytes => new byte[] { 169, 99, 111, 109, 112, 111, 110, 101, 110, 116 }; +#else + private static readonly byte[] InstrumentationNameBytes = new byte[] { 169, 99, 111, 109, 112, 111, 110, 101, 110, 116 }; +#endif + // AspNetCoreRouteBytes = MessagePack.Serialize("aspnet_core.route"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreRouteBytes => new byte[] { 177, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 114, 111, 117, 116, 101 }; +#else + private static readonly byte[] AspNetCoreRouteBytes = new byte[] { 177, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 114, 111, 117, 116, 101 }; +#endif + // HttpRouteBytes = MessagePack.Serialize("http.route"); +#if NETCOREAPP + private static ReadOnlySpan HttpRouteBytes => new byte[] { 170, 104, 116, 116, 112, 46, 114, 111, 117, 116, 101 }; +#else + private static readonly byte[] HttpRouteBytes = new byte[] { 170, 104, 116, 116, 112, 46, 114, 111, 117, 116, 101 }; +#endif + // AspNetCoreEndpointBytes = MessagePack.Serialize("aspnet_core.endpoint"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreEndpointBytes => new byte[] { 180, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 101, 110, 100, 112, 111, 105, 110, 116 }; +#else + private static readonly byte[] AspNetCoreEndpointBytes = new byte[] { 180, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 101, 110, 100, 112, 111, 105, 110, 116 }; +#endif + // AspNetCoreControllerBytes = MessagePack.Serialize("aspnet_core.controller"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreControllerBytes => new byte[] { 182, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 99, 111, 110, 116, 114, 111, 108, 108, 101, 114 }; +#else + private static readonly byte[] AspNetCoreControllerBytes = new byte[] { 182, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 99, 111, 110, 116, 114, 111, 108, 108, 101, 114 }; +#endif + // AspNetCoreActionBytes = MessagePack.Serialize("aspnet_core.action"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreActionBytes => new byte[] { 178, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 97, 99, 116, 105, 111, 110 }; +#else + private static readonly byte[] AspNetCoreActionBytes = new byte[] { 178, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 97, 99, 116, 105, 111, 110 }; +#endif + // AspNetCoreAreaBytes = MessagePack.Serialize("aspnet_core.area"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreAreaBytes => new byte[] { 176, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 97, 114, 101, 97 }; +#else + private static readonly byte[] AspNetCoreAreaBytes = new byte[] { 176, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 97, 114, 101, 97 }; +#endif + // AspNetCorePageBytes = MessagePack.Serialize("aspnet_core.page"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCorePageBytes => new byte[] { 176, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 112, 97, 103, 101 }; +#else + private static readonly byte[] AspNetCorePageBytes = new byte[] { 176, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 112, 97, 103, 101 }; +#endif + + public override string? GetTag(string key) + { + return key switch + { + "component" => InstrumentationName, + "aspnet_core.route" => AspNetCoreRoute, + "http.route" => HttpRoute, + "aspnet_core.endpoint" => AspNetCoreEndpoint, + "aspnet_core.controller" => AspNetCoreController, + "aspnet_core.action" => AspNetCoreAction, + "aspnet_core.area" => AspNetCoreArea, + "aspnet_core.page" => AspNetCorePage, + _ => base.GetTag(key), + }; + } + + public override void SetTag(string key, string value) + { + switch(key) + { + case "aspnet_core.route": + AspNetCoreRoute = value; + break; + case "http.route": + HttpRoute = value; + break; + case "aspnet_core.endpoint": + AspNetCoreEndpoint = value; + break; + case "aspnet_core.controller": + AspNetCoreController = value; + break; + case "aspnet_core.action": + AspNetCoreAction = value; + break; + case "aspnet_core.area": + AspNetCoreArea = value; + break; + case "aspnet_core.page": + AspNetCorePage = value; + break; + case "component": + Logger.Value.Warning("Attempted to set readonly tag {TagName} on {TagType}. Ignoring.", key, nameof(AspNetCoreSingleSpanTags)); + break; + default: + base.SetTag(key, value); + break; + } + } + + public override void EnumerateTags(ref TProcessor processor) + { + if (InstrumentationName is not null) + { + processor.Process(new TagItem("component", InstrumentationName, InstrumentationNameBytes)); + } + + if (AspNetCoreRoute is not null) + { + processor.Process(new TagItem("aspnet_core.route", AspNetCoreRoute, AspNetCoreRouteBytes)); + } + + if (HttpRoute is not null) + { + processor.Process(new TagItem("http.route", HttpRoute, HttpRouteBytes)); + } + + if (AspNetCoreEndpoint is not null) + { + processor.Process(new TagItem("aspnet_core.endpoint", AspNetCoreEndpoint, AspNetCoreEndpointBytes)); + } + + if (AspNetCoreController is not null) + { + processor.Process(new TagItem("aspnet_core.controller", AspNetCoreController, AspNetCoreControllerBytes)); + } + + if (AspNetCoreAction is not null) + { + processor.Process(new TagItem("aspnet_core.action", AspNetCoreAction, AspNetCoreActionBytes)); + } + + if (AspNetCoreArea is not null) + { + processor.Process(new TagItem("aspnet_core.area", AspNetCoreArea, AspNetCoreAreaBytes)); + } + + if (AspNetCorePage is not null) + { + processor.Process(new TagItem("aspnet_core.page", AspNetCorePage, AspNetCorePageBytes)); + } + + base.EnumerateTags(ref processor); + } + + protected override void WriteAdditionalTags(System.Text.StringBuilder sb) + { + if (InstrumentationName is not null) + { + sb.Append("component (tag):") + .Append(InstrumentationName) + .Append(','); + } + + if (AspNetCoreRoute is not null) + { + sb.Append("aspnet_core.route (tag):") + .Append(AspNetCoreRoute) + .Append(','); + } + + if (HttpRoute is not null) + { + sb.Append("http.route (tag):") + .Append(HttpRoute) + .Append(','); + } + + if (AspNetCoreEndpoint is not null) + { + sb.Append("aspnet_core.endpoint (tag):") + .Append(AspNetCoreEndpoint) + .Append(','); + } + + if (AspNetCoreController is not null) + { + sb.Append("aspnet_core.controller (tag):") + .Append(AspNetCoreController) + .Append(','); + } + + if (AspNetCoreAction is not null) + { + sb.Append("aspnet_core.action (tag):") + .Append(AspNetCoreAction) + .Append(','); + } + + if (AspNetCoreArea is not null) + { + sb.Append("aspnet_core.area (tag):") + .Append(AspNetCoreArea) + .Append(','); + } + + if (AspNetCorePage is not null) + { + sb.Append("aspnet_core.page (tag):") + .Append(AspNetCorePage) + .Append(','); + } + + base.WriteAdditionalTags(sb); + } + } +} diff --git a/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/TagListGenerator/AspNetCoreSingleSpanTags.g.cs b/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/TagListGenerator/AspNetCoreSingleSpanTags.g.cs new file mode 100644 index 000000000000..245dc8a55cd2 --- /dev/null +++ b/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/TagListGenerator/AspNetCoreSingleSpanTags.g.cs @@ -0,0 +1,222 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// +// + +#nullable enable + +using Datadog.Trace.Processors; +using Datadog.Trace.Tagging; +using System; + +namespace Datadog.Trace.Tagging +{ + partial class AspNetCoreSingleSpanTags + { + // InstrumentationNameBytes = MessagePack.Serialize("component"); +#if NETCOREAPP + private static ReadOnlySpan InstrumentationNameBytes => new byte[] { 169, 99, 111, 109, 112, 111, 110, 101, 110, 116 }; +#else + private static readonly byte[] InstrumentationNameBytes = new byte[] { 169, 99, 111, 109, 112, 111, 110, 101, 110, 116 }; +#endif + // AspNetCoreRouteBytes = MessagePack.Serialize("aspnet_core.route"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreRouteBytes => new byte[] { 177, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 114, 111, 117, 116, 101 }; +#else + private static readonly byte[] AspNetCoreRouteBytes = new byte[] { 177, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 114, 111, 117, 116, 101 }; +#endif + // HttpRouteBytes = MessagePack.Serialize("http.route"); +#if NETCOREAPP + private static ReadOnlySpan HttpRouteBytes => new byte[] { 170, 104, 116, 116, 112, 46, 114, 111, 117, 116, 101 }; +#else + private static readonly byte[] HttpRouteBytes = new byte[] { 170, 104, 116, 116, 112, 46, 114, 111, 117, 116, 101 }; +#endif + // AspNetCoreEndpointBytes = MessagePack.Serialize("aspnet_core.endpoint"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreEndpointBytes => new byte[] { 180, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 101, 110, 100, 112, 111, 105, 110, 116 }; +#else + private static readonly byte[] AspNetCoreEndpointBytes = new byte[] { 180, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 101, 110, 100, 112, 111, 105, 110, 116 }; +#endif + // AspNetCoreControllerBytes = MessagePack.Serialize("aspnet_core.controller"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreControllerBytes => new byte[] { 182, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 99, 111, 110, 116, 114, 111, 108, 108, 101, 114 }; +#else + private static readonly byte[] AspNetCoreControllerBytes = new byte[] { 182, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 99, 111, 110, 116, 114, 111, 108, 108, 101, 114 }; +#endif + // AspNetCoreActionBytes = MessagePack.Serialize("aspnet_core.action"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreActionBytes => new byte[] { 178, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 97, 99, 116, 105, 111, 110 }; +#else + private static readonly byte[] AspNetCoreActionBytes = new byte[] { 178, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 97, 99, 116, 105, 111, 110 }; +#endif + // AspNetCoreAreaBytes = MessagePack.Serialize("aspnet_core.area"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCoreAreaBytes => new byte[] { 176, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 97, 114, 101, 97 }; +#else + private static readonly byte[] AspNetCoreAreaBytes = new byte[] { 176, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 97, 114, 101, 97 }; +#endif + // AspNetCorePageBytes = MessagePack.Serialize("aspnet_core.page"); +#if NETCOREAPP + private static ReadOnlySpan AspNetCorePageBytes => new byte[] { 176, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 112, 97, 103, 101 }; +#else + private static readonly byte[] AspNetCorePageBytes = new byte[] { 176, 97, 115, 112, 110, 101, 116, 95, 99, 111, 114, 101, 46, 112, 97, 103, 101 }; +#endif + + public override string? GetTag(string key) + { + return key switch + { + "component" => InstrumentationName, + "aspnet_core.route" => AspNetCoreRoute, + "http.route" => HttpRoute, + "aspnet_core.endpoint" => AspNetCoreEndpoint, + "aspnet_core.controller" => AspNetCoreController, + "aspnet_core.action" => AspNetCoreAction, + "aspnet_core.area" => AspNetCoreArea, + "aspnet_core.page" => AspNetCorePage, + _ => base.GetTag(key), + }; + } + + public override void SetTag(string key, string value) + { + switch(key) + { + case "aspnet_core.route": + AspNetCoreRoute = value; + break; + case "http.route": + HttpRoute = value; + break; + case "aspnet_core.endpoint": + AspNetCoreEndpoint = value; + break; + case "aspnet_core.controller": + AspNetCoreController = value; + break; + case "aspnet_core.action": + AspNetCoreAction = value; + break; + case "aspnet_core.area": + AspNetCoreArea = value; + break; + case "aspnet_core.page": + AspNetCorePage = value; + break; + case "component": + Logger.Value.Warning("Attempted to set readonly tag {TagName} on {TagType}. Ignoring.", key, nameof(AspNetCoreSingleSpanTags)); + break; + default: + base.SetTag(key, value); + break; + } + } + + public override void EnumerateTags(ref TProcessor processor) + { + if (InstrumentationName is not null) + { + processor.Process(new TagItem("component", InstrumentationName, InstrumentationNameBytes)); + } + + if (AspNetCoreRoute is not null) + { + processor.Process(new TagItem("aspnet_core.route", AspNetCoreRoute, AspNetCoreRouteBytes)); + } + + if (HttpRoute is not null) + { + processor.Process(new TagItem("http.route", HttpRoute, HttpRouteBytes)); + } + + if (AspNetCoreEndpoint is not null) + { + processor.Process(new TagItem("aspnet_core.endpoint", AspNetCoreEndpoint, AspNetCoreEndpointBytes)); + } + + if (AspNetCoreController is not null) + { + processor.Process(new TagItem("aspnet_core.controller", AspNetCoreController, AspNetCoreControllerBytes)); + } + + if (AspNetCoreAction is not null) + { + processor.Process(new TagItem("aspnet_core.action", AspNetCoreAction, AspNetCoreActionBytes)); + } + + if (AspNetCoreArea is not null) + { + processor.Process(new TagItem("aspnet_core.area", AspNetCoreArea, AspNetCoreAreaBytes)); + } + + if (AspNetCorePage is not null) + { + processor.Process(new TagItem("aspnet_core.page", AspNetCorePage, AspNetCorePageBytes)); + } + + base.EnumerateTags(ref processor); + } + + protected override void WriteAdditionalTags(System.Text.StringBuilder sb) + { + if (InstrumentationName is not null) + { + sb.Append("component (tag):") + .Append(InstrumentationName) + .Append(','); + } + + if (AspNetCoreRoute is not null) + { + sb.Append("aspnet_core.route (tag):") + .Append(AspNetCoreRoute) + .Append(','); + } + + if (HttpRoute is not null) + { + sb.Append("http.route (tag):") + .Append(HttpRoute) + .Append(','); + } + + if (AspNetCoreEndpoint is not null) + { + sb.Append("aspnet_core.endpoint (tag):") + .Append(AspNetCoreEndpoint) + .Append(','); + } + + if (AspNetCoreController is not null) + { + sb.Append("aspnet_core.controller (tag):") + .Append(AspNetCoreController) + .Append(','); + } + + if (AspNetCoreAction is not null) + { + sb.Append("aspnet_core.action (tag):") + .Append(AspNetCoreAction) + .Append(','); + } + + if (AspNetCoreArea is not null) + { + sb.Append("aspnet_core.area (tag):") + .Append(AspNetCoreArea) + .Append(','); + } + + if (AspNetCorePage is not null) + { + sb.Append("aspnet_core.page (tag):") + .Append(AspNetCorePage) + .Append(','); + } + + base.WriteAdditionalTags(sb); + } + } +} diff --git a/tracer/src/Datadog.Trace/Tagging/AspNetCoreSingleSpanTags.cs b/tracer/src/Datadog.Trace/Tagging/AspNetCoreSingleSpanTags.cs new file mode 100644 index 000000000000..fd1dc69042f6 --- /dev/null +++ b/tracer/src/Datadog.Trace/Tagging/AspNetCoreSingleSpanTags.cs @@ -0,0 +1,29 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using Datadog.Trace.SourceGenerators; + +namespace Datadog.Trace.Tagging; + +internal partial class AspNetCoreSingleSpanTags : WebTags +{ + private const string ComponentName = "aspnet_core"; + + [Tag(Trace.Tags.InstrumentationName)] + public string InstrumentationName { get; } = ComponentName; + + [Tag(Trace.Tags.AspNetCoreRoute)] + public string? AspNetCoreRoute { get; set; } + + [Tag(Tags.HttpRoute)] + public string? HttpRoute { get; set; } + + [Tag(Trace.Tags.AspNetCoreEndpoint)] + public string? AspNetCoreEndpoint { get; set; } + + // TODO: Additional executed endpoints e.g. multiple pipeline executions + // Error paths etc +} From 1dfab8f9f6b0666940af527ed8b7a0a118e8f50b Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Mon, 4 Dec 2023 14:32:52 +0000 Subject: [PATCH 5/8] Update single-span observer to "simplify" impact --- .../SingleSpanAspNetCoreDiagnosticObserver.cs | 305 +++++------------- .../AspNetCoreHttpRequestHandler.cs | 11 +- .../Tagging/AspNetCoreSingleSpanTags.cs | 12 + 3 files changed, 107 insertions(+), 221 deletions(-) diff --git a/tracer/src/Datadog.Trace/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserver.cs b/tracer/src/Datadog.Trace/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserver.cs index b056ade94e14..51f9e2aff0aa 100644 --- a/tracer/src/Datadog.Trace/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserver.cs +++ b/tracer/src/Datadog.Trace/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserver.cs @@ -211,43 +211,15 @@ protected override void OnNext(string eventName, object arg) } } - private static string GetLegacyResourceName(BeforeActionStruct typedArg) - { - ActionDescriptor actionDescriptor = typedArg.ActionDescriptor; - HttpRequest request = typedArg.HttpContext.Request; - - string httpMethod = request.Method?.ToUpperInvariant() ?? "UNKNOWN"; - string routeTemplate = actionDescriptor.AttributeRouteInfo?.Template; - if (routeTemplate is null) - { - string controllerName = actionDescriptor.RouteValues["controller"]; - string actionName = actionDescriptor.RouteValues["action"]; - - routeTemplate = $"{controllerName}/{actionName}"; - } - - return $"{httpMethod} {routeTemplate}"; - } - - private static Span StartMvcCoreSpan( + private static void UpdateSpanWithMvc( Tracer tracer, AspNetCoreHttpRequestHandler.RequestTrackingFeature trackingFeature, - BeforeActionStruct typedArg, + AspNetCoreDiagnosticObserver.BeforeActionStruct typedArg, HttpContext httpContext, HttpRequest request) { - // Create a child span for the MVC action - var mvcSpanTags = new AspNetCoreMvcTags(); - var mvcScope = tracer.StartActiveInternal(MvcOperationName, tags: mvcSpanTags); - tracer.TracerManager.Telemetry.IntegrationGeneratedSpan(IntegrationId); - var span = mvcScope.Span; - span.Type = SpanTypes.Web; - - // StartMvcCoreSpan is only called with new route names, so parent tags are always AspNetCoreEndpointTags var rootSpan = trackingFeature.RootScope.Span; - var rootSpanTags = (AspNetCoreEndpointTags)rootSpan.Tags; - - var isUsingEndpointRouting = trackingFeature.IsUsingEndpointRouting; + var rootSpanTags = (AspNetCoreSingleSpanTags)rootSpan.Tags; var isFirstExecution = trackingFeature.IsFirstPipelineExecution; if (isFirstExecution) @@ -261,27 +233,11 @@ private static Span StartMvcCoreSpan( } } - ActionDescriptor actionDescriptor = typedArg.ActionDescriptor; - IDictionary routeValues = actionDescriptor.RouteValues; - - string controllerName = routeValues.TryGetValue("controller", out controllerName) - ? controllerName?.ToLowerInvariant() - : null; - string actionName = routeValues.TryGetValue("action", out actionName) - ? actionName?.ToLowerInvariant() - : null; - string areaName = routeValues.TryGetValue("area", out areaName) - ? areaName?.ToLowerInvariant() - : null; - string pagePath = routeValues.TryGetValue("page", out pagePath) - ? pagePath?.ToLowerInvariant() - : null; - string aspNetRoute = trackingFeature.Route; - string resourceName = trackingFeature.ResourceName; - - if (aspNetRoute is null || resourceName is null) + // We only need to extract these if we're _not_ doing endpoint routing + // OR this is a re-execution, otherwise they're already extracted in OnRoutingEndpointMatched + if (!trackingFeature.IsUsingEndpointRouting || !isFirstExecution) { - // Not using endpoint routing + ActionDescriptor actionDescriptor = typedArg.ActionDescriptor; string rawRouteTemplate = actionDescriptor.AttributeRouteInfo?.Template; RouteTemplate routeTemplate = null; if (rawRouteTemplate is not null) @@ -303,46 +259,62 @@ private static Span StartMvcCoreSpan( } } - if (routeTemplate is not null) + var aspNetRoute = routeTemplate?.TemplateText.ToLowerInvariant(); + + if (!trackingFeature.IsUsingEndpointRouting && isFirstExecution) { - // If we have a route, overwrite the existing resource name - var resourcePathName = AspNetCoreResourceNameHelper.SimplifyRouteTemplate( - routeTemplate, - typedArg.RouteData.Values, - areaName: areaName, - controllerName: controllerName, - actionName: actionName, - expandRouteParameters: tracer.Settings.ExpandRouteTemplatesEnabled); + // not using endpoint routing, so need to update everything + IDictionary routeValues = actionDescriptor.RouteValues; - resourceName = $"{rootSpanTags.HttpMethod} {request.PathBase.ToUriComponent()}{resourcePathName}"; + string controllerName = routeValues.TryGetValue("controller", out controllerName) + ? controllerName?.ToLowerInvariant() + : null; + string actionName = routeValues.TryGetValue("action", out actionName) + ? actionName?.ToLowerInvariant() + : null; + string areaName = routeValues.TryGetValue("area", out areaName) + ? areaName?.ToLowerInvariant() + : null; + string pagePath = routeValues.TryGetValue("page", out pagePath) + ? pagePath?.ToLowerInvariant() + : null; + + // record the values + rootSpanTags.AspNetCoreAction = actionName; + rootSpanTags.AspNetCoreController = controllerName; + rootSpanTags.AspNetCoreArea = areaName; + rootSpanTags.AspNetCorePage = pagePath; + + string resourceName = null; + if (routeTemplate is not null) + { + // If we have a route, overwrite the existing resource name + var resourcePathName = AspNetCoreResourceNameHelper.SimplifyRouteTemplate( + routeTemplate, + typedArg.RouteData.Values, + areaName: areaName, + controllerName: controllerName, + actionName: actionName, + expandRouteParameters: tracer.Settings.ExpandRouteTemplatesEnabled); + + resourceName = $"{rootSpanTags.HttpMethod} {request.PathBase.ToUriComponent()}{resourcePathName}"; + } - aspNetRoute = routeTemplate?.TemplateText.ToLowerInvariant(); + // not using endpoint routing, so need to update everything + rootSpan.ResourceName = resourceName + ?? (string.IsNullOrEmpty(rootSpan.ResourceName) + ? AspNetCoreRequestHandler.GetDefaultResourceName(httpContext.Request) + : rootSpan.ResourceName); + rootSpanTags.AspNetCoreRoute = aspNetRoute; + rootSpanTags.HttpRoute = aspNetRoute; } - } - - // mirror the parent if we couldn't extract a route for some reason - // (and the parent is not using the placeholder resource name) - span.ResourceName = resourceName - ?? (string.IsNullOrEmpty(rootSpan.ResourceName) - ? AspNetCoreRequestHandler.GetDefaultResourceName(httpContext.Request) - : rootSpan.ResourceName); - mvcSpanTags.AspNetCoreAction = actionName; - mvcSpanTags.AspNetCoreController = controllerName; - mvcSpanTags.AspNetCoreArea = areaName; - mvcSpanTags.AspNetCorePage = pagePath; - mvcSpanTags.AspNetCoreRoute = aspNetRoute; - - if (!isUsingEndpointRouting && isFirstExecution) - { - // If we're using endpoint routing or this is a pipeline re-execution, - // these will already be set correctly - rootSpanTags.AspNetCoreRoute = aspNetRoute; - rootSpan.ResourceName = span.ResourceName; - rootSpanTags.HttpRoute = aspNetRoute; + if (!isFirstExecution) + { + // TODO: record the aspnetroute in a list of "re-executed" routes + // So we don't lose any data compared to previous versions + } } - - return span; } private void OnHostingHttpRequestInStart(object arg) @@ -358,14 +330,14 @@ private void OnHostingHttpRequestInStart(object arg) return; } - if (arg.TryDuckCast(out var requestStruct)) + if (arg.TryDuckCast(out var requestStruct)) { HttpContext httpContext = requestStruct.HttpContext; if (shouldTrace) { // Use an empty resource name here, as we will likely replace it as part of the request // If we don't, update it in OnHostingHttpRequestInStop or OnHostingUnhandledException - var scope = AspNetCoreRequestHandler.StartAspNetCorePipelineScope(tracer, CurrentSecurity, httpContext, resourceName: string.Empty); + var scope = AspNetCoreRequestHandler.StartAspNetCorePipelineScope(tracer, CurrentSecurity, httpContext, resourceName: string.Empty, new AspNetCoreSingleSpanTags()); if (shouldSecure) { CoreHttpContextStore.Instance.Set(httpContext); @@ -378,20 +350,18 @@ private void OnHostingHttpRequestInStart(object arg) private void OnRoutingEndpointMatched(object arg) { var tracer = CurrentTracer; - - if (!tracer.Settings.IsIntegrationEnabled(IntegrationId) || - !tracer.Settings.RouteTemplateResourceNamesEnabled) + if (!tracer.Settings.IsIntegrationEnabled(IntegrationId)) { return; } - if (arg.TryDuckCast(out var typedArg) + if (arg.TryDuckCast(out var typedArg) && typedArg.HttpContext is { } httpContext && httpContext.Features.Get() is { RootScope.Span: { } rootSpan } trackingFeature) { - if (rootSpan.Tags is not AspNetCoreEndpointTags tags) + if (rootSpan.Tags is not AspNetCoreSingleSpanTags tags) { - // customer is using legacy resource names + // should never happen return; } @@ -401,6 +371,7 @@ private void OnRoutingEndpointMatched(object arg) trackingFeature.IsUsingEndpointRouting = true; trackingFeature.IsFirstPipelineExecution = false; + // TODO: Do we need this? Can we determine this from the values we store when we create the tags initially? if (!trackingFeature.MatchesOriginalPath(httpContext.Request)) { // URL has changed from original, so treat this execution as a "subsequent" request @@ -433,7 +404,7 @@ private void OnRoutingEndpointMatched(object arg) } } - if (routeEndpoint is null && rawEndpointFeature.TryDuckCast(out var endpointFeatureStruct)) + if (routeEndpoint is null && rawEndpointFeature.TryDuckCast(out var endpointFeatureStruct)) { if (endpointFeatureStruct.Endpoint.TryDuckCast(out var routeEndpointObj)) { @@ -458,21 +429,23 @@ private void OnRoutingEndpointMatched(object arg) var normalizedRoute = routePattern.RawText?.ToLowerInvariant(); trackingFeature.Route = normalizedRoute; - var request = httpContext.Request.DuckCast(); + var request = httpContext.Request.DuckCast(); RouteValueDictionary routeValues = request.RouteValues; - // No need to ToLowerInvariant() these strings, as we lower case - // the whole route later object raw; string controllerName = routeValues.TryGetValue("controller", out raw) - ? raw as string + ? (raw as string)?.ToLowerInvariant() : null; string actionName = routeValues.TryGetValue("action", out raw) - ? raw as string + ? (raw as string)?.ToLowerInvariant() : null; string areaName = routeValues.TryGetValue("area", out raw) - ? raw as string + ? (raw as string)?.ToLowerInvariant() : null; + string pagePath = routeValues.TryGetValue("page", out raw) + ? (raw as string)?.ToLowerInvariant() + : null; + var resourcePathName = AspNetCoreResourceNameHelper.SimplifyRoutePattern( routePattern, routeValues, @@ -483,7 +456,7 @@ private void OnRoutingEndpointMatched(object arg) var resourceName = $"{tags.HttpMethod} {request.PathBase.ToUriComponent()}{resourcePathName}"; - // NOTE: We could set the controller/action/area tags on the parent span + // NOTE: We could store the controller/action/area tags in the tracking context // But instead we re-extract them in the MVC endpoint as these are MVC // constructs. this is likely marginally less efficient, but simplifies the // already complex logic in the MVC handler @@ -494,6 +467,12 @@ private void OnRoutingEndpointMatched(object arg) rootSpan.ResourceName = resourceName; tags.AspNetCoreRoute = normalizedRoute; tags.HttpRoute = normalizedRoute; + + // We're not going to create a child span, so set these on the span directly + tags.AspNetCoreAction = actionName; + tags.AspNetCoreController = controllerName; + tags.AspNetCoreArea = areaName; + tags.AspNetCorePage = pagePath; } CurrentSecurity.CheckPathParams(httpContext, rootSpan, routeValues); @@ -519,7 +498,7 @@ private void OnMvcBeforeAction(object arg) return; } - if (arg.TryDuckCast(out var typedArg) + if (arg.TryDuckCast(out var typedArg) && typedArg.HttpContext is { } httpContext && httpContext.Features.Get() is { RootScope.Span: { } rootSpan } trackingFeature) { @@ -527,23 +506,14 @@ private void OnMvcBeforeAction(object arg) // NOTE: This event is the start of the action pipeline. The action has been selected, the route // has been selected but no filters have run and model binding hasn't occurred. - Span span = null; if (shouldTrace) { - if (!tracer.Settings.RouteTemplateResourceNamesEnabled) - { - // override the parent's resource name with the simplified MVC route template - rootSpan.ResourceName = GetLegacyResourceName(typedArg); - } - else - { - span = StartMvcCoreSpan(tracer, trackingFeature, typedArg, httpContext, request); - } + UpdateSpanWithMvc(tracer, trackingFeature, typedArg, httpContext, request); } - if (span is not null) + if (rootSpan is not null) { - CurrentSecurity.CheckPathParamsFromAction(httpContext, span, typedArg.ActionDescriptor?.Parameters, typedArg.RouteData.Values); + CurrentSecurity.CheckPathParamsFromAction(httpContext, rootSpan, typedArg.ActionDescriptor?.Parameters, typedArg.RouteData.Values); } if (shouldUseIast) @@ -557,8 +527,7 @@ private void OnMvcAfterAction(object arg) { var tracer = CurrentTracer; - if (!tracer.Settings.IsIntegrationEnabled(IntegrationId) || - !tracer.Settings.RouteTemplateResourceNamesEnabled) + if (!tracer.Settings.IsIntegrationEnabled(IntegrationId)) { return; } @@ -602,7 +571,7 @@ private void OnHostingHttpRequestInStop(object arg) return; } - if (arg.DuckCast().HttpContext is { } httpContext + if (arg.DuckCast().HttpContext is { } httpContext && httpContext.Features.Get() is { RootScope: { } rootScope }) { AspNetCoreRequestHandler.StopAspNetCorePipelineScope(tracer, CurrentSecurity, rootScope, httpContext); @@ -620,7 +589,7 @@ private void OnHostingUnhandledException(object arg) return; } - if (arg.TryDuckCast(out var unhandledStruct) + if (arg.TryDuckCast(out var unhandledStruct) && unhandledStruct.HttpContext is { } httpContext && httpContext.Features.Get() is { RootScope.Span: { } rootSpan }) { @@ -629,106 +598,6 @@ private void OnHostingUnhandledException(object arg) // If we don't have a span, no need to call Handle exception } - - [DuckCopy] - internal struct HttpRequestInStartStruct - { - [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase)] - public HttpContext HttpContext; - } - - [DuckCopy] - internal struct HttpRequestInStopStruct - { - [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase)] - public HttpContext HttpContext; - } - - [DuckCopy] - internal struct UnhandledExceptionStruct - { - [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase)] - public HttpContext HttpContext; - - [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase)] - public Exception Exception; - } - - [DuckCopy] - internal struct BeforeActionStruct - { - [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase)] - public HttpContext HttpContext; - - [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase)] - public ActionDescriptor ActionDescriptor; - - [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase)] - public RouteData RouteData; - } - - [DuckCopy] - internal struct BadHttpRequestExceptionStruct - { - [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase | BindingFlags.NonPublic)] - public int StatusCode; - } - - [DuckCopy] - internal struct HttpRequestInEndpointMatchedStruct - { - [Duck(BindingFlags = DuckAttribute.DefaultFlags | BindingFlags.IgnoreCase)] - public HttpContext HttpContext; - } - - /// - /// Proxy for ducktyping IEndpointFeature when the interface is not implemented explicitly - /// - /// - [DuckCopy] - internal struct EndpointFeatureStruct - { - public object Endpoint; - } - - [DuckCopy] - internal struct HttpRequestStruct - { - public string Method; - public RouteValueDictionary RouteValues; - public PathString PathBase; - } - - /// - /// Proxy for https://github1s.com/dotnet/aspnetcore/blob/v3.0.3/src/Http/Routing/src/Patterns/RoutePatternPathSegment.cs - /// - [DuckCopy] - internal struct RoutePatternPathSegmentStruct - { - public IEnumerable Parts; - } - - /// - /// Proxy for https://github1s.com/dotnet/aspnetcore/blob/v3.0.3/src/Http/Routing/src/Patterns/RoutePatternLiteralPart.cs - /// and https://github1s.com/dotnet/aspnetcore/blob/v3.0.3/src/Http/Routing/src/Patterns/RoutePatternSeparatorPart.cs - /// - [DuckCopy] - internal struct RoutePatternContentPartStruct - { - public string Content; - } - - /// - /// Proxy for https://github1s.com/dotnet/aspnetcore/blob/v3.0.3/src/Http/Routing/src/Patterns/RoutePatternParameterPart.cs - /// - [DuckCopy] - internal struct RoutePatternParameterPartStruct - { - public string Name; - public bool IsOptional; - public bool IsCatchAll; - public bool EncodeSlashes; - } } } #endif diff --git a/tracer/src/Datadog.Trace/PlatformHelpers/AspNetCoreHttpRequestHandler.cs b/tracer/src/Datadog.Trace/PlatformHelpers/AspNetCoreHttpRequestHandler.cs index bae5d3b3aa8a..0a97b359bbfd 100644 --- a/tracer/src/Datadog.Trace/PlatformHelpers/AspNetCoreHttpRequestHandler.cs +++ b/tracer/src/Datadog.Trace/PlatformHelpers/AspNetCoreHttpRequestHandler.cs @@ -97,6 +97,14 @@ private void AddHeaderTagsToSpan(ISpan span, HttpRequest request, Tracer tracer) } public Scope StartAspNetCorePipelineScope(Tracer tracer, Security security, HttpContext httpContext, string resourceName) + { + var routeTemplateResourceNames = tracer.Settings.RouteTemplateResourceNamesEnabled; + var tags = routeTemplateResourceNames ? new AspNetCoreEndpointTags() : new AspNetCoreTags(); + return StartAspNetCorePipelineScope(tracer, security, httpContext, resourceName, tags); + } + + public Scope StartAspNetCorePipelineScope(Tracer tracer, Security security, HttpContext httpContext, string resourceName, T tags) + where T : WebTags { var request = httpContext.Request; string host = request.Host.Value; @@ -108,9 +116,6 @@ public Scope StartAspNetCorePipelineScope(Tracer tracer, Security security, Http SpanContext propagatedContext = ExtractPropagatedContext(request); - var routeTemplateResourceNames = tracer.Settings.RouteTemplateResourceNamesEnabled; - var tags = routeTemplateResourceNames ? new AspNetCoreEndpointTags() : new AspNetCoreTags(); - var scope = tracer.StartActiveInternal(_requestInOperationName, propagatedContext, tags: tags); scope.Span.DecorateWebServerSpan(resourceName, httpMethod, host, url, userAgent, tags); AddHeaderTagsToSpan(scope.Span, request, tracer); diff --git a/tracer/src/Datadog.Trace/Tagging/AspNetCoreSingleSpanTags.cs b/tracer/src/Datadog.Trace/Tagging/AspNetCoreSingleSpanTags.cs index fd1dc69042f6..2a1823d9e811 100644 --- a/tracer/src/Datadog.Trace/Tagging/AspNetCoreSingleSpanTags.cs +++ b/tracer/src/Datadog.Trace/Tagging/AspNetCoreSingleSpanTags.cs @@ -24,6 +24,18 @@ internal partial class AspNetCoreSingleSpanTags : WebTags [Tag(Trace.Tags.AspNetCoreEndpoint)] public string? AspNetCoreEndpoint { get; set; } + [Tag(Trace.Tags.AspNetCoreController)] + public string? AspNetCoreController { get; set; } + + [Tag(Trace.Tags.AspNetCoreAction)] + public string? AspNetCoreAction { get; set; } + + [Tag(Trace.Tags.AspNetCoreArea)] + public string? AspNetCoreArea { get; set; } + + [Tag(Trace.Tags.AspNetCorePage)] + public string? AspNetCorePage { get; set; } + // TODO: Additional executed endpoints e.g. multiple pipeline executions // Error paths etc } From 0c7db70d7621e35b9cbb86a0039f9c60055e023e Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Mon, 4 Dec 2023 14:38:40 +0000 Subject: [PATCH 6/8] Split the RequestTrackingFeature implementation to reduce allocations --- .../SingleSpanAspNetCoreDiagnosticObserver.cs | 71 +++++++++++++++---- .../AspNetCoreHttpRequestHandler.cs | 8 +-- 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/tracer/src/Datadog.Trace/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserver.cs b/tracer/src/Datadog.Trace/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserver.cs index 51f9e2aff0aa..1212ad053691 100644 --- a/tracer/src/Datadog.Trace/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserver.cs +++ b/tracer/src/Datadog.Trace/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserver.cs @@ -213,7 +213,7 @@ protected override void OnNext(string eventName, object arg) private static void UpdateSpanWithMvc( Tracer tracer, - AspNetCoreHttpRequestHandler.RequestTrackingFeature trackingFeature, + SingleSpanTrackingFeature trackingFeature, AspNetCoreDiagnosticObserver.BeforeActionStruct typedArg, HttpContext httpContext, HttpRequest request) @@ -337,7 +337,13 @@ private void OnHostingHttpRequestInStart(object arg) { // Use an empty resource name here, as we will likely replace it as part of the request // If we don't, update it in OnHostingHttpRequestInStop or OnHostingUnhandledException - var scope = AspNetCoreRequestHandler.StartAspNetCorePipelineScope(tracer, CurrentSecurity, httpContext, resourceName: string.Empty, new AspNetCoreSingleSpanTags()); + var scope = AspNetCoreRequestHandler.StartAspNetCorePipelineScope( + tracer, + CurrentSecurity, + httpContext, + resourceName: string.Empty, + new AspNetCoreSingleSpanTags(), + static (path, scope) => new SingleSpanTrackingFeature(path, scope)); if (shouldSecure) { CoreHttpContextStore.Instance.Set(httpContext); @@ -357,7 +363,7 @@ private void OnRoutingEndpointMatched(object arg) if (arg.TryDuckCast(out var typedArg) && typedArg.HttpContext is { } httpContext - && httpContext.Features.Get() is { RootScope.Span: { } rootSpan } trackingFeature) + && httpContext.Features.Get() is { RootScope.Span: { } rootSpan } trackingFeature) { if (rootSpan.Tags is not AspNetCoreSingleSpanTags tags) { @@ -427,7 +433,6 @@ private void OnRoutingEndpointMatched(object arg) // Have to pass this value through to the MVC span, as not available there var normalizedRoute = routePattern.RawText?.ToLowerInvariant(); - trackingFeature.Route = normalizedRoute; var request = httpContext.Request.DuckCast(); RouteValueDictionary routeValues = request.RouteValues; @@ -456,12 +461,6 @@ private void OnRoutingEndpointMatched(object arg) var resourceName = $"{tags.HttpMethod} {request.PathBase.ToUriComponent()}{resourcePathName}"; - // NOTE: We could store the controller/action/area tags in the tracking context - // But instead we re-extract them in the MVC endpoint as these are MVC - // constructs. this is likely marginally less efficient, but simplifies the - // already complex logic in the MVC handler - // Overwrite the route in the parent span - trackingFeature.ResourceName = resourceName; if (isFirstExecution) { rootSpan.ResourceName = resourceName; @@ -500,7 +499,7 @@ private void OnMvcBeforeAction(object arg) if (arg.TryDuckCast(out var typedArg) && typedArg.HttpContext is { } httpContext - && httpContext.Features.Get() is { RootScope.Span: { } rootSpan } trackingFeature) + && httpContext.Features.Get() is { RootScope.Span: { } rootSpan } trackingFeature) { HttpRequest request = httpContext.Request; @@ -572,7 +571,7 @@ private void OnHostingHttpRequestInStop(object arg) } if (arg.DuckCast().HttpContext is { } httpContext - && httpContext.Features.Get() is { RootScope: { } rootScope }) + && httpContext.Features.Get() is { RootScope: { } rootScope }) { AspNetCoreRequestHandler.StopAspNetCorePipelineScope(tracer, CurrentSecurity, rootScope, httpContext); } @@ -591,13 +590,59 @@ private void OnHostingUnhandledException(object arg) if (arg.TryDuckCast(out var unhandledStruct) && unhandledStruct.HttpContext is { } httpContext - && httpContext.Features.Get() is { RootScope.Span: { } rootSpan }) + && httpContext.Features.Get() is { RootScope.Span: { } rootSpan }) { AspNetCoreRequestHandler.HandleAspNetCoreException(tracer, CurrentSecurity, rootSpan, httpContext, unhandledStruct.Exception); } // If we don't have a span, no need to call Handle exception } + + /// + /// Holds state that we want to pass between diagnostic source events + /// + internal class SingleSpanTrackingFeature + { + public SingleSpanTrackingFeature(PathString originalPath, Scope rootAspNetCoreScope) + { + OriginalPath = originalPath; + RootScope = rootAspNetCoreScope; + } + + /// + /// Gets or sets a value indicating whether the pipeline using endpoint routing + /// + public bool IsUsingEndpointRouting { get; set; } + + /// + /// Gets or sets a value indicating whether this is the first pipeline execution + /// + public bool IsFirstPipelineExecution { get; set; } = true; + + /// + /// Gets a value indicating the original combined Path and PathBase + /// + public PathString OriginalPath { get; } + + /// + /// Gets the root ASP.NET Core Scope + /// + public Scope RootScope { get; } + + public bool MatchesOriginalPath(HttpRequest request) + { + if (!request.PathBase.HasValue) + { + return OriginalPath.Equals(request.Path, StringComparison.OrdinalIgnoreCase); + } + + return OriginalPath.StartsWithSegments( + request.PathBase, + StringComparison.OrdinalIgnoreCase, + out var remaining) + && remaining.Equals(request.Path, StringComparison.OrdinalIgnoreCase); + } + } } } #endif diff --git a/tracer/src/Datadog.Trace/PlatformHelpers/AspNetCoreHttpRequestHandler.cs b/tracer/src/Datadog.Trace/PlatformHelpers/AspNetCoreHttpRequestHandler.cs index 0a97b359bbfd..c97f4ee655d5 100644 --- a/tracer/src/Datadog.Trace/PlatformHelpers/AspNetCoreHttpRequestHandler.cs +++ b/tracer/src/Datadog.Trace/PlatformHelpers/AspNetCoreHttpRequestHandler.cs @@ -100,11 +100,11 @@ public Scope StartAspNetCorePipelineScope(Tracer tracer, Security security, Http { var routeTemplateResourceNames = tracer.Settings.RouteTemplateResourceNamesEnabled; var tags = routeTemplateResourceNames ? new AspNetCoreEndpointTags() : new AspNetCoreTags(); - return StartAspNetCorePipelineScope(tracer, security, httpContext, resourceName, tags); + return StartAspNetCorePipelineScope(tracer, security, httpContext, resourceName, tags, static (path, scope) => new RequestTrackingFeature(path, scope)); } - public Scope StartAspNetCorePipelineScope(Tracer tracer, Security security, HttpContext httpContext, string resourceName, T tags) - where T : WebTags + public Scope StartAspNetCorePipelineScope(Tracer tracer, Security security, HttpContext httpContext, string resourceName, TTags tags, Func createTrackingFeature) + where TTags : WebTags { var request = httpContext.Request; string host = request.Host.Value; @@ -121,7 +121,7 @@ public Scope StartAspNetCorePipelineScope(Tracer tracer, Security security, H AddHeaderTagsToSpan(scope.Span, request, tracer); var originalPath = request.PathBase.HasValue ? request.PathBase.Add(request.Path) : request.Path; - httpContext.Features.Set(new RequestTrackingFeature(originalPath, scope)); + httpContext.Features.Set(createTrackingFeature(originalPath, scope)); if (tracer.Settings.IpHeaderEnabled || security.Enabled) { From 3d1af55654fd994cd21078d238c81624b77d6aad Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Mon, 4 Dec 2023 15:23:23 +0000 Subject: [PATCH 7/8] TEMP enable single-span in throughput tests --- tracer/build/crank/Samples.AspNetCoreSimpleController.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/tracer/build/crank/Samples.AspNetCoreSimpleController.yml b/tracer/build/crank/Samples.AspNetCoreSimpleController.yml index 684c731aa298..f1be66a52dde 100644 --- a/tracer/build/crank/Samples.AspNetCoreSimpleController.yml +++ b/tracer/build/crank/Samples.AspNetCoreSimpleController.yml @@ -43,6 +43,7 @@ scenarios: CORECLR_ENABLE_PROFILING: 1 DD_CLR_ENABLE_INLINING: 1 DD_CLR_ENABLE_NGEN: 1 + DD_TRACE_SINGLE_WEB_SPAN_ENABLED: "1" load: job: bombardier variables: From 12ebce602d0a01997e98629ed868bfef16ee61f6 Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Mon, 4 Dec 2023 17:05:42 +0000 Subject: [PATCH 8/8] Port diagnosticobserver unit tests --- ...leSpanAspNetCoreDiagnosticObserverTests.cs | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 tracer/test/Datadog.Trace.Tests/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserverTests.cs diff --git a/tracer/test/Datadog.Trace.Tests/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserverTests.cs b/tracer/test/Datadog.Trace.Tests/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserverTests.cs new file mode 100644 index 000000000000..722ea3b98a85 --- /dev/null +++ b/tracer/test/Datadog.Trace.Tests/DiagnosticListeners/SingleSpanAspNetCoreDiagnosticObserverTests.cs @@ -0,0 +1,129 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// +#if !NETFRAMEWORK + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Datadog.Trace.Agent; +using Datadog.Trace.Configuration; +using Datadog.Trace.DiagnosticListeners; +using Datadog.Trace.Sampling; +using Datadog.Trace.TestHelpers; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Datadog.Trace.Tests.DiagnosticListeners; + +[Collection(nameof(TracerInstanceTestCollection))] +[TracerRestorer] +public class SingleSpanAspNetCoreDiagnosticObserverTests +{ + [Fact] + public async Task CompleteDiagnosticObserverTest() + { + TracerRestorerAttribute.SetTracer(GetTracer()); + + var builder = new WebHostBuilder() + .UseStartup(); + + var testServer = new TestServer(builder); + var client = testServer.CreateClient(); + var tracer = GetTracer(); + var observers = new List { new SingleSpanAspNetCoreDiagnosticObserver(tracer, security: null) }; + string retValue = null; + + using (var diagnosticManager = new DiagnosticManager(observers)) + { + diagnosticManager.Start(); + DiagnosticManager.Instance = diagnosticManager; + retValue = await client.GetStringAsync("/Home"); + try + { + await client.GetStringAsync("/Home/error"); + } + catch { } + DiagnosticManager.Instance = null; + } + + return retValue; + } + + [Fact] + public void HttpRequestIn_PopulateSpan() + { + var tracer = GetTracer(); + + IObserver> observer = new SingleSpanAspNetCoreDiagnosticObserver(tracer, null); + + var context = new HostingApplication.Context { HttpContext = GetHttpContext() }; + + observer.OnNext(new KeyValuePair("Microsoft.AspNetCore.Hosting.HttpRequestIn.Start", context)); + + var scope = tracer.ActiveScope; + + Assert.NotNull(scope); + + var span = scope.Span; + + Assert.NotNull(span); + + Assert.Equal("aspnet_core.request", span.OperationName); + Assert.Equal("aspnet_core", span.GetTag(Tags.InstrumentationName)); + Assert.Equal(SpanTypes.Web, span.Type); + Assert.Equal(SpanKinds.Server, span.GetTag(Tags.SpanKind)); + Assert.Equal("GET", span.GetTag(Tags.HttpMethod)); + Assert.Equal("localhost", span.GetTag(Tags.HttpRequestHeadersHost)); + Assert.Equal("http://localhost/home/1/action", span.GetTag(Tags.HttpUrl)); + + // Resource isn't populated until request end + observer.OnNext(new KeyValuePair("Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop", context)); + Assert.Equal("GET /home/?/action", span.ResourceName); + } + + private static Tracer GetTracer() + { + var settings = new TracerSettings(); + var writerMock = new Mock(); + var samplerMock = new Mock(); + + return new Tracer(settings, writerMock.Object, samplerMock.Object, scopeManager: null, statsd: null); + } + + private static HttpContext GetHttpContext() + { + var httpContext = new DefaultHttpContext(); + + httpContext.Request.Headers.Add("hello", "hello"); + httpContext.Request.Headers.Add("world", "world"); + + httpContext.Request.Host = new HostString("localhost"); + httpContext.Request.Scheme = "http"; + httpContext.Request.Path = "/home/1/action"; + httpContext.Request.Method = "GET"; + + return httpContext; + } + + private class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc(); + } + + public void Configure(IApplicationBuilder builder) + { + builder.UseMvcWithDefaultRoute(); + } + } +} +#endif