| | | 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.Diagnostics.CodeAnalysis; |
| | | 7 | | using System.Net.Http.Headers; |
| | | 8 | | using System.Threading; |
| | | 9 | | using System.Threading.Tasks; |
| | | 10 | | |
| | | 11 | | namespace System.Net.Http |
| | | 12 | | { |
| | | 13 | | /// <summary> |
| | | 14 | | /// DiagnosticHandler notifies DiagnosticSource subscribers about outgoing Http requests |
| | | 15 | | /// </summary> |
| | | 16 | | internal sealed class DiagnosticsHandler : HttpMessageHandlerStage |
| | | 17 | | { |
| | 0 | 18 | | private static readonly DiagnosticListener s_diagnosticListener = new DiagnosticListener(DiagnosticsHandlerLoggi |
| | 0 | 19 | | internal static readonly ActivitySource s_activitySource = new ActivitySource(DiagnosticsHandlerLoggingStrings.R |
| | | 20 | | |
| | | 21 | | private readonly HttpMessageHandler _innerHandler; |
| | | 22 | | private readonly DistributedContextPropagator _propagator; |
| | | 23 | | private readonly HeaderDescriptor[]? _propagatorFields; |
| | | 24 | | private readonly IWebProxy? _proxy; |
| | | 25 | | |
| | 0 | 26 | | public DiagnosticsHandler(HttpMessageHandler innerHandler, DistributedContextPropagator propagator, IWebProxy? p |
| | 0 | 27 | | { |
| | 0 | 28 | | Debug.Assert(GlobalHttpSettings.DiagnosticsHandler.EnableActivityPropagation); |
| | 0 | 29 | | Debug.Assert(innerHandler is not null && propagator is not null); |
| | | 30 | | |
| | 0 | 31 | | _innerHandler = innerHandler; |
| | 0 | 32 | | _propagator = propagator; |
| | 0 | 33 | | _proxy = proxy; |
| | | 34 | | |
| | | 35 | | // Prepare HeaderDescriptors for fields we need to clear when following redirects |
| | 0 | 36 | | if (autoRedirect && _propagator.Fields is IReadOnlyCollection<string> fields && fields.Count > 0) |
| | 0 | 37 | | { |
| | 0 | 38 | | var fieldDescriptors = new List<HeaderDescriptor>(fields.Count); |
| | 0 | 39 | | foreach (string field in fields) |
| | 0 | 40 | | { |
| | 0 | 41 | | if (field is not null && HeaderDescriptor.TryGet(field, out HeaderDescriptor descriptor)) |
| | 0 | 42 | | { |
| | 0 | 43 | | fieldDescriptors.Add(descriptor); |
| | 0 | 44 | | } |
| | 0 | 45 | | } |
| | 0 | 46 | | _propagatorFields = fieldDescriptors.ToArray(); |
| | 0 | 47 | | } |
| | 0 | 48 | | } |
| | | 49 | | |
| | | 50 | | private static bool IsEnabled() |
| | 0 | 51 | | { |
| | | 52 | | // check if there is a parent Activity or if someone listens to "System.Net.Http" ActivitySource or "HttpHan |
| | 0 | 53 | | return Activity.Current != null || |
| | 0 | 54 | | s_activitySource.HasListeners() || |
| | 0 | 55 | | s_diagnosticListener.IsEnabled(); |
| | 0 | 56 | | } |
| | | 57 | | |
| | | 58 | | private static Activity? StartActivity(HttpRequestMessage request) |
| | 0 | 59 | | { |
| | 0 | 60 | | Activity? activity = null; |
| | 0 | 61 | | if (s_activitySource.HasListeners()) |
| | 0 | 62 | | { |
| | 0 | 63 | | activity = s_activitySource.StartActivity(DiagnosticsHandlerLoggingStrings.RequestActivityName, Activity |
| | 0 | 64 | | } |
| | | 65 | | |
| | 0 | 66 | | if (activity is null && |
| | 0 | 67 | | (Activity.Current is not null || |
| | 0 | 68 | | s_diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.RequestActivityName, request))) |
| | 0 | 69 | | { |
| | 0 | 70 | | activity = new Activity(DiagnosticsHandlerLoggingStrings.RequestActivityName).Start(); |
| | 0 | 71 | | } |
| | | 72 | | |
| | 0 | 73 | | return activity; |
| | 0 | 74 | | } |
| | | 75 | | |
| | | 76 | | internal override ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, CancellationT |
| | 0 | 77 | | { |
| | 0 | 78 | | if (IsEnabled()) |
| | 0 | 79 | | { |
| | 0 | 80 | | return SendAsyncCore(request, async, cancellationToken); |
| | | 81 | | } |
| | | 82 | | else |
| | 0 | 83 | | { |
| | 0 | 84 | | return async ? |
| | 0 | 85 | | new ValueTask<HttpResponseMessage>(_innerHandler.SendAsync(request, cancellationToken)) : |
| | 0 | 86 | | new ValueTask<HttpResponseMessage>(_innerHandler.Send(request, cancellationToken)); |
| | | 87 | | } |
| | 0 | 88 | | } |
| | | 89 | | |
| | | 90 | | private async ValueTask<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request, bool async, CancellationT |
| | 0 | 91 | | { |
| | | 92 | | // HttpClientHandler is responsible to call static GlobalHttpSettings.DiagnosticsHandler.IsEnabled before fo |
| | | 93 | | // It will check if propagation is on (because parent Activity exists or there is a listener) or off (forcib |
| | | 94 | | // This code won't be called unless consumer unsubscribes from DiagnosticListener right after the check. |
| | | 95 | | // So some requests happening right after subscription starts might not be instrumented. Similarly, |
| | | 96 | | // when consumer unsubscribes, extra requests might be instrumented |
| | | 97 | | |
| | | 98 | | // Since we are reusing the request message instance on redirects, clear any existing headers |
| | | 99 | | // Do so before writing DiagnosticListener events as instrumentations use those to inject headers |
| | 0 | 100 | | if (request.WasPropagatorStateInjectedByDiagnosticsHandler() && _propagatorFields is HeaderDescriptor[] fiel |
| | 0 | 101 | | { |
| | 0 | 102 | | foreach (HeaderDescriptor field in fields) |
| | 0 | 103 | | { |
| | 0 | 104 | | request.Headers.Remove(field); |
| | 0 | 105 | | } |
| | 0 | 106 | | } |
| | | 107 | | |
| | 0 | 108 | | DiagnosticListener diagnosticListener = s_diagnosticListener; |
| | | 109 | | |
| | 0 | 110 | | Guid loggingRequestId = Guid.Empty; |
| | 0 | 111 | | Activity? activity = StartActivity(request); |
| | | 112 | | |
| | 0 | 113 | | if (activity is not null) |
| | 0 | 114 | | { |
| | | 115 | | // https://github.com/open-telemetry/semantic-conventions/blob/release/v1.23.x/docs/http/http-spans.md#n |
| | 0 | 116 | | activity.DisplayName = HttpMethod.GetKnownMethod(request.Method.Method)?.Method ?? "HTTP"; |
| | | 117 | | |
| | 0 | 118 | | if (activity.IsAllDataRequested) |
| | 0 | 119 | | { |
| | | 120 | | // Add standard tags known before sending the request. |
| | 0 | 121 | | KeyValuePair<string, object?> methodTag = DiagnosticsHelper.GetMethodTag(request.Method, out bool is |
| | 0 | 122 | | activity.SetTag(methodTag.Key, methodTag.Value); |
| | 0 | 123 | | if (isUnknownMethod) |
| | 0 | 124 | | { |
| | 0 | 125 | | activity.SetTag("http.request.method_original", request.Method.Method); |
| | 0 | 126 | | } |
| | | 127 | | |
| | 0 | 128 | | if (request.RequestUri is Uri requestUri && requestUri.IsAbsoluteUri) |
| | 0 | 129 | | { |
| | 0 | 130 | | activity.SetTag("server.address", DiagnosticsHelper.GetServerAddress(request, _proxy)); |
| | 0 | 131 | | activity.SetTag("server.port", requestUri.Port); |
| | 0 | 132 | | activity.SetTag("url.full", UriRedactionHelper.GetRedactedUriString(requestUri)); |
| | 0 | 133 | | } |
| | 0 | 134 | | } |
| | | 135 | | |
| | | 136 | | // Only send start event to users who subscribed for it. |
| | 0 | 137 | | if (diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.RequestActivityStartName)) |
| | 0 | 138 | | { |
| | 0 | 139 | | Write(diagnosticListener, DiagnosticsHandlerLoggingStrings.RequestActivityStartName, new ActivitySta |
| | 0 | 140 | | } |
| | 0 | 141 | | } |
| | | 142 | | |
| | | 143 | | // Try to write System.Net.Http.Request event (deprecated) |
| | 0 | 144 | | if (diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.RequestWriteNameDeprecated)) |
| | 0 | 145 | | { |
| | 0 | 146 | | long timestamp = Stopwatch.GetTimestamp(); |
| | 0 | 147 | | loggingRequestId = Guid.NewGuid(); |
| | 0 | 148 | | Write(diagnosticListener, DiagnosticsHandlerLoggingStrings.RequestWriteNameDeprecated, |
| | 0 | 149 | | new RequestData( |
| | 0 | 150 | | request, |
| | 0 | 151 | | loggingRequestId, |
| | 0 | 152 | | timestamp)); |
| | 0 | 153 | | } |
| | | 154 | | |
| | 0 | 155 | | if (activity is not null) |
| | 0 | 156 | | { |
| | 0 | 157 | | InjectHeaders(activity, request); |
| | 0 | 158 | | } |
| | | 159 | | |
| | 0 | 160 | | HttpResponseMessage? response = null; |
| | 0 | 161 | | Exception? exception = null; |
| | 0 | 162 | | TaskStatus taskStatus = TaskStatus.RanToCompletion; |
| | | 163 | | try |
| | 0 | 164 | | { |
| | 0 | 165 | | response = async ? |
| | 0 | 166 | | await _innerHandler.SendAsync(request, cancellationToken).ConfigureAwait(false) : |
| | 0 | 167 | | _innerHandler.Send(request, cancellationToken); |
| | 0 | 168 | | return response; |
| | | 169 | | } |
| | 0 | 170 | | catch (OperationCanceledException ex) |
| | 0 | 171 | | { |
| | 0 | 172 | | taskStatus = TaskStatus.Canceled; |
| | 0 | 173 | | exception = ex; |
| | | 174 | | |
| | | 175 | | // we'll report task status in HttpRequestOut.Stop |
| | 0 | 176 | | throw; |
| | | 177 | | } |
| | 0 | 178 | | catch (Exception ex) |
| | 0 | 179 | | { |
| | 0 | 180 | | taskStatus = TaskStatus.Faulted; |
| | 0 | 181 | | exception = ex; |
| | | 182 | | |
| | 0 | 183 | | if (diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.ExceptionEventName)) |
| | 0 | 184 | | { |
| | | 185 | | // If request was initially instrumented, Activity.Current has all necessary context for logging |
| | | 186 | | // Request is passed to provide some context if instrumentation was disabled and to avoid |
| | | 187 | | // extensive Activity.Tags usage to tunnel request properties |
| | 0 | 188 | | Write(diagnosticListener, DiagnosticsHandlerLoggingStrings.ExceptionEventName, new ExceptionData(ex, |
| | 0 | 189 | | } |
| | 0 | 190 | | throw; |
| | | 191 | | } |
| | | 192 | | finally |
| | 0 | 193 | | { |
| | | 194 | | // Always stop activity if it was started. |
| | 0 | 195 | | if (activity is not null) |
| | 0 | 196 | | { |
| | 0 | 197 | | activity.SetEndTime(DateTime.UtcNow); |
| | | 198 | | |
| | 0 | 199 | | if (activity.IsAllDataRequested) |
| | 0 | 200 | | { |
| | | 201 | | // Add standard tags known at request completion. |
| | 0 | 202 | | if (response is not null) |
| | 0 | 203 | | { |
| | 0 | 204 | | activity.SetTag("http.response.status_code", DiagnosticsHelper.GetBoxedInt32((int)response.S |
| | 0 | 205 | | activity.SetTag("network.protocol.version", DiagnosticsHelper.GetProtocolVersionString(respo |
| | 0 | 206 | | } |
| | | 207 | | |
| | 0 | 208 | | if (DiagnosticsHelper.TryGetErrorType(response, exception, out string? errorType)) |
| | 0 | 209 | | { |
| | 0 | 210 | | activity.SetTag("error.type", errorType); |
| | | 211 | | |
| | | 212 | | // The presence of error.type indicates that the conditions for setting Error status are als |
| | | 213 | | // https://github.com/open-telemetry/semantic-conventions/blob/v1.34.0/docs/http/http-spans. |
| | 0 | 214 | | activity.SetStatus(ActivityStatusCode.Error); |
| | | 215 | | |
| | 0 | 216 | | if (exception is not null) |
| | 0 | 217 | | { |
| | | 218 | | // Records the exception as per https://github.com/open-telemetry/opentelemetry-specific |
| | | 219 | | // Add the exception event with a timestamp matching the activity's end time |
| | | 220 | | // to ensure it falls within the activity's duration. |
| | 0 | 221 | | activity.AddException(exception, timestamp: activity.StartTimeUtc + activity.Duration); |
| | 0 | 222 | | } |
| | 0 | 223 | | } |
| | 0 | 224 | | } |
| | | 225 | | |
| | | 226 | | // Only send stop event to users who subscribed for it. |
| | 0 | 227 | | if (diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.RequestActivityStopName)) |
| | 0 | 228 | | { |
| | 0 | 229 | | Write(diagnosticListener, DiagnosticsHandlerLoggingStrings.RequestActivityStopName, new Activity |
| | 0 | 230 | | } |
| | | 231 | | |
| | 0 | 232 | | activity.Stop(); |
| | 0 | 233 | | } |
| | | 234 | | |
| | | 235 | | // Try to write System.Net.Http.Response event (deprecated) |
| | 0 | 236 | | if (diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.ResponseWriteNameDeprecated)) |
| | 0 | 237 | | { |
| | 0 | 238 | | long timestamp = Stopwatch.GetTimestamp(); |
| | 0 | 239 | | Write(diagnosticListener, DiagnosticsHandlerLoggingStrings.ResponseWriteNameDeprecated, |
| | 0 | 240 | | new ResponseData( |
| | 0 | 241 | | response, |
| | 0 | 242 | | loggingRequestId, |
| | 0 | 243 | | timestamp, |
| | 0 | 244 | | taskStatus)); |
| | 0 | 245 | | } |
| | 0 | 246 | | } |
| | 0 | 247 | | } |
| | | 248 | | |
| | | 249 | | protected override void Dispose(bool disposing) |
| | 0 | 250 | | { |
| | 0 | 251 | | if (disposing) |
| | 0 | 252 | | { |
| | 0 | 253 | | _innerHandler.Dispose(); |
| | 0 | 254 | | } |
| | | 255 | | |
| | 0 | 256 | | base.Dispose(disposing); |
| | 0 | 257 | | } |
| | | 258 | | |
| | | 259 | | #region private |
| | | 260 | | |
| | | 261 | | private sealed class ActivityStartData |
| | | 262 | | { |
| | | 263 | | // matches the properties selected in https://github.com/dotnet/diagnostics/blob/ffd0254da3bcc47847b1183fa54 |
| | | 264 | | [DynamicDependency(nameof(HttpRequestMessage.RequestUri), typeof(HttpRequestMessage))] |
| | | 265 | | [DynamicDependency(nameof(HttpRequestMessage.Method), typeof(HttpRequestMessage))] |
| | | 266 | | [DynamicDependency(nameof(Uri.Host), typeof(Uri))] |
| | | 267 | | [DynamicDependency(nameof(Uri.Port), typeof(Uri))] |
| | 0 | 268 | | internal ActivityStartData(HttpRequestMessage request) |
| | 0 | 269 | | { |
| | 0 | 270 | | Request = request; |
| | 0 | 271 | | } |
| | | 272 | | |
| | 0 | 273 | | public HttpRequestMessage Request { get; } |
| | | 274 | | |
| | 0 | 275 | | public override string ToString() => $"{{ {nameof(Request)} = {Request} }}"; |
| | | 276 | | } |
| | | 277 | | |
| | | 278 | | private sealed class ActivityStopData |
| | | 279 | | { |
| | 0 | 280 | | internal ActivityStopData(HttpResponseMessage? response, HttpRequestMessage request, TaskStatus requestTaskS |
| | 0 | 281 | | { |
| | 0 | 282 | | Response = response; |
| | 0 | 283 | | Request = request; |
| | 0 | 284 | | RequestTaskStatus = requestTaskStatus; |
| | 0 | 285 | | } |
| | | 286 | | |
| | 0 | 287 | | public HttpResponseMessage? Response { get; } |
| | 0 | 288 | | public HttpRequestMessage Request { get; } |
| | 0 | 289 | | public TaskStatus RequestTaskStatus { get; } |
| | | 290 | | |
| | 0 | 291 | | public override string ToString() => $"{{ {nameof(Response)} = {Response}, {nameof(Request)} = {Request}, {n |
| | | 292 | | } |
| | | 293 | | |
| | | 294 | | private sealed class ExceptionData |
| | | 295 | | { |
| | | 296 | | // preserve the same properties as ActivityStartData above + common Exception properties |
| | | 297 | | [DynamicDependency(nameof(HttpRequestMessage.RequestUri), typeof(HttpRequestMessage))] |
| | | 298 | | [DynamicDependency(nameof(HttpRequestMessage.Method), typeof(HttpRequestMessage))] |
| | | 299 | | [DynamicDependency(nameof(Uri.Host), typeof(Uri))] |
| | | 300 | | [DynamicDependency(nameof(Uri.Port), typeof(Uri))] |
| | | 301 | | [DynamicDependency(nameof(System.Exception.Message), typeof(Exception))] |
| | | 302 | | [DynamicDependency(nameof(System.Exception.StackTrace), typeof(Exception))] |
| | 0 | 303 | | internal ExceptionData(Exception exception, HttpRequestMessage request) |
| | 0 | 304 | | { |
| | 0 | 305 | | Exception = exception; |
| | 0 | 306 | | Request = request; |
| | 0 | 307 | | } |
| | | 308 | | |
| | 0 | 309 | | public Exception Exception { get; } |
| | 0 | 310 | | public HttpRequestMessage Request { get; } |
| | | 311 | | |
| | 0 | 312 | | public override string ToString() => $"{{ {nameof(Exception)} = {Exception}, {nameof(Request)} = {Request} } |
| | | 313 | | } |
| | | 314 | | |
| | | 315 | | private sealed class RequestData |
| | | 316 | | { |
| | | 317 | | // preserve the same properties as ActivityStartData above |
| | | 318 | | [DynamicDependency(nameof(HttpRequestMessage.RequestUri), typeof(HttpRequestMessage))] |
| | | 319 | | [DynamicDependency(nameof(HttpRequestMessage.Method), typeof(HttpRequestMessage))] |
| | | 320 | | [DynamicDependency(nameof(Uri.Host), typeof(Uri))] |
| | | 321 | | [DynamicDependency(nameof(Uri.Port), typeof(Uri))] |
| | 0 | 322 | | internal RequestData(HttpRequestMessage request, Guid loggingRequestId, long timestamp) |
| | 0 | 323 | | { |
| | 0 | 324 | | Request = request; |
| | 0 | 325 | | LoggingRequestId = loggingRequestId; |
| | 0 | 326 | | Timestamp = timestamp; |
| | 0 | 327 | | } |
| | | 328 | | |
| | 0 | 329 | | public HttpRequestMessage Request { get; } |
| | 0 | 330 | | public Guid LoggingRequestId { get; } |
| | 0 | 331 | | public long Timestamp { get; } |
| | | 332 | | |
| | 0 | 333 | | public override string ToString() => $"{{ {nameof(Request)} = {Request}, {nameof(LoggingRequestId)} = {Loggi |
| | | 334 | | } |
| | | 335 | | |
| | | 336 | | private sealed class ResponseData |
| | | 337 | | { |
| | | 338 | | [DynamicDependency(nameof(HttpResponseMessage.StatusCode), typeof(HttpResponseMessage))] |
| | 0 | 339 | | internal ResponseData(HttpResponseMessage? response, Guid loggingRequestId, long timestamp, TaskStatus reque |
| | 0 | 340 | | { |
| | 0 | 341 | | Response = response; |
| | 0 | 342 | | LoggingRequestId = loggingRequestId; |
| | 0 | 343 | | Timestamp = timestamp; |
| | 0 | 344 | | RequestTaskStatus = requestTaskStatus; |
| | 0 | 345 | | } |
| | | 346 | | |
| | 0 | 347 | | public HttpResponseMessage? Response { get; } |
| | 0 | 348 | | public Guid LoggingRequestId { get; } |
| | 0 | 349 | | public long Timestamp { get; } |
| | 0 | 350 | | public TaskStatus RequestTaskStatus { get; } |
| | | 351 | | |
| | 0 | 352 | | public override string ToString() => $"{{ {nameof(Response)} = {Response}, {nameof(LoggingRequestId)} = {Log |
| | | 353 | | } |
| | | 354 | | |
| | | 355 | | private void InjectHeaders(Activity currentActivity, HttpRequestMessage request) |
| | 0 | 356 | | { |
| | 0 | 357 | | _propagator.Inject(currentActivity, request, static (carrier, key, value) => |
| | 0 | 358 | | { |
| | 0 | 359 | | if (carrier is HttpRequestMessage request && key is not null) |
| | 0 | 360 | | { |
| | 0 | 361 | | HeaderDescriptor descriptor = request.Headers.GetHeaderDescriptor(key); |
| | 0 | 362 | | |
| | 0 | 363 | | if (!request.Headers.Contains(descriptor)) |
| | 0 | 364 | | { |
| | 0 | 365 | | request.Headers.Add(descriptor, value); |
| | 0 | 366 | | } |
| | 0 | 367 | | } |
| | 0 | 368 | | }); |
| | 0 | 369 | | request.MarkPropagatorStateInjectedByDiagnosticsHandler(); |
| | 0 | 370 | | } |
| | | 371 | | |
| | | 372 | | [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", |
| | | 373 | | Justification = "The values being passed into Write have the commonly used properties being preserved with D |
| | | 374 | | private static void Write<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>( |
| | | 375 | | DiagnosticSource diagnosticSource, |
| | | 376 | | string name, |
| | | 377 | | T value) |
| | 0 | 378 | | { |
| | 0 | 379 | | diagnosticSource.Write(name, value); |
| | 0 | 380 | | } |
| | | 381 | | #endregion |
| | | 382 | | } |
| | | 383 | | } |