-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Consume DistributedContextPropagator APIs in DiagnosticsHandler #55392
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,36 +13,59 @@ namespace System.Net.Http | |
/// <summary> | ||
/// DiagnosticHandler notifies DiagnosticSource subscribers about outgoing Http requests | ||
/// </summary> | ||
internal sealed class DiagnosticsHandler : DelegatingHandler | ||
internal sealed class DiagnosticsHandler : HttpMessageHandlerStage | ||
{ | ||
private static readonly DiagnosticListener s_diagnosticListener = | ||
new DiagnosticListener(DiagnosticsHandlerLoggingStrings.DiagnosticListenerName); | ||
|
||
/// <summary> | ||
/// DiagnosticHandler constructor | ||
/// </summary> | ||
/// <param name="innerHandler">Inner handler: Windows or Unix implementation of HttpMessageHandler. | ||
/// Note that DiagnosticHandler is the latest in the pipeline </param> | ||
public DiagnosticsHandler(HttpMessageHandler innerHandler) : base(innerHandler) | ||
private readonly HttpMessageHandler _innerHandler; | ||
private readonly DistributedContextPropagator _propagator; | ||
private readonly HeaderDescriptor[]? _propagatorFields; | ||
|
||
public DiagnosticsHandler(HttpMessageHandler innerHandler, DistributedContextPropagator propagator, bool autoRedirect = false) | ||
{ | ||
Debug.Assert(IsGloballyEnabled()); | ||
Debug.Assert(innerHandler is not null && propagator is not null); | ||
|
||
_innerHandler = innerHandler; | ||
_propagator = propagator; | ||
|
||
// Prepare HeaderDescriptors for fields we need to clear when following redirects | ||
if (autoRedirect && _propagator.Fields is IReadOnlyCollection<string> fields && fields.Count > 0) | ||
{ | ||
var fieldDescriptors = new List<HeaderDescriptor>(fields.Count); | ||
foreach (string field in fields) | ||
{ | ||
if (field is not null && HeaderDescriptor.TryGet(field, out HeaderDescriptor descriptor)) | ||
{ | ||
fieldDescriptors.Add(descriptor); | ||
} | ||
} | ||
_propagatorFields = fieldDescriptors.ToArray(); | ||
} | ||
} | ||
|
||
internal static bool IsEnabled() | ||
private static bool IsEnabled() | ||
MihaZupan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
// check if there is a parent Activity (and propagation is not suppressed) | ||
// or if someone listens to HttpHandlerDiagnosticListener | ||
return IsGloballyEnabled() && (Activity.Current != null || s_diagnosticListener.IsEnabled()); | ||
// check if there is a parent Activity or if someone listens to HttpHandlerDiagnosticListener | ||
return Activity.Current != null || s_diagnosticListener.IsEnabled(); | ||
} | ||
|
||
internal static bool IsGloballyEnabled() => GlobalHttpSettings.DiagnosticsHandler.EnableActivityPropagation; | ||
|
||
// SendAsyncCore returns already completed ValueTask for when async: false is passed. | ||
// Internally, it calls the synchronous Send method of the base class. | ||
protected internal override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) => | ||
SendAsyncCore(request, async: false, cancellationToken).AsTask().GetAwaiter().GetResult(); | ||
|
||
protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => | ||
SendAsyncCore(request, async: true, cancellationToken).AsTask(); | ||
internal override ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) | ||
{ | ||
if (IsEnabled()) | ||
{ | ||
return SendAsyncCore(request, async, cancellationToken); | ||
} | ||
else | ||
{ | ||
return async ? | ||
new ValueTask<HttpResponseMessage>(_innerHandler.SendAsync(request, cancellationToken)) : | ||
new ValueTask<HttpResponseMessage>(_innerHandler.Send(request, cancellationToken)); | ||
} | ||
} | ||
|
||
private async ValueTask<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request, bool async, | ||
CancellationToken cancellationToken) | ||
|
@@ -58,6 +81,16 @@ private async ValueTask<HttpResponseMessage> SendAsyncCore(HttpRequestMessage re | |
throw new ArgumentNullException(nameof(request), SR.net_http_handler_norequest); | ||
} | ||
|
||
// Since we are reusing the request message instance on redirects, clear any existing headers | ||
// Do so before writing DiagnosticListener events as instrumentations use those to inject headers | ||
if (request.WasRedirected() && _propagatorFields is HeaderDescriptor[] fields) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I recall OpenTelemetry defines Fields as a hint about which fields would be inspected but not a guarantee. In particular they left the door open that some protocol would have field names dynamically determined so it would be impossible to provide an exhaustive list. Of course in practice I am not aware of any propagator implementation that does this. If we want to do this we should probably clarify in our API comments that our definition of Fields is strict and we don't support the dynamic shenanigans that OT left the door open to. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That would be my preference. I can't think of a different approach that wouldn't break some scenarios. For example doing the check inside the setter callback and removing the header there instead is too late since instrumentations like AI/Otel may try to add headers before us - we really have to clear the headers in advance. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tarekgh - making sure you see this : ) |
||
{ | ||
foreach (HeaderDescriptor field in fields) | ||
{ | ||
request.Headers.Remove(field); | ||
MihaZupan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
Activity? activity = null; | ||
DiagnosticListener diagnosticListener = s_diagnosticListener; | ||
|
||
|
@@ -72,8 +105,8 @@ private async ValueTask<HttpResponseMessage> SendAsyncCore(HttpRequestMessage re | |
try | ||
{ | ||
return async ? | ||
await base.SendAsync(request, cancellationToken).ConfigureAwait(false) : | ||
base.Send(request, cancellationToken); | ||
await _innerHandler.SendAsync(request, cancellationToken).ConfigureAwait(false) : | ||
_innerHandler.Send(request, cancellationToken); | ||
} | ||
finally | ||
{ | ||
|
@@ -119,8 +152,8 @@ await base.SendAsync(request, cancellationToken).ConfigureAwait(false) : | |
try | ||
{ | ||
response = async ? | ||
await base.SendAsync(request, cancellationToken).ConfigureAwait(false) : | ||
base.Send(request, cancellationToken); | ||
await _innerHandler.SendAsync(request, cancellationToken).ConfigureAwait(false) : | ||
_innerHandler.Send(request, cancellationToken); | ||
return response; | ||
} | ||
catch (OperationCanceledException) | ||
|
@@ -170,6 +203,16 @@ await base.SendAsync(request, cancellationToken).ConfigureAwait(false) : | |
} | ||
} | ||
|
||
protected override void Dispose(bool disposing) | ||
{ | ||
if (disposing) | ||
{ | ||
_innerHandler.Dispose(); | ||
} | ||
|
||
base.Dispose(disposing); | ||
} | ||
|
||
#region private | ||
|
||
private sealed class ActivityStartData | ||
|
@@ -269,42 +312,18 @@ internal ResponseData(HttpResponseMessage? response, Guid loggingRequestId, long | |
public override string ToString() => $"{{ {nameof(Response)} = {Response}, {nameof(LoggingRequestId)} = {LoggingRequestId}, {nameof(Timestamp)} = {Timestamp}, {nameof(RequestTaskStatus)} = {RequestTaskStatus} }}"; | ||
} | ||
|
||
private static void InjectHeaders(Activity currentActivity, HttpRequestMessage request) | ||
private void InjectHeaders(Activity currentActivity, HttpRequestMessage request) | ||
{ | ||
if (currentActivity.IdFormat == ActivityIdFormat.W3C) | ||
_propagator.Inject(currentActivity, request, static (carrier, key, value) => | ||
{ | ||
if (!request.Headers.Contains(DiagnosticsHandlerLoggingStrings.TraceParentHeaderName)) | ||
if (carrier is HttpRequestMessage request && | ||
key is not null && | ||
HeaderDescriptor.TryGet(key, out HeaderDescriptor descriptor) && | ||
!request.Headers.TryGetHeaderValue(descriptor, out _)) | ||
{ | ||
request.Headers.TryAddWithoutValidation(DiagnosticsHandlerLoggingStrings.TraceParentHeaderName, currentActivity.Id); | ||
if (currentActivity.TraceStateString != null) | ||
{ | ||
request.Headers.TryAddWithoutValidation(DiagnosticsHandlerLoggingStrings.TraceStateHeaderName, currentActivity.TraceStateString); | ||
} | ||
request.Headers.TryAddWithoutValidation(descriptor, value); | ||
} | ||
} | ||
else | ||
{ | ||
if (!request.Headers.Contains(DiagnosticsHandlerLoggingStrings.RequestIdHeaderName)) | ||
{ | ||
request.Headers.TryAddWithoutValidation(DiagnosticsHandlerLoggingStrings.RequestIdHeaderName, currentActivity.Id); | ||
} | ||
} | ||
|
||
// we expect baggage to be empty or contain a few items | ||
using (IEnumerator<KeyValuePair<string, string?>> e = currentActivity.Baggage.GetEnumerator()) | ||
{ | ||
if (e.MoveNext()) | ||
{ | ||
var baggage = new List<string>(); | ||
do | ||
{ | ||
KeyValuePair<string, string?> item = e.Current; | ||
baggage.Add(new NameValueHeaderValue(WebUtility.UrlEncode(item.Key), WebUtility.UrlEncode(item.Value)).ToString()); | ||
} | ||
while (e.MoveNext()); | ||
request.Headers.TryAddWithoutValidation(DiagnosticsHandlerLoggingStrings.CorrelationContextHeaderName, baggage); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", | ||
|
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -9,6 +9,7 @@ | |||||||
using System.Security.Cryptography.X509Certificates; | ||||||||
using System.Threading; | ||||||||
using System.Threading.Tasks; | ||||||||
using System.Diagnostics; | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
#if TARGET_BROWSER | ||||||||
using HttpHandlerType = System.Net.Http.BrowserHttpHandler; | ||||||||
#else | ||||||||
|
@@ -20,18 +21,30 @@ namespace System.Net.Http | |||||||
public partial class HttpClientHandler : HttpMessageHandler | ||||||||
{ | ||||||||
private readonly HttpHandlerType _underlyingHandler; | ||||||||
private readonly DiagnosticsHandler? _diagnosticsHandler; | ||||||||
|
||||||||
private HttpMessageHandler Handler | ||||||||
#if TARGET_BROWSER | ||||||||
{ get; } | ||||||||
#else | ||||||||
=> _underlyingHandler; | ||||||||
#endif | ||||||||
|
||||||||
private ClientCertificateOption _clientCertificateOptions; | ||||||||
|
||||||||
private volatile bool _disposed; | ||||||||
|
||||||||
public HttpClientHandler() | ||||||||
{ | ||||||||
_underlyingHandler = new HttpHandlerType(); | ||||||||
|
||||||||
#if TARGET_BROWSER | ||||||||
Handler = _underlyingHandler; | ||||||||
if (DiagnosticsHandler.IsGloballyEnabled()) | ||||||||
{ | ||||||||
_diagnosticsHandler = new DiagnosticsHandler(_underlyingHandler); | ||||||||
Handler = new DiagnosticsHandler(Handler, DistributedContextPropagator.Current); | ||||||||
} | ||||||||
#endif | ||||||||
|
||||||||
ClientCertificateOptions = ClientCertificateOption.Manual; | ||||||||
} | ||||||||
|
||||||||
|
@@ -288,21 +301,11 @@ public SslProtocols SslProtocols | |||||||
public IDictionary<string, object?> Properties => _underlyingHandler.Properties; | ||||||||
|
||||||||
[UnsupportedOSPlatform("browser")] | ||||||||
protected internal override HttpResponseMessage Send(HttpRequestMessage request, | ||||||||
CancellationToken cancellationToken) | ||||||||
{ | ||||||||
return DiagnosticsHandler.IsEnabled() && _diagnosticsHandler != null ? | ||||||||
_diagnosticsHandler.Send(request, cancellationToken) : | ||||||||
_underlyingHandler.Send(request, cancellationToken); | ||||||||
} | ||||||||
protected internal override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) => | ||||||||
Handler.Send(request, cancellationToken); | ||||||||
|
||||||||
protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, | ||||||||
CancellationToken cancellationToken) | ||||||||
{ | ||||||||
return DiagnosticsHandler.IsEnabled() && _diagnosticsHandler != null ? | ||||||||
_diagnosticsHandler.SendAsync(request, cancellationToken) : | ||||||||
_underlyingHandler.SendAsync(request, cancellationToken); | ||||||||
} | ||||||||
protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => | ||||||||
Handler.SendAsync(request, cancellationToken); | ||||||||
|
||||||||
// lazy-load the validator func so it can be trimmed by the ILLinker if it isn't used. | ||||||||
private static Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool>? s_dangerousAcceptAnyServerCertificateValidator; | ||||||||
|
Uh oh!
There was an error while loading. Please reload this page.