From af8d47aa3f6325e079bc52078b1759db79d67237 Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Tue, 9 Sep 2025 11:32:53 +0200 Subject: [PATCH 01/19] new http latency mediator class --- .../Latency/Internal/HttpLatencyMediator.cs | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs new file mode 100644 index 00000000000..148881fe197 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using System.Net.Http; +using System.Text; +using Microsoft.Extensions.Diagnostics.Latency; + +namespace Microsoft.Extensions.Http.Latency.Internal; + +/// +/// Mediator for HTTP latency operations that coordinates recording HTTP metrics in a latency context. +/// +internal sealed class HttpLatencyMediator +{ + // Measure tokens + private readonly MeasureToken _requestContentLength; + private readonly MeasureToken _responseContentLength; + + // Tag tokens + private readonly TagToken _httpMethod; + private readonly TagToken _httpStatusCode; + private readonly TagToken _requestHost; + private readonly TagToken _requestPath; + private readonly TagToken _responseContentType; + private readonly TagToken _hasException; + + // Checkpoint tokens + private readonly CheckpointToken _enricherInvoked; + + public HttpLatencyMediator(ILatencyContextTokenIssuer tokenIssuer) + { + // Initialize checkpoint tokens + _enricherInvoked = tokenIssuer.GetCheckpointToken(HttpCheckpoints.EnricherInvoked); + + // Initialize measure tokens + _requestContentLength = tokenIssuer.GetMeasureToken("Http.Request.ContentLength"); + _responseContentLength = tokenIssuer.GetMeasureToken("Http.Response.ContentLength"); + + // Initialize tag tokens + _httpMethod = tokenIssuer.GetTagToken("Http.Method"); + _httpStatusCode = tokenIssuer.GetTagToken("Http.StatusCode"); + _requestHost = tokenIssuer.GetTagToken("Http.Request.Host"); + _requestPath = tokenIssuer.GetTagToken("Http.Request.Path"); + _responseContentType = tokenIssuer.GetTagToken("Http.Response.ContentType"); + _hasException = tokenIssuer.GetTagToken("Http.HasException"); + } + + /// + /// Records HTTP request information in the latency context. + /// + public void RecordRequest(ILatencyContext context, HttpRequestMessage request) + { + if (context == null) + { + return; + } + + // Add checkpoint for request processing + context.AddCheckpoint(_enricherInvoked); + + // Collect request-related data + context.SetTag(_httpMethod, request.Method.Method); + + if (request.RequestUri != null) + { + context.SetTag(_requestHost, request.RequestUri.Host); + context.SetTag(_requestPath, request.RequestUri.AbsolutePath); + } + + // Collect request content length if available + if (request.Content?.Headers.ContentLength.HasValue == true) + { + context.RecordMeasure(_requestContentLength, request.Content.Headers.ContentLength.Value); + } + } + + /// + /// Records HTTP response information in the latency context. + /// + public void RecordResponse(ILatencyContext context, HttpResponseMessage response) + { + if (context == null || response == null) + { + return; + } + + // Add response-related data + context.SetTag(_httpStatusCode, ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture)); + + // Collect response content type if available + if (response.Content.Headers.ContentType != null) + { + context.SetTag(_responseContentType, response.Content.Headers.ContentType.MediaType!); + } + + // Collect response content length if available + if (response.Content.Headers.ContentLength.HasValue) + { + context.RecordMeasure(_responseContentLength, response.Content.Headers.ContentLength.Value); + } + } + + /// + /// Records exception information in the latency context. + /// + public void RecordException(ILatencyContext context, Exception? exception) + { + if (context == null) + { + return; + } + + context.SetTag(_hasException, exception != null ? "true" : "false"); + } + + /// + /// Appends checkpoint data to the provided string builder. + /// + public void AppendCheckpoints(ILatencyContext context, StringBuilder stringBuilder) + { + if (context == null || stringBuilder == null) + { + return; + } + + var latencyData = context.LatencyData; + for (int i = 0; i < latencyData.Checkpoints.Length; i++) + { + _ = stringBuilder.Append(latencyData.Checkpoints[i].Name); + _ = stringBuilder.Append('/'); + } + + _ = stringBuilder.Append(','); + foreach (var checkpoint in latencyData.Checkpoints) + { + var ms = (double)checkpoint.Elapsed / checkpoint.Frequency * 1000; + _ = stringBuilder.Append(ms); + _ = stringBuilder.Append('/'); + } + } +} + From 438fc0d391f72678de017e9f436de239a4a3a167 Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Tue, 9 Sep 2025 11:40:48 +0200 Subject: [PATCH 02/19] use mediator in the log enricher --- .../HttpClientLatencyTelemetryExtensions.cs | 1 + .../Internal/HttpClientLatencyLogEnricher.cs | 56 ++++++++----------- 2 files changed, 24 insertions(+), 33 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs index fa77360a8c1..747326f4b8c 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs @@ -32,6 +32,7 @@ public static IServiceCollection AddHttpClientLatencyTelemetry(this IServiceColl _ = services.AddOptions(); _ = services.AddSingleton(); _ = services.AddSingleton(); + _ = services.AddSingleton(); _ = services.AddTransient(); _ = services.AddHttpClientLogEnricher(); diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs index bad9b23a415..09ff2503822 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs @@ -23,32 +23,40 @@ internal sealed class HttpClientLatencyLogEnricher : IHttpClientLogEnricher { private static readonly ObjectPool _builderPool = PoolFactory.SharedStringBuilderPool; private readonly HttpClientLatencyContext _latencyContext; + private readonly HttpLatencyMediator _mediator; - private readonly CheckpointToken _enricherInvoked; - - public HttpClientLatencyLogEnricher(HttpClientLatencyContext latencyContext, ILatencyContextTokenIssuer tokenIssuer) + public HttpClientLatencyLogEnricher(HttpClientLatencyContext latencyContext, ILatencyContextTokenIssuer tokenIssuer, + HttpLatencyMediator mediator) { _latencyContext = latencyContext; - _enricherInvoked = tokenIssuer.GetCheckpointToken(HttpCheckpoints.EnricherInvoked); + _mediator = mediator; } public void Enrich(IEnrichmentTagCollector collector, HttpRequestMessage request, HttpResponseMessage? response, Exception? exception) { - if (response != null) + var lc = _latencyContext.Get(); + if (lc == null) { - var lc = _latencyContext.Get(); - lc?.AddCheckpoint(_enricherInvoked); + return; + } + + // Use the mediator to record metrics + _mediator.RecordRequest(lc, request); + _mediator.RecordException(lc, exception); + if (response != null) + { + // Record response metrics + _mediator.RecordResponse(lc, response); + StringBuilder stringBuilder = _builderPool.Get(); - // Add serverName, checkpoints to outgoing http logs. + // Add serverName to outgoing http logs AppendServerName(response.Headers, stringBuilder); - _ = stringBuilder.Append(','); - - if (lc != null) - { - AppendCheckpoints(lc, stringBuilder); - } + stringBuilder.Append(','); + + // Use mediator to append checkpoint data + _mediator.AppendCheckpoints(lc, stringBuilder); collector.Add("LatencyInfo", stringBuilder.ToString()); @@ -60,25 +68,7 @@ private static void AppendServerName(HttpHeaders headers, StringBuilder stringBu { if (headers.TryGetValues(TelemetryConstants.ServerApplicationNameHeader, out var values)) { - _ = stringBuilder.Append(values!.First()); - } - } - - private static void AppendCheckpoints(ILatencyContext latencyContext, StringBuilder stringBuilder) - { - var latencyData = latencyContext.LatencyData; - for (int i = 0; i < latencyData.Checkpoints.Length; i++) - { - _ = stringBuilder.Append(latencyData.Checkpoints[i].Name); - _ = stringBuilder.Append('/'); - } - - _ = stringBuilder.Append(','); - for (int i = 0; i < latencyData.Checkpoints.Length; i++) - { - var ms = ((double)latencyData.Checkpoints[i].Elapsed / latencyData.Checkpoints[i].Frequency) * 1000; - _ = stringBuilder.Append(ms); - _ = stringBuilder.Append('/'); + stringBuilder.Append(values!.First()); } } } From 5710343f02fab578639a9889f3c4711ddf1803ba Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Tue, 9 Sep 2025 14:54:12 +0200 Subject: [PATCH 03/19] fix tests, update docs --- .../Internal/HttpClientLatencyLogEnricher.cs | 11 +++--- .../Latency/Internal/HttpLatencyMediator.cs | 38 ++++++++++++------- .../HttpClientLatencyLogEnricherTest.cs | 14 +++++-- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs index 09ff2503822..060f5d85767 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs @@ -39,7 +39,7 @@ public void Enrich(IEnrichmentTagCollector collector, HttpRequestMessage request { return; } - + // Use the mediator to record metrics _mediator.RecordRequest(lc, request); _mediator.RecordException(lc, exception); @@ -48,13 +48,12 @@ public void Enrich(IEnrichmentTagCollector collector, HttpRequestMessage request { // Record response metrics _mediator.RecordResponse(lc, response); - + StringBuilder stringBuilder = _builderPool.Get(); - // Add serverName to outgoing http logs AppendServerName(response.Headers, stringBuilder); - stringBuilder.Append(','); - + _ = stringBuilder.Append(','); + // Use mediator to append checkpoint data _mediator.AppendCheckpoints(lc, stringBuilder); @@ -68,7 +67,7 @@ private static void AppendServerName(HttpHeaders headers, StringBuilder stringBu { if (headers.TryGetValues(TelemetryConstants.ServerApplicationNameHeader, out var values)) { - stringBuilder.Append(values!.First()); + _ = stringBuilder.Append(values.First()); } } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs index 148881fe197..3be69d20ae3 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.Http.Latency.Internal; /// /// Mediator for HTTP latency operations that coordinates recording HTTP metrics in a latency context. /// -internal sealed class HttpLatencyMediator +internal class HttpLatencyMediator { // Measure tokens private readonly MeasureToken _requestContentLength; @@ -29,6 +29,10 @@ internal sealed class HttpLatencyMediator // Checkpoint tokens private readonly CheckpointToken _enricherInvoked; + /// + /// Initializes a new instance of the class. + /// + /// Token issuer for getting latency tokens. public HttpLatencyMediator(ILatencyContextTokenIssuer tokenIssuer) { // Initialize checkpoint tokens @@ -50,9 +54,11 @@ public HttpLatencyMediator(ILatencyContextTokenIssuer tokenIssuer) /// /// Records HTTP request information in the latency context. /// - public void RecordRequest(ILatencyContext context, HttpRequestMessage request) + /// The latency context to update. + /// The HTTP request message. + public virtual void RecordRequest(ILatencyContext context, HttpRequestMessage? request) { - if (context == null) + if (context == null || request == null) { return; } @@ -79,24 +85,26 @@ public void RecordRequest(ILatencyContext context, HttpRequestMessage request) /// /// Records HTTP response information in the latency context. /// - public void RecordResponse(ILatencyContext context, HttpResponseMessage response) + /// The latency context to update. + /// The HTTP response message. + public virtual void RecordResponse(ILatencyContext context, HttpResponseMessage response) { if (context == null || response == null) { return; } - // Add response-related data + // Add response-related data with culture-invariant string conversion context.SetTag(_httpStatusCode, ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture)); // Collect response content type if available - if (response.Content.Headers.ContentType != null) + if (response.Content?.Headers.ContentType != null) { - context.SetTag(_responseContentType, response.Content.Headers.ContentType.MediaType!); + context.SetTag(_responseContentType, response.Content.Headers.ContentType.MediaType); } // Collect response content length if available - if (response.Content.Headers.ContentLength.HasValue) + if (response.Content?.Headers.ContentLength.HasValue == true) { context.RecordMeasure(_responseContentLength, response.Content.Headers.ContentLength.Value); } @@ -105,7 +113,9 @@ public void RecordResponse(ILatencyContext context, HttpResponseMessage response /// /// Records exception information in the latency context. /// - public void RecordException(ILatencyContext context, Exception? exception) + /// The latency context to update. + /// The exception that occurred, if any. + public virtual void RecordException(ILatencyContext context, Exception? exception) { if (context == null) { @@ -118,7 +128,9 @@ public void RecordException(ILatencyContext context, Exception? exception) /// /// Appends checkpoint data to the provided string builder. /// - public void AppendCheckpoints(ILatencyContext context, StringBuilder stringBuilder) + /// The latency context containing checkpoint data. + /// The string builder to append data to. + public virtual void AppendCheckpoints(ILatencyContext context, StringBuilder stringBuilder) { if (context == null || stringBuilder == null) { @@ -133,10 +145,10 @@ public void AppendCheckpoints(ILatencyContext context, StringBuilder stringBuild } _ = stringBuilder.Append(','); - foreach (var checkpoint in latencyData.Checkpoints) + for (int i = 0; i < latencyData.Checkpoints.Length; i++) { - var ms = (double)checkpoint.Elapsed / checkpoint.Frequency * 1000; - _ = stringBuilder.Append(ms); + var ms = (double)latencyData.Checkpoints[i].Elapsed / latencyData.Checkpoints[i].Frequency * 1000; + _ = stringBuilder.Append(ms.ToString(CultureInfo.InvariantCulture)); _ = stringBuilder.Append('/'); } } diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs index 71941df0e7e..30f9f0ae923 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs @@ -3,6 +3,7 @@ using System; using System.Net.Http; +using System.Text; using Microsoft.Extensions.Diagnostics.Enrichment; using Microsoft.Extensions.Diagnostics.Latency; using Microsoft.Extensions.Http.Diagnostics; @@ -18,6 +19,7 @@ public class HttpClientLatencyLogEnricherTest public void HttpClientLatencyLogEnricher_NoOp_OnRequest() { var lcti = HttpMockProvider.GetTokenIssuer(); + var mockMediator = new Mock(lcti.Object); var checkpoints = new ArraySegment(new[] { new Checkpoint("a", default, default), new Checkpoint("b", default, default) }); var ld = new LatencyData(default, checkpoints, default, default, default); var lc = HttpMockProvider.GetLatencyContext(); @@ -25,7 +27,7 @@ public void HttpClientLatencyLogEnricher_NoOp_OnRequest() var context = new HttpClientLatencyContext(); context.Set(lc.Object); - var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object); + var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mockMediator.Object); Mock mockEnrichmentPropertyBag = new Mock(); enricher.Enrich(mockEnrichmentPropertyBag.Object, null!, null, null); mockEnrichmentPropertyBag.Verify(m => m.Add(It.IsAny(), It.IsAny()), Times.Never); @@ -35,6 +37,7 @@ public void HttpClientLatencyLogEnricher_NoOp_OnRequest() public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithoutHeader() { var lcti = HttpMockProvider.GetTokenIssuer(); + var mockMediator = new Mock(lcti.Object); var checkpoints = new ArraySegment(new[] { new Checkpoint("a", default, default), new Checkpoint("b", default, default) }); var ld = new LatencyData(default, checkpoints, default, default, default); var lc = HttpMockProvider.GetLatencyContext(); @@ -43,8 +46,10 @@ public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithoutHeader() context.Set(lc.Object); using HttpResponseMessage httpResponseMessage = new(); + mockMediator.Setup(m => m.AppendCheckpoints(It.IsAny(), It.IsAny())) + .Callback((_, sb) => sb.Append("a/b")); - var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object); + var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mockMediator.Object); Mock mockEnrichmentPropertyBag = new Mock(); enricher.Enrich(mockEnrichmentPropertyBag.Object, null!, httpResponseMessage, null); @@ -55,6 +60,7 @@ public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithoutHeader() public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithHeader() { var lcti = HttpMockProvider.GetTokenIssuer(); + var mockMediator = new Mock(lcti.Object); var checkpoints = new ArraySegment(new[] { new Checkpoint("a", default, default), new Checkpoint("b", default, default) }); var ld = new LatencyData(default, checkpoints, default, default, default); var lc = HttpMockProvider.GetLatencyContext(); @@ -65,8 +71,10 @@ public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithHeader() using HttpResponseMessage httpResponseMessage = new(); string serverName = "serverNameVal"; httpResponseMessage.Headers.Add(TelemetryConstants.ServerApplicationNameHeader, serverName); + mockMediator.Setup(m => m.AppendCheckpoints(It.IsAny(), It.IsAny())) + .Callback((_, sb) => sb.Append("a/b")); - var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object); + var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mockMediator.Object); Mock mockEnrichmentPropertyBag = new Mock(); enricher.Enrich(mockEnrichmentPropertyBag.Object, null!, httpResponseMessage, null); From ec58e85b268d5f6b292f9cda494eabf629105502 Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Tue, 9 Sep 2025 15:04:59 +0200 Subject: [PATCH 04/19] restore original enricher behavior --- .../Internal/HttpClientLatencyLogEnricher.cs | 29 ++++++++++++------- .../Latency/Internal/HttpLatencyMediator.cs | 13 ++------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs index 060f5d85767..56168809329 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs @@ -24,33 +24,40 @@ internal sealed class HttpClientLatencyLogEnricher : IHttpClientLogEnricher private static readonly ObjectPool _builderPool = PoolFactory.SharedStringBuilderPool; private readonly HttpClientLatencyContext _latencyContext; private readonly HttpLatencyMediator _mediator; + private readonly CheckpointToken _enricherInvoked; - public HttpClientLatencyLogEnricher(HttpClientLatencyContext latencyContext, ILatencyContextTokenIssuer tokenIssuer, + public HttpClientLatencyLogEnricher( + HttpClientLatencyContext latencyContext, + ILatencyContextTokenIssuer tokenIssuer, HttpLatencyMediator mediator) { _latencyContext = latencyContext; _mediator = mediator; + _enricherInvoked = tokenIssuer.GetCheckpointToken(HttpCheckpoints.EnricherInvoked); } - public void Enrich(IEnrichmentTagCollector collector, HttpRequestMessage request, HttpResponseMessage? response, Exception? exception) + public void Enrich(IEnrichmentTagCollector collector, HttpRequestMessage? request, HttpResponseMessage? response, Exception? exception) { - var lc = _latencyContext.Get(); - if (lc == null) + if (response != null) { - return; - } + var lc = _latencyContext.Get(); + if (lc == null) + { + return; + } - // Use the mediator to record metrics - _mediator.RecordRequest(lc, request); - _mediator.RecordException(lc, exception); + lc.AddCheckpoint(_enricherInvoked); + + // Record request and exception data + _mediator.RecordRequest(lc, request); + _mediator.RecordException(lc, exception); - if (response != null) - { // Record response metrics _mediator.RecordResponse(lc, response); StringBuilder stringBuilder = _builderPool.Get(); + // Add serverName to outgoing http logs AppendServerName(response.Headers, stringBuilder); _ = stringBuilder.Append(','); diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs index 3be69d20ae3..b6dba7db1cb 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs @@ -26,18 +26,12 @@ internal class HttpLatencyMediator private readonly TagToken _responseContentType; private readonly TagToken _hasException; - // Checkpoint tokens - private readonly CheckpointToken _enricherInvoked; - /// /// Initializes a new instance of the class. /// /// Token issuer for getting latency tokens. public HttpLatencyMediator(ILatencyContextTokenIssuer tokenIssuer) { - // Initialize checkpoint tokens - _enricherInvoked = tokenIssuer.GetCheckpointToken(HttpCheckpoints.EnricherInvoked); - // Initialize measure tokens _requestContentLength = tokenIssuer.GetMeasureToken("Http.Request.ContentLength"); _responseContentLength = tokenIssuer.GetMeasureToken("Http.Response.ContentLength"); @@ -63,9 +57,6 @@ public virtual void RecordRequest(ILatencyContext context, HttpRequestMessage? r return; } - // Add checkpoint for request processing - context.AddCheckpoint(_enricherInvoked); - // Collect request-related data context.SetTag(_httpMethod, request.Method.Method); @@ -95,7 +86,9 @@ public virtual void RecordResponse(ILatencyContext context, HttpResponseMessage } // Add response-related data with culture-invariant string conversion +#pragma warning disable LA0002 context.SetTag(_httpStatusCode, ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture)); +#pragma warning restore LA0002 // Collect response content type if available if (response.Content?.Headers.ContentType != null) @@ -147,7 +140,7 @@ public virtual void AppendCheckpoints(ILatencyContext context, StringBuilder str _ = stringBuilder.Append(','); for (int i = 0; i < latencyData.Checkpoints.Length; i++) { - var ms = (double)latencyData.Checkpoints[i].Elapsed / latencyData.Checkpoints[i].Frequency * 1000; + var ms = ((double)latencyData.Checkpoints[i].Elapsed / latencyData.Checkpoints[i].Frequency) * 1000; _ = stringBuilder.Append(ms.ToString(CultureInfo.InvariantCulture)); _ = stringBuilder.Append('/'); } From 0ec86fe182b7a2a87c5dac7fc70610a61600f3c4 Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Tue, 9 Sep 2025 15:57:26 +0200 Subject: [PATCH 05/19] update the mediator logic, wip --- .../Internal/HttpClientLatencyLogEnricher.cs | 114 ++++++++++++--- .../Latency/Internal/HttpLatencyMediator.cs | 136 ++---------------- .../Latency/Internal/HttpMeasures.cs | 16 +++ .../Latency/Internal/HttpTags.cs | 14 ++ .../HttpClientLatencyLogEnricherTest.cs | 7 +- 5 files changed, 142 insertions(+), 145 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpMeasures.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpTags.cs diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs index 56168809329..952f84fb7fe 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs @@ -23,16 +23,16 @@ internal sealed class HttpClientLatencyLogEnricher : IHttpClientLogEnricher { private static readonly ObjectPool _builderPool = PoolFactory.SharedStringBuilderPool; private readonly HttpClientLatencyContext _latencyContext; - private readonly HttpLatencyMediator _mediator; + private readonly HttpLatencyMediator _latencyTelemetryMediator; private readonly CheckpointToken _enricherInvoked; public HttpClientLatencyLogEnricher( HttpClientLatencyContext latencyContext, ILatencyContextTokenIssuer tokenIssuer, - HttpLatencyMediator mediator) + HttpLatencyMediator latencyTelemetryMediator) { _latencyContext = latencyContext; - _mediator = mediator; + _latencyTelemetryMediator = latencyTelemetryMediator; _enricherInvoked = tokenIssuer.GetCheckpointToken(HttpCheckpoints.EnricherInvoked); } @@ -46,27 +46,44 @@ public void Enrich(IEnrichmentTagCollector collector, HttpRequestMessage? reques return; } + // Add the checkpoint lc.AddCheckpoint(_enricherInvoked); - // Record request and exception data - _mediator.RecordRequest(lc, request); - _mediator.RecordException(lc, exception); - - // Record response metrics - _mediator.RecordResponse(lc, response); + // Use the mediator to record all metrics + _latencyTelemetryMediator.RecordEnd(lc, request, response); StringBuilder stringBuilder = _builderPool.Get(); - // Add serverName to outgoing http logs - AppendServerName(response.Headers, stringBuilder); - _ = stringBuilder.Append(','); + try + { + /* Add version, serverName, checkpoints, and measures to outgoing http logs. + * Schemas: 1) ServerName,CheckpointName,CheckpointValue + * 2) v1.0,ServerName,TagName,TagValue,CheckpointName,CheckpointValue,MetricName,MetricValue + */ + + // Add version + _ = stringBuilder.Append("v1.0"); + _ = stringBuilder.Append(','); + + // Add server name + AppendServerName(response.Headers, stringBuilder); + _ = stringBuilder.Append(','); + + // Add tags, checkpoints, and measures + AppendTags(lc, stringBuilder); + _ = stringBuilder.Append(','); - // Use mediator to append checkpoint data - _mediator.AppendCheckpoints(lc, stringBuilder); + AppendCheckpoints(lc, stringBuilder); + _ = stringBuilder.Append(','); - collector.Add("LatencyInfo", stringBuilder.ToString()); + AppendMeasures(lc, stringBuilder); - _builderPool.Return(stringBuilder); + collector.Add("LatencyInfo", stringBuilder.ToString()); + } + finally + { + _builderPool.Return(stringBuilder); + } } } @@ -77,4 +94,67 @@ private static void AppendServerName(HttpHeaders headers, StringBuilder stringBu _ = stringBuilder.Append(values.First()); } } -} + + private static void AppendCheckpoints(ILatencyContext latencyContext, StringBuilder stringBuilder) + { + const int MillisecondsPerSecond = 1000; + + var latencyData = latencyContext.LatencyData; + var checkpointCount = latencyData.Checkpoints.Length; + + for (int i = 0; i < checkpointCount; i++) + { + _ = stringBuilder.Append(latencyData.Checkpoints[i].Name); + _ = stringBuilder.Append('/'); + } + + _ = stringBuilder.Append(','); + + for (int i = 0; i < checkpointCount; i++) + { + var cp = latencyData.Checkpoints[i]; + _ = stringBuilder.Append((long)Math.Round(((double)cp.Elapsed / cp.Frequency) * MillisecondsPerSecond)); + _ = stringBuilder.Append('/'); + } + } + + private static void AppendMeasures(ILatencyContext latencyContext, StringBuilder stringBuilder) + { + var latencyData = latencyContext.LatencyData; + var measureCount = latencyData.Measures.Length; + + for (int i = 0; i < measureCount; i++) + { + _ = stringBuilder.Append(latencyData.Measures[i].Name); + _ = stringBuilder.Append('/'); + } + + _ = stringBuilder.Append(','); + + for (int i = 0; i < measureCount; i++) + { + _ = stringBuilder.Append(latencyData.Measures[i].Value); + _ = stringBuilder.Append('/'); + } + } + + private static void AppendTags(ILatencyContext latencyContext, StringBuilder stringBuilder) + { + var latencyData = latencyContext.LatencyData; + var tagCount = latencyData.Tags.Length; + + for (int i = 0; i < tagCount; i++) + { + _ = stringBuilder.Append(latencyData.Tags[i].Name); + _ = stringBuilder.Append('/'); + } + + _ = stringBuilder.Append(','); + + for (int i = 0; i < tagCount; i++) + { + _ = stringBuilder.Append(latencyData.Tags[i].Value); + _ = stringBuilder.Append('/'); + } + } +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs index b6dba7db1cb..a5837dabe94 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs @@ -14,135 +14,27 @@ namespace Microsoft.Extensions.Http.Latency.Internal; /// internal class HttpLatencyMediator { - // Measure tokens - private readonly MeasureToken _requestContentLength; - private readonly MeasureToken _responseContentLength; +#if !NETFRAMEWORK + private readonly MeasureToken _gcPauseTime; +#endif + private readonly TagToken _httpVersionTag; - // Tag tokens - private readonly TagToken _httpMethod; - private readonly TagToken _httpStatusCode; - private readonly TagToken _requestHost; - private readonly TagToken _requestPath; - private readonly TagToken _responseContentType; - private readonly TagToken _hasException; - - /// - /// Initializes a new instance of the class. - /// - /// Token issuer for getting latency tokens. public HttpLatencyMediator(ILatencyContextTokenIssuer tokenIssuer) { - // Initialize measure tokens - _requestContentLength = tokenIssuer.GetMeasureToken("Http.Request.ContentLength"); - _responseContentLength = tokenIssuer.GetMeasureToken("Http.Response.ContentLength"); - - // Initialize tag tokens - _httpMethod = tokenIssuer.GetTagToken("Http.Method"); - _httpStatusCode = tokenIssuer.GetTagToken("Http.StatusCode"); - _requestHost = tokenIssuer.GetTagToken("Http.Request.Host"); - _requestPath = tokenIssuer.GetTagToken("Http.Request.Path"); - _responseContentType = tokenIssuer.GetTagToken("Http.Response.ContentType"); - _hasException = tokenIssuer.GetTagToken("Http.HasException"); - } - - /// - /// Records HTTP request information in the latency context. - /// - /// The latency context to update. - /// The HTTP request message. - public virtual void RecordRequest(ILatencyContext context, HttpRequestMessage? request) - { - if (context == null || request == null) - { - return; - } - - // Collect request-related data - context.SetTag(_httpMethod, request.Method.Method); - - if (request.RequestUri != null) - { - context.SetTag(_requestHost, request.RequestUri.Host); - context.SetTag(_requestPath, request.RequestUri.AbsolutePath); - } - - // Collect request content length if available - if (request.Content?.Headers.ContentLength.HasValue == true) - { - context.RecordMeasure(_requestContentLength, request.Content.Headers.ContentLength.Value); - } - } - - /// - /// Records HTTP response information in the latency context. - /// - /// The latency context to update. - /// The HTTP response message. - public virtual void RecordResponse(ILatencyContext context, HttpResponseMessage response) - { - if (context == null || response == null) - { - return; - } - - // Add response-related data with culture-invariant string conversion -#pragma warning disable LA0002 - context.SetTag(_httpStatusCode, ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture)); -#pragma warning restore LA0002 - - // Collect response content type if available - if (response.Content?.Headers.ContentType != null) - { - context.SetTag(_responseContentType, response.Content.Headers.ContentType.MediaType); - } - - // Collect response content length if available - if (response.Content?.Headers.ContentLength.HasValue == true) - { - context.RecordMeasure(_responseContentLength, response.Content.Headers.ContentLength.Value); - } - } - - /// - /// Records exception information in the latency context. - /// - /// The latency context to update. - /// The exception that occurred, if any. - public virtual void RecordException(ILatencyContext context, Exception? exception) - { - if (context == null) - { - return; - } - - context.SetTag(_hasException, exception != null ? "true" : "false"); +#if !NETFRAMEWORK + _gcPauseTime = tokenIssuer.GetMeasureToken(HttpMeasures.GCPauseTime); +#endif + _httpVersionTag = tokenIssuer.GetTagToken(HttpTags.HttpVersion); } - /// - /// Appends checkpoint data to the provided string builder. - /// - /// The latency context containing checkpoint data. - /// The string builder to append data to. - public virtual void AppendCheckpoints(ILatencyContext context, StringBuilder stringBuilder) + public void RecordEnd(ILatencyContext latencyContext, HttpRequestMessage? request = null, HttpResponseMessage? response = null) { - if (context == null || stringBuilder == null) - { - return; - } - - var latencyData = context.LatencyData; - for (int i = 0; i < latencyData.Checkpoints.Length; i++) - { - _ = stringBuilder.Append(latencyData.Checkpoints[i].Name); - _ = stringBuilder.Append('/'); - } - - _ = stringBuilder.Append(','); - for (int i = 0; i < latencyData.Checkpoints.Length; i++) +#if NET + latencyContext.AddMeasure(_gcPauseTime, (long)GC.GetTotalPauseDuration().TotalMilliseconds); +#endif + if (response != null) { - var ms = ((double)latencyData.Checkpoints[i].Elapsed / latencyData.Checkpoints[i].Frequency) * 1000; - _ = stringBuilder.Append(ms.ToString(CultureInfo.InvariantCulture)); - _ = stringBuilder.Append('/'); + latencyContext.SetTag(_httpVersionTag, response.Version.ToString()); } } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpMeasures.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpMeasures.cs new file mode 100644 index 00000000000..adb086a99cc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpMeasures.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Latency.Internal; + +internal static class HttpMeasures +{ + public const string GCPauseTime = "gcp"; + public const string ConnectionInitiated = "coni"; + + public static readonly string[] Measures = + [ + GCPauseTime, + ConnectionInitiated + ]; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpTags.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpTags.cs new file mode 100644 index 00000000000..dd15191e22d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpTags.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Latency.Internal; + +internal static class HttpTags +{ + public const string HttpVersion = "httpver"; + + public static readonly string[] Tags = + [ + HttpVersion + ]; +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs index 30f9f0ae923..5d380060f4a 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs @@ -3,7 +3,6 @@ using System; using System.Net.Http; -using System.Text; using Microsoft.Extensions.Diagnostics.Enrichment; using Microsoft.Extensions.Diagnostics.Latency; using Microsoft.Extensions.Http.Diagnostics; @@ -46,8 +45,6 @@ public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithoutHeader() context.Set(lc.Object); using HttpResponseMessage httpResponseMessage = new(); - mockMediator.Setup(m => m.AppendCheckpoints(It.IsAny(), It.IsAny())) - .Callback((_, sb) => sb.Append("a/b")); var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mockMediator.Object); Mock mockEnrichmentPropertyBag = new Mock(); @@ -71,9 +68,7 @@ public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithHeader() using HttpResponseMessage httpResponseMessage = new(); string serverName = "serverNameVal"; httpResponseMessage.Headers.Add(TelemetryConstants.ServerApplicationNameHeader, serverName); - mockMediator.Setup(m => m.AppendCheckpoints(It.IsAny(), It.IsAny())) - .Callback((_, sb) => sb.Append("a/b")); - + var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mockMediator.Object); Mock mockEnrichmentPropertyBag = new Mock(); From 932b361fa78b6322dc9839538ea2208c7e069357 Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Tue, 9 Sep 2025 17:25:15 +0200 Subject: [PATCH 06/19] update mediator usage --- .../Latency/Internal/HttpLatencyMediator.cs | 11 +++++++++-- .../Latency/Internal/HttpLatencyTelemetryHandler.cs | 6 +++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs index a5837dabe94..544837a4b5f 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Globalization; using System.Net.Http; -using System.Text; using Microsoft.Extensions.Diagnostics.Latency; namespace Microsoft.Extensions.Http.Latency.Internal; @@ -27,6 +25,15 @@ public HttpLatencyMediator(ILatencyContextTokenIssuer tokenIssuer) _httpVersionTag = tokenIssuer.GetTagToken(HttpTags.HttpVersion); } +#pragma warning disable CA1822 + public void RecordStart(ILatencyContext latencyContext, HttpRequestMessage? request = null, HttpResponseMessage? response = null) + { +#pragma warning restore CA1822 +#if NET + latencyContext.RecordMeasure(_gcPauseTime, (long)GC.GetTotalPauseDuration().TotalMilliseconds * -1L); +#endif + } + public void RecordEnd(ILatencyContext latencyContext, HttpRequestMessage? request = null, HttpResponseMessage? response = null) { #if NET diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs index 3180cb890c6..2bc2d7b9131 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs @@ -21,14 +21,16 @@ internal sealed class HttpLatencyTelemetryHandler : DelegatingHandler private readonly ILatencyContextProvider _latencyContextProvider; private readonly CheckpointToken _handlerStart; private readonly string _applicationName; + private readonly HttpLatencyMediator _latencyMediator; public HttpLatencyTelemetryHandler(HttpRequestLatencyListener latencyListener, ILatencyContextTokenIssuer tokenIssuer, ILatencyContextProvider latencyContextProvider, - IOptions options, IOptions appMetadata) + IOptions options, IOptions appMetadata, HttpLatencyMediator latencyTelemetryMediator) { _latencyListener = latencyListener; _latencyContextProvider = latencyContextProvider; _handlerStart = tokenIssuer.GetCheckpointToken(HttpCheckpoints.HandlerRequestStart); _applicationName = appMetadata.Value.ApplicationName; + _latencyMediator = latencyTelemetryMediator; if (options.Value.EnableDetailedLatencyBreakdown) { @@ -42,6 +44,8 @@ protected async override Task SendAsync(HttpRequestMessage context.AddCheckpoint(_handlerStart); _latencyListener.LatencyContext.Set(context); + _latencyMediator.RecordStart(context, request); + _ = request.Headers.TryAddWithoutValidation(TelemetryConstants.ClientApplicationNameHeader, _applicationName); return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); From 9c305696c400aaa87a006859b95c07d310a0ed8c Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Thu, 11 Sep 2025 14:03:58 +0200 Subject: [PATCH 07/19] latency mediator interface --- .../Latency/Internal/HttpLatencyMediator.cs | 2 +- .../Latency/Internal/IHttpLatencyMediator.cs | 36 +++++++++++++++++++ .../HttpClientLatencyLogEnricherTest.cs | 2 +- .../HttpLatencyTelemetryHandlerTest.cs | 9 +++-- 4 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/IHttpLatencyMediator.cs diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs index 544837a4b5f..c328f585510 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.Http.Latency.Internal; /// /// Mediator for HTTP latency operations that coordinates recording HTTP metrics in a latency context. /// -internal class HttpLatencyMediator +internal sealed class HttpLatencyMediator { #if !NETFRAMEWORK private readonly MeasureToken _gcPauseTime; diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/IHttpLatencyMediator.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/IHttpLatencyMediator.cs new file mode 100644 index 00000000000..a261779cc8a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/IHttpLatencyMediator.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Text; +using Microsoft.Extensions.Diagnostics.Latency; + +namespace Microsoft.Extensions.Http.Latency.Internal; + +/// +/// Interface for mediating HTTP latency operations that coordinates recording HTTP metrics in a latency context. +/// +internal interface IHttpLatencyMediator +{ + /// + /// Records the start of an HTTP request in the latency context. + /// + /// The latency context to update. + /// The HTTP request message. + void RecordStart(ILatencyContext context, HttpRequestMessage request); + + /// + /// Records the end of an HTTP request/response cycle in the latency context. + /// + /// The latency context to update. + /// The HTTP request message (optional if already recorded in RecordStart). + /// The HTTP response message. + void RecordEnd(ILatencyContext context, HttpRequestMessage? request = null, HttpResponseMessage? response = null); + + /// + /// Appends checkpoint data to the provided string builder. + /// + /// The latency context containing checkpoint data. + /// The string builder to append data to. + void AppendCheckpoints(ILatencyContext context, StringBuilder stringBuilder); +} \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs index 5d380060f4a..d14d78fb153 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs @@ -68,7 +68,7 @@ public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithHeader() using HttpResponseMessage httpResponseMessage = new(); string serverName = "serverNameVal"; httpResponseMessage.Headers.Add(TelemetryConstants.ServerApplicationNameHeader, serverName); - + var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mockMediator.Object); Mock mockEnrichmentPropertyBag = new Mock(); diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs index 2f787284b26..70edf075665 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs @@ -32,8 +32,9 @@ public void HttpLatencyTelemetryHandler_InvokesTokenIssuer() var lcti = HttpMockProvider.GetTokenIssuer(); var lcti2 = HttpMockProvider.GetTokenIssuer(); + var mockMediator = new Mock(lcti2.Object); using var listener = HttpMockProvider.GetListener(context, lcti.Object); - using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object); + using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object, mockMediator.Object); lcti2.Verify(a => a.GetCheckpointToken(It.Is(s => !HttpCheckpoints.Checkpoints.Contains(s))), Times.Never); lcti2.Verify(a => a.GetCheckpointToken(It.Is(s => HttpCheckpoints.Checkpoints.Contains(s)))); @@ -53,6 +54,7 @@ public async Task HttpLatencyTelemetryHandler_SetsLatencyContext() var lcti = HttpMockProvider.GetTokenIssuer(); var lcti2 = HttpMockProvider.GetTokenIssuer(); + var mockMediator = new Mock(lcti2.Object); using var listener = HttpMockProvider.GetListener(context, lcti.Object); using var req = new HttpRequestMessage { @@ -71,7 +73,7 @@ public async Task HttpLatencyTelemetryHandler_SetsLatencyContext() Assert.True(req.Headers.Contains(TelemetryConstants.ClientApplicationNameHeader)); }).Returns(Task.FromResult(resp.Object)); - using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object) + using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object, mockMediator.Object) { InnerHandler = mockHandler.Object }; @@ -93,8 +95,9 @@ public void HttpLatencyTelemetryHandler_IfDetailsDisabled_DoesNotEnableListener( hop.Setup(a => a.Value).Returns(new HttpClientLatencyTelemetryOptions { EnableDetailedLatencyBreakdown = false }); var lcti = HttpMockProvider.GetTokenIssuer(); + var mockMediator = new Mock(lcti.Object); using var listener = HttpMockProvider.GetListener(context, lcti.Object); - using var handler = new HttpLatencyTelemetryHandler(listener, lcti.Object, lcp.Object, hop.Object, sop.Object); + using var handler = new HttpLatencyTelemetryHandler(listener, lcti.Object, lcp.Object, hop.Object, sop.Object, mockMediator.Object); Assert.False(listener.Enabled); } } From 6e3af1c2e57eecc8650be863f0773f67dcbecb34 Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Thu, 11 Sep 2025 14:12:23 +0200 Subject: [PATCH 08/19] fix build and types --- .../Latency/HttpClientLatencyTelemetryExtensions.cs | 2 +- .../Latency/Internal/HttpClientLatencyLogEnricher.cs | 8 ++++---- .../Latency/Internal/HttpLatencyMediator.cs | 4 ++-- .../Latency/Internal/HttpLatencyTelemetryHandler.cs | 4 ++-- .../Latency/Internal/IHttpLatencyMediator.cs | 11 ++--------- .../Internal/HttpLatencyTelemetryHandlerTest.cs | 6 +++--- 6 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs index 747326f4b8c..b95decc29f7 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs @@ -32,7 +32,7 @@ public static IServiceCollection AddHttpClientLatencyTelemetry(this IServiceColl _ = services.AddOptions(); _ = services.AddSingleton(); _ = services.AddSingleton(); - _ = services.AddSingleton(); + _ = services.AddSingleton(); _ = services.AddTransient(); _ = services.AddHttpClientLogEnricher(); diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs index 952f84fb7fe..99c26aeb664 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs @@ -23,16 +23,16 @@ internal sealed class HttpClientLatencyLogEnricher : IHttpClientLogEnricher { private static readonly ObjectPool _builderPool = PoolFactory.SharedStringBuilderPool; private readonly HttpClientLatencyContext _latencyContext; - private readonly HttpLatencyMediator _latencyTelemetryMediator; + private readonly IHttpLatencyMediator _httpLatencyMediator; private readonly CheckpointToken _enricherInvoked; public HttpClientLatencyLogEnricher( HttpClientLatencyContext latencyContext, ILatencyContextTokenIssuer tokenIssuer, - HttpLatencyMediator latencyTelemetryMediator) + IHttpLatencyMediator httpLatencyMediator) { _latencyContext = latencyContext; - _latencyTelemetryMediator = latencyTelemetryMediator; + _httpLatencyMediator = httpLatencyMediator; _enricherInvoked = tokenIssuer.GetCheckpointToken(HttpCheckpoints.EnricherInvoked); } @@ -50,7 +50,7 @@ public void Enrich(IEnrichmentTagCollector collector, HttpRequestMessage? reques lc.AddCheckpoint(_enricherInvoked); // Use the mediator to record all metrics - _latencyTelemetryMediator.RecordEnd(lc, request, response); + _httpLatencyMediator.RecordEnd(lc, request, response); StringBuilder stringBuilder = _builderPool.Get(); diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs index c328f585510..b4a80e9bbb5 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.Http.Latency.Internal; /// /// Mediator for HTTP latency operations that coordinates recording HTTP metrics in a latency context. /// -internal sealed class HttpLatencyMediator +internal sealed class HttpLatencyMediator : IHttpLatencyMediator { #if !NETFRAMEWORK private readonly MeasureToken _gcPauseTime; @@ -26,7 +26,7 @@ public HttpLatencyMediator(ILatencyContextTokenIssuer tokenIssuer) } #pragma warning disable CA1822 - public void RecordStart(ILatencyContext latencyContext, HttpRequestMessage? request = null, HttpResponseMessage? response = null) + public void RecordStart(ILatencyContext latencyContext, HttpRequestMessage? request = null) { #pragma warning restore CA1822 #if NET diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs index 2bc2d7b9131..06a07d53d1f 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs @@ -21,10 +21,10 @@ internal sealed class HttpLatencyTelemetryHandler : DelegatingHandler private readonly ILatencyContextProvider _latencyContextProvider; private readonly CheckpointToken _handlerStart; private readonly string _applicationName; - private readonly HttpLatencyMediator _latencyMediator; + private readonly IHttpLatencyMediator _latencyMediator; public HttpLatencyTelemetryHandler(HttpRequestLatencyListener latencyListener, ILatencyContextTokenIssuer tokenIssuer, ILatencyContextProvider latencyContextProvider, - IOptions options, IOptions appMetadata, HttpLatencyMediator latencyTelemetryMediator) + IOptions options, IOptions appMetadata, IHttpLatencyMediator latencyTelemetryMediator) { _latencyListener = latencyListener; _latencyContextProvider = latencyContextProvider; diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/IHttpLatencyMediator.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/IHttpLatencyMediator.cs index a261779cc8a..2e7987f980a 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/IHttpLatencyMediator.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/IHttpLatencyMediator.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; -using System.Text; using Microsoft.Extensions.Diagnostics.Latency; namespace Microsoft.Extensions.Http.Latency.Internal; @@ -17,7 +16,8 @@ internal interface IHttpLatencyMediator /// /// The latency context to update. /// The HTTP request message. - void RecordStart(ILatencyContext context, HttpRequestMessage request); + /// The HTTP response message. + void RecordStart(ILatencyContext context, HttpRequestMessage? request); /// /// Records the end of an HTTP request/response cycle in the latency context. @@ -26,11 +26,4 @@ internal interface IHttpLatencyMediator /// The HTTP request message (optional if already recorded in RecordStart). /// The HTTP response message. void RecordEnd(ILatencyContext context, HttpRequestMessage? request = null, HttpResponseMessage? response = null); - - /// - /// Appends checkpoint data to the provided string builder. - /// - /// The latency context containing checkpoint data. - /// The string builder to append data to. - void AppendCheckpoints(ILatencyContext context, StringBuilder stringBuilder); } \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs index 70edf075665..9f41dd9a1c6 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs @@ -32,7 +32,7 @@ public void HttpLatencyTelemetryHandler_InvokesTokenIssuer() var lcti = HttpMockProvider.GetTokenIssuer(); var lcti2 = HttpMockProvider.GetTokenIssuer(); - var mockMediator = new Mock(lcti2.Object); + var mockMediator = new Mock(lcti2.Object); using var listener = HttpMockProvider.GetListener(context, lcti.Object); using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object, mockMediator.Object); @@ -54,7 +54,7 @@ public async Task HttpLatencyTelemetryHandler_SetsLatencyContext() var lcti = HttpMockProvider.GetTokenIssuer(); var lcti2 = HttpMockProvider.GetTokenIssuer(); - var mockMediator = new Mock(lcti2.Object); + var mockMediator = new Mock(lcti2.Object); using var listener = HttpMockProvider.GetListener(context, lcti.Object); using var req = new HttpRequestMessage { @@ -95,7 +95,7 @@ public void HttpLatencyTelemetryHandler_IfDetailsDisabled_DoesNotEnableListener( hop.Setup(a => a.Value).Returns(new HttpClientLatencyTelemetryOptions { EnableDetailedLatencyBreakdown = false }); var lcti = HttpMockProvider.GetTokenIssuer(); - var mockMediator = new Mock(lcti.Object); + var mockMediator = new Mock(lcti.Object); using var listener = HttpMockProvider.GetListener(context, lcti.Object); using var handler = new HttpLatencyTelemetryHandler(listener, lcti.Object, lcp.Object, hop.Object, sop.Object, mockMediator.Object); Assert.False(listener.Enabled); From 0afc077878761e6f155e95c2e6daa211bb982860 Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Thu, 11 Sep 2025 15:52:37 +0200 Subject: [PATCH 09/19] fix mocks in tests --- .../Internal/HttpClientLatencyLogEnricherTest.cs | 12 ++++++------ .../Internal/HttpLatencyTelemetryHandlerTest.cs | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs index d14d78fb153..77ccec704d4 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs @@ -18,7 +18,7 @@ public class HttpClientLatencyLogEnricherTest public void HttpClientLatencyLogEnricher_NoOp_OnRequest() { var lcti = HttpMockProvider.GetTokenIssuer(); - var mockMediator = new Mock(lcti.Object); + var mediator = new HttpLatencyMediator(lcti.Object); var checkpoints = new ArraySegment(new[] { new Checkpoint("a", default, default), new Checkpoint("b", default, default) }); var ld = new LatencyData(default, checkpoints, default, default, default); var lc = HttpMockProvider.GetLatencyContext(); @@ -26,7 +26,7 @@ public void HttpClientLatencyLogEnricher_NoOp_OnRequest() var context = new HttpClientLatencyContext(); context.Set(lc.Object); - var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mockMediator.Object); + var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mediator); Mock mockEnrichmentPropertyBag = new Mock(); enricher.Enrich(mockEnrichmentPropertyBag.Object, null!, null, null); mockEnrichmentPropertyBag.Verify(m => m.Add(It.IsAny(), It.IsAny()), Times.Never); @@ -36,7 +36,7 @@ public void HttpClientLatencyLogEnricher_NoOp_OnRequest() public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithoutHeader() { var lcti = HttpMockProvider.GetTokenIssuer(); - var mockMediator = new Mock(lcti.Object); + var mediator = new HttpLatencyMediator(lcti.Object); var checkpoints = new ArraySegment(new[] { new Checkpoint("a", default, default), new Checkpoint("b", default, default) }); var ld = new LatencyData(default, checkpoints, default, default, default); var lc = HttpMockProvider.GetLatencyContext(); @@ -46,7 +46,7 @@ public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithoutHeader() using HttpResponseMessage httpResponseMessage = new(); - var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mockMediator.Object); + var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mediator); Mock mockEnrichmentPropertyBag = new Mock(); enricher.Enrich(mockEnrichmentPropertyBag.Object, null!, httpResponseMessage, null); @@ -57,7 +57,7 @@ public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithoutHeader() public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithHeader() { var lcti = HttpMockProvider.GetTokenIssuer(); - var mockMediator = new Mock(lcti.Object); + var mediator = new HttpLatencyMediator(lcti.Object); var checkpoints = new ArraySegment(new[] { new Checkpoint("a", default, default), new Checkpoint("b", default, default) }); var ld = new LatencyData(default, checkpoints, default, default, default); var lc = HttpMockProvider.GetLatencyContext(); @@ -69,7 +69,7 @@ public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithHeader() string serverName = "serverNameVal"; httpResponseMessage.Headers.Add(TelemetryConstants.ServerApplicationNameHeader, serverName); - var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mockMediator.Object); + var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mediator); Mock mockEnrichmentPropertyBag = new Mock(); enricher.Enrich(mockEnrichmentPropertyBag.Object, null!, httpResponseMessage, null); diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs index 9f41dd9a1c6..3c67819af13 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs @@ -32,9 +32,9 @@ public void HttpLatencyTelemetryHandler_InvokesTokenIssuer() var lcti = HttpMockProvider.GetTokenIssuer(); var lcti2 = HttpMockProvider.GetTokenIssuer(); - var mockMediator = new Mock(lcti2.Object); + var mediator = new HttpLatencyMediator(lcti2.Object); using var listener = HttpMockProvider.GetListener(context, lcti.Object); - using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object, mockMediator.Object); + using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object, mediator); lcti2.Verify(a => a.GetCheckpointToken(It.Is(s => !HttpCheckpoints.Checkpoints.Contains(s))), Times.Never); lcti2.Verify(a => a.GetCheckpointToken(It.Is(s => HttpCheckpoints.Checkpoints.Contains(s)))); @@ -54,7 +54,7 @@ public async Task HttpLatencyTelemetryHandler_SetsLatencyContext() var lcti = HttpMockProvider.GetTokenIssuer(); var lcti2 = HttpMockProvider.GetTokenIssuer(); - var mockMediator = new Mock(lcti2.Object); + var mediator = new HttpLatencyMediator(lcti2.Object); using var listener = HttpMockProvider.GetListener(context, lcti.Object); using var req = new HttpRequestMessage { @@ -73,7 +73,7 @@ public async Task HttpLatencyTelemetryHandler_SetsLatencyContext() Assert.True(req.Headers.Contains(TelemetryConstants.ClientApplicationNameHeader)); }).Returns(Task.FromResult(resp.Object)); - using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object, mockMediator.Object) + using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object, mediator) { InnerHandler = mockHandler.Object }; @@ -95,9 +95,9 @@ public void HttpLatencyTelemetryHandler_IfDetailsDisabled_DoesNotEnableListener( hop.Setup(a => a.Value).Returns(new HttpClientLatencyTelemetryOptions { EnableDetailedLatencyBreakdown = false }); var lcti = HttpMockProvider.GetTokenIssuer(); - var mockMediator = new Mock(lcti.Object); + var mediator = new HttpLatencyMediator(lcti.Object); using var listener = HttpMockProvider.GetListener(context, lcti.Object); - using var handler = new HttpLatencyTelemetryHandler(listener, lcti.Object, lcp.Object, hop.Object, sop.Object, mockMediator.Object); + using var handler = new HttpLatencyTelemetryHandler(listener, lcti.Object, lcp.Object, hop.Object, sop.Object, mediator); Assert.False(listener.Enabled); } } From 8628605dae70f4869ad61abb2b5fe2d7da80defc Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Fri, 12 Sep 2025 11:10:40 +0200 Subject: [PATCH 10/19] update optional compilation --- .../HttpClientLatencyTelemetryExtensions.cs | 1 - .../Internal/HttpClientLatencyLogEnricher.cs | 18 ++++++++++--- .../Latency/Internal/HttpLatencyMediator.cs | 25 ++++++----------- .../Internal/HttpLatencyMediator.netfx.cs | 27 +++++++++++++++++++ .../Internal/HttpLatencyTelemetryHandler.cs | 15 +++++++++-- .../Latency/Internal/IHttpLatencyMediator.cs | 4 +-- .../HttpLatencyTelemetryHandlerTest.cs | 4 +++ 7 files changed, 67 insertions(+), 27 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.netfx.cs diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs index b95decc29f7..fa77360a8c1 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs @@ -32,7 +32,6 @@ public static IServiceCollection AddHttpClientLatencyTelemetry(this IServiceColl _ = services.AddOptions(); _ = services.AddSingleton(); _ = services.AddSingleton(); - _ = services.AddSingleton(); _ = services.AddTransient(); _ = services.AddHttpClientLogEnricher(); diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs index 99c26aeb664..e976dc804cf 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs @@ -23,16 +23,24 @@ internal sealed class HttpClientLatencyLogEnricher : IHttpClientLogEnricher { private static readonly ObjectPool _builderPool = PoolFactory.SharedStringBuilderPool; private readonly HttpClientLatencyContext _latencyContext; - private readonly IHttpLatencyMediator _httpLatencyMediator; +#if NET + + private readonly HttpLatencyMediator _httpLatencyMediator; +#endif private readonly CheckpointToken _enricherInvoked; public HttpClientLatencyLogEnricher( HttpClientLatencyContext latencyContext, - ILatencyContextTokenIssuer tokenIssuer, - IHttpLatencyMediator httpLatencyMediator) + ILatencyContextTokenIssuer tokenIssuer +#if NET + , HttpLatencyMediator httpLatencyMediator +#endif + ) { _latencyContext = latencyContext; +#if NET _httpLatencyMediator = httpLatencyMediator; +#endif _enricherInvoked = tokenIssuer.GetCheckpointToken(HttpCheckpoints.EnricherInvoked); } @@ -49,8 +57,10 @@ public void Enrich(IEnrichmentTagCollector collector, HttpRequestMessage? reques // Add the checkpoint lc.AddCheckpoint(_enricherInvoked); +#if NET // Use the mediator to record all metrics - _httpLatencyMediator.RecordEnd(lc, request, response); + _httpLatencyMediator.RecordEnd(lc, response); +#endif StringBuilder stringBuilder = _builderPool.Get(); diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs index b4a80e9bbb5..25543ad9c5a 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; +#if NET using System.Net.Http; using Microsoft.Extensions.Diagnostics.Latency; @@ -10,39 +10,30 @@ namespace Microsoft.Extensions.Http.Latency.Internal; /// /// Mediator for HTTP latency operations that coordinates recording HTTP metrics in a latency context. /// -internal sealed class HttpLatencyMediator : IHttpLatencyMediator +internal sealed class HttpLatencyMediator { -#if !NETFRAMEWORK private readonly MeasureToken _gcPauseTime; -#endif private readonly TagToken _httpVersionTag; public HttpLatencyMediator(ILatencyContextTokenIssuer tokenIssuer) { -#if !NETFRAMEWORK _gcPauseTime = tokenIssuer.GetMeasureToken(HttpMeasures.GCPauseTime); -#endif _httpVersionTag = tokenIssuer.GetTagToken(HttpTags.HttpVersion); } -#pragma warning disable CA1822 - public void RecordStart(ILatencyContext latencyContext, HttpRequestMessage? request = null) + public void RecordStart(ILatencyContext latencyContext) { -#pragma warning restore CA1822 -#if NET - latencyContext.RecordMeasure(_gcPauseTime, (long)GC.GetTotalPauseDuration().TotalMilliseconds * -1L); -#endif + latencyContext.RecordMeasure(_gcPauseTime, (long)System.GC.GetTotalPauseDuration().TotalMilliseconds * -1L); } - public void RecordEnd(ILatencyContext latencyContext, HttpRequestMessage? request = null, HttpResponseMessage? response = null) + public void RecordEnd(ILatencyContext latencyContext, HttpResponseMessage? response = null) { -#if NET - latencyContext.AddMeasure(_gcPauseTime, (long)GC.GetTotalPauseDuration().TotalMilliseconds); -#endif + latencyContext.AddMeasure(_gcPauseTime, (long)System.GC.GetTotalPauseDuration().TotalMilliseconds); + if (response != null) { latencyContext.SetTag(_httpVersionTag, response.Version.ToString()); } } } - +#endif diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.netfx.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.netfx.cs new file mode 100644 index 00000000000..b3143ed1b8e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.netfx.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.Extensions.Diagnostics.Latency; + +#if NETFRAMEWORK +namespace Microsoft.Extensions.Http.Latency.Internal; + +internal sealed class HttpLatencyMediator +{ + private readonly TagToken _httpVersionTag; + + public HttpLatencyMediator(ILatencyContextTokenIssuer tokenIssuer) + { + _httpVersionTag = tokenIssuer.GetTagToken(HttpTags.HttpVersion); + } + + public void RecordEnd(ILatencyContext latencyContext, HttpResponseMessage? response = null) + { + if (response != null) + { + latencyContext?.SetTag(_httpVersionTag, response.Version.ToString()); + } + } +} +#endif \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs index 06a07d53d1f..83abef1d8d7 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs @@ -21,16 +21,25 @@ internal sealed class HttpLatencyTelemetryHandler : DelegatingHandler private readonly ILatencyContextProvider _latencyContextProvider; private readonly CheckpointToken _handlerStart; private readonly string _applicationName; - private readonly IHttpLatencyMediator _latencyMediator; + +#if NET + private readonly HttpLatencyMediator _latencyMediator; +#endif public HttpLatencyTelemetryHandler(HttpRequestLatencyListener latencyListener, ILatencyContextTokenIssuer tokenIssuer, ILatencyContextProvider latencyContextProvider, - IOptions options, IOptions appMetadata, IHttpLatencyMediator latencyTelemetryMediator) + IOptions options, IOptions appMetadata +#if NET + , HttpLatencyMediator latencyTelemetryMediator +#endif + ) { _latencyListener = latencyListener; _latencyContextProvider = latencyContextProvider; _handlerStart = tokenIssuer.GetCheckpointToken(HttpCheckpoints.HandlerRequestStart); _applicationName = appMetadata.Value.ApplicationName; +#if NET _latencyMediator = latencyTelemetryMediator; +#endif if (options.Value.EnableDetailedLatencyBreakdown) { @@ -44,7 +53,9 @@ protected async override Task SendAsync(HttpRequestMessage context.AddCheckpoint(_handlerStart); _latencyListener.LatencyContext.Set(context); +#if NET _latencyMediator.RecordStart(context, request); +#endif _ = request.Headers.TryAddWithoutValidation(TelemetryConstants.ClientApplicationNameHeader, _applicationName); diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/IHttpLatencyMediator.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/IHttpLatencyMediator.cs index 2e7987f980a..2c342f865c3 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/IHttpLatencyMediator.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/IHttpLatencyMediator.cs @@ -16,14 +16,12 @@ internal interface IHttpLatencyMediator /// /// The latency context to update. /// The HTTP request message. - /// The HTTP response message. void RecordStart(ILatencyContext context, HttpRequestMessage? request); /// /// Records the end of an HTTP request/response cycle in the latency context. /// /// The latency context to update. - /// The HTTP request message (optional if already recorded in RecordStart). /// The HTTP response message. - void RecordEnd(ILatencyContext context, HttpRequestMessage? request = null, HttpResponseMessage? response = null); + void RecordEnd(ILatencyContext context, HttpResponseMessage? response = null); } \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs index 3c67819af13..59b4ded55b3 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs @@ -34,7 +34,11 @@ public void HttpLatencyTelemetryHandler_InvokesTokenIssuer() var mediator = new HttpLatencyMediator(lcti2.Object); using var listener = HttpMockProvider.GetListener(context, lcti.Object); +#if NET using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object, mediator); + #else + +#endif lcti2.Verify(a => a.GetCheckpointToken(It.Is(s => !HttpCheckpoints.Checkpoints.Contains(s))), Times.Never); lcti2.Verify(a => a.GetCheckpointToken(It.Is(s => HttpCheckpoints.Checkpoints.Contains(s)))); From 879cd3ee6b7f79f950f289548c73df5634e59d31 Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Fri, 12 Sep 2025 13:27:17 +0200 Subject: [PATCH 11/19] resolve build errors --- .../Internal/HttpLatencyMediator.netfx.cs | 2 +- .../Internal/HttpLatencyTelemetryHandler.cs | 2 +- .../HttpClientLatencyLogEnricherTest.cs | 12 ++++++++++++ .../Internal/HttpLatencyTelemetryHandlerTest.cs | 17 +++++++++++++++-- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.netfx.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.netfx.cs index b3143ed1b8e..2402cd39064 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.netfx.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.netfx.cs @@ -1,10 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if NETFRAMEWORK using System.Net.Http; using Microsoft.Extensions.Diagnostics.Latency; -#if NETFRAMEWORK namespace Microsoft.Extensions.Http.Latency.Internal; internal sealed class HttpLatencyMediator diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs index 83abef1d8d7..c6912972d8b 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs @@ -54,7 +54,7 @@ protected async override Task SendAsync(HttpRequestMessage _latencyListener.LatencyContext.Set(context); #if NET - _latencyMediator.RecordStart(context, request); + _latencyMediator.RecordStart(context); #endif _ = request.Headers.TryAddWithoutValidation(TelemetryConstants.ClientApplicationNameHeader, _applicationName); diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs index 77ccec704d4..41c989173ce 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs @@ -26,7 +26,11 @@ public void HttpClientLatencyLogEnricher_NoOp_OnRequest() var context = new HttpClientLatencyContext(); context.Set(lc.Object); +#if NET var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mediator); +#else + var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object); +#endif Mock mockEnrichmentPropertyBag = new Mock(); enricher.Enrich(mockEnrichmentPropertyBag.Object, null!, null, null); mockEnrichmentPropertyBag.Verify(m => m.Add(It.IsAny(), It.IsAny()), Times.Never); @@ -46,7 +50,11 @@ public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithoutHeader() using HttpResponseMessage httpResponseMessage = new(); +#if NET var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mediator); +#else + var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object); +#endif Mock mockEnrichmentPropertyBag = new Mock(); enricher.Enrich(mockEnrichmentPropertyBag.Object, null!, httpResponseMessage, null); @@ -69,7 +77,11 @@ public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithHeader() string serverName = "serverNameVal"; httpResponseMessage.Headers.Add(TelemetryConstants.ServerApplicationNameHeader, serverName); +#if NET var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mediator); +#else + var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object); +#endif Mock mockEnrichmentPropertyBag = new Mock(); enricher.Enrich(mockEnrichmentPropertyBag.Object, null!, httpResponseMessage, null); diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs index 59b4ded55b3..2946dde0db1 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs @@ -36,8 +36,8 @@ public void HttpLatencyTelemetryHandler_InvokesTokenIssuer() using var listener = HttpMockProvider.GetListener(context, lcti.Object); #if NET using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object, mediator); - #else - +#else + using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object); #endif lcti2.Verify(a => a.GetCheckpointToken(It.Is(s => !HttpCheckpoints.Checkpoints.Contains(s))), Times.Never); @@ -77,10 +77,17 @@ public async Task HttpLatencyTelemetryHandler_SetsLatencyContext() Assert.True(req.Headers.Contains(TelemetryConstants.ClientApplicationNameHeader)); }).Returns(Task.FromResult(resp.Object)); +#if NET using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object, mediator) { InnerHandler = mockHandler.Object }; +#else + using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object) + { + InnerHandler = mockHandler.Object + }; +#endif using var client = new System.Net.Http.HttpClient(handler); await client.SendAsync(req, It.IsAny()); @@ -101,7 +108,13 @@ public void HttpLatencyTelemetryHandler_IfDetailsDisabled_DoesNotEnableListener( var mediator = new HttpLatencyMediator(lcti.Object); using var listener = HttpMockProvider.GetListener(context, lcti.Object); +#if NET using var handler = new HttpLatencyTelemetryHandler(listener, lcti.Object, lcp.Object, hop.Object, sop.Object, mediator); + +#else + using var handler = new HttpLatencyTelemetryHandler(listener, lcti.Object, lcp.Object, hop.Object, sop.Object); + +#endif Assert.False(listener.Enabled); } } From 518b6dde0b74a2be0d7f0c50eba6990aa97e8650 Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Fri, 12 Sep 2025 15:23:56 +0200 Subject: [PATCH 12/19] fix tetst, remove unusued interface --- .../HttpClientLatencyTelemetryExtensions.cs | 3 +++ .../Latency/Internal/IHttpLatencyMediator.cs | 27 ------------------- 2 files changed, 3 insertions(+), 27 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/IHttpLatencyMediator.cs diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs index fa77360a8c1..ccb52d7b412 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs @@ -33,6 +33,9 @@ public static IServiceCollection AddHttpClientLatencyTelemetry(this IServiceColl _ = services.AddSingleton(); _ = services.AddSingleton(); _ = services.AddTransient(); +#if NET + _ = services.AddSingleton(); +#endif _ = services.AddHttpClientLogEnricher(); return services.ConfigureAll( diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/IHttpLatencyMediator.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/IHttpLatencyMediator.cs deleted file mode 100644 index 2c342f865c3..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/IHttpLatencyMediator.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net.Http; -using Microsoft.Extensions.Diagnostics.Latency; - -namespace Microsoft.Extensions.Http.Latency.Internal; - -/// -/// Interface for mediating HTTP latency operations that coordinates recording HTTP metrics in a latency context. -/// -internal interface IHttpLatencyMediator -{ - /// - /// Records the start of an HTTP request in the latency context. - /// - /// The latency context to update. - /// The HTTP request message. - void RecordStart(ILatencyContext context, HttpRequestMessage? request); - - /// - /// Records the end of an HTTP request/response cycle in the latency context. - /// - /// The latency context to update. - /// The HTTP response message. - void RecordEnd(ILatencyContext context, HttpResponseMessage? response = null); -} \ No newline at end of file From 6291a920d126bd07b9314b87fcc797e46743f4de Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Fri, 12 Sep 2025 15:27:23 +0200 Subject: [PATCH 13/19] fix build warnings --- .../Latency/Internal/HttpClientLatencyLogEnricher.cs | 11 +++++++---- .../Latency/Internal/HttpLatencyTelemetryHandler.cs | 10 +++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs index e976dc804cf..7e6205884a0 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs @@ -29,13 +29,16 @@ internal sealed class HttpClientLatencyLogEnricher : IHttpClientLogEnricher #endif private readonly CheckpointToken _enricherInvoked; +#if NET public HttpClientLatencyLogEnricher( HttpClientLatencyContext latencyContext, - ILatencyContextTokenIssuer tokenIssuer -#if NET - , HttpLatencyMediator httpLatencyMediator + ILatencyContextTokenIssuer tokenIssuer, + HttpLatencyMediator httpLatencyMediator) +#else + public HttpClientLatencyLogEnricher( + HttpClientLatencyContext latencyContext, + ILatencyContextTokenIssuer tokenIssuer) #endif - ) { _latencyContext = latencyContext; #if NET diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs index c6912972d8b..7116b6cd38a 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs @@ -25,13 +25,13 @@ internal sealed class HttpLatencyTelemetryHandler : DelegatingHandler #if NET private readonly HttpLatencyMediator _latencyMediator; #endif - - public HttpLatencyTelemetryHandler(HttpRequestLatencyListener latencyListener, ILatencyContextTokenIssuer tokenIssuer, ILatencyContextProvider latencyContextProvider, - IOptions options, IOptions appMetadata #if NET - , HttpLatencyMediator latencyTelemetryMediator + public HttpLatencyTelemetryHandler(HttpRequestLatencyListener latencyListener, ILatencyContextTokenIssuer tokenIssuer, ILatencyContextProvider latencyContextProvider, + IOptions options, IOptions appMetadata, HttpLatencyMediator latencyTelemetryMediator) +#else + public HttpLatencyTelemetryHandler(HttpRequestLatencyListener latencyListener, ILatencyContextTokenIssuer tokenIssuer, ILatencyContextProvider latencyContextProvider, + IOptions options, IOptions appMetadata) #endif - ) { _latencyListener = latencyListener; _latencyContextProvider = latencyContextProvider; From 3efbc8dca1b9168bb74c9a734fde63f7d08081e7 Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Fri, 12 Sep 2025 17:26:43 +0200 Subject: [PATCH 14/19] fix PR comments --- .../HttpClientLatencyTelemetryExtensions.cs | 2 ++ .../Internal/HttpClientLatencyLogEnricher.cs | 17 ++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs index ccb52d7b412..4d99c1b007b 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs @@ -35,6 +35,8 @@ public static IServiceCollection AddHttpClientLatencyTelemetry(this IServiceColl _ = services.AddTransient(); #if NET _ = services.AddSingleton(); + _ = services.RegisterMeasureNames(HttpMeasures.Measures); + _ = services.RegisterTagNames(HttpTags.Tags); #endif _ = services.AddHttpClientLogEnricher(); diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs index 7e6205884a0..5ad192fd5dc 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs @@ -52,18 +52,17 @@ public void Enrich(IEnrichmentTagCollector collector, HttpRequestMessage? reques if (response != null) { var lc = _latencyContext.Get(); - if (lc == null) - { - return; - } - // Add the checkpoint - lc.AddCheckpoint(_enricherInvoked); + if (lc != null) + { + // Add the checkpoint + lc.AddCheckpoint(_enricherInvoked); #if NET - // Use the mediator to record all metrics - _httpLatencyMediator.RecordEnd(lc, response); -#endif + // Use the mediator to record all metrics + _httpLatencyMediator.RecordEnd(lc, response); +#endif + } StringBuilder stringBuilder = _builderPool.Get(); From 1e0e46bdf631049b3764d2e12981f9009a9e4c8b Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Mon, 15 Sep 2025 14:55:09 +0200 Subject: [PATCH 15/19] latency mediator tests --- .../Internal/HttpLatencyMediatorTests.cs | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyMediatorTests.cs diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyMediatorTests.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyMediatorTests.cs new file mode 100644 index 00000000000..14aae7e3613 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyMediatorTests.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AmbientMetadata; +using Microsoft.Extensions.Diagnostics.Latency; +using Microsoft.Extensions.Http.Latency.Internal; +using Microsoft.Extensions.Options; +using Moq; +using Moq.Protected; +using Xunit; + +namespace Microsoft.Extensions.Http.Latency.Test.Internal; + +#if NET +public class HttpLatencyMediatorTests +{ + [Fact] + public void RecordStart_RecordsGCPauseMeasure() + { + // Arrange + var lcti = HttpMockProvider.GetTokenIssuer(); + var measureToken = new MeasureToken(HttpMeasures.GCPauseTime, 0); + lcti.Setup(i => i.GetMeasureToken(HttpMeasures.GCPauseTime)) + .Returns(measureToken); + + var lc = HttpMockProvider.GetLatencyContext(); + var mediator = new HttpLatencyMediator(lcti.Object); + + // Act + mediator.RecordStart(lc.Object); + + // Assert + // Verify RecordMeasure was called with the correct token + lc.Verify(c => c.RecordMeasure( + measureToken, + It.Is(v => v <= 0)), // Value should be negative (start value) + Times.Once); + } + + [Fact] + public async Task HttpLatencyTelemetryHandler_UsesMediator() + { + // Arrange + var lc = HttpMockProvider.GetLatencyContext(); + var lcp = HttpMockProvider.GetContextProvider(lc); + lcp.Setup(p => p.CreateContext()).Returns(lc.Object); + + var context = new HttpClientLatencyContext(); + + var sop = new Mock>(); + sop.Setup(a => a.Value).Returns(new ApplicationMetadata()); + var hop = new Mock>(); + hop.Setup(a => a.Value).Returns(new HttpClientLatencyTelemetryOptions()); + + var lcti = HttpMockProvider.GetTokenIssuer(); + + // Create a mediator + var mediator = new HttpLatencyMediator(lcti.Object); + + using var listener = HttpMockProvider.GetListener(context, lcti.Object); + using var req = new HttpRequestMessage(); + req.Method = HttpMethod.Post; + req.RequestUri = new Uri($"https://default-uri.com/foo"); + + var resp = new HttpResponseMessage(); + var mockHandler = new Mock(); + mockHandler.Protected().Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(resp); + + using var handler = new HttpLatencyTelemetryHandler( + listener, lcti.Object, lcp.Object, hop.Object, sop.Object, mediator); + handler.InnerHandler = mockHandler.Object; + // Act + using var client = new HttpClient(handler); + await client.SendAsync(req, It.IsAny()); + + // Verify that the latency context was created and properly used + lcp.Verify(p => p.CreateContext(), Times.Once); + } + + [Fact] + public void RecordEnd_RecordsGCPauseMeasure() + { + // Arrange + var lcti = HttpMockProvider.GetTokenIssuer(); + var measureToken = new MeasureToken(HttpMeasures.GCPauseTime, 0); + lcti.Setup(i => i.GetMeasureToken(HttpMeasures.GCPauseTime)) + .Returns(measureToken); + + var lc = HttpMockProvider.GetLatencyContext(); + var mediator = new HttpLatencyMediator(lcti.Object); + + // Act + mediator.RecordEnd(lc.Object); + + lc.Verify(c => c.AddMeasure(measureToken, It.IsAny()), Times.Once); + } + + [Fact] + public void RecordEnd_WithResponse_SetsHttpVersionTag() + { + // Arrange + var lcti = HttpMockProvider.GetTokenIssuer(); + var httpVersionToken = new TagToken(HttpTags.HttpVersion, 0); + lcti.Setup(i => i.GetTagToken(HttpTags.HttpVersion)) + .Returns(httpVersionToken); + + var lc = HttpMockProvider.GetLatencyContext(); + var mediator = new HttpLatencyMediator(lcti.Object); + + using var response = new HttpResponseMessage(); + response.Version = new Version(2, 0); + + // Act + mediator.RecordEnd(lc.Object, response); + + // Assert + lc.Verify(c => c.SetTag( + httpVersionToken, + "2.0"), + Times.Once); + } + + [Fact] + public void RecordEnd_WithNullResponse_DoesNotSetHttpVersionTag() + { + // Arrange + var lcti = HttpMockProvider.GetTokenIssuer(); + var httpVersionToken = new TagToken("Http.Version", 0); + lcti.Setup(i => i.GetTagToken(HttpTags.HttpVersion)) + .Returns(httpVersionToken); + + var lc = HttpMockProvider.GetLatencyContext(); + var mediator = new HttpLatencyMediator(lcti.Object); + + // Act + mediator.RecordEnd(lc.Object); + + // Assert + lc.Verify(c => c.SetTag( + It.Is(t => t.Name == HttpTags.HttpVersion), + It.IsAny()), + Times.Never); + } +} +#endif \ No newline at end of file From a56a57013ee1965405dc0e900f42fc6694dd417e Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Tue, 16 Sep 2025 11:55:21 +0200 Subject: [PATCH 16/19] remove conditional compilation --- .../HttpClientLatencyTelemetryExtensions.cs | 2 -- .../Internal/HttpClientLatencyLogEnricher.cs | 27 +++++++------------ .../Internal/HttpLatencyMediator.netfx.cs | 2 +- .../Internal/HttpLatencyTelemetryHandler.cs | 11 ++------ .../HttpClientLatencyLogEnricherTest.cs | 14 ---------- .../HttpLatencyTelemetryHandlerTest.cs | 19 +------------ 6 files changed, 13 insertions(+), 62 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs index 4d99c1b007b..2e955605fdc 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs @@ -33,11 +33,9 @@ public static IServiceCollection AddHttpClientLatencyTelemetry(this IServiceColl _ = services.AddSingleton(); _ = services.AddSingleton(); _ = services.AddTransient(); -#if NET _ = services.AddSingleton(); _ = services.RegisterMeasureNames(HttpMeasures.Measures); _ = services.RegisterTagNames(HttpTags.Tags); -#endif _ = services.AddHttpClientLogEnricher(); return services.ConfigureAll( diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs index 5ad192fd5dc..160cd8d7851 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs @@ -23,27 +23,17 @@ internal sealed class HttpClientLatencyLogEnricher : IHttpClientLogEnricher { private static readonly ObjectPool _builderPool = PoolFactory.SharedStringBuilderPool; private readonly HttpClientLatencyContext _latencyContext; -#if NET - private readonly HttpLatencyMediator _httpLatencyMediator; -#endif private readonly CheckpointToken _enricherInvoked; -#if NET + public HttpClientLatencyLogEnricher( HttpClientLatencyContext latencyContext, ILatencyContextTokenIssuer tokenIssuer, HttpLatencyMediator httpLatencyMediator) -#else - public HttpClientLatencyLogEnricher( - HttpClientLatencyContext latencyContext, - ILatencyContextTokenIssuer tokenIssuer) -#endif { _latencyContext = latencyContext; -#if NET _httpLatencyMediator = httpLatencyMediator; -#endif _enricherInvoked = tokenIssuer.GetCheckpointToken(HttpCheckpoints.EnricherInvoked); } @@ -58,10 +48,8 @@ public void Enrich(IEnrichmentTagCollector collector, HttpRequestMessage? reques // Add the checkpoint lc.AddCheckpoint(_enricherInvoked); -#if NET // Use the mediator to record all metrics _httpLatencyMediator.RecordEnd(lc, response); -#endif } StringBuilder stringBuilder = _builderPool.Get(); @@ -82,13 +70,16 @@ public void Enrich(IEnrichmentTagCollector collector, HttpRequestMessage? reques _ = stringBuilder.Append(','); // Add tags, checkpoints, and measures - AppendTags(lc, stringBuilder); - _ = stringBuilder.Append(','); + if (lc != null) + { + AppendTags(lc, stringBuilder); + _ = stringBuilder.Append(','); - AppendCheckpoints(lc, stringBuilder); - _ = stringBuilder.Append(','); + AppendCheckpoints(lc, stringBuilder); + _ = stringBuilder.Append(','); - AppendMeasures(lc, stringBuilder); + AppendMeasures(lc, stringBuilder); + } collector.Add("LatencyInfo", stringBuilder.ToString()); } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.netfx.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.netfx.cs index 2402cd39064..c5cb350397e 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.netfx.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyMediator.netfx.cs @@ -1,6 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NETFRAMEWORK +#if !NET using System.Net.Http; using Microsoft.Extensions.Diagnostics.Latency; diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs index 7116b6cd38a..2a91ae0f707 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs @@ -22,24 +22,17 @@ internal sealed class HttpLatencyTelemetryHandler : DelegatingHandler private readonly CheckpointToken _handlerStart; private readonly string _applicationName; -#if NET + private readonly HttpLatencyMediator _latencyMediator; -#endif -#if NET + public HttpLatencyTelemetryHandler(HttpRequestLatencyListener latencyListener, ILatencyContextTokenIssuer tokenIssuer, ILatencyContextProvider latencyContextProvider, IOptions options, IOptions appMetadata, HttpLatencyMediator latencyTelemetryMediator) -#else - public HttpLatencyTelemetryHandler(HttpRequestLatencyListener latencyListener, ILatencyContextTokenIssuer tokenIssuer, ILatencyContextProvider latencyContextProvider, - IOptions options, IOptions appMetadata) -#endif { _latencyListener = latencyListener; _latencyContextProvider = latencyContextProvider; _handlerStart = tokenIssuer.GetCheckpointToken(HttpCheckpoints.HandlerRequestStart); _applicationName = appMetadata.Value.ApplicationName; -#if NET _latencyMediator = latencyTelemetryMediator; -#endif if (options.Value.EnableDetailedLatencyBreakdown) { diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs index 41c989173ce..79753bfb996 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs @@ -25,12 +25,7 @@ public void HttpClientLatencyLogEnricher_NoOp_OnRequest() lc.Setup(lc => lc.LatencyData).Returns(ld); var context = new HttpClientLatencyContext(); context.Set(lc.Object); - -#if NET var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mediator); -#else - var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object); -#endif Mock mockEnrichmentPropertyBag = new Mock(); enricher.Enrich(mockEnrichmentPropertyBag.Object, null!, null, null); mockEnrichmentPropertyBag.Verify(m => m.Add(It.IsAny(), It.IsAny()), Times.Never); @@ -49,12 +44,7 @@ public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithoutHeader() context.Set(lc.Object); using HttpResponseMessage httpResponseMessage = new(); - -#if NET var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mediator); -#else - var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object); -#endif Mock mockEnrichmentPropertyBag = new Mock(); enricher.Enrich(mockEnrichmentPropertyBag.Object, null!, httpResponseMessage, null); @@ -77,11 +67,7 @@ public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithHeader() string serverName = "serverNameVal"; httpResponseMessage.Headers.Add(TelemetryConstants.ServerApplicationNameHeader, serverName); -#if NET var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object, mediator); -#else - var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object); -#endif Mock mockEnrichmentPropertyBag = new Mock(); enricher.Enrich(mockEnrichmentPropertyBag.Object, null!, httpResponseMessage, null); diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs index 2946dde0db1..8fe5107a02f 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs @@ -34,11 +34,7 @@ public void HttpLatencyTelemetryHandler_InvokesTokenIssuer() var mediator = new HttpLatencyMediator(lcti2.Object); using var listener = HttpMockProvider.GetListener(context, lcti.Object); -#if NET using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object, mediator); -#else - using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object); -#endif lcti2.Verify(a => a.GetCheckpointToken(It.Is(s => !HttpCheckpoints.Checkpoints.Contains(s))), Times.Never); lcti2.Verify(a => a.GetCheckpointToken(It.Is(s => HttpCheckpoints.Checkpoints.Contains(s)))); @@ -77,19 +73,12 @@ public async Task HttpLatencyTelemetryHandler_SetsLatencyContext() Assert.True(req.Headers.Contains(TelemetryConstants.ClientApplicationNameHeader)); }).Returns(Task.FromResult(resp.Object)); -#if NET using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object, mediator) { InnerHandler = mockHandler.Object }; -#else - using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object) - { - InnerHandler = mockHandler.Object - }; -#endif - using var client = new System.Net.Http.HttpClient(handler); + using var client = new HttpClient(handler); await client.SendAsync(req, It.IsAny()); Assert.Null(context.Get()); } @@ -108,13 +97,7 @@ public void HttpLatencyTelemetryHandler_IfDetailsDisabled_DoesNotEnableListener( var mediator = new HttpLatencyMediator(lcti.Object); using var listener = HttpMockProvider.GetListener(context, lcti.Object); -#if NET using var handler = new HttpLatencyTelemetryHandler(listener, lcti.Object, lcp.Object, hop.Object, sop.Object, mediator); - -#else - using var handler = new HttpLatencyTelemetryHandler(listener, lcti.Object, lcp.Object, hop.Object, sop.Object); - -#endif Assert.False(listener.Enabled); } } From 86f0e0c005be11676c1f6d61b866e860f4fd10d7 Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Tue, 16 Sep 2025 16:02:42 +0200 Subject: [PATCH 17/19] fix warnings --- .../Latency/Internal/HttpClientLatencyLogEnricher.cs | 1 - .../Latency/Internal/HttpLatencyTelemetryHandler.cs | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs index 160cd8d7851..753b5b9a9f6 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyLogEnricher.cs @@ -26,7 +26,6 @@ internal sealed class HttpClientLatencyLogEnricher : IHttpClientLogEnricher private readonly HttpLatencyMediator _httpLatencyMediator; private readonly CheckpointToken _enricherInvoked; - public HttpClientLatencyLogEnricher( HttpClientLatencyContext latencyContext, ILatencyContextTokenIssuer tokenIssuer, diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs index 2a91ae0f707..81a0878b3f5 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs @@ -21,9 +21,9 @@ internal sealed class HttpLatencyTelemetryHandler : DelegatingHandler private readonly ILatencyContextProvider _latencyContextProvider; private readonly CheckpointToken _handlerStart; private readonly string _applicationName; - - +#if NET private readonly HttpLatencyMediator _latencyMediator; +#endif public HttpLatencyTelemetryHandler(HttpRequestLatencyListener latencyListener, ILatencyContextTokenIssuer tokenIssuer, ILatencyContextProvider latencyContextProvider, IOptions options, IOptions appMetadata, HttpLatencyMediator latencyTelemetryMediator) @@ -32,7 +32,9 @@ public HttpLatencyTelemetryHandler(HttpRequestLatencyListener latencyListener, I _latencyContextProvider = latencyContextProvider; _handlerStart = tokenIssuer.GetCheckpointToken(HttpCheckpoints.HandlerRequestStart); _applicationName = appMetadata.Value.ApplicationName; +#if NET _latencyMediator = latencyTelemetryMediator; +#endif if (options.Value.EnableDetailedLatencyBreakdown) { From 9b8147fb8a6e567f0a1ede55c65ef4c5adaff619 Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Tue, 16 Sep 2025 16:57:57 +0200 Subject: [PATCH 18/19] fix build warnings --- .../Latency/Internal/HttpLatencyMediatorTests.cs | 4 +++- .../Latency/Internal/HttpLatencyTelemetryHandlerTest.cs | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyMediatorTests.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyMediatorTests.cs index 14aae7e3613..b82b815e430 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyMediatorTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyMediatorTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if NET using System; using System.Net.Http; using System.Threading; @@ -15,7 +16,6 @@ namespace Microsoft.Extensions.Http.Latency.Test.Internal; -#if NET public class HttpLatencyMediatorTests { [Fact] @@ -77,12 +77,14 @@ public async Task HttpLatencyTelemetryHandler_UsesMediator() using var handler = new HttpLatencyTelemetryHandler( listener, lcti.Object, lcp.Object, hop.Object, sop.Object, mediator); handler.InnerHandler = mockHandler.Object; + // Act using var client = new HttpClient(handler); await client.SendAsync(req, It.IsAny()); // Verify that the latency context was created and properly used lcp.Verify(p => p.CreateContext(), Times.Once); + resp.Dispose(); } [Fact] diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs index 8fe5107a02f..e70f8135d72 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Linq; using System.Net.Http; using System.Threading; From 489058163f1c9b923c2ebbe280f5bb12dcb1fc18 Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov Date: Thu, 18 Sep 2025 11:59:59 +0200 Subject: [PATCH 19/19] update latency listener --- .../Internal/HttpRequestLatencyListener.cs | 131 ++++++++++++++---- 1 file changed, 106 insertions(+), 25 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs index 5d7238ed8a3..ae5eef41179 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Collections.Concurrent; using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics.Tracing; @@ -13,12 +15,19 @@ internal sealed class HttpRequestLatencyListener : EventListener { private const string SocketProviderName = "System.Net.Sockets"; private const string HttpProviderName = "System.Net.Http"; - private const string NameResolutionProivderName = "System.Net.NameResolution"; + private const string NameResolutionProviderName = "System.Net.NameResolution"; - private readonly FrozenDictionary> _eventToTokenMap; + private readonly ConcurrentDictionary _eventSources = new() + { + [SocketProviderName] = null, + [HttpProviderName] = null, + [NameResolutionProviderName] = null + }; internal HttpClientLatencyContext LatencyContext { get; } + private readonly EventToToken _eventToToken; + private int _enabled; internal bool Enabled => _enabled == 1; @@ -26,40 +35,59 @@ internal sealed class HttpRequestLatencyListener : EventListener public HttpRequestLatencyListener(HttpClientLatencyContext latencyContext, ILatencyContextTokenIssuer tokenIssuer) { LatencyContext = latencyContext; - _eventToTokenMap = EventToCheckpointToken.Build(tokenIssuer); + _eventToToken = new(tokenIssuer); } public void Enable() { if (Interlocked.CompareExchange(ref _enabled, 1, 0) == 0) { + // Enable any already discovered event sources + foreach (var eventSource in _eventSources) + { + if (eventSource.Value != null) + { + EnableEventSource(eventSource.Value); + } + } + #if NETSTANDARD foreach (var eventSource in EventSource.GetSources()) { OnEventSourceCreated(eventSource.Name, eventSource); } #else - // process already existing listeners once again + // Process already existing listeners once again EventSourceCreated += (_, args) => OnEventSourceCreated(args.EventSource!); #endif - } } internal void OnEventWritten(string eventSourceName, string? eventName) { // If event of interest, add a checkpoint for it. - if (eventName != null && _eventToTokenMap[eventSourceName].TryGetValue(eventName, out var token)) + CheckpointToken? token = _eventToToken.GetCheckpointToken(eventSourceName, eventName); + if (token.HasValue) { - LatencyContext.Get()?.AddCheckpoint(token); + var latencyContext = LatencyContext.Get(); + latencyContext?.AddCheckpoint(token.Value); + + // If event of interest, add a presence measure for it. + MeasureToken? mtoken = _eventToToken.GetMeasureToken(eventSourceName, eventName); + + if (mtoken.HasValue) + { + latencyContext?.AddMeasure(mtoken.Value, 1L); + } } } internal void OnEventSourceCreated(string eventSourceName, EventSource eventSource) { - if (Enabled && _eventToTokenMap.ContainsKey(eventSourceName)) + if (_eventSources.ContainsKey(eventSourceName)) { - EnableEvents(eventSource, EventLevel.Informational); + _eventSources[eventSourceName] = eventSource; + EnableEventSource(eventSource); } } @@ -73,7 +101,15 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData) OnEventWritten(eventData.EventSource.Name, eventData.EventName); } - private static class EventToCheckpointToken + private void EnableEventSource(EventSource eventSource) + { + if (Enabled) + { + EnableEvents(eventSource, EventLevel.Informational); + } + } + + private sealed class EventToToken { private static readonly Dictionary _socketMap = new() { @@ -101,32 +137,77 @@ private static class EventToCheckpointToken { "ResponseContentStop", HttpCheckpoints.ResponseContentEnd } }; - public static FrozenDictionary> Build(ILatencyContextTokenIssuer tokenIssuer) + private static readonly Dictionary _httpMeasureMap = new() + { + { "ConnectionEstablished", HttpMeasures.ConnectionInitiated } + }; + + private readonly FrozenDictionary> _eventToCheckpointTokenMap; + private readonly FrozenDictionary> _eventToMeasureTokenMap; + + public EventToToken(ILatencyContextTokenIssuer tokenIssuer) { - Dictionary socket = []; - foreach (var kv in _socketMap) + Dictionary socket = new(); + foreach (string key in _socketMap.Keys) + { + socket[key] = tokenIssuer.GetCheckpointToken(_socketMap[key]); + } + + Dictionary nameResolution = new(); + foreach (string key in _nameResolutionMap.Keys) { - socket[kv.Key] = tokenIssuer.GetCheckpointToken(kv.Value); + nameResolution[key] = tokenIssuer.GetCheckpointToken(_nameResolutionMap[key]); } - Dictionary nameResolution = []; - foreach (var kv in _nameResolutionMap) + Dictionary http = new(); + foreach (string key in _httpMap.Keys) { - nameResolution[kv.Key] = tokenIssuer.GetCheckpointToken(kv.Value); + http[key] = tokenIssuer.GetCheckpointToken(_httpMap[key]); } - Dictionary http = []; - foreach (var kv in _httpMap) + Dictionary httpMeasures = new(); + foreach (string key in _httpMeasureMap.Keys) { - http[kv.Key] = tokenIssuer.GetCheckpointToken(kv.Value); + httpMeasures[key] = tokenIssuer.GetMeasureToken(_httpMeasureMap[key]); } - return new Dictionary> + _eventToCheckpointTokenMap = new Dictionary> { - { SocketProviderName, socket.ToFrozenDictionary() }, - { NameResolutionProivderName, nameResolution.ToFrozenDictionary() }, - { HttpProviderName, http.ToFrozenDictionary() } - }.ToFrozenDictionary(); + { SocketProviderName, socket.ToFrozenDictionary(StringComparer.Ordinal) }, + { NameResolutionProviderName, nameResolution.ToFrozenDictionary(StringComparer.Ordinal) }, + { HttpProviderName, http.ToFrozenDictionary(StringComparer.Ordinal) } + }.ToFrozenDictionary(StringComparer.Ordinal); + + _eventToMeasureTokenMap = new Dictionary> + { + { HttpProviderName, httpMeasures.ToFrozenDictionary(StringComparer.Ordinal) } + }.ToFrozenDictionary(StringComparer.Ordinal); + } + + public CheckpointToken? GetCheckpointToken(string eventSourceName, string? eventName) + { + if (eventName != null && _eventToCheckpointTokenMap.TryGetValue(eventSourceName, out var events)) + { + if (events.TryGetValue(eventName, out var token)) + { + return token; + } + } + + return null; + } + + public MeasureToken? GetMeasureToken(string eventSourceName, string? eventName) + { + if (eventName != null && _eventToMeasureTokenMap.TryGetValue(eventSourceName, out var events)) + { + if (events.TryGetValue(eventName, out var token)) + { + return token; + } + } + + return null; } } }