From 272cc6f4b4f3669914ac7d48a9d19c6e40aa9801 Mon Sep 17 00:00:00 2001 From: Ryan Karg Date: Fri, 23 May 2025 11:35:50 -0700 Subject: [PATCH 01/11] Feature for setting activity tags on creation --- .../Internal/HostingApplicationDiagnostics.cs | 4 ++-- .../src/IHttpActivityCreationTagsFeature.cs | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 src/Http/Http.Features/src/IHttpActivityCreationTagsFeature.cs diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index 1a344203727c..fb3842f17274 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -388,7 +388,7 @@ private void RecordRequestStartMetrics(HttpContext httpContext) private Activity? StartActivity(HttpContext httpContext, bool loggingEnabled, bool diagnosticListenerActivityCreationEnabled, out bool hasDiagnosticListener) { hasDiagnosticListener = false; - + var tagsForCreation = httpContext.Features.Get()?.ActivityCreationTags; var headers = httpContext.Request.Headers; var activity = ActivityCreator.CreateFromRemote( _activitySource, @@ -402,7 +402,7 @@ private void RecordRequestStartMetrics(HttpContext httpContext) }, ActivityName, ActivityKind.Server, - tags: null, + tags: tagsForCreation, links: null, loggingEnabled || diagnosticListenerActivityCreationEnabled); if (activity is null) diff --git a/src/Http/Http.Features/src/IHttpActivityCreationTagsFeature.cs b/src/Http/Http.Features/src/IHttpActivityCreationTagsFeature.cs new file mode 100644 index 000000000000..68a76aedfaab --- /dev/null +++ b/src/Http/Http.Features/src/IHttpActivityCreationTagsFeature.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Provides a mechanism to add tags to the at creation time for incoming HTTP requests. +/// These tags can be used for tracing when making sampling decisions. +/// +public interface IHttpActivityCreationTagsFeature +{ + /// + /// A collection of tags to be added to the when it is created for the current HTTP request. + /// These tags are available at Activity creation time and can be used for sampling decisions. + /// + /// An containing tags to add to the Activity at creation time. + ActivityTagsCollection? ActivityCreationTags { get; } +} + From 67c63634f2a7dd5474713ab9c0129efab446fbe5 Mon Sep 17 00:00:00 2001 From: Ryan Karg Date: Fri, 23 May 2025 12:06:03 -0700 Subject: [PATCH 02/11] adding unshipped public API entries --- src/Http/Http.Features/src/PublicAPI.Unshipped.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Http/Http.Features/src/PublicAPI.Unshipped.txt b/src/Http/Http.Features/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..f172642dc828 100644 --- a/src/Http/Http.Features/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Features/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Http.Features.IHttpActivityCreationTagsFeature +Microsoft.AspNetCore.Http.Features.IHttpActivityCreationTagsFeature.ActivityCreationTags.get -> ActivityTagsCollection? From 2edbad548a2142ab2168547c1c808ada8e29b403 Mon Sep 17 00:00:00 2001 From: Ryan Karg Date: Fri, 23 May 2025 12:11:16 -0700 Subject: [PATCH 03/11] moving to TagList --- .../Http.Features/src/IHttpActivityCreationTagsFeature.cs | 5 ++--- src/Http/Http.Features/src/PublicAPI.Unshipped.txt | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Http/Http.Features/src/IHttpActivityCreationTagsFeature.cs b/src/Http/Http.Features/src/IHttpActivityCreationTagsFeature.cs index 68a76aedfaab..6007dc698063 100644 --- a/src/Http/Http.Features/src/IHttpActivityCreationTagsFeature.cs +++ b/src/Http/Http.Features/src/IHttpActivityCreationTagsFeature.cs @@ -15,7 +15,6 @@ public interface IHttpActivityCreationTagsFeature /// A collection of tags to be added to the when it is created for the current HTTP request. /// These tags are available at Activity creation time and can be used for sampling decisions. /// - /// An containing tags to add to the Activity at creation time. - ActivityTagsCollection? ActivityCreationTags { get; } + /// An containing tags to add to the Activity at creation time. + TagList? ActivityCreationTags { get; } } - diff --git a/src/Http/Http.Features/src/PublicAPI.Unshipped.txt b/src/Http/Http.Features/src/PublicAPI.Unshipped.txt index f172642dc828..3af4f4c5de98 100644 --- a/src/Http/Http.Features/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Features/src/PublicAPI.Unshipped.txt @@ -1,3 +1,3 @@ #nullable enable Microsoft.AspNetCore.Http.Features.IHttpActivityCreationTagsFeature -Microsoft.AspNetCore.Http.Features.IHttpActivityCreationTagsFeature.ActivityCreationTags.get -> ActivityTagsCollection? +Microsoft.AspNetCore.Http.Features.IHttpActivityCreationTagsFeature.ActivityCreationTags.get -> TagList? From 3fcaf854c9c9fee4c0a4a12989003db4a0366756 Mon Sep 17 00:00:00 2001 From: Ryan Karg Date: Fri, 23 May 2025 12:42:10 -0700 Subject: [PATCH 04/11] Less specific property type also fixed up unshipped API documentation --- .../Http.Features/src/IHttpActivityCreationTagsFeature.cs | 6 ++---- src/Http/Http.Features/src/PublicAPI.Unshipped.txt | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Http/Http.Features/src/IHttpActivityCreationTagsFeature.cs b/src/Http/Http.Features/src/IHttpActivityCreationTagsFeature.cs index 6007dc698063..56fe7c1126f8 100644 --- a/src/Http/Http.Features/src/IHttpActivityCreationTagsFeature.cs +++ b/src/Http/Http.Features/src/IHttpActivityCreationTagsFeature.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; - namespace Microsoft.AspNetCore.Http.Features; /// @@ -15,6 +13,6 @@ public interface IHttpActivityCreationTagsFeature /// A collection of tags to be added to the when it is created for the current HTTP request. /// These tags are available at Activity creation time and can be used for sampling decisions. /// - /// An containing tags to add to the Activity at creation time. - TagList? ActivityCreationTags { get; } + /// Tags to add to the Activity at creation time. + IEnumerable>? ActivityCreationTags { get; } } diff --git a/src/Http/Http.Features/src/PublicAPI.Unshipped.txt b/src/Http/Http.Features/src/PublicAPI.Unshipped.txt index 3af4f4c5de98..6b8cd9b46d82 100644 --- a/src/Http/Http.Features/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Features/src/PublicAPI.Unshipped.txt @@ -1,3 +1,3 @@ #nullable enable Microsoft.AspNetCore.Http.Features.IHttpActivityCreationTagsFeature -Microsoft.AspNetCore.Http.Features.IHttpActivityCreationTagsFeature.ActivityCreationTags.get -> TagList? +Microsoft.AspNetCore.Http.Features.IHttpActivityCreationTagsFeature.ActivityCreationTags.get -> System.Collections.Generic.IEnumerable>? From 3ec35012076e37b82cd85f7497858e99b9b11215 Mon Sep 17 00:00:00 2001 From: rkargMsft <164392675+rkargMsft@users.noreply.github.com> Date: Mon, 16 Jun 2025 17:34:32 -0700 Subject: [PATCH 05/11] Update HostingApplicationDiagnostics.cs --- .../Hosting/src/Internal/HostingApplicationDiagnostics.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index fb3842f17274..cbe566ef7c1e 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -388,6 +388,7 @@ private void RecordRequestStartMetrics(HttpContext httpContext) private Activity? StartActivity(HttpContext httpContext, bool loggingEnabled, bool diagnosticListenerActivityCreationEnabled, out bool hasDiagnosticListener) { hasDiagnosticListener = false; + var tagsForCreation = httpContext.Features.Get()?.ActivityCreationTags; var headers = httpContext.Request.Headers; var activity = ActivityCreator.CreateFromRemote( From de03968fe4e4cfa31d531f7797320f05fe1e2107 Mon Sep 17 00:00:00 2001 From: rkargMsft <164392675+rkargMsft@users.noreply.github.com> Date: Wed, 25 Jun 2025 10:22:41 -0700 Subject: [PATCH 06/11] unconditionally adding host/port tags to Activity creation This removes the need to expose a new Feature or make any other public changes. --- .../Internal/HostingApplicationDiagnostics.cs | 22 +++++++++++++++++-- src/Http/Http.Abstractions/src/HostString.cs | 16 ++++++++++++++ .../src/IHttpActivityCreationTagsFeature.cs | 18 --------------- .../Http.Features/src/PublicAPI.Unshipped.txt | 2 -- .../Routing/src/Matching/HostMatcherPolicy.cs | 12 +++++----- 5 files changed, 42 insertions(+), 28 deletions(-) delete mode 100644 src/Http/Http.Features/src/IHttpActivityCreationTagsFeature.cs diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index cbe566ef7c1e..5370b3fa7990 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -388,8 +388,26 @@ private void RecordRequestStartMetrics(HttpContext httpContext) private Activity? StartActivity(HttpContext httpContext, bool loggingEnabled, bool diagnosticListenerActivityCreationEnabled, out bool hasDiagnosticListener) { hasDiagnosticListener = false; - - var tagsForCreation = httpContext.Features.Get()?.ActivityCreationTags; + + var tagsForCreation = new TagList(); + if (_activitySource.HasListeners() && _context.Request.Host.HasValue) + { + var (host, port) = _context.Request.Host.HostAndPort; + tags.Add("server.address", host); + if (port is not null) + { + tags.Add("server.port", port); + } + else if (string.Equals("https", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase)) + { + tags.Add("server.port", 443); + } + else if (string.Equals("http", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase)) + { + tags.Add("server.port", 80); + } + } + var headers = httpContext.Request.Headers; var activity = ActivityCreator.CreateFromRemote( _activitySource, diff --git a/src/Http/Http.Abstractions/src/HostString.cs b/src/Http/Http.Abstractions/src/HostString.cs index 39347033114f..a79e761a7661 100644 --- a/src/Http/Http.Abstractions/src/HostString.cs +++ b/src/Http/Http.Abstractions/src/HostString.cs @@ -118,6 +118,22 @@ public int? Port } } + internal (string host, int? port) HostAndPort + { + get + { + GetParts(_value, out var host, out var port); + + if (!StringSegment.IsNullOrEmpty(port) + && int.TryParse(port.AsSpan(), NumberStyles.None, CultureInfo.InvariantCulture, out var p)) + { + return (host.ToString(), p); + } + + return (host.ToString(), null); + } + } + /// /// Returns the value as normalized by ToUriComponent(). /// diff --git a/src/Http/Http.Features/src/IHttpActivityCreationTagsFeature.cs b/src/Http/Http.Features/src/IHttpActivityCreationTagsFeature.cs deleted file mode 100644 index 56fe7c1126f8..000000000000 --- a/src/Http/Http.Features/src/IHttpActivityCreationTagsFeature.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http.Features; - -/// -/// Provides a mechanism to add tags to the at creation time for incoming HTTP requests. -/// These tags can be used for tracing when making sampling decisions. -/// -public interface IHttpActivityCreationTagsFeature -{ - /// - /// A collection of tags to be added to the when it is created for the current HTTP request. - /// These tags are available at Activity creation time and can be used for sampling decisions. - /// - /// Tags to add to the Activity at creation time. - IEnumerable>? ActivityCreationTags { get; } -} diff --git a/src/Http/Http.Features/src/PublicAPI.Unshipped.txt b/src/Http/Http.Features/src/PublicAPI.Unshipped.txt index 6b8cd9b46d82..7dc5c58110bf 100644 --- a/src/Http/Http.Features/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Features/src/PublicAPI.Unshipped.txt @@ -1,3 +1 @@ #nullable enable -Microsoft.AspNetCore.Http.Features.IHttpActivityCreationTagsFeature -Microsoft.AspNetCore.Http.Features.IHttpActivityCreationTagsFeature.ActivityCreationTags.get -> System.Collections.Generic.IEnumerable>? diff --git a/src/Http/Routing/src/Matching/HostMatcherPolicy.cs b/src/Http/Routing/src/Matching/HostMatcherPolicy.cs index fa21fcbcd9e2..dd2827ac272f 100644 --- a/src/Http/Routing/src/Matching/HostMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HostMatcherPolicy.cs @@ -332,22 +332,22 @@ private static int GetScore(in EdgeKey key) private static (string host, int? port) GetHostAndPort(HttpContext httpContext) { - var hostString = httpContext.Request.Host; - if (hostString.Port != null) + var (host, port) = httpContext.Request.Host.HostAndPort; + if (port != null) { - return (hostString.Host, hostString.Port); + return (host, port); } else if (string.Equals("https", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase)) { - return (hostString.Host, 443); + return (host, 443); } else if (string.Equals("http", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase)) { - return (hostString.Host, 80); + return (host, 80); } else { - return (hostString.Host, null); + return (host, null); } } From 50cf8a9be65ba6a50d5758fef1d06c42d92bc438 Mon Sep 17 00:00:00 2001 From: rkargMsft <164392675+rkargMsft@users.noreply.github.com> Date: Wed, 25 Jun 2025 10:32:51 -0700 Subject: [PATCH 07/11] correcting variable naming --- .../Hosting/src/Internal/HostingApplicationDiagnostics.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index 5370b3fa7990..f5844f75563f 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -390,9 +390,9 @@ private void RecordRequestStartMetrics(HttpContext httpContext) hasDiagnosticListener = false; var tagsForCreation = new TagList(); - if (_activitySource.HasListeners() && _context.Request.Host.HasValue) + if (_activitySource.HasListeners() && httpContext.Request.Host.HasValue) { - var (host, port) = _context.Request.Host.HostAndPort; + var (host, port) = httpContext.Request.Host.HostAndPort; tags.Add("server.address", host); if (port is not null) { From af4426e2509b76e36afaaf16e4947370cd3020d1 Mon Sep 17 00:00:00 2001 From: rkargMsft <164392675+rkargMsft@users.noreply.github.com> Date: Wed, 25 Jun 2025 11:03:40 -0700 Subject: [PATCH 08/11] reverting HostAndPort property --- .../Internal/HostingApplicationDiagnostics.cs | 3 ++- src/Http/Http.Abstractions/src/HostString.cs | 16 ---------------- .../Routing/src/Matching/HostMatcherPolicy.cs | 12 ++++++------ 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index f5844f75563f..04d99cf6820d 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -392,7 +392,8 @@ private void RecordRequestStartMetrics(HttpContext httpContext) var tagsForCreation = new TagList(); if (_activitySource.HasListeners() && httpContext.Request.Host.HasValue) { - var (host, port) = httpContext.Request.Host.HostAndPort; + var host = httpContext.Request.Host.Host; + var port = httpContext.Request.Host.Port; tags.Add("server.address", host); if (port is not null) { diff --git a/src/Http/Http.Abstractions/src/HostString.cs b/src/Http/Http.Abstractions/src/HostString.cs index a79e761a7661..39347033114f 100644 --- a/src/Http/Http.Abstractions/src/HostString.cs +++ b/src/Http/Http.Abstractions/src/HostString.cs @@ -118,22 +118,6 @@ public int? Port } } - internal (string host, int? port) HostAndPort - { - get - { - GetParts(_value, out var host, out var port); - - if (!StringSegment.IsNullOrEmpty(port) - && int.TryParse(port.AsSpan(), NumberStyles.None, CultureInfo.InvariantCulture, out var p)) - { - return (host.ToString(), p); - } - - return (host.ToString(), null); - } - } - /// /// Returns the value as normalized by ToUriComponent(). /// diff --git a/src/Http/Routing/src/Matching/HostMatcherPolicy.cs b/src/Http/Routing/src/Matching/HostMatcherPolicy.cs index dd2827ac272f..fa21fcbcd9e2 100644 --- a/src/Http/Routing/src/Matching/HostMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HostMatcherPolicy.cs @@ -332,22 +332,22 @@ private static int GetScore(in EdgeKey key) private static (string host, int? port) GetHostAndPort(HttpContext httpContext) { - var (host, port) = httpContext.Request.Host.HostAndPort; - if (port != null) + var hostString = httpContext.Request.Host; + if (hostString.Port != null) { - return (host, port); + return (hostString.Host, hostString.Port); } else if (string.Equals("https", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase)) { - return (host, 443); + return (hostString.Host, 443); } else if (string.Equals("http", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase)) { - return (host, 80); + return (hostString.Host, 80); } else { - return (host, null); + return (hostString.Host, null); } } From 8a8dcbbd7c326f75fc90d837b6a80cf9292eb9d0 Mon Sep 17 00:00:00 2001 From: rkargMsft <164392675+rkargMsft@users.noreply.github.com> Date: Wed, 25 Jun 2025 11:15:26 -0700 Subject: [PATCH 09/11] naming --- .../Hosting/src/Internal/HostingApplicationDiagnostics.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index 04d99cf6820d..9115f92e694d 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -394,18 +394,18 @@ private void RecordRequestStartMetrics(HttpContext httpContext) { var host = httpContext.Request.Host.Host; var port = httpContext.Request.Host.Port; - tags.Add("server.address", host); + tagsForCreation.Add("server.address", host); if (port is not null) { - tags.Add("server.port", port); + tagsForCreation.Add("server.port", port); } else if (string.Equals("https", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase)) { - tags.Add("server.port", 443); + tagsForCreation.Add("server.port", 443); } else if (string.Equals("http", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase)) { - tags.Add("server.port", 80); + tagsForCreation.Add("server.port", 80); } } From 9a45fbf2b8aaed4c12e9878fd88cda64f3572cda Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 15 Aug 2025 16:35:03 +0800 Subject: [PATCH 10/11] Initialize hosting trace with OTEL tags for sampling --- .../Internal/HostingApplicationDiagnostics.cs | 79 +++++++---- .../Hosting/src/Internal/HostingMetrics.cs | 75 +--------- .../src/Internal/HostingTelemetryHelpers.cs | 132 ++++++++++++++++++ .../HostingApplicationDiagnosticsTests.cs | 64 ++++++++- src/Shared/Diagnostics/ActivityCreator.cs | 5 + 5 files changed, 257 insertions(+), 98 deletions(-) create mode 100644 src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index 9115f92e694d..f1d5512b0d72 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -33,6 +33,7 @@ internal sealed class HostingApplicationDiagnostics private readonly HostingEventSource _eventSource; private readonly HostingMetrics _metrics; private readonly ILogger _logger; + private readonly bool _suppressActivityOpenTelemetryData; public HostingApplicationDiagnostics( ILogger logger, @@ -48,6 +49,7 @@ public HostingApplicationDiagnostics( _propagator = propagator; _eventSource = eventSource; _metrics = metrics; + _suppressActivityOpenTelemetryData = AppContext.TryGetSwitch("Microsoft.AspNetCore.Hosting.SuppressActivityOpenTelemetryData", out var enabled) && enabled; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -88,9 +90,9 @@ public void BeginRequest(HttpContext httpContext, HostingApplication.Context con var diagnosticListenerActivityCreationEnabled = (diagnosticListenerEnabled && _diagnosticListener.IsEnabled(ActivityName, httpContext)); var loggingEnabled = _logger.IsEnabled(LogLevel.Critical); - if (loggingEnabled || diagnosticListenerActivityCreationEnabled || _activitySource.HasListeners()) + if (ActivityCreator.IsActivityCreated(_activitySource, loggingEnabled || diagnosticListenerActivityCreationEnabled)) { - context.Activity = StartActivity(httpContext, loggingEnabled, diagnosticListenerActivityCreationEnabled, out var hasDiagnosticListener); + context.Activity = StartActivity(httpContext, loggingEnabled || diagnosticListenerActivityCreationEnabled, out var hasDiagnosticListener); context.HasDiagnosticListener = hasDiagnosticListener; if (context.Activity != null) @@ -385,29 +387,17 @@ private void RecordRequestStartMetrics(HttpContext httpContext) } [MethodImpl(MethodImplOptions.NoInlining)] - private Activity? StartActivity(HttpContext httpContext, bool loggingEnabled, bool diagnosticListenerActivityCreationEnabled, out bool hasDiagnosticListener) + private Activity? StartActivity(HttpContext httpContext, bool diagnosticsOrLoggingEnabled, out bool hasDiagnosticListener) { + // StartActivity is only called if an Activity is already verified to be created. + Debug.Assert(ActivityCreator.IsActivityCreated(_activitySource, diagnosticsOrLoggingEnabled), + "Activity should only be created if diagnostics or logging is enabled."); + hasDiagnosticListener = false; - var tagsForCreation = new TagList(); - if (_activitySource.HasListeners() && httpContext.Request.Host.HasValue) - { - var host = httpContext.Request.Host.Host; - var port = httpContext.Request.Host.Port; - tagsForCreation.Add("server.address", host); - if (port is not null) - { - tagsForCreation.Add("server.port", port); - } - else if (string.Equals("https", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase)) - { - tagsForCreation.Add("server.port", 443); - } - else if (string.Equals("http", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase)) - { - tagsForCreation.Add("server.port", 80); - } - } + var initializeTags = !_suppressActivityOpenTelemetryData + ? CreateInitializeActivityTags(httpContext) + : (TagList?)null; var headers = httpContext.Request.Headers; var activity = ActivityCreator.CreateFromRemote( @@ -422,9 +412,9 @@ private void RecordRequestStartMetrics(HttpContext httpContext) }, ActivityName, ActivityKind.Server, - tags: tagsForCreation, + tags: initializeTags, links: null, - loggingEnabled || diagnosticListenerActivityCreationEnabled); + diagnosticsOrLoggingEnabled); if (activity is null) { return null; @@ -445,6 +435,47 @@ private void RecordRequestStartMetrics(HttpContext httpContext) return activity; } + private static TagList CreateInitializeActivityTags(HttpContext httpContext) + { + // The tags here are set when the activity is created. They can be used in sampling decisions. + // Most values in semantic conventions that are present at creation are specified: + // https://github.com/open-telemetry/semantic-conventions/blob/27735ccca3746d7bb7fa061dfb73d93bcbae2b6e/docs/http/http-spans.md#L581-L592 + // Missing values recommended by the spec are: + // - url.query (need configuration around redaction to do properly) + // - http.request.header. + + var request = httpContext.Request; + var creationTags = new TagList(); + + if (request.Host.HasValue) + { + creationTags.Add(HostingTelemetryHelpers.AttributeServerAddress, request.Host.Host); + + if (HostingTelemetryHelpers.TryGetServerPort(request.Host, request.Scheme, out var port)) + { + creationTags.Add(HostingTelemetryHelpers.AttributeServerPort, port); + } + } + + HostingTelemetryHelpers.SetActivityHttpMethodTags(ref creationTags, request.Method); + + if (request.Headers.TryGetValue("User-Agent", out var values)) + { + var userAgent = values.Count > 0 ? values[0] : null; + if (!string.IsNullOrEmpty(userAgent)) + { + creationTags.Add(HostingTelemetryHelpers.AttributeUserAgentOriginal, userAgent); + } + } + + creationTags.Add(HostingTelemetryHelpers.AttributeUrlScheme, request.Scheme); + + var path = (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/"; + creationTags.Add(HostingTelemetryHelpers.AttributeUrlPath, path); + + return creationTags; + } + [MethodImpl(MethodImplOptions.NoInlining)] private void StopActivity(HttpContext httpContext, Activity activity, bool hasDiagnosticListener) { diff --git a/src/Hosting/Hosting/src/Internal/HostingMetrics.cs b/src/Hosting/Hosting/src/Internal/HostingMetrics.cs index 129542fec15a..583c4d6f3348 100644 --- a/src/Hosting/Hosting/src/Internal/HostingMetrics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingMetrics.cs @@ -1,9 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Frozen; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using Microsoft.AspNetCore.Http; @@ -55,7 +53,7 @@ public void RequestEnd(string protocol, string scheme, string method, string? ro if (!disableHttpRequestDurationMetric && _requestDuration.Enabled) { - if (TryGetHttpVersion(protocol, out var httpVersion)) + if (HostingTelemetryHelpers.TryGetHttpVersion(protocol, out var httpVersion)) { tags.Add("network.protocol.version", httpVersion); } @@ -65,7 +63,7 @@ public void RequestEnd(string protocol, string scheme, string method, string? ro } // Add information gathered during request. - tags.Add("http.response.status_code", GetBoxedStatusCode(statusCode)); + tags.Add("http.response.status_code", HostingTelemetryHelpers.GetBoxedStatusCode(statusCode)); if (route != null) { tags.Add("http.route", route); @@ -104,73 +102,6 @@ public void Dispose() private static void InitializeRequestTags(ref TagList tags, string scheme, string method) { tags.Add("url.scheme", scheme); - tags.Add("http.request.method", ResolveHttpMethod(method)); - } - - private static readonly object[] BoxedStatusCodes = new object[512]; - - private static object GetBoxedStatusCode(int statusCode) - { - object[] boxes = BoxedStatusCodes; - return (uint)statusCode < (uint)boxes.Length - ? boxes[statusCode] ??= statusCode - : statusCode; - } - - private static readonly FrozenDictionary KnownMethods = FrozenDictionary.ToFrozenDictionary(new[] - { - KeyValuePair.Create(HttpMethods.Connect, HttpMethods.Connect), - KeyValuePair.Create(HttpMethods.Delete, HttpMethods.Delete), - KeyValuePair.Create(HttpMethods.Get, HttpMethods.Get), - KeyValuePair.Create(HttpMethods.Head, HttpMethods.Head), - KeyValuePair.Create(HttpMethods.Options, HttpMethods.Options), - KeyValuePair.Create(HttpMethods.Patch, HttpMethods.Patch), - KeyValuePair.Create(HttpMethods.Post, HttpMethods.Post), - KeyValuePair.Create(HttpMethods.Put, HttpMethods.Put), - KeyValuePair.Create(HttpMethods.Trace, HttpMethods.Trace) - }, StringComparer.OrdinalIgnoreCase); - - private static string ResolveHttpMethod(string method) - { - // TODO: Support configuration for configuring known methods - if (KnownMethods.TryGetValue(method, out var result)) - { - // KnownMethods ignores case. Use the value returned by the dictionary to have a consistent case. - return result; - } - return "_OTHER"; - } - - private static bool TryGetHttpVersion(string protocol, [NotNullWhen(true)] out string? version) - { - if (HttpProtocol.IsHttp11(protocol)) - { - version = "1.1"; - return true; - } - if (HttpProtocol.IsHttp2(protocol)) - { - // HTTP/2 only has one version. - version = "2"; - return true; - } - if (HttpProtocol.IsHttp3(protocol)) - { - // HTTP/3 only has one version. - version = "3"; - return true; - } - if (HttpProtocol.IsHttp10(protocol)) - { - version = "1.0"; - return true; - } - if (HttpProtocol.IsHttp09(protocol)) - { - version = "0.9"; - return true; - } - version = null; - return false; + tags.Add("http.request.method", HostingTelemetryHelpers.GetNormalizedHttpMethod(method)); } } diff --git a/src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs b/src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs new file mode 100644 index 000000000000..e415869e9cff --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Frozen; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Hosting; + +internal static class HostingTelemetryHelpers +{ + // Semantic Conventions for HTTP. + // Note: Not all telemetry code is using these const attribute names yet. + public const string AttributeHttpRequestMethod = "http.request.method"; + public const string AttributeHttpRequestMethodOriginal = "http.request.method_original"; + public const string AttributeUrlScheme = "url.scheme"; + public const string AttributeUrlPath = "url.path"; + public const string AttributeServerAddress = "server.address"; + public const string AttributeServerPort = "server.port"; + public const string AttributeUserAgentOriginal = "user_agent.original"; + + // The value "_OTHER" is used for non-standard HTTP methods. + // https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#common-attributes + private const string OtherHttpMethod = "_OTHER"; + + private static readonly object[] BoxedStatusCodes = new object[512]; + + private static readonly FrozenDictionary KnownHttpMethods = FrozenDictionary.ToFrozenDictionary([ + KeyValuePair.Create(HttpMethods.Connect, HttpMethods.Connect), + KeyValuePair.Create(HttpMethods.Delete, HttpMethods.Delete), + KeyValuePair.Create(HttpMethods.Get, HttpMethods.Get), + KeyValuePair.Create(HttpMethods.Head, HttpMethods.Head), + KeyValuePair.Create(HttpMethods.Options, HttpMethods.Options), + KeyValuePair.Create(HttpMethods.Patch, HttpMethods.Patch), + KeyValuePair.Create(HttpMethods.Post, HttpMethods.Post), + KeyValuePair.Create(HttpMethods.Put, HttpMethods.Put), + KeyValuePair.Create(HttpMethods.Trace, HttpMethods.Trace) + ], StringComparer.OrdinalIgnoreCase); + + // Boxed port values for HTTP and HTTPS. + private static readonly object HttpPort = 80; + private static readonly object HttpsPort = 443; + + public static bool TryGetServerPort(HostString host, string scheme, [NotNullWhen(true)] out object? port) + { + if (host.Port.HasValue) + { + port = host.Port.Value; + return true; + } + + // If the port is not specified, use the default port for the scheme. + if (string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase)) + { + port = HttpPort; + return true; + } + else if (string.Equals(scheme, "https", StringComparison.OrdinalIgnoreCase)) + { + port = HttpsPort; + return true; + } + + // Unknown scheme, no default port. + port = null; + return false; + } + + public static object GetBoxedStatusCode(int statusCode) + { + object[] boxes = BoxedStatusCodes; + return (uint)statusCode < (uint)boxes.Length + ? boxes[statusCode] ??= statusCode + : statusCode; + } + + public static string GetNormalizedHttpMethod(string method) + { + // TODO: Support configuration for configuring known methods + if (method != null && KnownHttpMethods.TryGetValue(method, out var result)) + { + // KnownHttpMethods ignores case. Use the value returned by the dictionary to have a consistent case. + return result; + } + return OtherHttpMethod; + } + + public static bool TryGetHttpVersion(string protocol, [NotNullWhen(true)] out string? version) + { + if (HttpProtocol.IsHttp11(protocol)) + { + version = "1.1"; + return true; + } + if (HttpProtocol.IsHttp2(protocol)) + { + // HTTP/2 only has one version. + version = "2"; + return true; + } + if (HttpProtocol.IsHttp3(protocol)) + { + // HTTP/3 only has one version. + version = "3"; + return true; + } + if (HttpProtocol.IsHttp10(protocol)) + { + version = "1.0"; + return true; + } + if (HttpProtocol.IsHttp09(protocol)) + { + version = "0.9"; + return true; + } + version = null; + return false; + } + + public static void SetActivityHttpMethodTags(ref TagList tags, string originalHttpMethod) + { + var normalizedHttpMethod = GetNormalizedHttpMethod(originalHttpMethod); + tags.Add(AttributeHttpRequestMethod, normalizedHttpMethod); + + if (originalHttpMethod != normalizedHttpMethod) + { + tags.Add(AttributeHttpRequestMethodOriginal, originalHttpMethod); + } + } +} diff --git a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs index ff6735d1b5e3..676602bf5161 100644 --- a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs +++ b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs @@ -1021,6 +1021,7 @@ public void ActivityListenersAreCalled() var testSource = new ActivitySource(Path.GetRandomFileName()); var hostingApplication = CreateApplication(out var features, activitySource: testSource); var parentSpanId = ""; + var tags = new List>(); using var listener = new ActivityListener { ShouldListenTo = activitySource => ReferenceEquals(activitySource, testSource), @@ -1028,6 +1029,7 @@ public void ActivityListenersAreCalled() ActivityStarted = activity => { parentSpanId = Activity.Current.ParentSpanId.ToHexString(); + tags = Activity.Current.TagObjects.OrderBy(t => t.Key).ToList(); } }; @@ -1039,12 +1041,70 @@ public void ActivityListenersAreCalled() { {"traceparent", "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01"}, {"tracestate", "TraceState1"}, - {"baggage", "Key1=value1, Key2=value2"} - } + {"baggage", "Key1=value1, Key2=value2"}, + {"host", "localhost:8080" } + }, + PathBase = "/path_base", + Path = "/path", + Scheme = "http", + Method = "CUSTOM_METHOD", + Protocol = "HTTP/1.1" }); hostingApplication.CreateContext(features); Assert.Equal("0123456789abcdef", parentSpanId); + + Assert.Collection(tags, + kvp => AssertKeyValuePair(kvp, "http.request.method", "_OTHER"), + kvp => AssertKeyValuePair(kvp, "http.request.method_original", "CUSTOM_METHOD"), + kvp => AssertKeyValuePair(kvp, "server.address", "localhost"), + kvp => AssertKeyValuePair(kvp, "server.port", 8080), + kvp => AssertKeyValuePair(kvp, "url.path", "/path_base/path"), + kvp => AssertKeyValuePair(kvp, "url.scheme", "http")); + + static void AssertKeyValuePair(KeyValuePair pair, string key, T value) + { + Assert.Equal(key, pair.Key); + Assert.Equal(value, pair.Value); + } + } + + [Theory] + [InlineData("http", 80)] + [InlineData("HTTP", 80)] + [InlineData("https", 443)] + [InlineData("HTTPS", 443)] + [InlineData("other", null)] + public void ActivityListeners_DefaultPorts(string scheme, int? expectedPort) + { + var testSource = new ActivitySource(Path.GetRandomFileName()); + var hostingApplication = CreateApplication(out var features, activitySource: testSource); + var tags = new Dictionary(); + using var listener = new ActivityListener + { + ShouldListenTo = activitySource => ReferenceEquals(activitySource, testSource), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activity => + { + tags = Activity.Current.TagObjects.ToDictionary(); + } + }; + + ActivitySource.AddActivityListener(listener); + + features.Set(new HttpRequestFeature() + { + Headers = new HeaderDictionary() + { + {"host", "localhost" } + }, + Scheme = scheme, + }); + + hostingApplication.CreateContext(features); + + Assert.Equal(expectedPort != null, tags.TryGetValue("server.port", out var actualPort)); + Assert.Equal(expectedPort, (int?)actualPort); } [Fact] diff --git a/src/Shared/Diagnostics/ActivityCreator.cs b/src/Shared/Diagnostics/ActivityCreator.cs index 170e9cff267d..9960ff91be84 100644 --- a/src/Shared/Diagnostics/ActivityCreator.cs +++ b/src/Shared/Diagnostics/ActivityCreator.cs @@ -7,6 +7,11 @@ namespace Microsoft.AspNetCore.Shared; internal static class ActivityCreator { + public static bool IsActivityCreated(ActivitySource activitySource, bool diagnosticsOrLoggingEnabled) + { + return activitySource.HasListeners() || diagnosticsOrLoggingEnabled; + } + /// /// Create an activity with details received from a remote source. /// From 4d2936300a6ff0d5ef9b9840d86990330851e055 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 19 Aug 2025 15:27:04 +0800 Subject: [PATCH 11/11] Default to off --- .../src/Internal/HostingApplication.cs | 7 +++ .../Internal/HostingApplicationDiagnostics.cs | 20 +++++-- .../HostingApplicationDiagnosticsTests.cs | 52 ++++++++++++++++++- 3 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/Hosting/Hosting/src/Internal/HostingApplication.cs b/src/Hosting/Hosting/src/Internal/HostingApplication.cs index b610173be531..37996905bdf1 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplication.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplication.cs @@ -17,6 +17,13 @@ internal sealed class HostingApplication : IHttpApplication _diagnostics.SuppressActivityOpenTelemetryData; + set => _diagnostics.SuppressActivityOpenTelemetryData = value; + } + public HostingApplication( RequestDelegate application, ILogger logger, diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index f1d5512b0d72..67f584462e6e 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -33,7 +33,9 @@ internal sealed class HostingApplicationDiagnostics private readonly HostingEventSource _eventSource; private readonly HostingMetrics _metrics; private readonly ILogger _logger; - private readonly bool _suppressActivityOpenTelemetryData; + + // Internal for testing purposes only + internal bool SuppressActivityOpenTelemetryData { get; set; } public HostingApplicationDiagnostics( ILogger logger, @@ -49,7 +51,19 @@ public HostingApplicationDiagnostics( _propagator = propagator; _eventSource = eventSource; _metrics = metrics; - _suppressActivityOpenTelemetryData = AppContext.TryGetSwitch("Microsoft.AspNetCore.Hosting.SuppressActivityOpenTelemetryData", out var enabled) && enabled; + + SuppressActivityOpenTelemetryData = GetSuppressActivityOpenTelemetryData(); + } + + private static bool GetSuppressActivityOpenTelemetryData() + { + // Default to true if the switch isn't set. + if (!AppContext.TryGetSwitch("Microsoft.AspNetCore.Hosting.SuppressActivityOpenTelemetryData", out var enabled)) + { + return true; + } + + return enabled; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -395,7 +409,7 @@ private void RecordRequestStartMetrics(HttpContext httpContext) hasDiagnosticListener = false; - var initializeTags = !_suppressActivityOpenTelemetryData + var initializeTags = !SuppressActivityOpenTelemetryData ? CreateInitializeActivityTags(httpContext) : (TagList?)null; diff --git a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs index 676602bf5161..d9878ad03b3e 100644 --- a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs +++ b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs @@ -1054,6 +1054,48 @@ public void ActivityListenersAreCalled() hostingApplication.CreateContext(features); Assert.Equal("0123456789abcdef", parentSpanId); + Assert.Empty(tags); + } + + [Fact] + public void ActivityListeners_DontSuppressActivityTags_TagsAdded() + { + var testSource = new ActivitySource(Path.GetRandomFileName()); + var hostingApplication = CreateApplication(out var features, activitySource: testSource, suppressActivityOpenTelemetryData: false); + var parentSpanId = ""; + var tags = new List>(); + using var listener = new ActivityListener + { + ShouldListenTo = activitySource => ReferenceEquals(activitySource, testSource), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activity => + { + parentSpanId = Activity.Current.ParentSpanId.ToHexString(); + tags = Activity.Current.TagObjects.OrderBy(t => t.Key).ToList(); + } + }; + + ActivitySource.AddActivityListener(listener); + + features.Set(new HttpRequestFeature() + { + Headers = new HeaderDictionary() + { + {"traceparent", "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01"}, + {"tracestate", "TraceState1"}, + {"baggage", "Key1=value1, Key2=value2"}, + {"host", "localhost:8080" } + }, + PathBase = "/path_base", + Path = "/path", + Scheme = "http", + Method = "CUSTOM_METHOD", + Protocol = "HTTP/1.1" + }); + + hostingApplication.CreateContext(features); + Assert.Equal("0123456789abcdef", parentSpanId); + Assert.Collection(tags, kvp => AssertKeyValuePair(kvp, "http.request.method", "_OTHER"), kvp => AssertKeyValuePair(kvp, "http.request.method_original", "CUSTOM_METHOD"), @@ -1078,7 +1120,7 @@ static void AssertKeyValuePair(KeyValuePair pair, string key, T va public void ActivityListeners_DefaultPorts(string scheme, int? expectedPort) { var testSource = new ActivitySource(Path.GetRandomFileName()); - var hostingApplication = CreateApplication(out var features, activitySource: testSource); + var hostingApplication = CreateApplication(out var features, activitySource: testSource, suppressActivityOpenTelemetryData: false); var tags = new Dictionary(); using var listener = new ActivityListener { @@ -1152,7 +1194,8 @@ private static void AssertProperty(object o, string name) private static HostingApplication CreateApplication(out FeatureCollection features, DiagnosticListener diagnosticListener = null, ActivitySource activitySource = null, ILogger logger = null, - Action configure = null, HostingEventSource eventSource = null, IMeterFactory meterFactory = null) + Action configure = null, HostingEventSource eventSource = null, IMeterFactory meterFactory = null, + bool? suppressActivityOpenTelemetryData = null) { var httpContextFactory = new Mock(); @@ -1174,6 +1217,11 @@ private static HostingApplication CreateApplication(out FeatureCollection featur eventSource ?? HostingEventSource.Log, new HostingMetrics(meterFactory ?? new TestMeterFactory())); + if (suppressActivityOpenTelemetryData is { } suppress) + { + hostingApplication.SuppressActivityOpenTelemetryData = suppress; + } + return hostingApplication; }