Skip to content
7 changes: 7 additions & 0 deletions src/Hosting/Hosting/src/Internal/HostingApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ internal sealed class HostingApplication : IHttpApplication<HostingApplication.C
private readonly DefaultHttpContextFactory? _defaultHttpContextFactory;
private readonly HostingApplicationDiagnostics _diagnostics;

// Internal for testing purposes only
internal bool SuppressActivityOpenTelemetryData
{
get => _diagnostics.SuppressActivityOpenTelemetryData;
set => _diagnostics.SuppressActivityOpenTelemetryData = value;
}

public HostingApplication(
RequestDelegate application,
ILogger logger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ internal sealed class HostingApplicationDiagnostics
private readonly HostingMetrics _metrics;
private readonly ILogger _logger;

// Internal for testing purposes only
internal bool SuppressActivityOpenTelemetryData { get; set; }

public HostingApplicationDiagnostics(
ILogger logger,
DiagnosticListener diagnosticListener,
Expand All @@ -48,6 +51,19 @@ public HostingApplicationDiagnostics(
_propagator = propagator;
_eventSource = eventSource;
_metrics = metrics;

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)]
Expand Down Expand Up @@ -88,9 +104,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 +401,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 +426,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 +449,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.

Choose a reason for hiding this comment

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

I support updating semconv, but it's also totally fine to add arbitrary known methods. The goal is to protect metrics from cardinality explosion, nothing else.

https://github.com/open-telemetry/semantic-conventions/blob/68a52f658391be7beb3fea2b9379a080b391fac0/docs/http/http-spans.md?plain=1#L173C1-L175C88

[1] http.request.method: HTTP request method value SHOULD be "known" to the instrumentation.
By default, this convention defines "known" methods as the ones listed in RFC9110
and the PATCH method defined in RFC5789.


// 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