Skip to content

Commit 231674f

Browse files
github-actions[bot]rkarg-blizzrkargMsftJamesNK
authored
[release/10.0-rc1] Initialize hosting trace with OTEL tags for sampling (#63338)
* Feature for setting activity tags on creation * adding unshipped public API entries * moving to TagList * Less specific property type also fixed up unshipped API documentation * Update HostingApplicationDiagnostics.cs * unconditionally adding host/port tags to Activity creation This removes the need to expose a new Feature or make any other public changes. * correcting variable naming * reverting HostAndPort property * naming * Initialize hosting trace with OTEL tags for sampling * Default to off --------- Co-authored-by: Ryan Karg <[email protected]> Co-authored-by: rkargMsft <[email protected]> Co-authored-by: James Newton-King <[email protected]>
1 parent d4dfec9 commit 231674f

File tree

6 files changed

+327
-79
lines changed

6 files changed

+327
-79
lines changed

src/Hosting/Hosting/src/Internal/HostingApplication.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ internal sealed class HostingApplication : IHttpApplication<HostingApplication.C
1717
private readonly DefaultHttpContextFactory? _defaultHttpContextFactory;
1818
private readonly HostingApplicationDiagnostics _diagnostics;
1919

20+
// Internal for testing purposes only
21+
internal bool SuppressActivityOpenTelemetryData
22+
{
23+
get => _diagnostics.SuppressActivityOpenTelemetryData;
24+
set => _diagnostics.SuppressActivityOpenTelemetryData = value;
25+
}
26+
2027
public HostingApplication(
2128
RequestDelegate application,
2229
ILogger logger,

src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ internal sealed class HostingApplicationDiagnostics
3434
private readonly HostingMetrics _metrics;
3535
private readonly ILogger _logger;
3636

37+
// Internal for testing purposes only
38+
internal bool SuppressActivityOpenTelemetryData { get; set; }
39+
3740
public HostingApplicationDiagnostics(
3841
ILogger logger,
3942
DiagnosticListener diagnosticListener,
@@ -48,6 +51,19 @@ public HostingApplicationDiagnostics(
4851
_propagator = propagator;
4952
_eventSource = eventSource;
5053
_metrics = metrics;
54+
55+
SuppressActivityOpenTelemetryData = GetSuppressActivityOpenTelemetryData();
56+
}
57+
58+
private static bool GetSuppressActivityOpenTelemetryData()
59+
{
60+
// Default to true if the switch isn't set.
61+
if (!AppContext.TryGetSwitch("Microsoft.AspNetCore.Hosting.SuppressActivityOpenTelemetryData", out var enabled))
62+
{
63+
return true;
64+
}
65+
66+
return enabled;
5167
}
5268

5369
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -88,9 +104,9 @@ public void BeginRequest(HttpContext httpContext, HostingApplication.Context con
88104
var diagnosticListenerActivityCreationEnabled = (diagnosticListenerEnabled && _diagnosticListener.IsEnabled(ActivityName, httpContext));
89105
var loggingEnabled = _logger.IsEnabled(LogLevel.Critical);
90106

91-
if (loggingEnabled || diagnosticListenerActivityCreationEnabled || _activitySource.HasListeners())
107+
if (ActivityCreator.IsActivityCreated(_activitySource, loggingEnabled || diagnosticListenerActivityCreationEnabled))
92108
{
93-
context.Activity = StartActivity(httpContext, loggingEnabled, diagnosticListenerActivityCreationEnabled, out var hasDiagnosticListener);
109+
context.Activity = StartActivity(httpContext, loggingEnabled || diagnosticListenerActivityCreationEnabled, out var hasDiagnosticListener);
94110
context.HasDiagnosticListener = hasDiagnosticListener;
95111

96112
if (context.Activity != null)
@@ -385,10 +401,18 @@ private void RecordRequestStartMetrics(HttpContext httpContext)
385401
}
386402

387403
[MethodImpl(MethodImplOptions.NoInlining)]
388-
private Activity? StartActivity(HttpContext httpContext, bool loggingEnabled, bool diagnosticListenerActivityCreationEnabled, out bool hasDiagnosticListener)
404+
private Activity? StartActivity(HttpContext httpContext, bool diagnosticsOrLoggingEnabled, out bool hasDiagnosticListener)
389405
{
406+
// StartActivity is only called if an Activity is already verified to be created.
407+
Debug.Assert(ActivityCreator.IsActivityCreated(_activitySource, diagnosticsOrLoggingEnabled),
408+
"Activity should only be created if diagnostics or logging is enabled.");
409+
390410
hasDiagnosticListener = false;
391411

412+
var initializeTags = !SuppressActivityOpenTelemetryData
413+
? CreateInitializeActivityTags(httpContext)
414+
: (TagList?)null;
415+
392416
var headers = httpContext.Request.Headers;
393417
var activity = ActivityCreator.CreateFromRemote(
394418
_activitySource,
@@ -402,9 +426,9 @@ private void RecordRequestStartMetrics(HttpContext httpContext)
402426
},
403427
ActivityName,
404428
ActivityKind.Server,
405-
tags: null,
429+
tags: initializeTags,
406430
links: null,
407-
loggingEnabled || diagnosticListenerActivityCreationEnabled);
431+
diagnosticsOrLoggingEnabled);
408432
if (activity is null)
409433
{
410434
return null;
@@ -425,6 +449,47 @@ private void RecordRequestStartMetrics(HttpContext httpContext)
425449
return activity;
426450
}
427451

452+
private static TagList CreateInitializeActivityTags(HttpContext httpContext)
453+
{
454+
// The tags here are set when the activity is created. They can be used in sampling decisions.
455+
// Most values in semantic conventions that are present at creation are specified:
456+
// https://github.com/open-telemetry/semantic-conventions/blob/27735ccca3746d7bb7fa061dfb73d93bcbae2b6e/docs/http/http-spans.md#L581-L592
457+
// Missing values recommended by the spec are:
458+
// - url.query (need configuration around redaction to do properly)
459+
// - http.request.header.<key>
460+
461+
var request = httpContext.Request;
462+
var creationTags = new TagList();
463+
464+
if (request.Host.HasValue)
465+
{
466+
creationTags.Add(HostingTelemetryHelpers.AttributeServerAddress, request.Host.Host);
467+
468+
if (HostingTelemetryHelpers.TryGetServerPort(request.Host, request.Scheme, out var port))
469+
{
470+
creationTags.Add(HostingTelemetryHelpers.AttributeServerPort, port);
471+
}
472+
}
473+
474+
HostingTelemetryHelpers.SetActivityHttpMethodTags(ref creationTags, request.Method);
475+
476+
if (request.Headers.TryGetValue("User-Agent", out var values))
477+
{
478+
var userAgent = values.Count > 0 ? values[0] : null;
479+
if (!string.IsNullOrEmpty(userAgent))
480+
{
481+
creationTags.Add(HostingTelemetryHelpers.AttributeUserAgentOriginal, userAgent);
482+
}
483+
}
484+
485+
creationTags.Add(HostingTelemetryHelpers.AttributeUrlScheme, request.Scheme);
486+
487+
var path = (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/";
488+
creationTags.Add(HostingTelemetryHelpers.AttributeUrlPath, path);
489+
490+
return creationTags;
491+
}
492+
428493
[MethodImpl(MethodImplOptions.NoInlining)]
429494
private void StopActivity(HttpContext httpContext, Activity activity, bool hasDiagnosticListener)
430495
{

src/Hosting/Hosting/src/Internal/HostingMetrics.cs

Lines changed: 3 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Collections.Frozen;
54
using System.Diagnostics;
6-
using System.Diagnostics.CodeAnalysis;
75
using System.Diagnostics.Metrics;
86
using Microsoft.AspNetCore.Http;
97

@@ -55,7 +53,7 @@ public void RequestEnd(string protocol, string scheme, string method, string? ro
5553

5654
if (!disableHttpRequestDurationMetric && _requestDuration.Enabled)
5755
{
58-
if (TryGetHttpVersion(protocol, out var httpVersion))
56+
if (HostingTelemetryHelpers.TryGetHttpVersion(protocol, out var httpVersion))
5957
{
6058
tags.Add("network.protocol.version", httpVersion);
6159
}
@@ -65,7 +63,7 @@ public void RequestEnd(string protocol, string scheme, string method, string? ro
6563
}
6664

6765
// Add information gathered during request.
68-
tags.Add("http.response.status_code", GetBoxedStatusCode(statusCode));
66+
tags.Add("http.response.status_code", HostingTelemetryHelpers.GetBoxedStatusCode(statusCode));
6967
if (route != null)
7068
{
7169
tags.Add("http.route", route);
@@ -104,73 +102,6 @@ public void Dispose()
104102
private static void InitializeRequestTags(ref TagList tags, string scheme, string method)
105103
{
106104
tags.Add("url.scheme", scheme);
107-
tags.Add("http.request.method", ResolveHttpMethod(method));
108-
}
109-
110-
private static readonly object[] BoxedStatusCodes = new object[512];
111-
112-
private static object GetBoxedStatusCode(int statusCode)
113-
{
114-
object[] boxes = BoxedStatusCodes;
115-
return (uint)statusCode < (uint)boxes.Length
116-
? boxes[statusCode] ??= statusCode
117-
: statusCode;
118-
}
119-
120-
private static readonly FrozenDictionary<string, string> KnownMethods = FrozenDictionary.ToFrozenDictionary(new[]
121-
{
122-
KeyValuePair.Create(HttpMethods.Connect, HttpMethods.Connect),
123-
KeyValuePair.Create(HttpMethods.Delete, HttpMethods.Delete),
124-
KeyValuePair.Create(HttpMethods.Get, HttpMethods.Get),
125-
KeyValuePair.Create(HttpMethods.Head, HttpMethods.Head),
126-
KeyValuePair.Create(HttpMethods.Options, HttpMethods.Options),
127-
KeyValuePair.Create(HttpMethods.Patch, HttpMethods.Patch),
128-
KeyValuePair.Create(HttpMethods.Post, HttpMethods.Post),
129-
KeyValuePair.Create(HttpMethods.Put, HttpMethods.Put),
130-
KeyValuePair.Create(HttpMethods.Trace, HttpMethods.Trace)
131-
}, StringComparer.OrdinalIgnoreCase);
132-
133-
private static string ResolveHttpMethod(string method)
134-
{
135-
// TODO: Support configuration for configuring known methods
136-
if (KnownMethods.TryGetValue(method, out var result))
137-
{
138-
// KnownMethods ignores case. Use the value returned by the dictionary to have a consistent case.
139-
return result;
140-
}
141-
return "_OTHER";
142-
}
143-
144-
private static bool TryGetHttpVersion(string protocol, [NotNullWhen(true)] out string? version)
145-
{
146-
if (HttpProtocol.IsHttp11(protocol))
147-
{
148-
version = "1.1";
149-
return true;
150-
}
151-
if (HttpProtocol.IsHttp2(protocol))
152-
{
153-
// HTTP/2 only has one version.
154-
version = "2";
155-
return true;
156-
}
157-
if (HttpProtocol.IsHttp3(protocol))
158-
{
159-
// HTTP/3 only has one version.
160-
version = "3";
161-
return true;
162-
}
163-
if (HttpProtocol.IsHttp10(protocol))
164-
{
165-
version = "1.0";
166-
return true;
167-
}
168-
if (HttpProtocol.IsHttp09(protocol))
169-
{
170-
version = "0.9";
171-
return true;
172-
}
173-
version = null;
174-
return false;
105+
tags.Add("http.request.method", HostingTelemetryHelpers.GetNormalizedHttpMethod(method));
175106
}
176107
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Frozen;
5+
using System.Diagnostics;
6+
using System.Diagnostics.CodeAnalysis;
7+
using Microsoft.AspNetCore.Http;
8+
9+
namespace Microsoft.AspNetCore.Hosting;
10+
11+
internal static class HostingTelemetryHelpers
12+
{
13+
// Semantic Conventions for HTTP.
14+
// Note: Not all telemetry code is using these const attribute names yet.
15+
public const string AttributeHttpRequestMethod = "http.request.method";
16+
public const string AttributeHttpRequestMethodOriginal = "http.request.method_original";
17+
public const string AttributeUrlScheme = "url.scheme";
18+
public const string AttributeUrlPath = "url.path";
19+
public const string AttributeServerAddress = "server.address";
20+
public const string AttributeServerPort = "server.port";
21+
public const string AttributeUserAgentOriginal = "user_agent.original";
22+
23+
// The value "_OTHER" is used for non-standard HTTP methods.
24+
// https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#common-attributes
25+
private const string OtherHttpMethod = "_OTHER";
26+
27+
private static readonly object[] BoxedStatusCodes = new object[512];
28+
29+
private static readonly FrozenDictionary<string, string> KnownHttpMethods = FrozenDictionary.ToFrozenDictionary([
30+
KeyValuePair.Create(HttpMethods.Connect, HttpMethods.Connect),
31+
KeyValuePair.Create(HttpMethods.Delete, HttpMethods.Delete),
32+
KeyValuePair.Create(HttpMethods.Get, HttpMethods.Get),
33+
KeyValuePair.Create(HttpMethods.Head, HttpMethods.Head),
34+
KeyValuePair.Create(HttpMethods.Options, HttpMethods.Options),
35+
KeyValuePair.Create(HttpMethods.Patch, HttpMethods.Patch),
36+
KeyValuePair.Create(HttpMethods.Post, HttpMethods.Post),
37+
KeyValuePair.Create(HttpMethods.Put, HttpMethods.Put),
38+
KeyValuePair.Create(HttpMethods.Trace, HttpMethods.Trace)
39+
], StringComparer.OrdinalIgnoreCase);
40+
41+
// Boxed port values for HTTP and HTTPS.
42+
private static readonly object HttpPort = 80;
43+
private static readonly object HttpsPort = 443;
44+
45+
public static bool TryGetServerPort(HostString host, string scheme, [NotNullWhen(true)] out object? port)
46+
{
47+
if (host.Port.HasValue)
48+
{
49+
port = host.Port.Value;
50+
return true;
51+
}
52+
53+
// If the port is not specified, use the default port for the scheme.
54+
if (string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase))
55+
{
56+
port = HttpPort;
57+
return true;
58+
}
59+
else if (string.Equals(scheme, "https", StringComparison.OrdinalIgnoreCase))
60+
{
61+
port = HttpsPort;
62+
return true;
63+
}
64+
65+
// Unknown scheme, no default port.
66+
port = null;
67+
return false;
68+
}
69+
70+
public static object GetBoxedStatusCode(int statusCode)
71+
{
72+
object[] boxes = BoxedStatusCodes;
73+
return (uint)statusCode < (uint)boxes.Length
74+
? boxes[statusCode] ??= statusCode
75+
: statusCode;
76+
}
77+
78+
public static string GetNormalizedHttpMethod(string method)
79+
{
80+
// TODO: Support configuration for configuring known methods
81+
if (method != null && KnownHttpMethods.TryGetValue(method, out var result))
82+
{
83+
// KnownHttpMethods ignores case. Use the value returned by the dictionary to have a consistent case.
84+
return result;
85+
}
86+
return OtherHttpMethod;
87+
}
88+
89+
public static bool TryGetHttpVersion(string protocol, [NotNullWhen(true)] out string? version)
90+
{
91+
if (HttpProtocol.IsHttp11(protocol))
92+
{
93+
version = "1.1";
94+
return true;
95+
}
96+
if (HttpProtocol.IsHttp2(protocol))
97+
{
98+
// HTTP/2 only has one version.
99+
version = "2";
100+
return true;
101+
}
102+
if (HttpProtocol.IsHttp3(protocol))
103+
{
104+
// HTTP/3 only has one version.
105+
version = "3";
106+
return true;
107+
}
108+
if (HttpProtocol.IsHttp10(protocol))
109+
{
110+
version = "1.0";
111+
return true;
112+
}
113+
if (HttpProtocol.IsHttp09(protocol))
114+
{
115+
version = "0.9";
116+
return true;
117+
}
118+
version = null;
119+
return false;
120+
}
121+
122+
public static void SetActivityHttpMethodTags(ref TagList tags, string originalHttpMethod)
123+
{
124+
var normalizedHttpMethod = GetNormalizedHttpMethod(originalHttpMethod);
125+
tags.Add(AttributeHttpRequestMethod, normalizedHttpMethod);
126+
127+
if (originalHttpMethod != normalizedHttpMethod)
128+
{
129+
tags.Add(AttributeHttpRequestMethodOriginal, originalHttpMethod);
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)