Skip to content

Commit c7fa446

Browse files
authored
Add standard tags to HttpClient native trace instrumentation (dotnet#104251)
Add the following standard tags to the HTTP Request Activities started in DelegatingHandler: http.request.method http.request.method_original server.address server.port url.full error.type http.response.status_code network.protocol.version Just like in dotnet#103769, url.full is being redacted by removing UserInfo and the query string, while exposing a System.Net.Http.DisableQueryRedaction switch for opting-out from the latter.
1 parent e733c2f commit c7fa446

File tree

10 files changed

+535
-125
lines changed

10 files changed

+535
-125
lines changed

src/libraries/System.Net.Http/src/System.Net.Http.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<Compile Include="System\Net\Http\DelegatingHandler.cs" />
3939
<Compile Include="System\Net\Http\DiagnosticsHandler.cs" />
4040
<Compile Include="System\Net\Http\DiagnosticsHandlerLoggingStrings.cs" />
41+
<Compile Include="System\Net\Http\DiagnosticsHelper.cs" />
4142
<Compile Include="System\Net\Http\EmptyContent.cs" />
4243
<Compile Include="System\Net\Http\EmptyReadStream.cs" />
4344
<Compile Include="System\Net\Http\FormUrlEncodedContent.cs" />

src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,19 @@ private static bool IsEnabled()
5353
s_diagnosticListener.IsEnabled();
5454
}
5555

56-
private static Activity? CreateActivity(HttpRequestMessage requestMessage)
56+
private static Activity? StartActivity(HttpRequestMessage request)
5757
{
5858
Activity? activity = null;
5959
if (s_activitySource.HasListeners())
6060
{
61-
activity = s_activitySource.CreateActivity(DiagnosticsHandlerLoggingStrings.ActivityName, ActivityKind.Client);
61+
activity = s_activitySource.StartActivity(DiagnosticsHandlerLoggingStrings.ActivityName, ActivityKind.Client);
6262
}
6363

64-
if (activity is null)
64+
if (activity is null &&
65+
(Activity.Current is not null ||
66+
s_diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.ActivityName, request)))
6567
{
66-
if (Activity.Current is not null || s_diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.ActivityName, requestMessage))
67-
{
68-
activity = new Activity(DiagnosticsHandlerLoggingStrings.ActivityName);
69-
}
68+
activity = new Activity(DiagnosticsHandlerLoggingStrings.ActivityName).Start();
7069
}
7170

7271
return activity;
@@ -109,12 +108,30 @@ private async ValueTask<HttpResponseMessage> SendAsyncCore(HttpRequestMessage re
109108
DiagnosticListener diagnosticListener = s_diagnosticListener;
110109

111110
Guid loggingRequestId = Guid.Empty;
112-
Activity? activity = CreateActivity(request);
111+
Activity? activity = StartActivity(request);
113112

114-
// Start activity anyway if it was created.
115113
if (activity is not null)
116114
{
117-
activity.Start();
115+
// https://github.com/open-telemetry/semantic-conventions/blob/release/v1.23.x/docs/http/http-spans.md#name
116+
activity.DisplayName = HttpMethod.GetKnownMethod(request.Method.Method)?.Method ?? "HTTP";
117+
118+
if (activity.IsAllDataRequested)
119+
{
120+
// Add standard tags known before sending the request.
121+
KeyValuePair<string, object?> methodTag = DiagnosticsHelper.GetMethodTag(request.Method, out bool isUnknownMethod);
122+
activity.SetTag(methodTag.Key, methodTag.Value);
123+
if (isUnknownMethod)
124+
{
125+
activity.SetTag("http.request.method_original", request.Method.Method);
126+
}
127+
128+
if (request.RequestUri is Uri requestUri && requestUri.IsAbsoluteUri)
129+
{
130+
activity.SetTag("server.address", requestUri.Host);
131+
activity.SetTag("server.port", requestUri.Port);
132+
activity.SetTag("url.full", DiagnosticsHelper.GetRedactedUriString(requestUri));
133+
}
134+
}
118135

119136
// Only send start event to users who subscribed for it.
120137
if (diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.ActivityStartName))
@@ -141,6 +158,7 @@ private async ValueTask<HttpResponseMessage> SendAsyncCore(HttpRequestMessage re
141158
}
142159

143160
HttpResponseMessage? response = null;
161+
Exception? exception = null;
144162
TaskStatus taskStatus = TaskStatus.RanToCompletion;
145163
try
146164
{
@@ -159,6 +177,7 @@ await _innerHandler.SendAsync(request, cancellationToken).ConfigureAwait(false)
159177
catch (Exception ex)
160178
{
161179
taskStatus = TaskStatus.Faulted;
180+
exception = ex;
162181

163182
if (diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.ExceptionEventName))
164183
{
@@ -176,6 +195,25 @@ await _innerHandler.SendAsync(request, cancellationToken).ConfigureAwait(false)
176195
{
177196
activity.SetEndTime(DateTime.UtcNow);
178197

198+
if (activity.IsAllDataRequested)
199+
{
200+
// Add standard tags known at request completion.
201+
if (response is not null)
202+
{
203+
activity.SetTag("http.response.status_code", DiagnosticsHelper.GetBoxedStatusCode((int)response.StatusCode));
204+
activity.SetTag("network.protocol.version", DiagnosticsHelper.GetProtocolVersionString(response.Version));
205+
}
206+
207+
if (DiagnosticsHelper.TryGetErrorType(response, exception, out string? errorType))
208+
{
209+
activity.SetTag("error.type", errorType);
210+
211+
// The presence of error.type indicates that the conditions for setting Error status are also met.
212+
// https://github.com/open-telemetry/semantic-conventions/blob/v1.26.0/docs/http/http-spans.md#status
213+
activity.SetStatus(ActivityStatusCode.Error);
214+
}
215+
}
216+
179217
// Only send stop event to users who subscribed for it.
180218
if (diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.ActivityStopName))
181219
{
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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.Generic;
5+
using System.Diagnostics;
6+
using System.Threading;
7+
8+
namespace System.Net.Http
9+
{
10+
internal static class DiagnosticsHelper
11+
{
12+
internal static string GetRedactedUriString(Uri uri)
13+
{
14+
Debug.Assert(uri.IsAbsoluteUri);
15+
16+
if (GlobalHttpSettings.DiagnosticsHandler.DisableUriRedaction)
17+
{
18+
return uri.AbsoluteUri;
19+
}
20+
21+
string pathAndQuery = uri.PathAndQuery;
22+
int queryIndex = pathAndQuery.IndexOf('?');
23+
24+
bool redactQuery = queryIndex >= 0 && // Query is present.
25+
queryIndex < pathAndQuery.Length - 1; // Query is not empty.
26+
27+
return (redactQuery, uri.IsDefaultPort) switch
28+
{
29+
(true, true) => $"{uri.Scheme}://{uri.Host}{pathAndQuery.AsSpan(0, queryIndex + 1)}*",
30+
(true, false) => $"{uri.Scheme}://{uri.Host}:{uri.Port}{pathAndQuery.AsSpan(0, queryIndex + 1)}*",
31+
(false, true) => $"{uri.Scheme}://{uri.Host}{pathAndQuery}",
32+
(false, false) => $"{uri.Scheme}://{uri.Host}:{uri.Port}{pathAndQuery}"
33+
};
34+
}
35+
36+
internal static KeyValuePair<string, object?> GetMethodTag(HttpMethod method, out bool isUnknownMethod)
37+
{
38+
// Return canonical names for known methods and "_OTHER" for unknown ones.
39+
HttpMethod? known = HttpMethod.GetKnownMethod(method.Method);
40+
isUnknownMethod = known is null;
41+
return new KeyValuePair<string, object?>("http.request.method", isUnknownMethod ? "_OTHER" : known!.Method);
42+
}
43+
44+
internal static string GetProtocolVersionString(Version httpVersion) => (httpVersion.Major, httpVersion.Minor) switch
45+
{
46+
(1, 0) => "1.0",
47+
(1, 1) => "1.1",
48+
(2, 0) => "2",
49+
(3, 0) => "3",
50+
_ => httpVersion.ToString()
51+
};
52+
53+
public static bool TryGetErrorType(HttpResponseMessage? response, Exception? exception, out string? errorType)
54+
{
55+
if (response is not null)
56+
{
57+
int statusCode = (int)response.StatusCode;
58+
59+
// In case the status code indicates a client or a server error, return the string representation of the status code.
60+
// See the paragraph Status and the definition of 'error.type' in
61+
// https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-spans.md
62+
if (statusCode >= 400 && statusCode <= 599)
63+
{
64+
errorType = GetErrorStatusCodeString(statusCode);
65+
return true;
66+
}
67+
}
68+
69+
if (exception is null)
70+
{
71+
errorType = null;
72+
return false;
73+
}
74+
75+
Debug.Assert(Enum.GetValues<HttpRequestError>().Length == 12, "We need to extend the mapping in case new values are added to HttpRequestError.");
76+
errorType = (exception as HttpRequestException)?.HttpRequestError switch
77+
{
78+
HttpRequestError.NameResolutionError => "name_resolution_error",
79+
HttpRequestError.ConnectionError => "connection_error",
80+
HttpRequestError.SecureConnectionError => "secure_connection_error",
81+
HttpRequestError.HttpProtocolError => "http_protocol_error",
82+
HttpRequestError.ExtendedConnectNotSupported => "extended_connect_not_supported",
83+
HttpRequestError.VersionNegotiationError => "version_negotiation_error",
84+
HttpRequestError.UserAuthenticationError => "user_authentication_error",
85+
HttpRequestError.ProxyTunnelError => "proxy_tunnel_error",
86+
HttpRequestError.InvalidResponse => "invalid_response",
87+
HttpRequestError.ResponseEnded => "response_ended",
88+
HttpRequestError.ConfigurationLimitExceeded => "configuration_limit_exceeded",
89+
90+
// Fall back to the exception type name in case of HttpRequestError.Unknown or when exception is not an HttpRequestException.
91+
_ => exception.GetType().FullName!
92+
};
93+
return true;
94+
}
95+
96+
private static object[]? s_boxedStatusCodes;
97+
private static string[]? s_statusCodeStrings;
98+
99+
#pragma warning disable CA1859 // we explictly box here
100+
public static object GetBoxedStatusCode(int statusCode)
101+
{
102+
object[] boxes = LazyInitializer.EnsureInitialized(ref s_boxedStatusCodes, static () => new object[512]);
103+
104+
return (uint)statusCode < (uint)boxes.Length
105+
? boxes[statusCode] ??= statusCode
106+
: statusCode;
107+
}
108+
#pragma warning restore
109+
110+
private static string GetErrorStatusCodeString(int statusCode)
111+
{
112+
Debug.Assert(statusCode >= 400 && statusCode <= 599);
113+
114+
string[] strings = LazyInitializer.EnsureInitialized(ref s_statusCodeStrings, static () => new string[200]);
115+
int index = statusCode - 400;
116+
return (uint)index < (uint)strings.Length
117+
? strings[index] ??= statusCode.ToString()
118+
: statusCode.ToString();
119+
}
120+
}
121+
}

src/libraries/System.Net.Http/src/System/Net/Http/GlobalHttpSettings.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ internal static class DiagnosticsHandler
1414
"System.Net.Http.EnableActivityPropagation",
1515
"DOTNET_SYSTEM_NET_HTTP_ENABLEACTIVITYPROPAGATION",
1616
true);
17+
18+
public static bool DisableUriRedaction { get; } = RuntimeSettingParser.QueryRuntimeSettingSwitch(
19+
"System.Net.Http.DisableUriRedaction",
20+
"DOTNET_SYSTEM_NET_HTTP_DISABLEURIREDACTION",
21+
false);
1722
}
1823

1924
internal static class SocketsHttpHandler

src/libraries/System.Net.Http/src/System/Net/Http/Metrics/MetricsHandler.cs

Lines changed: 4 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,11 @@ private void RequestStop(HttpRequestMessage request, HttpResponseMessage? respon
109109

110110
if (response is not null)
111111
{
112-
tags.Add("http.response.status_code", GetBoxedStatusCode((int)response.StatusCode));
113-
tags.Add("network.protocol.version", GetProtocolVersionString(response.Version));
112+
tags.Add("http.response.status_code", DiagnosticsHelper.GetBoxedStatusCode((int)response.StatusCode));
113+
tags.Add("network.protocol.version", DiagnosticsHelper.GetProtocolVersionString(response.Version));
114114
}
115115

116-
if (TryGetErrorType(response, exception, out string? errorType))
116+
if (DiagnosticsHelper.TryGetErrorType(response, exception, out string? errorType))
117117
{
118118
tags.Add("error.type", errorType);
119119
}
@@ -131,58 +131,6 @@ private void RequestStop(HttpRequestMessage request, HttpResponseMessage? respon
131131
}
132132
}
133133

134-
private static bool TryGetErrorType(HttpResponseMessage? response, Exception? exception, out string? errorType)
135-
{
136-
if (response is not null)
137-
{
138-
int statusCode = (int)response.StatusCode;
139-
140-
// In case the status code indicates a client or a server error, return the string representation of the status code.
141-
// See the paragraph Status and the definition of 'error.type' in
142-
// https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-spans.md
143-
if (statusCode >= 400 && statusCode <= 599)
144-
{
145-
errorType = GetErrorStatusCodeString(statusCode);
146-
return true;
147-
}
148-
}
149-
150-
if (exception is null)
151-
{
152-
errorType = null;
153-
return false;
154-
}
155-
156-
Debug.Assert(Enum.GetValues<HttpRequestError>().Length == 12, "We need to extend the mapping in case new values are added to HttpRequestError.");
157-
errorType = (exception as HttpRequestException)?.HttpRequestError switch
158-
{
159-
HttpRequestError.NameResolutionError => "name_resolution_error",
160-
HttpRequestError.ConnectionError => "connection_error",
161-
HttpRequestError.SecureConnectionError => "secure_connection_error",
162-
HttpRequestError.HttpProtocolError => "http_protocol_error",
163-
HttpRequestError.ExtendedConnectNotSupported => "extended_connect_not_supported",
164-
HttpRequestError.VersionNegotiationError => "version_negotiation_error",
165-
HttpRequestError.UserAuthenticationError => "user_authentication_error",
166-
HttpRequestError.ProxyTunnelError => "proxy_tunnel_error",
167-
HttpRequestError.InvalidResponse => "invalid_response",
168-
HttpRequestError.ResponseEnded => "response_ended",
169-
HttpRequestError.ConfigurationLimitExceeded => "configuration_limit_exceeded",
170-
171-
// Fall back to the exception type name in case of HttpRequestError.Unknown or when exception is not an HttpRequestException.
172-
_ => exception.GetType().FullName!
173-
};
174-
return true;
175-
}
176-
177-
private static string GetProtocolVersionString(Version httpVersion) => (httpVersion.Major, httpVersion.Minor) switch
178-
{
179-
(1, 0) => "1.0",
180-
(1, 1) => "1.1",
181-
(2, 0) => "2",
182-
(3, 0) => "3",
183-
_ => httpVersion.ToString()
184-
};
185-
186134
private static TagList InitializeCommonTags(HttpRequestMessage request)
187135
{
188136
TagList tags = default;
@@ -197,43 +145,11 @@ private static TagList InitializeCommonTags(HttpRequestMessage request)
197145
tags.Add("server.port", requestUri.Port);
198146
}
199147
}
200-
tags.Add(GetMethodTag(request.Method));
148+
tags.Add(DiagnosticsHelper.GetMethodTag(request.Method, out _));
201149

202150
return tags;
203151
}
204152

205-
internal static KeyValuePair<string, object?> GetMethodTag(HttpMethod method)
206-
{
207-
// Return canonical names for known methods and "_OTHER" for unknown ones.
208-
HttpMethod? known = HttpMethod.GetKnownMethod(method.Method);
209-
return new KeyValuePair<string, object?>("http.request.method", known?.Method ?? "_OTHER");
210-
}
211-
212-
private static object[]? s_boxedStatusCodes;
213-
private static string[]? s_statusCodeStrings;
214-
215-
#pragma warning disable CA1859 // we explictly box here
216-
private static object GetBoxedStatusCode(int statusCode)
217-
{
218-
object[] boxes = LazyInitializer.EnsureInitialized(ref s_boxedStatusCodes, static () => new object[512]);
219-
220-
return (uint)statusCode < (uint)boxes.Length
221-
? boxes[statusCode] ??= statusCode
222-
: statusCode;
223-
}
224-
#pragma warning restore
225-
226-
private static string GetErrorStatusCodeString(int statusCode)
227-
{
228-
Debug.Assert(statusCode >= 400 && statusCode <= 599);
229-
230-
string[] strings = LazyInitializer.EnsureInitialized(ref s_statusCodeStrings, static () => new string[200]);
231-
int index = statusCode - 400;
232-
return (uint)index < (uint)strings.Length
233-
? strings[index] ??= statusCode.ToString()
234-
: statusCode.ToString();
235-
}
236-
237153
private sealed class SharedMeter : Meter
238154
{
239155
public static Meter Instance { get; } = new SharedMeter();

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/SocketsHttpHandlerMetrics.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public void RequestLeftQueue(HttpRequestMessage request, HttpConnectionPool pool
4747
tags.Add("server.port", pool.OriginAuthority.Port);
4848
}
4949

50-
tags.Add(MetricsHandler.GetMethodTag(request.Method));
50+
tags.Add(DiagnosticsHelper.GetMethodTag(request.Method, out _));
5151

5252
RequestsQueueDuration.Record(duration.TotalSeconds, tags);
5353
}

0 commit comments

Comments
 (0)