/// <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()
{
- // 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)
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)
+ {
+ foreach (HeaderDescriptor field in fields)
+ {
+ request.Headers.Remove(field);
+ }
+ }
+
Activity? activity = null;
DiagnosticListener diagnosticListener = s_diagnosticListener;
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
{
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)
}
}
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _innerHandler.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
#region private
private sealed class ActivityStartData
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",
GetProperty<HttpRequestMessage>(kvp.Value, "Request");
TaskStatus status = GetProperty<TaskStatus>(kvp.Value, "RequestTaskStatus");
Assert.Equal(TaskStatus.Canceled, status);
- activityStopTcs.SetResult();;
+ activityStopTcs.SetResult();
}
});
parentActivity.AddBaggage("correlationId", Guid.NewGuid().ToString("N").ToString());
parentActivity.AddBaggage("moreBaggage", Guid.NewGuid().ToString("N").ToString());
parentActivity.AddTag("tag", "tag"); // add tag to ensure it is not injected into request
+ parentActivity.TraceStateString = "Foo";
parentActivity.Start();
activityStopResponseLogged = GetProperty<HttpResponseMessage>(kvp.Value, "Response");
TaskStatus requestStatus = GetProperty<TaskStatus>(kvp.Value, "RequestTaskStatus");
Assert.Equal(TaskStatus.RanToCompletion, requestStatus);
- activityStopTcs.SetResult();;
+ activityStopTcs.SetResult();
}
});
HttpRequestMessage request = GetProperty<HttpRequestMessage>(kvp.Value, "Request");
Assert.True(request.Headers.TryGetValues("Request-Id", out var requestId));
Assert.True(request.Headers.TryGetValues("Correlation-Context", out var correlationContext));
- Assert.Equal(3, correlationContext.Count());
- Assert.Contains("key=value", correlationContext);
- Assert.Contains("bad%2Fkey=value", correlationContext);
- Assert.Contains("goodkey=bad%2Fvalue", correlationContext);
+ Assert.Equal("key=value, goodkey=bad%2Fvalue, bad%2Fkey=value", Assert.Single(correlationContext));
TaskStatus requestStatus = GetProperty<TaskStatus>(kvp.Value, "RequestTaskStatus");
Assert.Equal(TaskStatus.RanToCompletion, requestStatus);
- activityStopTcs.SetResult();;
+ activityStopTcs.SetResult();
}
else if (kvp.Key.Equals("System.Net.Http.Exception"))
{
Assert.False(request.Headers.TryGetValues("traceparent", out var _));
Assert.False(request.Headers.TryGetValues("tracestate", out var _));
- activityStopTcs.SetResult();;
+ activityStopTcs.SetResult();
}
});
}
else if (kvp.Key.Equals("System.Net.Http.HttpRequestOut.Stop"))
{
- activityStopTcs.SetResult();;
+ activityStopTcs.SetResult();
}
});
GetProperty<HttpRequestMessage>(kvp.Value, "Request");
TaskStatus requestStatus = GetProperty<TaskStatus>(kvp.Value, "RequestTaskStatus");
Assert.Equal(TaskStatus.Faulted, requestStatus);
- activityStopTcs.SetResult();;
+ activityStopTcs.SetResult();
}
else if (kvp.Key.Equals("System.Net.Http.Exception"))
{
GetProperty<HttpRequestMessage>(kvp.Value, "Request");
TaskStatus requestStatus = GetProperty<TaskStatus>(kvp.Value, "RequestTaskStatus");
Assert.Equal(TaskStatus.Faulted, requestStatus);
- activityStopTcs.SetResult();;
+ activityStopTcs.SetResult();
}
else if (kvp.Key.Equals("System.Net.Http.Exception"))
{
}, UseVersion.ToString(), TestAsync.ToString()).Dispose();
}
- [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
- public void SendAsync_ExpectedActivityPropagationWithoutListener()
+ public static IEnumerable<object[]> UseSocketsHttpHandler_WithIdFormat_MemberData()
{
- RemoteExecutor.Invoke(async (useVersion, testAsync) =>
- {
- Activity parent = new Activity("parent").Start();
+ yield return new object[] { true, ActivityIdFormat.Hierarchical };
+ yield return new object[] { true, ActivityIdFormat.W3C };
+ yield return new object[] { false, ActivityIdFormat.Hierarchical };
+ yield return new object[] { false, ActivityIdFormat.W3C };
+ }
- await GetFactoryForVersion(useVersion).CreateClientAndServerAsync(
- async uri =>
- {
- await GetAsync(useVersion, testAsync, uri);
- },
- async server =>
- {
- HttpRequestData requestData = await server.AcceptConnectionSendResponseAndCloseAsync();
- AssertHeadersAreInjected(requestData, parent);
- });
- }, UseVersion.ToString(), TestAsync.ToString()).Dispose();
+ [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))]
+ [MemberData(nameof(UseSocketsHttpHandler_WithIdFormat_MemberData))]
+ public async Task SendAsync_ExpectedActivityPropagationWithoutListener(bool useSocketsHttpHandler, ActivityIdFormat idFormat)
+ {
+ Activity parent = new Activity("parent");
+ parent.SetIdFormat(idFormat);
+ parent.Start();
+
+ await GetFactoryForVersion(UseVersion).CreateClientAndServerAsync(
+ async uri =>
+ {
+ await GetAsync(UseVersion.ToString(), TestAsync.ToString(), uri, useSocketsHttpHandler: useSocketsHttpHandler);
+ },
+ async server =>
+ {
+ HttpRequestData requestData = await server.HandleRequestAsync();
+ AssertHeadersAreInjected(requestData, parent);
+ });
}
- [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
- public void SendAsync_ExpectedActivityPropagationWithoutListenerOrParentActivity()
+ [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task SendAsync_ExpectedActivityPropagationWithoutListenerOrParentActivity(bool useSocketsHttpHandler)
{
- RemoteExecutor.Invoke(async (useVersion, testAsync) =>
- {
- await GetFactoryForVersion(useVersion).CreateClientAndServerAsync(
- async uri =>
- {
- await GetAsync(useVersion, testAsync, uri);
- },
- async server =>
- {
- HttpRequestData requestData = await server.AcceptConnectionSendResponseAndCloseAsync();
- AssertNoHeadersAreInjected(requestData);
- });
- }, UseVersion.ToString(), TestAsync.ToString()).Dispose();
+ await GetFactoryForVersion(UseVersion).CreateClientAndServerAsync(
+ async uri =>
+ {
+ await GetAsync(UseVersion.ToString(), TestAsync.ToString(), uri, useSocketsHttpHandler: useSocketsHttpHandler);
+ },
+ async server =>
+ {
+ HttpRequestData requestData = await server.HandleRequestAsync();
+ AssertNoHeadersAreInjected(requestData);
+ });
}
[ConditionalTheory(nameof(EnableActivityPropagationEnvironmentVariableIsNotSetAndRemoteExecutorSupported))]
}, UseVersion.ToString(), TestAsync.ToString(), envVarValue).Dispose();
}
+ [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))]
+ [MemberData(nameof(UseSocketsHttpHandler_WithIdFormat_MemberData))]
+ public async Task SendAsync_HeadersAreInjectedOnRedirects(bool useSocketsHttpHandler, ActivityIdFormat idFormat)
+ {
+ Activity parent = new Activity("parent");
+ parent.SetIdFormat(idFormat);
+ parent.TraceStateString = "Foo";
+ parent.Start();
+
+ await GetFactoryForVersion(UseVersion).CreateServerAsync(async (originalServer, originalUri) =>
+ {
+ await GetFactoryForVersion(UseVersion).CreateServerAsync(async (redirectServer, redirectUri) =>
+ {
+ Task clientTask = GetAsync(UseVersion.ToString(), TestAsync.ToString(), originalUri, useSocketsHttpHandler: useSocketsHttpHandler);
+
+ Task<HttpRequestData> serverTask = originalServer.HandleRequestAsync(HttpStatusCode.Redirect, new[] { new HttpHeaderData("Location", redirectUri.AbsoluteUri) });
+
+ await Task.WhenAny(clientTask, serverTask);
+ Assert.False(clientTask.IsCompleted, $"{clientTask.Status}: {clientTask.Exception}");
+ HttpRequestData firstRequestData = await serverTask;
+ AssertHeadersAreInjected(firstRequestData, parent);
+
+ serverTask = redirectServer.HandleRequestAsync();
+ await TestHelper.WhenAllCompletedOrAnyFailed(clientTask, serverTask);
+ HttpRequestData secondRequestData = await serverTask;
+ AssertHeadersAreInjected(secondRequestData, parent);
+
+ if (idFormat == ActivityIdFormat.W3C)
+ {
+ string firstParent = GetHeaderValue(firstRequestData, "traceparent");
+ string firstState = GetHeaderValue(firstRequestData, "tracestate");
+ Assert.True(ActivityContext.TryParse(firstParent, firstState, out ActivityContext firstContext));
+
+ string secondParent = GetHeaderValue(secondRequestData, "traceparent");
+ string secondState = GetHeaderValue(secondRequestData, "tracestate");
+ Assert.True(ActivityContext.TryParse(secondParent, secondState, out ActivityContext secondContext));
+
+ Assert.Equal(firstContext.TraceId, secondContext.TraceId);
+ Assert.Equal(firstContext.TraceFlags, secondContext.TraceFlags);
+ Assert.Equal(firstContext.TraceState, secondContext.TraceState);
+ Assert.NotEqual(firstContext.SpanId, secondContext.SpanId);
+ }
+ else
+ {
+ Assert.NotEqual(GetHeaderValue(firstRequestData, "Request-Id"), GetHeaderValue(secondRequestData, "Request-Id"));
+ }
+ });
+ });
+ }
+
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[InlineData(true)]
[InlineData(false)]
(HttpRequestMessage request, _) = await GetAsync(useVersion, testAsync, uri);
string headerName = parent.IdFormat == ActivityIdFormat.Hierarchical ? "Request-Id" : "traceparent";
+
Assert.Equal(bool.Parse(switchValue), request.Headers.Contains(headerName));
},
async server => await server.HandleRequestAsync());
}, UseVersion.ToString(), TestAsync.ToString(), switchValue.ToString()).Dispose();
}
+ public static IEnumerable<object[]> SocketsHttpHandlerPropagators_WithIdFormat_MemberData()
+ {
+ foreach (var propagator in new[] { null, DistributedContextPropagator.CreateDefaultPropagator(), DistributedContextPropagator.CreateNoOutputPropagator(), DistributedContextPropagator.CreatePassThroughPropagator() })
+ {
+ foreach (ActivityIdFormat format in new[] { ActivityIdFormat.Hierarchical, ActivityIdFormat.W3C })
+ {
+ yield return new object[] { propagator, format };
+ }
+ }
+ }
+
+ [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))]
+ [MemberData(nameof(SocketsHttpHandlerPropagators_WithIdFormat_MemberData))]
+ public async Task SendAsync_CustomSocketsHttpHandlerPropagator_PropagatorIsUsed(DistributedContextPropagator propagator, ActivityIdFormat idFormat)
+ {
+ Activity parent = new Activity("parent");
+ parent.SetIdFormat(idFormat);
+ parent.Start();
+
+ await GetFactoryForVersion(UseVersion).CreateClientAndServerAsync(
+ async uri =>
+ {
+ using var handler = new SocketsHttpHandler { ActivityHeadersPropagator = propagator };
+ handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
+ using var client = new HttpClient(handler);
+ var request = CreateRequest(HttpMethod.Get, uri, UseVersion, exactVersion: true);
+ await client.SendAsync(TestAsync, request);
+ },
+ async server =>
+ {
+ HttpRequestData requestData = await server.HandleRequestAsync();
+
+ if (propagator is null || ReferenceEquals(propagator, DistributedContextPropagator.CreateNoOutputPropagator()))
+ {
+ AssertNoHeadersAreInjected(requestData);
+ }
+ else
+ {
+ AssertHeadersAreInjected(requestData, parent, ReferenceEquals(propagator, DistributedContextPropagator.CreatePassThroughPropagator()));
+ }
+ });
+ }
+
private static T GetProperty<T>(object obj, string propertyName)
{
Type t = obj.GetType();
Assert.Null(GetHeaderValue(request, "Correlation-Context"));
}
- private static void AssertHeadersAreInjected(HttpRequestData request, Activity parent)
+ private static void AssertHeadersAreInjected(HttpRequestData request, Activity parent, bool passthrough = false)
{
string requestId = GetHeaderValue(request, "Request-Id");
string traceparent = GetHeaderValue(request, "traceparent");
{
Assert.True(requestId != null, "Request-Id was not injected when instrumentation was enabled");
Assert.StartsWith(parent.Id, requestId);
- Assert.NotEqual(parent.Id, requestId);
+ Assert.Equal(passthrough, parent.Id == requestId);
Assert.Null(traceparent);
Assert.Null(tracestate);
}
Assert.Null(requestId);
Assert.True(traceparent != null, "traceparent was not injected when W3C instrumentation was enabled");
Assert.StartsWith($"00-{parent.TraceId.ToHexString()}-", traceparent);
+ Assert.Equal(passthrough, parent.Id == traceparent);
Assert.Equal(parent.TraceStateString, tracestate);
}
}
}
- private static async Task<(HttpRequestMessage, HttpResponseMessage)> GetAsync(string useVersion, string testAsync, Uri uri, CancellationToken cancellationToken = default)
+ private static async Task<(HttpRequestMessage, HttpResponseMessage)> GetAsync(string useVersion, string testAsync, Uri uri, CancellationToken cancellationToken = default, bool useSocketsHttpHandler = false)
{
- HttpClientHandler handler = CreateHttpClientHandler(useVersion);
- handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates;
+ HttpMessageHandler handler;
+ if (useSocketsHttpHandler)
+ {
+ var socketsHttpHandler = new SocketsHttpHandler();
+ socketsHttpHandler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
+ handler = socketsHttpHandler;
+ }
+ else
+ {
+ handler = new HttpClientHandler
+ {
+ ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates
+ };
+ }
+
using var client = new HttpClient(handler);
var request = CreateRequest(HttpMethod.Get, uri, Version.Parse(useVersion), exactVersion: true);
return (request, await client.SendAsync(bool.Parse(testAsync), request, cancellationToken));