From 968a2456a4c0ecd1b6ce3583fa372ecda094f136 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 Date: Mon, 25 Aug 2025 17:50:56 +0200 Subject: [PATCH 1/2] Turn DownstreamDependencyMetadataManager to abstract class --- ...aultDownstreamDependencyMetadataManager.cs | 11 +++ .../DownstreamDependencyMetadataManager.cs | 92 +++++++++++-------- ...pDiagnosticsServiceCollectionExtensions.cs | 5 +- .../Logging/Internal/HttpRequestReader.cs | 7 +- .../IDownstreamDependencyMetadataManager.cs | 28 ------ ...ownstreamDependencyMetadataManagerTests.cs | 6 +- 6 files changed, 74 insertions(+), 75 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DefaultDownstreamDependencyMetadataManager.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/IDownstreamDependencyMetadataManager.cs diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DefaultDownstreamDependencyMetadataManager.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DefaultDownstreamDependencyMetadataManager.cs new file mode 100644 index 00000000000..5edabacd6a9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DefaultDownstreamDependencyMetadataManager.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.Http.Diagnostics; + +internal sealed class DefaultDownstreamDependencyMetadataManager( + IEnumerable downstreamDependencyMetadata) + : DownstreamDependencyMetadataManager(downstreamDependencyMetadata); + diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DownstreamDependencyMetadataManager.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DownstreamDependencyMetadataManager.cs index c8b03913533..a7cb625f0d4 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DownstreamDependencyMetadataManager.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DownstreamDependencyMetadataManager.cs @@ -8,13 +8,19 @@ using System.Net; using System.Net.Http; using System.Text.RegularExpressions; -using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Shared.DiagnosticIds; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Http.Diagnostics; -internal sealed class DownstreamDependencyMetadataManager : IDownstreamDependencyMetadataManager +/// +/// Manages downstream dependency metadata and resolves route information for outgoing HTTP requests. +/// +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +[SuppressMessage("Minor Code Smell", "S1694:An abstract class should have both abstract and concrete methods", Justification = "Want abstract class for extensibility.")] +public abstract class DownstreamDependencyMetadataManager { - internal readonly struct ProcessedMetadata + private readonly struct ProcessedMetadata { public FrozenRequestMetadataTrieNode[] Nodes { get; init; } public RequestMetadata[] RequestMetadatas { get; init; } @@ -27,10 +33,16 @@ internal readonly struct ProcessedMetadata private readonly HostSuffixTrieNode _hostSuffixTrieRoot = new(); private readonly FrozenDictionary _frozenProcessedMetadataMap; - public DownstreamDependencyMetadataManager(IEnumerable downstreamDependencyMetadata) + /// + /// Initializes a new instance of the class. + /// + /// A collection of downstream dependency metadata. + protected DownstreamDependencyMetadataManager(IEnumerable downstreamDependencyMetadata) { + _ = Throw.IfNull(downstreamDependencyMetadata); + Dictionary dependencyTrieMap = []; - foreach (var dependency in downstreamDependencyMetadata) + foreach (IDownstreamDependencyMetadata dependency in downstreamDependencyMetadata) { AddDependency(dependency, dependencyTrieMap); } @@ -38,8 +50,14 @@ public DownstreamDependencyMetadataManager(IEnumerable + /// Gets request metadata for the specified HTTP request message. + /// + /// The HTTP request. + /// The resolved if found; otherwise, . + public virtual RequestMetadata? GetRequestMetadata(HttpRequestMessage requestMessage) { + _ = Throw.IfNull(requestMessage); #pragma warning disable CA1031 // Do not catch general exception types try { @@ -48,7 +66,7 @@ public DownstreamDependencyMetadataManager(IEnumerable + /// Gets request metadata for the specified HTTP web request. + /// + /// The HTTP web request. + /// The resolved if found; otherwise, null. + public virtual RequestMetadata? GetRequestMetadata(HttpWebRequest requestMessage) { + _ = Throw.IfNull(requestMessage); #pragma warning disable CA1031 // Do not catch general exception types try { - var hostMetadata = GetHostMetadata(requestMessage.RequestUri.Host); + HostSuffixTrieNode? hostMetadata = GetHostMetadata(requestMessage.RequestUri.Host); return GetRequestMetadataInternal(requestMessage.Method, requestMessage.RequestUri.AbsolutePath, hostMetadata); } catch (Exception) @@ -97,10 +121,10 @@ private static void AddRouteToTrie(RequestMetadata routeMetadata, Dictionary ProcessDownstreamDependencyMetadata(Dictionary dependencyTrieMap) { Dictionary finalArrayDict = []; - foreach (var dep in dependencyTrieMap) + foreach (KeyValuePair dep in dependencyTrieMap) { - var finalArray = ProcessDownstreamDependencyMetadataInternal(dep.Value); + ProcessedMetadata finalArray = ProcessDownstreamDependencyMetadataInternal(dep.Value); finalArrayDict.Add(dep.Key, finalArray); } @@ -202,11 +226,11 @@ private static ProcessedMetadata ProcessDownstreamDependencyMetadataInternal(Req int requestMetadataArraySize = 1; while (queue.Count > 0) { - var trieNode = queue.Dequeue(); + RequestMetadataTrieNode trieNode = queue.Dequeue(); finalArraySize += trieNode.ChildNodesCount; for (int i = 0; i < Constants.ASCIICharCount; i++) { - var node = trieNode.Nodes[i]; + RequestMetadataTrieNode node = trieNode.Nodes[i]; if (node != null) { if (node.RequestMetadata != null) @@ -237,10 +261,10 @@ private static ProcessedMetadata ProcessDownstreamDependencyMetadataInternal(Req int requestMetadataIndex = 0; while (queue.Count > 0) { - var trieNode = queue.Dequeue(); + RequestMetadataTrieNode trieNode = queue.Dequeue(); for (int i = 0; i < Constants.ASCIICharCount; i++) { - var node = trieNode.Nodes[i]; + RequestMetadataTrieNode node = trieNode.Nodes[i]; if (node != null) { var d = new FrozenRequestMetadataTrieNode @@ -274,23 +298,18 @@ private static ProcessedMetadata ProcessDownstreamDependencyMetadataInternal(Req private static FrozenRequestMetadataTrieNode? GetChildNode(char ch, FrozenRequestMetadataTrieNode node, ProcessedMetadata routeMetadataRoot) { bool isValid = ch >= node.YoungestChild && ch <= node.YoungestChild + node.ChildNodesCount; - if (isValid) - { - return routeMetadataRoot.Nodes![node.ChildStartIndex + ch - node.YoungestChild]; - } - - return null; + return isValid ? routeMetadataRoot.Nodes[node.ChildStartIndex + ch - node.YoungestChild] : null; } private void AddDependency(IDownstreamDependencyMetadata downstreamDependencyMetadata, Dictionary dependencyTrieMap) { - foreach (var hostNameSuffix in downstreamDependencyMetadata.UniqueHostNameSuffixes) + foreach (string hostNameSuffix in downstreamDependencyMetadata.UniqueHostNameSuffixes) { // Add hostname to hostname suffix trie AddHostnameToTrie(hostNameSuffix, downstreamDependencyMetadata.DependencyName); } - foreach (var routeMetadata in downstreamDependencyMetadata.RequestMetadata) + foreach (RequestMetadata routeMetadata in downstreamDependencyMetadata.RequestMetadata) { routeMetadata.DependencyName = downstreamDependencyMetadata.DependencyName; @@ -302,7 +321,7 @@ private void AddDependency(IDownstreamDependencyMetadata downstreamDependencyMet private void AddHostnameToTrie(string hostNameSuffix, string dependencyName) { hostNameSuffix = hostNameSuffix.ToUpperInvariant(); - var trieCurrent = _hostSuffixTrieRoot; + HostSuffixTrieNode trieCurrent = _hostSuffixTrieRoot; for (int i = hostNameSuffix.Length - 1; i >= 0; i--) { char ch = hostNameSuffix[i]; @@ -323,8 +342,7 @@ private void AddHostnameToTrie(string hostNameSuffix, string dependencyName) private HostSuffixTrieNode? GetHostMetadata(string host) { HostSuffixTrieNode? hostMetadataNode = null; - string dependencyName = string.Empty; - var trieCurrent = _hostSuffixTrieRoot; + HostSuffixTrieNode trieCurrent = _hostSuffixTrieRoot; for (int i = host.Length - 1; i >= 0; i--) { char ch = host[i]; @@ -376,17 +394,17 @@ private void AddHostnameToTrie(string hostNameSuffix, string dependencyName) } } - var trieCurrent = routeMetadataTrieRoot.Nodes[0]; - var lastStartNode = trieCurrent; - var requestPathEndIndex = requestRouteAsSpan.Length; + FrozenRequestMetadataTrieNode trieCurrent = routeMetadataTrieRoot.Nodes[0]; + FrozenRequestMetadataTrieNode lastStartNode = trieCurrent; + int requestPathEndIndex = requestRouteAsSpan.Length; for (int i = 0; i < requestPathEndIndex; i++) { char ch = _toUpper[requestRouteAsSpan[i]]; - var childNode = GetChildNode(ch, trieCurrent, routeMetadataTrieRoot); + FrozenRequestMetadataTrieNode? childNode = GetChildNode(ch, trieCurrent, routeMetadataTrieRoot); if (childNode == null) { trieCurrent = lastStartNode; - var asteriskChildNode = GetChildNode(AsteriskChar, trieCurrent, routeMetadataTrieRoot); + FrozenRequestMetadataTrieNode? asteriskChildNode = GetChildNode(AsteriskChar, trieCurrent, routeMetadataTrieRoot); if (asteriskChildNode == null) { break; @@ -401,10 +419,10 @@ private void AddHostnameToTrie(string hostNameSuffix, string dependencyName) } // we add i to the index, because the index returned from ReadOnlySpan is the index in the new slice, not in the original slice. - var nextDelimiterIndex = requestRouteAsSpan.Slice(i, requestPathEndIndex - i).IndexOf(trieCurrent.Delimiter) + i; + int nextDelimiterIndex = requestRouteAsSpan.Slice(i, requestPathEndIndex - i).IndexOf(trieCurrent.Delimiter) + i; // if we reached end of the request path or end of trie, break - var delimChildNode = GetChildNode(trieCurrent.Delimiter, trieCurrent, routeMetadataTrieRoot); + FrozenRequestMetadataTrieNode? delimChildNode = GetChildNode(trieCurrent.Delimiter, trieCurrent, routeMetadataTrieRoot); if (nextDelimiterIndex == i - 1 || delimChildNode == null) { break; @@ -434,7 +452,7 @@ private void AddHostnameToTrie(string hostNameSuffix, string dependencyName) for (int j = 0; j < httpMethod.Length; j++) { char ch = _toUpper[httpMethod[j]]; - var childNode = GetChildNode(ch, trieCurrent, routeMetadataTrieRoot); + FrozenRequestMetadataTrieNode? childNode = GetChildNode(ch, trieCurrent, routeMetadataTrieRoot); if (childNode == null) { // Return the default request metadata for the host which diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/HttpDiagnosticsServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/HttpDiagnosticsServiceCollectionExtensions.cs index 6cdcd74b263..a28b8ea4284 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/HttpDiagnosticsServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/HttpDiagnosticsServiceCollectionExtensions.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Http.Diagnostics; -using Microsoft.Extensions.Telemetry.Internal; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.DependencyInjection; @@ -23,7 +22,7 @@ public static class HttpDiagnosticsServiceCollectionExtensions public static IServiceCollection AddDownstreamDependencyMetadata(this IServiceCollection services, IDownstreamDependencyMetadata downstreamDependencyMetadata) { _ = Throw.IfNull(services); - services.TryAddSingleton(); + services.TryAddSingleton(); _ = services.AddSingleton(downstreamDependencyMetadata); return services; @@ -39,7 +38,7 @@ public static IServiceCollection AddDownstreamDependencyMetadata(this IServiceCo where T : class, IDownstreamDependencyMetadata { _ = Throw.IfNull(services); - services.TryAddSingleton(); + services.TryAddSingleton(); _ = services.AddSingleton(); return services; diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestReader.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestReader.cs index a9eaf982ac4..2a98bba1279 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestReader.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestReader.cs @@ -12,7 +12,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http.Diagnostics; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Telemetry.Internal; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Http.Logging.Internal; @@ -40,7 +39,7 @@ internal sealed class HttpRequestReader : IHttpRequestReader private readonly OutgoingPathLoggingMode _outgoingPathLogMode; private readonly IOutgoingRequestContext _requestMetadataContext; - private readonly IDownstreamDependencyMetadataManager? _downstreamDependencyMetadataManager; + private readonly DownstreamDependencyMetadataManager? _downstreamDependencyMetadataManager; public HttpRequestReader( IServiceProvider serviceProvider, @@ -48,7 +47,7 @@ public HttpRequestReader( IHttpRouteFormatter routeFormatter, IHttpRouteParser httpRouteParser, IOutgoingRequestContext requestMetadataContext, - IDownstreamDependencyMetadataManager? downstreamDependencyMetadataManager = null, + DownstreamDependencyMetadataManager? downstreamDependencyMetadataManager = null, [ServiceKey] string? serviceKey = null) : this( optionsMonitor.GetKeyedOrCurrent(serviceKey), @@ -66,7 +65,7 @@ internal HttpRequestReader( IHttpRouteParser httpRouteParser, IHttpHeadersReader httpHeadersReader, IOutgoingRequestContext requestMetadataContext, - IDownstreamDependencyMetadataManager? downstreamDependencyMetadataManager = null) + DownstreamDependencyMetadataManager? downstreamDependencyMetadataManager = null) { _outgoingPathLogMode = Throw.IfOutOfRange(options.RequestPathLoggingMode); _httpHeadersReader = httpHeadersReader; diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/IDownstreamDependencyMetadataManager.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/IDownstreamDependencyMetadataManager.cs deleted file mode 100644 index 5c49827a3b0..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/IDownstreamDependencyMetadataManager.cs +++ /dev/null @@ -1,28 +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; -using System.Net.Http; -using Microsoft.Extensions.Http.Diagnostics; - -namespace Microsoft.Extensions.Telemetry.Internal; - -/// -/// Interface to manage dependency metadata. -/// -internal interface IDownstreamDependencyMetadataManager -{ - /// - /// Get metadata for the given request. - /// - /// Request object. - /// object. - RequestMetadata? GetRequestMetadata(HttpRequestMessage requestMessage); - - /// - /// Get metadata for the given request. - /// - /// Request object. - /// object. - RequestMetadata? GetRequestMetadata(HttpWebRequest requestMessage); -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Telemetry/DownstreamDependencyMetadataManagerTests.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Telemetry/DownstreamDependencyMetadataManagerTests.cs index e7cdb4d3f32..bca9127c729 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Telemetry/DownstreamDependencyMetadataManagerTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Telemetry/DownstreamDependencyMetadataManagerTests.cs @@ -4,14 +4,14 @@ using System; using System.Net.Http; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Extensions.Http.Diagnostics; using Xunit; namespace Microsoft.Extensions.Telemetry.Telemetry; public class DownstreamDependencyMetadataManagerTests : IDisposable { - private readonly IDownstreamDependencyMetadataManager _depMetadataManager; + private readonly DownstreamDependencyMetadataManager _depMetadataManager; private readonly ServiceProvider _sp; public DownstreamDependencyMetadataManagerTests() @@ -20,7 +20,7 @@ public DownstreamDependencyMetadataManagerTests() .AddDownstreamDependencyMetadata(new BackslashDownstreamDependencyMetadata()) .AddDownstreamDependencyMetadata(new EmptyRouteDependencyMetadata()) .BuildServiceProvider(); - _depMetadataManager = _sp.GetRequiredService(); + _depMetadataManager = _sp.GetRequiredService(); } [Theory] From eb365cb6fbab41690f98a60b6b78bab4bd333c9c Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 Date: Tue, 26 Aug 2025 12:58:17 +0200 Subject: [PATCH 2/2] Initial commit --- .../HttpLoggingRedactionInterceptor.cs | 8 +- .../DownstreamDependencyMetadataManager.cs | 11 +- .../Logging/Internal/HttpRequestReader.cs | 12 +- .../Http/DefaultHttpRouteFormatter.cs | 10 + .../Http/DefaultHttpRouteParser.cs | 8 + .../Http/HttpRouteFormatter.cs | 148 ++++++++----- .../Http/HttpRouteParameter.cs | 6 +- .../Http/HttpRouteParser.cs | 199 +++++++++++------- .../Http/IHttpRouteFormatter.cs | 34 --- .../Http/IHttpRouteParser.cs | 37 ---- .../Http/ParsedRouteSegments.cs | 14 +- .../Http/Segment.cs | 8 +- .../Http/TelemetryCommonExtensions.cs | 5 +- .../Logging/HttpClientLoggerTest.cs | 30 +-- .../Logging/HttpRequestReaderTest.cs | 20 +- .../TelemetryCommonExtensionsTests.cs | 4 +- .../Http/HttpParserTests.cs | 6 +- .../Http/HttpRouteFormatterTests.cs | 4 +- .../Http/TelemetryCommonExtensionsTests.cs | 4 +- 19 files changed, 299 insertions(+), 269 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Http/DefaultHttpRouteFormatter.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Http/DefaultHttpRouteParser.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Http/IHttpRouteFormatter.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Http/IHttpRouteParser.cs diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/HttpLoggingRedactionInterceptor.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/HttpLoggingRedactionInterceptor.cs index 31a253f68db..7dfac004475 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/HttpLoggingRedactionInterceptor.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/HttpLoggingRedactionInterceptor.cs @@ -26,8 +26,8 @@ internal sealed class HttpLoggingRedactionInterceptor : IHttpLoggingInterceptor private readonly IncomingPathLoggingMode _requestPathLogMode; private readonly HttpRouteParameterRedactionMode _parameterRedactionMode; private readonly ILogger _logger; - private readonly IHttpRouteParser _httpRouteParser; - private readonly IHttpRouteFormatter _httpRouteFormatter; + private readonly HttpRouteParser _httpRouteParser; + private readonly HttpRouteFormatter _httpRouteFormatter; private readonly IIncomingHttpRouteUtility _httpRouteUtility; private readonly HeaderReader _requestHeadersReader; private readonly HeaderReader _responseHeadersReader; @@ -39,8 +39,8 @@ public HttpLoggingRedactionInterceptor( IOptions options, ILogger logger, IEnumerable httpLogEnrichers, - IHttpRouteParser httpRouteParser, - IHttpRouteFormatter httpRouteFormatter, + HttpRouteParser httpRouteParser, + HttpRouteFormatter httpRouteFormatter, IRedactorProvider redactorProvider, IIncomingHttpRouteUtility httpRouteUtility) { diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DownstreamDependencyMetadataManager.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DownstreamDependencyMetadataManager.cs index a7cb625f0d4..c8aa6349572 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DownstreamDependencyMetadataManager.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DownstreamDependencyMetadataManager.cs @@ -115,7 +115,7 @@ private static char[] MakeToUpperArray() private static void AddRouteToTrie(RequestMetadata routeMetadata, Dictionary dependencyTrieMap) { - if (!dependencyTrieMap.TryGetValue(routeMetadata.DependencyName, out var routeMetadataTrieRoot)) + if (!dependencyTrieMap.TryGetValue(routeMetadata.DependencyName, out RequestMetadataTrieNode? routeMetadataTrieRoot)) { routeMetadataTrieRoot = new RequestMetadataTrieNode(); dependencyTrieMap.Add(routeMetadata.DependencyName, routeMetadataTrieRoot); @@ -213,11 +213,6 @@ private static Dictionary ProcessDownstreamDependency return finalArrayDict; } - // This method has 100% coverage but there is some issue with the code coverage tool in the CI pipeline which makes it - // buggy and complain about some parts the code in this method as not covered. If you make changes to this method, please - // remove the ExlcudeCodeCoverage attribute and ensure it's covered fully using local runs and enable it back before - // pushing the change to PR. - [ExcludeFromCodeCoverage] private static ProcessedMetadata ProcessDownstreamDependencyMetadataInternal(RequestMetadataTrieNode requestMetadataTrieRoot) { Queue queue = new(); @@ -231,9 +226,9 @@ private static ProcessedMetadata ProcessDownstreamDependencyMetadataInternal(Req for (int i = 0; i < Constants.ASCIICharCount; i++) { RequestMetadataTrieNode node = trieNode.Nodes[i]; - if (node != null) + if (node is not null) { - if (node.RequestMetadata != null) + if (node.RequestMetadata is not null) { requestMetadataArraySize++; } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestReader.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestReader.cs index 2a98bba1279..94bbb0c1976 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestReader.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestReader.cs @@ -18,8 +18,8 @@ namespace Microsoft.Extensions.Http.Logging.Internal; internal sealed class HttpRequestReader : IHttpRequestReader { - private readonly IHttpRouteFormatter _routeFormatter; - private readonly IHttpRouteParser _httpRouteParser; + private readonly HttpRouteFormatter _routeFormatter; + private readonly HttpRouteParser _httpRouteParser; private readonly IHttpHeadersReader _httpHeadersReader; private readonly FrozenDictionary _defaultSensitiveParameters; @@ -44,8 +44,8 @@ internal sealed class HttpRequestReader : IHttpRequestReader public HttpRequestReader( IServiceProvider serviceProvider, IOptionsMonitor optionsMonitor, - IHttpRouteFormatter routeFormatter, - IHttpRouteParser httpRouteParser, + HttpRouteFormatter routeFormatter, + HttpRouteParser httpRouteParser, IOutgoingRequestContext requestMetadataContext, DownstreamDependencyMetadataManager? downstreamDependencyMetadataManager = null, [ServiceKey] string? serviceKey = null) @@ -61,8 +61,8 @@ public HttpRequestReader( internal HttpRequestReader( LoggingOptions options, - IHttpRouteFormatter routeFormatter, - IHttpRouteParser httpRouteParser, + HttpRouteFormatter routeFormatter, + HttpRouteParser httpRouteParser, IHttpHeadersReader httpHeadersReader, IOutgoingRequestContext requestMetadataContext, DownstreamDependencyMetadataManager? downstreamDependencyMetadataManager = null) diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Http/DefaultHttpRouteFormatter.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Http/DefaultHttpRouteFormatter.cs new file mode 100644 index 00000000000..3c7c697711e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Http/DefaultHttpRouteFormatter.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Compliance.Redaction; + +namespace Microsoft.Extensions.Http.Diagnostics; + +internal sealed class DefaultHttpRouteFormatter(HttpRouteParser httpRouteParser, IRedactorProvider redactorProvider) + : HttpRouteFormatter(httpRouteParser, redactorProvider); + diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Http/DefaultHttpRouteParser.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Http/DefaultHttpRouteParser.cs new file mode 100644 index 00000000000..56e47b2de79 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Http/DefaultHttpRouteParser.cs @@ -0,0 +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 Microsoft.Extensions.Compliance.Redaction; + +namespace Microsoft.Extensions.Http.Diagnostics; + +internal sealed class DefaultHttpRouteParser(IRedactorProvider redactorProvider) : HttpRouteParser(redactorProvider); diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Http/HttpRouteFormatter.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Http/HttpRouteFormatter.cs index 0ee93b5e851..d35934c0381 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Http/HttpRouteFormatter.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Http/HttpRouteFormatter.cs @@ -3,19 +3,25 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text; using Microsoft.Extensions.Compliance.Classification; using Microsoft.Extensions.Compliance.Redaction; -using Microsoft.Extensions.Http.Diagnostics; +using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Pools; namespace Microsoft.Extensions.Http.Diagnostics; -internal sealed class HttpRouteFormatter : IHttpRouteFormatter +/// +/// Formats HTTP request paths using route templates with sensitive parameters optionally redacted. +/// +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +[SuppressMessage("Minor Code Smell", "S1694:An abstract class should have both abstract and concrete methods", Justification = "Abstract for extensibility; no abstract members required now.")] +public abstract class HttpRouteFormatter { private const char ForwardSlashSymbol = '/'; -#if NET6_0_OR_GREATER +#if NET private const char ForwardSlash = ForwardSlashSymbol; #else #pragma warning disable IDE1006 // Naming Styles @@ -23,50 +29,77 @@ internal sealed class HttpRouteFormatter : IHttpRouteFormatter #pragma warning restore IDE1006 // Naming Styles #endif - private readonly IHttpRouteParser _httpRouteParser; + private readonly HttpRouteParser _httpRouteParser; private readonly IRedactorProvider _redactorProvider; - public HttpRouteFormatter(IHttpRouteParser httpRouteParser, IRedactorProvider redactorProvider) + /// + /// Initializes a new instance of the class. + /// + /// The route parser. + /// The redactor provider used to redact sensitive parameters. + protected HttpRouteFormatter(HttpRouteParser httpRouteParser, IRedactorProvider redactorProvider) { _httpRouteParser = httpRouteParser; _redactorProvider = redactorProvider; } - public string Format(string httpRoute, string httpPath, HttpRouteParameterRedactionMode redactionMode, IReadOnlyDictionary parametersToRedact) + /// + /// Format the http path using the route template with sensitive parameters redacted. + /// + /// Http request route template. + /// Http request's absolute path. + /// Strategy to decide how parameters are redacted. + /// Dictionary of parameters with their data classification that needs to be redacted. + /// Returns formatted path with sensitive parameter values redacted. + public virtual string Format(string httpRoute, string httpPath, HttpRouteParameterRedactionMode redactionMode, IReadOnlyDictionary parametersToRedact) { - var routeSegments = _httpRouteParser.ParseRoute(httpRoute); + ParsedRouteSegments routeSegments = _httpRouteParser.ParseRoute(httpRoute); return Format(routeSegments, httpPath, redactionMode, parametersToRedact); } - public string Format( + /// + /// Format the http path using the route template with sensitive parameters redacted. + /// + /// Http request's route segments. + /// Http request's absolute path. + /// Strategy to decide how parameters are redacted. + /// Dictionary of parameters with their data classification that needs to be redacted. + /// Returns formatted path with sensitive parameter values redacted. + public virtual string Format( in ParsedRouteSegments routeSegments, string httpPath, HttpRouteParameterRedactionMode redactionMode, IReadOnlyDictionary parametersToRedact) { - if (routeSegments.ParameterCount == 0 || + if (httpPath is null) + { + return string.Empty; + } + + if (parametersToRedact is null || + routeSegments.ParameterCount == 0 || !IsRedactionRequired(routeSegments, redactionMode, parametersToRedact)) { return httpPath.Trim(ForwardSlash); } - var httpPathAsSpan = httpPath.AsSpan().TrimStart(ForwardSlash); - var pathStringBuilder = PoolFactory.SharedStringBuilderPool.Get(); + ReadOnlySpan httpPathAsSpan = httpPath.AsSpan().TrimStart(ForwardSlash); + StringBuilder pathStringBuilder = PoolFactory.SharedStringBuilderPool.Get(); try { int offset = 0; - for (var i = 0; i < routeSegments.Segments.Length; i++) + for (int i = 0; i < routeSegments.Segments.Length; i++) { - var segment = routeSegments.Segments[i]; + Segment segment = routeSegments.Segments[i]; if (segment.IsParam) { - var parameterContent = segment.Content; - var parameterTemplateLength = parameterContent.Length + 2; + string parameterContent = segment.Content; + int parameterTemplateLength = parameterContent.Length + 2; - var startIndex = segment.Start + offset; + int startIndex = segment.Start + offset; // If we exceed a length of the http path it means that the appropriate http route // has optional parameters or parameters with default values, and these parameters @@ -121,35 +154,42 @@ private static bool IsRedactionRequired( return false; } - foreach (var segment in routeSegments.Segments) + foreach (Segment segment in routeSegments.Segments) { if (!segment.IsParam) { continue; } - if (redactionMode == HttpRouteParameterRedactionMode.Strict) + switch (redactionMode) { - // If no data class exists for a parameter, and the parameter is not a well known parameter, then we redact it. - // If data class exists and it's anything other than DataClassification.None, then also we redact it. - if ((!parametersToRedact.TryGetValue(segment.ParamName, out DataClassification classification) && - !Segment.IsKnownUnredactableParameter(segment.ParamName)) || - classification != DataClassification.None) + case HttpRouteParameterRedactionMode.Strict: { - return true; + // If no data class exists for a parameter, and the parameter is not a well known parameter, then we redact it. + // If data class exists, and it's anything other than DataClassification.None, then also we redact it. + if ((!parametersToRedact.TryGetValue(segment.ParamName, out DataClassification classification) && + !Segment.IsKnownUnredactableParameter(segment.ParamName)) || + classification != DataClassification.None) + { + return true; + } + + break; } - } - else if (redactionMode == HttpRouteParameterRedactionMode.Loose) - { - // If data class exists for a parameter and it's anything other than DataClassification.None, then we redact it. - if (parametersToRedact.TryGetValue(segment.ParamName, out DataClassification classification) && classification != DataClassification.None) + + case HttpRouteParameterRedactionMode.Loose: { - return true; + // If data class exists for a parameter, and it's anything other than DataClassification.None, then we redact it. + if (parametersToRedact.TryGetValue(segment.ParamName, out DataClassification classification) && classification != DataClassification.None) + { + return true; + } + + break; } - } - else - { - throw new InvalidOperationException(TelemetryCommonExtensions.UnsupportedEnumValueExceptionMessage); + + default: + throw new InvalidOperationException(TelemetryCommonExtensions.UnsupportedEnumValueExceptionMessage); } } @@ -158,14 +198,15 @@ private static bool IsRedactionRequired( private static void RemoveTrailingForwardSlash(StringBuilder formattedHttpPath) { - if (formattedHttpPath.Length > 1) + if (formattedHttpPath.Length <= 1) { - int index = formattedHttpPath.Length - 1; + return; + } - if (formattedHttpPath[index] == ForwardSlashSymbol) - { - _ = formattedHttpPath.Remove(index, 1); - } + int lastCharIndex = formattedHttpPath.Length - 1; + if (formattedHttpPath[lastCharIndex] == ForwardSlashSymbol) + { + _ = formattedHttpPath.Remove(lastCharIndex, 1); } } @@ -178,15 +219,14 @@ private void FormatParameter( IReadOnlyDictionary parametersToRedact, StringBuilder outputBuffer) { - if (redactionMode == HttpRouteParameterRedactionMode.Strict) - { - FormatParameterInStrictMode(httpPath, segment, startIndex, length, parametersToRedact, outputBuffer); - return; - } - - if (redactionMode == HttpRouteParameterRedactionMode.Loose) + switch (redactionMode) { - FormatParameterInLooseMode(httpPath, segment, startIndex, length, parametersToRedact, outputBuffer); + case HttpRouteParameterRedactionMode.Strict: + FormatParameterInStrictMode(httpPath, segment, startIndex, length, parametersToRedact, outputBuffer); + return; + case HttpRouteParameterRedactionMode.Loose: + FormatParameterInLooseMode(httpPath, segment, startIndex, length, parametersToRedact, outputBuffer); + break; } } @@ -202,12 +242,12 @@ private void FormatParameterInStrictMode( { if (classification != DataClassification.None) { - var redactor = _redactorProvider.GetRedactor(classification); + Redactor redactor = _redactorProvider.GetRedactor(classification); _ = outputBuffer.AppendRedacted(redactor, httpPath.Slice(startIndex, length)); } else { -#if NETCOREAPP3_1_OR_GREATER +#if NET _ = outputBuffer.Append(httpPath.Slice(startIndex, length)); #else _ = outputBuffer.Append(httpPath.Slice(startIndex, length).ToString()); @@ -216,7 +256,7 @@ private void FormatParameterInStrictMode( } else if (Segment.IsKnownUnredactableParameter(httpRouteSegment.ParamName)) { -#if NETCOREAPP3_1_OR_GREATER +#if NET _ = outputBuffer.Append(httpPath.Slice(startIndex, length)); #else _ = outputBuffer.Append(httpPath.Slice(startIndex, length).ToString()); @@ -224,7 +264,7 @@ private void FormatParameterInStrictMode( } else { -#if NETCOREAPP3_1_OR_GREATER +#if NET _ = outputBuffer.Append(TelemetryConstants.Redacted.AsSpan()); #else _ = outputBuffer.Append(TelemetryConstants.Redacted); @@ -243,12 +283,12 @@ private void FormatParameterInLooseMode( if (parametersToRedact.TryGetValue(httpRouteSegment.ParamName, out DataClassification classification) && classification != DataClassification.None) { - var redactor = _redactorProvider.GetRedactor(classification); + Redactor redactor = _redactorProvider.GetRedactor(classification); _ = outputBuffer.AppendRedacted(redactor, httpPath.Slice(startIndex, length)); } else { -#if NETCOREAPP3_1_OR_GREATER +#if NET _ = outputBuffer.Append(httpPath.Slice(startIndex, length)); #else _ = outputBuffer.Append(httpPath.Slice(startIndex, length).ToString()); diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Http/HttpRouteParameter.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Http/HttpRouteParameter.cs index 68e07d86db8..5785c1a67a3 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Http/HttpRouteParameter.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Http/HttpRouteParameter.cs @@ -1,13 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; + namespace Microsoft.Extensions.Http.Diagnostics; #pragma warning disable CA1815 // Override equals and operator equals on value types /// /// Struct to hold metadata about a route parameter. /// -internal readonly struct HttpRouteParameter +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public readonly struct HttpRouteParameter #pragma warning restore CA1815 // Override equals and operator equals on value types { /// diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Http/HttpRouteParser.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Http/HttpRouteParser.cs index 6783785d838..1a86d2ea049 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Http/HttpRouteParser.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Http/HttpRouteParser.cs @@ -4,16 +4,22 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Compliance.Classification; using Microsoft.Extensions.Compliance.Redaction; -using Microsoft.Extensions.Http.Diagnostics; +using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Http.Diagnostics; -internal sealed class HttpRouteParser : IHttpRouteParser +/// +/// HTTP request route parser. +/// +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +[SuppressMessage("Minor Code Smell", "S1694:An abstract class should have both abstract and concrete methods", Justification = "Abstract for extensibility; no abstract members required now.")] +public abstract class HttpRouteParser { -#if NETCOREAPP3_1_OR_GREATER +#if NET private const char ForwardSlash = '/'; #else #pragma warning disable IDE1006 // Naming Styles @@ -24,12 +30,25 @@ internal sealed class HttpRouteParser : IHttpRouteParser private readonly IRedactorProvider _redactorProvider; private readonly ConcurrentDictionary _routeTemplateSegmentsCache = new(); - public HttpRouteParser(IRedactorProvider redactorProvider) + /// + /// Initializes a new instance of the class. + /// + /// Redactor provider to use for getting redactors for privacy data. + protected HttpRouteParser(IRedactorProvider redactorProvider) { _redactorProvider = redactorProvider; } - public bool TryExtractParameters( + /// + /// Extract parameters values from the http request path. + /// + /// Http request's absolute path. + /// Route segments containing text and parameter segments of the route. + /// Strategy to decide how parameters are redacted. + /// Dictionary of parameters with their data classification that needs to be redacted. + /// Output array where parameters will be stored. Caller must provide the array with enough capacity to hold all parameters in route segment. + /// if parameters were extracted successfully, otherwise. + public virtual bool TryExtractParameters( string httpPath, in ParsedRouteSegments routeSegments, HttpRouteParameterRedactionMode redactionMode, @@ -38,61 +57,70 @@ public bool TryExtractParameters( { int paramCount = routeSegments.ParameterCount; - if (httpRouteParameters == null || httpRouteParameters.Length < paramCount) + if (httpRouteParameters is null || httpRouteParameters.Length < paramCount) { return false; } - var httpPathAsSpan = httpPath.AsSpan(); + ReadOnlySpan httpPathAsSpan = httpPath.AsSpan(); httpPathAsSpan = httpPathAsSpan.TrimStart(ForwardSlash); - if (paramCount > 0) + if (paramCount <= 0) { - int offset = 0; - int index = 0; + return true; + } + + int offset = 0; + int index = 0; - foreach (Segment segment in routeSegments.Segments) + foreach (Segment segment in routeSegments.Segments) + { + if (!segment.IsParam) { - if (segment.IsParam) - { - var startIndex = segment.Start + offset; + continue; + } - // If we exceed a length of the http path it means that the appropriate http route - // has optional parameters or parameters with default values, and these parameters - // are omitted in the http path. In this case we return a default value of the - // omitted parameter. - string parameterValue = segment.DefaultValue; + int startIndex = segment.Start + offset; - bool isRedacted = false; + // If we exceed a length of the http path it means that the appropriate http route + // has optional parameters or parameters with default values, and these parameters + // are omitted in the http path. In this case we return a default value of the + // omitted parameter. + string parameterValue = segment.DefaultValue; - if (startIndex < httpPathAsSpan.Length) - { - var parameterContent = segment.Content; - var parameterTemplateLength = parameterContent.Length + 2; + bool isRedacted = false; - var length = httpPathAsSpan.Slice(startIndex).IndexOf(ForwardSlash); + if (startIndex < httpPathAsSpan.Length) + { + string parameterContent = segment.Content; + int parameterTemplateLength = parameterContent.Length + 2; - if (segment.IsCatchAll || length == -1) - { - length = httpPathAsSpan.Slice(startIndex).Length; - } + int length = httpPathAsSpan.Slice(startIndex).IndexOf(ForwardSlash); - offset += length - parameterTemplateLength; + if (segment.IsCatchAll || length == -1) + { + length = httpPathAsSpan.Slice(startIndex).Length; + } - parameterValue = GetRedactedParameterValue(httpPathAsSpan, segment, startIndex, length, redactionMode, parametersToRedact, ref isRedacted); - } + offset += length - parameterTemplateLength; - httpRouteParameters[index++] = new HttpRouteParameter(segment.ParamName, parameterValue, isRedacted); - } + parameterValue = GetRedactedParameterValue(httpPathAsSpan, segment, startIndex, length, redactionMode, parametersToRedact, ref isRedacted); } + + httpRouteParameters[index++] = new HttpRouteParameter(segment.ParamName, parameterValue, isRedacted); } return true; } - public ParsedRouteSegments ParseRoute(string httpRoute) + /// + /// Parses http route and breaks it into text and parameter segments. + /// + /// Http request's route template. + /// Returns text and parameter segments of route. + public virtual ParsedRouteSegments ParseRoute(string httpRoute) { - return _routeTemplateSegmentsCache.GetOrAdd(httpRoute, httpRoute => + return _routeTemplateSegmentsCache.GetOrAdd(httpRoute, _ => { httpRoute = httpRoute.TrimStart(ForwardSlash); @@ -163,44 +191,56 @@ private static Segment GetParameterSegment(string httpRoute, ref int pos) while ((ch = httpRoute[pos]) != '}') { - // The segment has a default value '='. The character indicates - // that we've met the end of the segment's parameter name and - // the start of the default value. - if (ch == '=') + switch (ch) { - if (paramNameEnd == PositionNotFound) + // The segment has a default value '='. The character indicates + // that we've met the end of the segment's parameter name and + // the start of the default value. + case '=': { - paramNameEnd = pos; - } + if (paramNameEnd == PositionNotFound) + { + paramNameEnd = pos; + } - defaultValueStart = pos + 1; - } + defaultValueStart = pos + 1; + break; + } - // The segment is optional '?' or has a constraint ':'. - // When we meet one of the above characters it indicates - // that we've met the end of the segment's parameter name. - else if (ch == '?' || ch == ':') - { - if (paramNameEnd == PositionNotFound) + // The segment is optional '?' or has a constraint ':'. + // When we meet one of the above characters it indicates + // that we've met the end of the segment's parameter name. + case '?': + case ':': { - paramNameEnd = pos; - } - } + if (paramNameEnd == PositionNotFound) + { + paramNameEnd = pos; + } - // The segment has '*' catch all parameter. - // When we meet the character it indicates param start position needs to be adjusted, so that we capture 'param' instead of '*param' - // *param can only appear after opening curly brace and position needs to be adjusted only once - else if (!catchAllParamFound && ch == '*' && pos > 0 && httpRoute[pos - 1] == '{') - { - paramNameStart++; + break; + } - // Catch all parameters can start with one or two '*' characters. - if (httpRoute[paramNameStart] == '*') + // The segment has '*' catch all parameter. + // When we meet the character it indicates param start position needs to be adjusted, so that we capture 'param' instead of '*param' + // *param can only appear after opening curly brace and position needs to be adjusted only once + default: { - paramNameStart++; - } + if (!catchAllParamFound && ch == '*' && pos > 0 && httpRoute[pos - 1] == '{') + { + paramNameStart++; + + // Catch all parameters can start with one or two '*' characters. + if (httpRoute[paramNameStart] == '*') + { + paramNameStart++; + } - catchAllParamFound = true; + catchAllParamFound = true; + } + + break; + } } pos++; @@ -271,15 +311,16 @@ private string GetRedactedParameterInStrictMode( { if (parametersToRedact.TryGetValue(segment.ParamName, out DataClassification classification)) { - if (classification != DataClassification.None) + if (classification == DataClassification.None) { - var redactor = _redactorProvider.GetRedactor(classification); - isRedacted = true; - - return redactor.Redact(httpPathAsSpan.Slice(startIndex, length)); + return httpPathAsSpan.Slice(startIndex, length).ToString(); } - return httpPathAsSpan.Slice(startIndex, length).ToString(); + Redactor redactor = _redactorProvider.GetRedactor(classification); + isRedacted = true; + + return redactor.Redact(httpPathAsSpan.Slice(startIndex, length)); + } if (Segment.IsKnownUnredactableParameter(segment.ParamName)) @@ -300,15 +341,17 @@ private string GetRedactedParameterInLooseMode( IReadOnlyDictionary parametersToRedact, ref bool isRedacted) { - if (parametersToRedact.TryGetValue(segment.ParamName, out DataClassification classification) - && classification != DataClassification.None) + if (!parametersToRedact.TryGetValue(segment.ParamName, out DataClassification classification) + || classification == DataClassification.None) { - var redactor = _redactorProvider.GetRedactor(classification); - isRedacted = true; - - return redactor.Redact(httpPathAsSpan.Slice(startIndex, length)); + return httpPathAsSpan.Slice(startIndex, length).ToString(); } - return httpPathAsSpan.Slice(startIndex, length).ToString(); + Redactor redactor = _redactorProvider.GetRedactor(classification); + isRedacted = true; + + return redactor.Redact(httpPathAsSpan.Slice(startIndex, length)); + } } + diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Http/IHttpRouteFormatter.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Http/IHttpRouteFormatter.cs deleted file mode 100644 index 973d8ebb071..00000000000 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Http/IHttpRouteFormatter.cs +++ /dev/null @@ -1,34 +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.Collections.Generic; -using Microsoft.Extensions.Compliance.Classification; -using Microsoft.Extensions.Http.Diagnostics; - -namespace Microsoft.Extensions.Http.Diagnostics; - -/// -/// Http request route formatter. -/// -internal interface IHttpRouteFormatter -{ - /// - /// Format the http path using the route template with sensitive parameters redacted. - /// - /// Http request route template. - /// Http request's absolute path. - /// Strategy to decide how parameters are redacted. - /// Dictionary of parameters with their data classification that needs to be redacted. - /// Returns formatted path with sensitive parameter values redacted. - string Format(string httpRoute, string httpPath, HttpRouteParameterRedactionMode redactionMode, IReadOnlyDictionary parametersToRedact); - - /// - /// Format the http path using the route template with sensitive parameters redacted. - /// - /// Http request's route segments. - /// Http request's absolute path. - /// Strategy to decide how parameters are redacted. - /// Dictionary of parameters with their data classification that needs to be redacted. - /// Returns formatted path with sensitive parameter values redacted. - string Format(in ParsedRouteSegments routeSegments, string httpPath, HttpRouteParameterRedactionMode redactionMode, IReadOnlyDictionary parametersToRedact); -} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Http/IHttpRouteParser.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Http/IHttpRouteParser.cs deleted file mode 100644 index 5831935f020..00000000000 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Http/IHttpRouteParser.cs +++ /dev/null @@ -1,37 +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.Collections.Generic; -using Microsoft.Extensions.Compliance.Classification; -using Microsoft.Extensions.Http.Diagnostics; - -namespace Microsoft.Extensions.Http.Diagnostics; - -/// -/// Http request route parser. -/// -internal interface IHttpRouteParser -{ - /// - /// Parses http route and breaks it into text and parameter segments. - /// - /// Http request's route template. - /// Returns text and parameter segments of route. - ParsedRouteSegments ParseRoute(string httpRoute); - - /// - /// Extract parameters values from the http request path. - /// - /// Http request's absolute path. - /// Route segments containing text and parameter segments of the route. - /// Strategy to decide how parameters are redacted. - /// Dictionary of parameters with their data classification that needs to be redacted. - /// Output array where parameters will be stored. Caller must provide the array with enough capacity to hold all parameters in route segment. - /// Returns true if parameters were extracted successfully, return false otherwise. - bool TryExtractParameters( - string httpPath, - in ParsedRouteSegments routeSegments, - HttpRouteParameterRedactionMode redactionMode, - IReadOnlyDictionary parametersToRedact, - ref HttpRouteParameter[] httpRouteParameters); -} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Http/ParsedRouteSegments.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Http/ParsedRouteSegments.cs index 99cdec3ca11..499a1e234ea 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Http/ParsedRouteSegments.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Http/ParsedRouteSegments.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.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Http.Diagnostics; @@ -8,9 +10,10 @@ namespace Microsoft.Extensions.Http.Diagnostics; /// /// Struct to hold the route segments created after parsing the route. /// -#pragma warning disable CA1815 // Override equals and operator equals on value types -internal readonly struct ParsedRouteSegments -#pragma warning restore CA1815 // Override equals and operator equals on value types +[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Comparing instances is not an expected scenario.")] +[SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "Preferring array for performance reasons, immutability is not a concern.")] +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public readonly struct ParsedRouteSegments { /// /// Initializes a new instance of the struct. @@ -24,7 +27,7 @@ public ParsedRouteSegments(string routeTemplate, Segment[] segments) Segments = segments; var paramCount = 0; - foreach (var segment in segments) + foreach (Segment segment in segments) { if (segment.IsParam) { @@ -44,13 +47,10 @@ public ParsedRouteSegments(string routeTemplate, Segment[] segments) /// /// Gets all segments of the route. /// -#pragma warning disable CA1819 // Properties should not return arrays public Segment[] Segments { get; } -#pragma warning restore CA1819 // Properties should not return arrays /// /// Gets the count of parameters in the route. /// public int ParameterCount { get; } } - diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Http/Segment.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Http/Segment.cs index 44f7d1b66aa..c51a04468d8 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Http/Segment.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Http/Segment.cs @@ -2,15 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Http.Diagnostics; /// /// Struct to hold the metadata about a route's segment. /// -#pragma warning disable CA1815 // Override equals and operator equals on value types -internal readonly struct Segment -#pragma warning restore CA1815 // Override equals and operator equals on value types +[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Comparing instances is not an expected scenario.")] +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public readonly struct Segment { private const string ControllerParameter = "controller"; private const string ActionParameter = "action"; diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Http/TelemetryCommonExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Http/TelemetryCommonExtensions.cs index 6031e1e662f..b08070c360f 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Http/TelemetryCommonExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Http/TelemetryCommonExtensions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http.Diagnostics; namespace Microsoft.Extensions.Http.Diagnostics; @@ -20,8 +19,8 @@ internal static class TelemetryCommonExtensions /// Returns object. public static IServiceCollection AddHttpRouteProcessor(this IServiceCollection services) { - services.TryAddActivatedSingleton(); - services.TryAddActivatedSingleton(); + services.TryAddActivatedSingleton(); + services.TryAddActivatedSingleton(); return services; } } diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpClientLoggerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpClientLoggerTest.cs index f57fb23b8d7..a70f3f2011f 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpClientLoggerTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpClientLoggerTest.cs @@ -97,7 +97,7 @@ public async Task SendAsync_HttpRequestException_ThrowsException() using var handler = new TestLoggingHandler( new HttpClientLogger( NullLogger.Instance, - new HttpRequestReader(options, GetHttpRouteFormatter(), Mock.Of(), headersReader, RequestMetadataContext), + new HttpRequestReader(options, GetHttpRouteFormatter(), new Mock(new FakeRedactorProvider()).Object, headersReader, RequestMetadataContext), Enumerable.Empty(), options), new TestingHandlerStub((_, _) => throw exception)); @@ -225,7 +225,7 @@ public async Task HttpLoggingHandler_AllOptions_LogsOutgoingRequest() new HttpRequestReader( options, GetHttpRouteFormatter(), - Mock.Of(), + new Mock(new FakeRedactorProvider()).Object, headersReader, RequestMetadataContext), new List { testEnricher }, options); @@ -322,7 +322,7 @@ public async Task HttpLoggingHandler_AllOptionsWithLogRequestStart_LogsOutgoingR new HttpRequestReader( options, GetHttpRouteFormatter(), - Mock.Of(), + new Mock(new FakeRedactorProvider()).Object, headersReader, RequestMetadataContext), new List { testEnricher }, options); @@ -429,7 +429,7 @@ public async Task HttpLoggingHandler_AllOptionsSendAsyncFailed_LogsRequestInform new HttpRequestReader( options, GetHttpRouteFormatter(), - Mock.Of(), + new Mock(new FakeRedactorProvider()).Object, headersReader, RequestMetadataContext), new List { testEnricher }, options), @@ -523,7 +523,7 @@ public async Task HttpLoggingHandler_ReadResponseThrows_LogsException() var exception = new InvalidOperationException("test"); - var actualRequestReader = new HttpRequestReader(options, GetHttpRouteFormatter(), Mock.Of(), headersReader, RequestMetadataContext); + var actualRequestReader = new HttpRequestReader(options, GetHttpRouteFormatter(), Mock.Of(), headersReader, RequestMetadataContext); var mockedRequestReader = new Mock(); mockedRequestReader .Setup(m => @@ -639,7 +639,7 @@ public async Task HttpLoggingHandler_AllOptionsTransferEncodingIsNotChunked_Logs new HttpRequestReader( options, GetHttpRouteFormatter(), - Mock.Of(), + new Mock(new FakeRedactorProvider()).Object, headersReader, RequestMetadataContext), new List { testEnricher }, options), @@ -682,7 +682,7 @@ public async Task HttpLoggingHandler_WithEnrichers_CallsEnrichMethodExactlyOnce( new HttpRequestReader( options, GetHttpRouteFormatter(), - Mock.Of(), + new Mock(new FakeRedactorProvider()).Object, new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object), RequestMetadataContext), new List { enricher1.Object, enricher2.Object }, @@ -725,7 +725,7 @@ public async Task HttpLoggingHandler_WithEnrichersAndLogRequestStart_CallsEnrich new HttpRequestReader( options, GetHttpRouteFormatter(), - Mock.Of(), + new Mock(new FakeRedactorProvider()).Object, new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object), RequestMetadataContext), new List { enricher1.Object, enricher2.Object }, @@ -762,7 +762,7 @@ public async Task HttpLoggingHandler_WithEnrichers_OneEnricherThrows_LogsEnrichm var fakeLogger = new FakeLogger(); var options = new LoggingOptions(); var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), Mock.Of()); - var requestReader = new HttpRequestReader(options, GetHttpRouteFormatter(), Mock.Of(), headersReader, RequestMetadataContext); + var requestReader = new HttpRequestReader(options, GetHttpRouteFormatter(), new Mock(new FakeRedactorProvider()).Object, headersReader, RequestMetadataContext); var enrichers = new List { enricher1.Object, enricher2.Object }; using var handler = new TestLoggingHandler( @@ -910,7 +910,7 @@ public async Task HttpLoggingHandler_AllOptionsTransferEncodingChunked_LogsOutgo new HttpRequestReader( options, GetHttpRouteFormatter(), - Mock.Of(), + new Mock(new FakeRedactorProvider()).Object, headersReader, RequestMetadataContext), new List { testEnricher }, options), @@ -954,7 +954,7 @@ public async Task HttpLoggingHandler_OnDifferentHttpStatusCodes_LogsOutgoingRequ var options = new LoggingOptions(); var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), new Mock().Object); var requestReader = new HttpRequestReader( - options, GetHttpRouteFormatter(), Mock.Of(), headersReader, RequestMetadataContext); + options, GetHttpRouteFormatter(), new Mock(new FakeRedactorProvider()).Object, headersReader, RequestMetadataContext); using var handler = new TestLoggingHandler( new HttpClientLogger(fakeLogger, requestReader, Array.Empty(), options), @@ -965,7 +965,7 @@ public async Task HttpLoggingHandler_OnDifferentHttpStatusCodes_LogsOutgoingRequ using var httpRequestMessage = new HttpRequestMessage { Method = HttpMethod.Post, - RequestUri = new($"http://default-uri.com/foo/bar"), + RequestUri = new Uri($"http://default-uri.com/foo/bar"), Content = new StringContent("request_content", Encoding.UTF8, TextPlain) }; @@ -1003,7 +1003,7 @@ public void SyncMethods_ShouldThrow() var options = new LoggingOptions(); var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), Mock.Of()); var requestReader = new HttpRequestReader( - options, GetHttpRouteFormatter(), Mock.Of(), headersReader, RequestMetadataContext); + options, GetHttpRouteFormatter(), new Mock(new FakeRedactorProvider()).Object, headersReader, RequestMetadataContext); var logger = new HttpClientLogger(new FakeLogger(), requestReader, Array.Empty(), options); using var httpRequestMessage = new HttpRequestMessage(); @@ -1014,14 +1014,14 @@ public void SyncMethods_ShouldThrow() Assert.Throws(() => logger.LogRequestFailed(null, httpRequestMessage, null, new InvalidOperationException(), TimeSpan.Zero)); } - private static IHttpRouteFormatter GetHttpRouteFormatter() + private static HttpRouteFormatter GetHttpRouteFormatter() { var builder = new ServiceCollection() .AddFakeRedaction() .AddHttpRouteProcessor() .BuildServiceProvider(); - return builder.GetService()!; + return builder.GetService()!; } private static void EnsureLogRecordDuration(string? actualValue) diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpRequestReaderTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpRequestReaderTest.cs index ba781c5d101..300f12927ab 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpRequestReaderTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpRequestReaderTest.cs @@ -74,8 +74,8 @@ public async Task ReadAsync_AllData_ReturnsLogRecord() var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(serviceKey), mockHeadersRedactor.Object, serviceKey); using var serviceProvider = GetServiceProvider(headersReader, serviceKey); - var reader = new HttpRequestReader(serviceProvider, options.ToOptionsMonitor(serviceKey), serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService(), RequestMetadataContext, serviceKey: serviceKey); + var reader = new HttpRequestReader(serviceProvider, options.ToOptionsMonitor(serviceKey), serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), RequestMetadataContext, serviceKey: serviceKey); using var httpRequestMessage = new HttpRequestMessage { @@ -137,8 +137,8 @@ public async Task ReadAsync_NoHost_ReturnsLogRecordWithoutHost() var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object); using var serviceProvider = GetServiceProvider(headersReader); - var reader = new HttpRequestReader(serviceProvider, options.ToOptionsMonitor(), serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService(), RequestMetadataContext); + var reader = new HttpRequestReader(serviceProvider, options.ToOptionsMonitor(), serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), RequestMetadataContext); using var httpRequestMessage = new HttpRequestMessage { @@ -201,7 +201,7 @@ public async Task ReadAsync_AllDataWithRequestMetadataSet_ReturnsLogRecord() using var serviceProvider = GetServiceProvider(headersReader); var reader = new HttpRequestReader(serviceProvider, opts.ToOptionsMonitor(), - serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); + serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); using var httpRequestMessage = new HttpRequestMessage { @@ -276,7 +276,7 @@ public async Task ReadAsync_FormatRequestPathDisabled_ReturnsLogRecordWithRoute( using var serviceProvider = GetServiceProvider(headersReader, configureRedaction: x => x.RedactionFormat = Redacted); var reader = new HttpRequestReader(serviceProvider, opts.ToOptionsMonitor(), - serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); + serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); using var httpRequestMessage = new HttpRequestMessage { @@ -348,7 +348,7 @@ public async Task ReadAsync_RouteParameterRedactionModeNone_ReturnsLogRecordWith using var serviceProvider = GetServiceProvider(headersReader); var reader = new HttpRequestReader(serviceProvider, opts.ToOptionsMonitor(), - serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); + serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); using var httpRequestMessage = new HttpRequestMessage { @@ -406,7 +406,7 @@ public async Task ReadAsync_RequestMetadataRequestNameSetAndRouteMissing_Returns using var serviceProvider = GetServiceProvider(headersReader); var reader = new HttpRequestReader(serviceProvider, opts.ToOptionsMonitor(), - serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); + serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); using var httpRequestMessage = new HttpRequestMessage { @@ -477,7 +477,7 @@ public async Task ReadAsync_NoMetadataUsesRedactedString_ReturnsLogRecord() using var serviceProvider = GetServiceProvider(headersReader); var reader = new HttpRequestReader(serviceProvider, opts.ToOptionsMonitor(), - serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); + serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); using var httpRequestMessage = new HttpRequestMessage { @@ -544,7 +544,7 @@ public async Task ReadAsync_MetadataWithoutRequestRouteOrNameUsesConstants_Retur using var serviceProvider = GetServiceProvider(headersReader); var reader = new HttpRequestReader(serviceProvider, opts.ToOptionsMonitor(), - serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); + serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); using var httpRequestMessage = new HttpRequestMessage { diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/TelemetryCommonExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/TelemetryCommonExtensionsTests.cs index a7a404079b6..81fae157e52 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/TelemetryCommonExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/TelemetryCommonExtensionsTests.cs @@ -39,8 +39,8 @@ public void AddHttpRouteProcessor_Registers_RouterParserAndFormatter() { var sp = new ServiceCollection().AddFakeRedaction().AddHttpRouteProcessor().BuildServiceProvider(); - Assert.NotNull(sp.GetRequiredService()); - Assert.NotNull(sp.GetRequiredService()); + Assert.NotNull(sp.GetRequiredService()); + Assert.NotNull(sp.GetRequiredService()); } [Fact] diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Http/HttpParserTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Http/HttpParserTests.cs index a3e40c53e38..a94ebea56d8 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Http/HttpParserTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Http/HttpParserTests.cs @@ -643,8 +643,8 @@ public void AddHttpRouteProcessor_ParserAndFormatterInstanceAdded() { var sp = new ServiceCollection().AddHttpRouteProcessor().AddFakeRedaction().BuildServiceProvider(); - var httpRouteParser = sp.GetRequiredService(); - var httpRouteFormatter = sp.GetRequiredService(); + var httpRouteParser = sp.GetRequiredService(); + var httpRouteFormatter = sp.GetRequiredService(); Assert.NotNull(httpRouteParser); Assert.NotNull(httpRouteFormatter); @@ -654,7 +654,7 @@ private static HttpRouteParser CreateHttpRouteParser() { var redactorProvider = new FakeRedactorProvider( new FakeRedactorOptions { RedactionFormat = "Redacted:{0}" }); - return new HttpRouteParser(redactorProvider); + return new DefaultHttpRouteParser(redactorProvider); } private static void ValidateRouteParameter( diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Http/HttpRouteFormatterTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Http/HttpRouteFormatterTests.cs index 49ed5222087..0ff16d014b6 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Http/HttpRouteFormatterTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Http/HttpRouteFormatterTests.cs @@ -496,7 +496,7 @@ private static HttpRouteFormatter CreateHttpRouteFormatter() { var redactorProvider = new FakeRedactorProvider( new FakeRedactorOptions { RedactionFormat = "Redacted:{0}" }); - var httpParser = new HttpRouteParser(redactorProvider); - return new HttpRouteFormatter(httpParser, redactorProvider); + var httpParser = new DefaultHttpRouteParser(redactorProvider); + return new DefaultHttpRouteFormatter(httpParser, redactorProvider); } } diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Http/TelemetryCommonExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Http/TelemetryCommonExtensionsTests.cs index eda62044717..dae64175e80 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Http/TelemetryCommonExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Http/TelemetryCommonExtensionsTests.cs @@ -37,7 +37,7 @@ public void AddHttpRouteProcessor_Registers_RouterParserAndFormatter() { var sp = new ServiceCollection().AddFakeRedaction().AddHttpRouteProcessor().BuildServiceProvider(); - Assert.NotNull(sp.GetRequiredService()); - Assert.NotNull(sp.GetRequiredService()); + Assert.NotNull(sp.GetRequiredService()); + Assert.NotNull(sp.GetRequiredService()); } }