Skip to content

Initialize hosting trace with OTEL tags for sampling #62090

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 19, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)]
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -385,10 +387,18 @@ 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 initializeTags = !_suppressActivityOpenTelemetryData
? CreateInitializeActivityTags(httpContext)
: (TagList?)null;

var headers = httpContext.Request.Headers;
var activity = ActivityCreator.CreateFromRemote(
_activitySource,
Expand All @@ -402,9 +412,9 @@ private void RecordRequestStartMetrics(HttpContext httpContext)
},
ActivityName,
ActivityKind.Server,
tags: null,
tags: initializeTags,
links: null,
loggingEnabled || diagnosticListenerActivityCreationEnabled);
diagnosticsOrLoggingEnabled);
if (activity is null)
{
return null;
Expand All @@ -425,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.<key>

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)
{
Expand Down
75 changes: 3 additions & 72 deletions src/Hosting/Hosting/src/Internal/HostingMetrics.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
Expand Down Expand Up @@ -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<string, string> 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));
}
}
132 changes: 132 additions & 0 deletions src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs
Original file line number Diff line number Diff line change
@@ -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<string, string> 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);
Comment on lines +29 to +39
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add QUERY given #63260 went in yesterday?

Copy link
Member

@JamesNK JamesNK Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This list is based on the known values list at https://opentelemetry.io/docs/specs/semconv/registry/attributes/http

image

You're welcome to create an issue with OTEL folks to add QUERY to the standard. Then we'll add it here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I just took at quick look at the table at the top and because it didn't have everything, just assumed that it was more open:

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// 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);
}
}
}
Loading
Loading