diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DownstreamDependencyMetadataManager.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DownstreamDependencyMetadataManager.cs index fbe54413cbf..c8b03913533 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DownstreamDependencyMetadataManager.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DownstreamDependencyMetadataManager.cs @@ -100,31 +100,40 @@ private static void AddRouteToTrie(RequestMetadata routeMetadata, Dictionary requestRouteAsSpan = routeMetadata.RequestRoute.AsSpan(); - - if (requestRouteAsSpan.Length > 0) + var route = routeMetadata.RequestRoute; + if (!string.IsNullOrEmpty(route)) { - if (requestRouteAsSpan[0] != '/') + var routeSpan = route.AsSpan(); + if (routeSpan.StartsWith("//".AsSpan())) { - requestRouteAsSpan = $"/{routeMetadata.RequestRoute}".AsSpan(); + routeSpan = routeSpan.Slice(1); } - else if (requestRouteAsSpan.StartsWith("//".AsSpan(), StringComparison.OrdinalIgnoreCase)) + + if (routeSpan.Length > 1 && routeSpan[routeSpan.Length - 1] == '/') { - requestRouteAsSpan = requestRouteAsSpan.Slice(1); + routeSpan = routeSpan.Slice(0, routeSpan.Length - 1); } - if (requestRouteAsSpan.Length > 1 && requestRouteAsSpan[requestRouteAsSpan.Length - 1] == '/') + if (routeSpan[0] != '/') { - requestRouteAsSpan = requestRouteAsSpan.Slice(0, requestRouteAsSpan.Length - 1); +#if NET + route = $"/{routeSpan}"; +#else + route = $"/{routeSpan.ToString()}"; +#endif } + else if (routeSpan.Length != route.Length) + { + route = routeSpan.ToString(); + } + + route = _routeRegex.Replace(route, "*").ToUpperInvariant(); } else { - requestRouteAsSpan = "/".AsSpan(); + route = "/"; } - var route = _routeRegex.Replace(requestRouteAsSpan.ToString(), "*"); - route = route.ToUpperInvariant(); for (int i = 0; i < route.Length; i++) { char ch = route[i]; diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs index 309f24e1f2d..fa77360a8c1 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs @@ -30,8 +30,8 @@ public static IServiceCollection AddHttpClientLatencyTelemetry(this IServiceColl _ = services.RegisterCheckpointNames(HttpCheckpoints.Checkpoints); _ = services.AddOptions(); - _ = services.AddActivatedSingleton(); - _ = services.AddActivatedSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); _ = services.AddTransient(); _ = services.AddHttpClientLogEnricher(); diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyContext.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyContext.cs index 338326cc690..d5b5f0a40fb 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyContext.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyContext.cs @@ -19,9 +19,4 @@ public void Set(ILatencyContext context) { _latencyContext.Value = context; } - - public void Unset() - { - _latencyContext.Value = null; - } } 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 d0d38a875ec..3180cb890c6 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.Diagnostics.Latency; using Microsoft.Extensions.Http.Diagnostics; using Microsoft.Extensions.Options; -using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Http.Latency.Internal; @@ -24,17 +23,14 @@ internal sealed class HttpLatencyTelemetryHandler : DelegatingHandler private readonly string _applicationName; public HttpLatencyTelemetryHandler(HttpRequestLatencyListener latencyListener, ILatencyContextTokenIssuer tokenIssuer, ILatencyContextProvider latencyContextProvider, - IOptions options, IOptions appMetdata) + IOptions options, IOptions appMetadata) { - var appMetadata = Throw.IfMemberNull(appMetdata, appMetdata.Value); - var telemetryOptions = Throw.IfMemberNull(options, options.Value); - _latencyListener = latencyListener; _latencyContextProvider = latencyContextProvider; _handlerStart = tokenIssuer.GetCheckpointToken(HttpCheckpoints.HandlerRequestStart); - _applicationName = appMetdata.Value.ApplicationName; + _applicationName = appMetadata.Value.ApplicationName; - if (telemetryOptions.EnableDetailedLatencyBreakdown) + if (options.Value.EnableDetailedLatencyBreakdown) { _latencyListener.Enable(); } @@ -46,12 +42,8 @@ protected async override Task SendAsync(HttpRequestMessage context.AddCheckpoint(_handlerStart); _latencyListener.LatencyContext.Set(context); - request.Headers.Add(TelemetryConstants.ClientApplicationNameHeader, _applicationName); - - var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); - - _latencyListener.LatencyContext.Unset(); + _ = request.Headers.TryAddWithoutValidation(TelemetryConstants.ClientApplicationNameHeader, _applicationName); - return response; + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); } } 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 744d7a5ddd3..214d877b92e 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs @@ -1,8 +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.Collections.Concurrent; using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics.Tracing; @@ -17,17 +15,10 @@ internal sealed class HttpRequestLatencyListener : EventListener private const string HttpProviderName = "System.Net.Http"; private const string NameResolutionProivderName = "System.Net.NameResolution"; - private readonly ConcurrentDictionary _eventSources = new() - { - [SocketProviderName] = null, - [HttpProviderName] = null, - [NameResolutionProivderName] = null - }; + private readonly FrozenDictionary> _eventToTokenMap; internal HttpClientLatencyContext LatencyContext { get; } - private readonly EventToCheckpointToken _eventToCheckpointToken; - private int _enabled; internal bool Enabled => _enabled == 1; @@ -35,39 +26,32 @@ internal sealed class HttpRequestLatencyListener : EventListener public HttpRequestLatencyListener(HttpClientLatencyContext latencyContext, ILatencyContextTokenIssuer tokenIssuer) { LatencyContext = latencyContext; - _eventToCheckpointToken = new(tokenIssuer); + _eventToTokenMap = EventToCheckpointToken.Build(tokenIssuer); } public void Enable() { if (Interlocked.CompareExchange(ref _enabled, 1, 0) == 0) { - foreach (var eventSource in _eventSources) - { - if (eventSource.Value != null) - { - EnableEventSource(eventSource.Value); - } - } + // process already existing listeners once again + EventSourceCreated += (_, args) => OnEventSourceCreated(args.EventSource!); } } internal void OnEventWritten(string eventSourceName, string? eventName) { // If event of interest, add a checkpoint for it. - CheckpointToken? token = _eventToCheckpointToken.GetCheckpointToken(eventSourceName, eventName); - if (token.HasValue) + if (eventName != null && _eventToTokenMap[eventSourceName].TryGetValue(eventName, out var token)) { - LatencyContext.Get()?.AddCheckpoint(token.Value); + LatencyContext.Get()?.AddCheckpoint(token); } } internal void OnEventSourceCreated(string eventSourceName, EventSource eventSource) { - if (_eventSources.ContainsKey(eventSourceName)) + if (Enabled && _eventToTokenMap.ContainsKey(eventSourceName)) { - _eventSources[eventSourceName] = eventSource; - EnableEventSource(eventSource); + EnableEvents(eventSource, EventLevel.Informational); } } @@ -81,15 +65,7 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData) OnEventWritten(eventData.EventSource.Name, eventData.EventName); } - private void EnableEventSource(EventSource eventSource) - { - if (Enabled && !eventSource.IsEnabled()) - { - EnableEvents(eventSource, EventLevel.Informational); - } - } - - private sealed class EventToCheckpointToken + private static class EventToCheckpointToken { private static readonly Dictionary _socketMap = new() { @@ -117,47 +93,32 @@ private sealed class EventToCheckpointToken { "ResponseContentStop", HttpCheckpoints.ResponseContentEnd } }; - private readonly FrozenDictionary> _eventToTokenMap; - - public EventToCheckpointToken(ILatencyContextTokenIssuer tokenIssuer) + public static FrozenDictionary> Build(ILatencyContextTokenIssuer tokenIssuer) { Dictionary socket = []; - foreach (string key in _socketMap.Keys) + foreach (var kv in _socketMap) { - socket[key] = tokenIssuer.GetCheckpointToken(_socketMap[key]); + socket[kv.Key] = tokenIssuer.GetCheckpointToken(kv.Value); } Dictionary nameResolution = []; - foreach (string key in _nameResolutionMap.Keys) + foreach (var kv in _nameResolutionMap) { - nameResolution[key] = tokenIssuer.GetCheckpointToken(_nameResolutionMap[key]); + nameResolution[kv.Key] = tokenIssuer.GetCheckpointToken(kv.Value); } Dictionary http = []; - foreach (string key in _httpMap.Keys) + foreach (var kv in _httpMap) { - http[key] = tokenIssuer.GetCheckpointToken(_httpMap[key]); + http[kv.Key] = tokenIssuer.GetCheckpointToken(kv.Value); } - _eventToTokenMap = new Dictionary> + return new Dictionary> { - { SocketProviderName, socket.ToFrozenDictionary(StringComparer.Ordinal) }, - { NameResolutionProivderName, nameResolution.ToFrozenDictionary(StringComparer.Ordinal) }, - { HttpProviderName, http.ToFrozenDictionary(StringComparer.Ordinal) } - }.ToFrozenDictionary(StringComparer.Ordinal); - } - - public CheckpointToken? GetCheckpointToken(string eventSourceName, string? eventName) - { - if (eventName != null && _eventToTokenMap.TryGetValue(eventSourceName, out var events)) - { - if (events.TryGetValue(eventName, out var token)) - { - return token; - } - } - - return null; + { SocketProviderName, socket.ToFrozenDictionary() }, + { NameResolutionProivderName, nameResolution.ToFrozenDictionary() }, + { HttpProviderName, http.ToFrozenDictionary() } + }.ToFrozenDictionary(); } } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpClientLogger.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpClientLogger.cs index bbf8095e384..0e1aeb6d565 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpClientLogger.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpClientLogger.cs @@ -109,22 +109,22 @@ internal HttpClientLogger( } } - public async ValueTask LogRequestStopAsync( + public ValueTask LogRequestStopAsync( object? context, HttpRequestMessage request, HttpResponseMessage response, TimeSpan elapsed, CancellationToken cancellationToken = default) - => await LogResponseAsync(context, request, response, null, elapsed, cancellationToken).ConfigureAwait(false); + => LogResponseAsync(context, request, response, null, elapsed, cancellationToken); - public async ValueTask LogRequestFailedAsync( + public ValueTask LogRequestFailedAsync( object? context, HttpRequestMessage request, HttpResponseMessage? response, Exception exception, TimeSpan elapsed, CancellationToken cancellationToken = default) - => await LogResponseAsync(context, request, response, exception, elapsed, cancellationToken).ConfigureAwait(false); + => LogResponseAsync(context, request, response, exception, elapsed, cancellationToken); public object? LogRequestStart(HttpRequestMessage request) => throw new NotSupportedException(SyncLoggingExceptionMessage); diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestBodyReader.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestBodyReader.cs index ed5a3c3f33d..c7abf7f0df8 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestBodyReader.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestBodyReader.cs @@ -2,21 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers; using System.Collections.Frozen; using System.IO; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; -#if NETCOREAPP3_1_OR_GREATER -using Microsoft.Extensions.ObjectPool; -#endif using Microsoft.Shared.Diagnostics; -#if NETCOREAPP3_1_OR_GREATER -using Microsoft.Shared.Pools; -#else -using System.Buffers; -#endif namespace Microsoft.Extensions.Http.Logging.Internal; @@ -27,9 +20,6 @@ internal sealed class HttpRequestBodyReader /// internal readonly TimeSpan RequestReadTimeout; -#if NETCOREAPP3_1_OR_GREATER - private static readonly ObjectPool> _bufferWriterPool = BufferWriterPool.SharedBufferWriterPool; -#endif private readonly FrozenSet _readableRequestContentTypes; private readonly int _requestReadLimit; @@ -93,33 +83,20 @@ private static async ValueTask ReadFromStreamAsync(HttpRequestMessage re #endif var readLimit = Math.Min(readSizeLimit, (int)streamToReadFrom.Length); -#if NETCOREAPP3_1_OR_GREATER - var bufferWriter = _bufferWriterPool.Get(); - try - { - var memory = bufferWriter.GetMemory(readLimit).Slice(0, readLimit); - var charsWritten = await streamToReadFrom.ReadAsync(memory, cancellationToken).ConfigureAwait(false); - - return Encoding.UTF8.GetString(memory[..charsWritten].Span); - } - finally - { - _bufferWriterPool.Return(bufferWriter); - streamToReadFrom.Seek(0, SeekOrigin.Begin); - } - -#else var buffer = ArrayPool.Shared.Rent(readLimit); try { - _ = await streamToReadFrom.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); - return Encoding.UTF8.GetString(buffer.AsSpan(0, readLimit).ToArray()); +#if NET + var read = await streamToReadFrom.ReadAsync(buffer.AsMemory(0, readLimit), cancellationToken).ConfigureAwait(false); +#else + var read = await streamToReadFrom.ReadAsync(buffer, 0, readLimit, cancellationToken).ConfigureAwait(false); +#endif + return Encoding.UTF8.GetString(buffer, 0, read); } finally { ArrayPool.Shared.Return(buffer); streamToReadFrom.Seek(0, SeekOrigin.Begin); } -#endif } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpResponseBodyReader.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpResponseBodyReader.cs index 0c5b6a672b1..8022ee74197 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpResponseBodyReader.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpResponseBodyReader.cs @@ -112,10 +112,7 @@ private static async ValueTask ReadFromStreamAsync(HttpResponseMessage r // if stream is not seekable we need to write the rest of the stream to the pipe // and create a new response content with the pipe reader as stream - _ = Task.Run(async () => - { - await WriteStreamToPipeAsync(streamToReadFrom, pipe.Writer, cancellationToken).ConfigureAwait(false); - }, CancellationToken.None); + _ = WriteStreamToPipeAsync(streamToReadFrom, pipe.Writer, cancellationToken); // use the pipe reader as stream for the new content var newContent = new StreamContent(pipe.Reader.AsStream()); @@ -130,41 +127,29 @@ private static async ValueTask ReadFromStreamAsync(HttpResponseMessage r } #if NET6_0_OR_GREATER - private static async Task BufferStreamAndWriteToPipeAsync(Stream stream, PipeWriter writer, int bufferSize, CancellationToken cancellationToken) + private static async ValueTask BufferStreamAndWriteToPipeAsync(Stream stream, PipeWriter writer, int bufferSize, CancellationToken cancellationToken) { Memory memory = writer.GetMemory(bufferSize)[..bufferSize]; -#if NET8_0_OR_GREATER int bytesRead = await stream.ReadAtLeastAsync(memory, bufferSize, false, cancellationToken).ConfigureAwait(false); -#else - int bytesRead = 0; - while (bytesRead < bufferSize) - { - int read = await stream.ReadAsync(memory.Slice(bytesRead), cancellationToken).ConfigureAwait(false); - if (read == 0) - { - break; - } - - bytesRead += read; - } -#endif - if (bytesRead == 0) { return string.Empty; } + var res = Encoding.UTF8.GetString(memory.Span[..bytesRead]); writer.Advance(bytesRead); - return Encoding.UTF8.GetString(memory[..bytesRead].Span); + return res; } private static async Task WriteStreamToPipeAsync(Stream stream, PipeWriter writer, CancellationToken cancellationToken) { + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + while (true) { - Memory memory = writer.GetMemory(ChunkSize)[..ChunkSize]; + Memory memory = writer.GetMemory(ChunkSize); int bytesRead = await stream.ReadAsync(memory, cancellationToken).ConfigureAwait(false); if (bytesRead == 0) @@ -216,7 +201,10 @@ private static async Task BufferStreamAndWriteToPipeAsync(Stream stream, return sb.ToString(); } - private static async Task WriteStreamToPipeAsync(Stream stream, PipeWriter writer, CancellationToken cancellationToken) + private static Task WriteStreamToPipeAsync(Stream stream, PipeWriter writer, CancellationToken cancellationToken) + => Task.Run(() => WriteStreamToPipeImplAsync(stream, writer, cancellationToken), CancellationToken.None); + + private static async Task WriteStreamToPipeImplAsync(Stream stream, PipeWriter writer, CancellationToken cancellationToken) { while (true) { diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj index cc00c907ded..53cd4c1e0d2 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj @@ -31,13 +31,12 @@ - - - + + diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj b/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj index 8d280d747cb..8ab678e8eb0 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj @@ -36,10 +36,6 @@ - - - - diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpRequestLatencyListenerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpRequestLatencyListenerTest.cs index 3675bc3901a..fa91daa1c3e 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpRequestLatencyListenerTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpRequestLatencyListenerTest.cs @@ -16,10 +16,9 @@ public void HttpClientLatencyContext_Set_BasicFunction() { var lc = HttpMockProvider.GetLatencyContext(); var context = new HttpClientLatencyContext(); + Assert.Null(context.Get()); context.Set(lc.Object); Assert.Equal(context.Get(), lc.Object); - context.Unset(); - Assert.Null(context.Get()); } [Fact] @@ -115,28 +114,6 @@ public void HttpRequestLatencyListener_OnEventSourceCreated_HttpSources() Assert.True(esNameRes.IsEnabled()); } - [Fact] - public void HttpRequestLatencyListener_OnEventSourceCreated_Twice() - { - var lcti = HttpMockProvider.GetTokenIssuer(); - var lc = HttpMockProvider.GetLatencyContext(); - var context = new HttpClientLatencyContext(); - context.Set(lc.Object); - - using var listener = HttpMockProvider.GetListener(context, lcti.Object); - Assert.NotNull(listener); - listener.Enable(); - - using var esSockets = new HttpMockProvider.SockeyMockEventSource(); - listener.OnEventSourceCreated("System.Net.Sockets", esSockets); - Assert.Equal(1, esSockets.OnEventInvoked); - Assert.True(esSockets.IsEnabled()); - - listener.OnEventSourceCreated("System.Net.Sockets", esSockets); - Assert.Equal(1, esSockets.OnEventInvoked); - Assert.True(esSockets.IsEnabled()); - } - [Fact] public void HttpRequestLatencyListener_OnEventWritten_DoesNotAddCheckpoints_NonHttp() { @@ -146,15 +123,18 @@ public void HttpRequestLatencyListener_OnEventWritten_DoesNotAddCheckpoints_NonH context.Set(lc.Object); using var listener = HttpMockProvider.GetListener(context, lcti.Object); + listener.Enable(); var events = new[] { "ConnectionEstablished", "RequestLeftQueue", "ResolutionStop", "ConnectStart", "New" }; + using var es = new HttpMockProvider.MockEventSource(); + listener.OnEventSourceCreated("System.Net", es); for (int i = 0; i < events.Length; i++) { - listener.OnEventWritten("System.Net", events[i]); + es.Write(events[i]); } lc.Verify(a => a.AddCheckpoint(It.IsAny()), Times.Never);