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 5da92a0456f..a9eaf982ac4 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestReader.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestReader.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers; using System.Collections.Frozen; using System.Collections.Generic; using System.Net.Http; @@ -19,6 +20,7 @@ namespace Microsoft.Extensions.Http.Logging.Internal; internal sealed class HttpRequestReader : IHttpRequestReader { private readonly IHttpRouteFormatter _routeFormatter; + private readonly IHttpRouteParser _httpRouteParser; private readonly IHttpHeadersReader _httpHeadersReader; private readonly FrozenDictionary _defaultSensitiveParameters; @@ -44,12 +46,14 @@ public HttpRequestReader( IServiceProvider serviceProvider, IOptionsMonitor optionsMonitor, IHttpRouteFormatter routeFormatter, + IHttpRouteParser httpRouteParser, IOutgoingRequestContext requestMetadataContext, IDownstreamDependencyMetadataManager? downstreamDependencyMetadataManager = null, [ServiceKey] string? serviceKey = null) : this( optionsMonitor.GetKeyedOrCurrent(serviceKey), routeFormatter, + httpRouteParser, serviceProvider.GetRequiredOrKeyedRequiredService(serviceKey), requestMetadataContext, downstreamDependencyMetadataManager) @@ -59,6 +63,7 @@ public HttpRequestReader( internal HttpRequestReader( LoggingOptions options, IHttpRouteFormatter routeFormatter, + IHttpRouteParser httpRouteParser, IHttpHeadersReader httpHeadersReader, IOutgoingRequestContext requestMetadataContext, IDownstreamDependencyMetadataManager? downstreamDependencyMetadataManager = null) @@ -67,6 +72,7 @@ internal HttpRequestReader( _httpHeadersReader = httpHeadersReader; _routeFormatter = routeFormatter; + _httpRouteParser = httpRouteParser; _requestMetadataContext = requestMetadataContext; _downstreamDependencyMetadataManager = downstreamDependencyMetadataManager; @@ -92,7 +98,7 @@ public async Task ReadRequestAsync(LogRecord logRecord, HttpRequestMessage reque { logRecord.Host = request.RequestUri?.Host ?? TelemetryConstants.Unknown; logRecord.Method = request.Method; - logRecord.Path = GetRedactedPath(request); + GetRedactedPathAndParameters(request, logRecord); if (_logRequestHeaders) { @@ -125,16 +131,19 @@ public async Task ReadResponseAsync(LogRecord logRecord, HttpResponseMessage res logRecord.StatusCode = (int)response.StatusCode; } - private string GetRedactedPath(HttpRequestMessage request) + private void GetRedactedPathAndParameters(HttpRequestMessage request, LogRecord logRecord) { + logRecord.PathParameters = null; if (request.RequestUri is null) { - return TelemetryConstants.Unknown; + logRecord.Path = TelemetryConstants.Unknown; + return; } if (_routeParameterRedactionMode == HttpRouteParameterRedactionMode.None) { - return request.RequestUri.AbsolutePath; + logRecord.Path = request.RequestUri.AbsolutePath; + return; } var requestMetadata = request.GetRequestMetadata() ?? @@ -143,19 +152,36 @@ private string GetRedactedPath(HttpRequestMessage request) if (requestMetadata == null) { - return TelemetryConstants.Redacted; + logRecord.Path = TelemetryConstants.Redacted; + return; } var route = requestMetadata.RequestRoute; if (route == TelemetryConstants.Unknown) { - return requestMetadata.RequestName; + logRecord.Path = requestMetadata.RequestName; + return; } - return _outgoingPathLogMode switch + var routeSegments = _httpRouteParser.ParseRoute(route); + + if (_outgoingPathLogMode == OutgoingPathLoggingMode.Formatted) + { + logRecord.Path = _routeFormatter.Format(in routeSegments, request.RequestUri.AbsolutePath, _routeParameterRedactionMode, _defaultSensitiveParameters); + logRecord.PathParameters = null; + } + else { - OutgoingPathLoggingMode.Formatted => _routeFormatter.Format(route, request.RequestUri.AbsolutePath, _routeParameterRedactionMode, _defaultSensitiveParameters), - _ => route - }; + // Case when logging mode is "OutgoingPathLoggingMode.Structured" + logRecord.Path = route; + var routeParams = ArrayPool.Shared.Rent(routeSegments.ParameterCount); + + // Setting this value right away to be able to return it back to pool in a callee's "finally" block: + logRecord.PathParameters = routeParams; + if (_httpRouteParser.TryExtractParameters(request.RequestUri.AbsolutePath, in routeSegments, _routeParameterRedactionMode, _defaultSensitiveParameters, ref routeParams)) + { + logRecord.PathParametersCount = routeSegments.ParameterCount; + } + } } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/Log.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/Log.cs index f7e6896b14b..2b8a627b687 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/Log.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/Log.cs @@ -34,7 +34,7 @@ internal static class Log "An error occurred in enricher '{Enricher}' while enriching the logger context for request: " + $"{{{HttpClientLoggingTagNames.Method}}} {{{HttpClientLoggingTagNames.Host}}}/{{{HttpClientLoggingTagNames.Path}}}"; - private static readonly Func _originalFormatValueFMTFunc = OriginalFormatValueFMT; + private static readonly Func _originalFormatValueFMTFunc = OriginalFormatValueFmt; public static void OutgoingRequest(ILogger logger, LogLevel level, LogRecord record) { @@ -115,7 +115,7 @@ public static void LoggerContextMissing(ILogger logger, Exception? exception, st new(0, nameof(LoggerContextMissing)), state, exception, - (s, _) => + static (s, _) => { var requestState = s.TagArray[3].Value ?? NullString; var method = s.TagArray[2].Value ?? NullString; @@ -142,7 +142,7 @@ public static void EnrichmentError(ILogger logger, Exception exception, string? new(0, nameof(EnrichmentError)), state, exception, - (s, _) => + static (s, _) => { var enricher = s.TagArray[4].Value ?? NullString; var method = s.TagArray[3].Value ?? NullString; @@ -171,7 +171,10 @@ private static void OutgoingRequest( var requestHeadersCount = record.RequestHeaders?.Count ?? 0; var responseHeadersCount = record.ResponseHeaders?.Count ?? 0; - var index = loggerMessageState.ReserveTagSpace(MinimalPropertyCount + statusCodePropertyCount + requestHeadersCount + responseHeadersCount); + var spaceToReserve = MinimalPropertyCount + statusCodePropertyCount + requestHeadersCount + responseHeadersCount + + record.PathParametersCount + (record.RequestBody is null ? 0 : 1) + (record.ResponseBody is null ? 0 : 1); + + var index = loggerMessageState.ReserveTagSpace(spaceToReserve); loggerMessageState.TagArray[index++] = new(HttpClientLoggingTagNames.Method, record.Method); loggerMessageState.TagArray[index++] = new(HttpClientLoggingTagNames.Host, record.Host); loggerMessageState.TagArray[index++] = new(HttpClientLoggingTagNames.Path, record.Path); @@ -192,14 +195,19 @@ private static void OutgoingRequest( loggerMessageState.AddResponseHeaders(record.ResponseHeaders!, ref index); } + if (record.PathParameters is not null) + { + loggerMessageState.AddPathParameters(record.PathParameters, record.PathParametersCount, ref index); + } + if (record.RequestBody is not null) { - loggerMessageState.AddTag(HttpClientLoggingTagNames.RequestBody, record.RequestBody); + loggerMessageState.TagArray[index++] = new(HttpClientLoggingTagNames.RequestBody, record.RequestBody); } if (record.ResponseBody is not null) { - loggerMessageState.AddTag(HttpClientLoggingTagNames.ResponseBody, record.ResponseBody); + loggerMessageState.TagArray[index] = new(HttpClientLoggingTagNames.ResponseBody, record.ResponseBody); } logger.Log( @@ -215,7 +223,7 @@ private static void OutgoingRequest( } } - private static string OriginalFormatValueFMT(LoggerMessageState request, Exception? _) + private static string OriginalFormatValueFmt(LoggerMessageState request, Exception? _) { int startIndex = FindStartIndex(request); var httpMethod = request[startIndex].Value; diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/LogRecord.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/LogRecord.cs index ada8e9f8134..93238e5809c 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/LogRecord.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/LogRecord.cs @@ -1,8 +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 System.Buffers; using System.Collections.Generic; using System.Net.Http; +using Microsoft.Extensions.Http.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ObjectPool; @@ -63,8 +65,24 @@ internal sealed class LogRecord : IResettable /// public LoggerMessageState? EnrichmentTags { get; set; } + /// + /// Gets or sets request path parameters. + /// + public HttpRouteParameter[]? PathParameters { get; set; } + + /// + /// Gets or sets request path parameters count for . + /// + public int PathParametersCount { get; set; } + public bool TryReset() { + if (PathParameters != null) + { + ArrayPool.Shared.Return(PathParameters); + PathParameters = null; + } + Host = string.Empty; Method = null; Path = string.Empty; @@ -75,6 +93,7 @@ public bool TryReset() EnrichmentTags = null; RequestHeaders = null; ResponseHeaders = null; + PathParametersCount = 0; return true; } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/LoggerMessageStateExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/LoggerMessageStateExtensions.cs index fa9912be06e..4bda377d865 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/LoggerMessageStateExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/LoggerMessageStateExtensions.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Http.Diagnostics; using Microsoft.Extensions.Logging; namespace Microsoft.Extensions.Http.Logging.Internal; @@ -26,8 +27,7 @@ public static void AddRequestHeaders(this LoggerMessageState state, List p + Normalize(x), - HttpClientLoggingTagNames.RequestHeaderPrefix); + static x => HttpClientLoggingTagNames.RequestHeaderPrefix + Normalize(x)); state.TagArray[index++] = new(key, items[i].Value); } @@ -46,17 +46,30 @@ public static void AddResponseHeaders(this LoggerMessageState state, List p + Normalize(x), - HttpClientLoggingTagNames.ResponseHeaderPrefix); + static x => HttpClientLoggingTagNames.ResponseHeaderPrefix + Normalize(x)); state.TagArray[index++] = new(key, items[i].Value); } } + /// + /// Adds path parameters to . + /// + /// A to be filled. + /// An array with path parameters. + /// A number of path parameters. + /// Represents an index to be used when writing tags into . + /// will be mutated to point to the next item. + public static void AddPathParameters(this LoggerMessageState state, HttpRouteParameter[] parameters, int paramsCount, ref int index) + { + for (var i = 0; i < paramsCount; i++) + { + state.TagArray[index++] = new(parameters[i].Name, parameters[i].Value); + } + } + [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "Normalization to lower case is required by OTel's semantic conventions")] private static string Normalize(string header) - { - return header.ToLowerInvariant(); - } + => header.ToLowerInvariant(); } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/LoggingOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/LoggingOptions.cs index 4359f22fb5d..184fca4a061 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/LoggingOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/LoggingOptions.cs @@ -123,8 +123,9 @@ public class LoggingOptions /// The default value is . /// /// - /// This option is applied only when the option is not set to , - /// otherwise this setting is ignored and the unredacted HTTP request path is logged. + /// This option is applied only when the option is not set to + /// , + /// otherwise this setting is ignored and the non-redacted HTTP request path is logged. /// public OutgoingPathLoggingMode RequestPathLoggingMode { get; set; } = DefaultPathLoggingMode; 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 b85b90b4c6f..dc712fd3275 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 @@ -42,6 +42,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Http/HttpRouteFormatter.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Http/HttpRouteFormatter.cs index 6f2a40d5cf1..0ee93b5e851 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Http/HttpRouteFormatter.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Http/HttpRouteFormatter.cs @@ -15,7 +15,7 @@ internal sealed class HttpRouteFormatter : IHttpRouteFormatter { private const char ForwardSlashSymbol = '/'; -#if NETCOREAPP3_1_OR_GREATER +#if NET6_0_OR_GREATER private const char ForwardSlash = ForwardSlashSymbol; #else #pragma warning disable IDE1006 // Naming Styles diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Http/IHttpRouteParser.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Http/IHttpRouteParser.cs index c95e6a3ce7b..5831935f020 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Http/IHttpRouteParser.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Http/IHttpRouteParser.cs @@ -28,12 +28,10 @@ internal interface IHttpRouteParser /// 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. -#pragma warning disable CA1045 // Do not pass types by reference bool TryExtractParameters( string httpPath, in ParsedRouteSegments routeSegments, HttpRouteParameterRedactionMode redactionMode, IReadOnlyDictionary parametersToRedact, ref HttpRouteParameter[] httpRouteParameters); -#pragma warning restore CA1045 // Do not pass types by reference } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj index bebeba6fb9a..21e6baea697 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj @@ -24,7 +24,7 @@ - + diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Microsoft.Extensions.Diagnostics.Probes.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Microsoft.Extensions.Diagnostics.Probes.Tests.csproj index d2fece93ad6..5cb8a9f8f62 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Microsoft.Extensions.Diagnostics.Probes.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Microsoft.Extensions.Diagnostics.Probes.Tests.csproj @@ -17,7 +17,7 @@ - + diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/AcceptanceTests.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/AcceptanceTests.cs index 603f7346d16..8e2df28853b 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/AcceptanceTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/AcceptanceTests.cs @@ -14,7 +14,6 @@ using Microsoft.Extensions.Http.Diagnostics; using Microsoft.Extensions.Http.Logging.Test.Internal; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Options; using Xunit; @@ -23,7 +22,7 @@ namespace Microsoft.Extensions.Http.Logging.Test; public class AcceptanceTests { private const string LoggingCategory = "Microsoft.Extensions.Http.Logging.HttpClientLogger"; - private static readonly Uri _unreachableRequestUri = new("https://we.wont.hit.this.doman.anyway"); + private static readonly Uri _unreachableRequestUri = new("https://we.wont.hit.this.domain.anyway"); [Fact] public async Task AddHttpClientLogEnricher_WhenNullEnricherRegistered_SkipsNullEnrichers() @@ -116,14 +115,15 @@ public async Task AddHttpClientLogging_ServiceCollectionAndEnrichers_EnrichesLog var logRecord = collector.GetSnapshot().Single(logRecord => logRecord.Category == LoggingCategory); Assert.Equal($"{httpRequestMessage.Method} {httpRequestMessage.RequestUri.Host}/{TelemetryConstants.Redacted}", logRecord.Message); - var state = logRecord.StructuredState; var enricher1 = sp.GetServices().SingleOrDefault(enn => enn is EnricherWithCounter) as EnricherWithCounter; var enricher2 = sp.GetServices().SingleOrDefault(enn => enn is TestEnricher) as TestEnricher; enricher1.Should().NotBeNull(); enricher2.Should().NotBeNull(); - enricher1!.TimesCalled.Should().Be(1); + + var state = logRecord.StructuredState; + state.Should().NotBeNull(); state!.Single(kvp => kvp.Key == enricher2!.KvpRequest.Key).Value.Should().Be(enricher2!.KvpRequest.Value!.ToString()); } @@ -170,7 +170,7 @@ public async Task AddHttpClientLogging_WithNamedHttpClients_WorksCorrectly() var responseString = await SendRequest(namedClient1, httpRequestMessage); var collector = provider.GetFakeLogCollector(); var logRecord = collector.GetSnapshot().Single(l => l.Category == LoggingCategory); - var state = logRecord.State as List>; + var state = logRecord.StructuredState; state.Should().Contain(kvp => kvp.Value == responseString); state.Should().Contain(kvp => kvp.Value == "Request Value"); state.Should().Contain(kvp => kvp.Value == "Request Value 2,Request Value 3"); @@ -186,7 +186,7 @@ public async Task AddHttpClientLogging_WithNamedHttpClients_WorksCorrectly() collector.Clear(); responseString = await SendRequest(namedClient2, httpRequestMessage2); logRecord = collector.GetSnapshot().Single(l => l.Category == LoggingCategory); - state = logRecord.State as List>; + state = logRecord.StructuredState; state.Should().Contain(kvp => kvp.Value == responseString); state.Should().Contain(kvp => kvp.Value == "Request Value"); state.Should().Contain(kvp => kvp.Value == "Request Value 2,Request Value 3"); @@ -256,7 +256,8 @@ public async Task AddHttpClientLogging_WithTypedHttpClients_WorksCorrectly() var responseString = Encoding.UTF8.GetString(buffer); var logRecord = collector.GetSnapshot().Single(l => l.Category == LoggingCategory); - var state = logRecord.State as List>; + var state = logRecord.StructuredState; + state.Should().NotBeNull(); state.Should().Contain(kvp => kvp.Value == responseString); state.Should().Contain(kvp => kvp.Value == "Request Value"); state.Should().Contain(kvp => kvp.Value == "Request Value 2,Request Value 3"); @@ -277,7 +278,7 @@ public async Task AddHttpClientLogging_WithTypedHttpClients_WorksCorrectly() responseString = Encoding.UTF8.GetString(buffer); logRecord = collector.GetSnapshot().Single(l => l.Category == LoggingCategory); - state = logRecord.State as List>; + state = logRecord.StructuredState; state.Should().Contain(kvp => kvp.Value == responseString); state.Should().Contain(kvp => kvp.Value == "Request Value"); state.Should().Contain(kvp => kvp.Value == "Request Value 2,Request Value 3"); @@ -320,10 +321,81 @@ public async Task AddHttpClientLogging_RedactSensitiveParams(HttpRouteParameterR var collector = sp.GetFakeLogCollector(); var logRecord = collector.GetSnapshot().Single(logRecord => logRecord.Category == LoggingCategory); - var state = logRecord.State as List>; + var state = logRecord.StructuredState; + state.Should().NotBeNull(); state!.Single(kvp => kvp.Key == HttpClientLoggingTagNames.Path).Value.Should().Be(redactedPath); } + [Theory] + [InlineData(HttpRouteParameterRedactionMode.Strict, "REDACTED", "")] + [InlineData(HttpRouteParameterRedactionMode.Loose, "999", "")] + [InlineData(HttpRouteParameterRedactionMode.None, "999", "123")] + public async Task AddHttpClientLogging_StructuredPathLogging_RedactsSensitiveParams( + HttpRouteParameterRedactionMode parameterRedactionMode, + string expectedUnitId, + string expectedUserId) + { + const string RequestPath = "https://fake.com/v1/unit/999/users/123"; + const string RequestRoute = "/v1/unit/{unitId}/users/{userId}"; + + await using var sp = new ServiceCollection() + .AddFakeLogging() + .AddFakeRedaction(o => o.RedactionFormat = "") + .AddHttpClient() + .AddExtendedHttpClientLogging(o => + { + o.RouteParameterDataClasses.Add("userId", FakeTaxonomy.PrivateData); + o.RequestPathParameterRedactionMode = parameterRedactionMode; + o.RequestPathLoggingMode = OutgoingPathLoggingMode.Structured; + }) + .BlockRemoteCall() + .BuildServiceProvider(); + + using var httpClient = sp.GetRequiredService().CreateClient(); + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(RequestPath) + }; + + httpRequestMessage.SetRequestMetadata(new RequestMetadata(httpRequestMessage.Method.ToString(), RequestRoute)); + + using var _ = await httpClient.SendAsync(httpRequestMessage).ConfigureAwait(false); + + var collector = sp.GetFakeLogCollector(); + var logRecord = collector.GetSnapshot().Single(logRecord => logRecord.Category == LoggingCategory); + var state = logRecord.StructuredState; + var loggedPath = state.Should().NotBeNull().And + .ContainSingle(kvp => kvp.Key == HttpClientLoggingTagNames.Path) + .Subject.Value; + + state.Should().ContainSingle(kvp => kvp.Key == HttpClientLoggingTagNames.Host) + .Which.Value.Should().Be(httpRequestMessage.RequestUri.Host); + + state.Should().ContainSingle(kvp => kvp.Key == HttpClientLoggingTagNames.Method) + .Which.Value.Should().Be(httpRequestMessage.Method.ToString()); + + state.Should().ContainSingle(kvp => kvp.Key == HttpClientLoggingTagNames.StatusCode) + .Which.Value.Should().Be("200"); + + state.Should().ContainSingle(kvp => kvp.Key == HttpClientLoggingTagNames.Duration) + .Which.Value.Should().NotBeEmpty(); + + // When the redaction mode is set to "None", the RequestPathLoggingMode is ignored + if (parameterRedactionMode == HttpRouteParameterRedactionMode.None) + { + loggedPath.Should().Be(httpRequestMessage.RequestUri.AbsolutePath); + state.Should().HaveCount(5); + } + else + { + loggedPath.Should().Be(RequestRoute); + state.Should().ContainSingle(kvp => kvp.Key == "userId").Which.Value.Should().Be(expectedUserId); + state.Should().ContainSingle(kvp => kvp.Key == "unitId").Which.Value.Should().Be(expectedUnitId); + state.Should().HaveCount(7); + } + } + [Theory] [InlineData(HttpRouteParameterRedactionMode.Strict, "v1/unit/REDACTED/users/REDACTED:123")] [InlineData(HttpRouteParameterRedactionMode.Loose, "v1/unit/999/users/REDACTED:123")] @@ -361,7 +433,8 @@ public async Task AddHttpClientLogging_NamedHttpClient_RedactSensitiveParams(Htt var collector = sp.GetFakeLogCollector(); var logRecord = collector.GetSnapshot().Single(logRecord => logRecord.Category == LoggingCategory); - var state = logRecord.State as List>; + var state = logRecord.StructuredState; + state.Should().NotBeNull(); state!.Single(kvp => kvp.Key == HttpClientLoggingTagNames.Path).Value.Should().Be(redactedPath); } @@ -514,7 +587,6 @@ public async Task AddHttpClientLogging_DisablesNetScope() .BlockRemoteCall() .BuildServiceProvider(); - var options = provider.GetRequiredService>().Get("test"); var client = provider.GetRequiredService().CreateClient("test"); using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _unreachableRequestUri); @@ -522,7 +594,7 @@ public async Task AddHttpClientLogging_DisablesNetScope() var collector = provider.GetFakeLogCollector(); var logRecord = collector.GetSnapshot().Single(l => l.Category == LoggingCategory); - logRecord.Scopes.Should().HaveCount(0); + logRecord.Scopes.Should().BeEmpty(); } [Fact] @@ -563,7 +635,7 @@ public async Task AddDefaultHttpClientLogging_DisablesNetScope() .AddExtendedHttpClientLogging() .BlockRemoteCall() .BuildServiceProvider(); - var options = provider.GetRequiredService>().Get("test"); + var client = provider.GetRequiredService().CreateClient("test"); using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _unreachableRequestUri); 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 bb26597eb53..caaeead61ce 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(), headersReader, RequestMetadataContext), + new HttpRequestReader(options, GetHttpRouteFormatter(), Mock.Of(), headersReader, RequestMetadataContext), Enumerable.Empty(), options), new TestingHandlerStub((_, _) => throw exception)); @@ -209,22 +209,23 @@ public async Task HttpLoggingHandler_AllOptions_LogsOutgoingRequest() BodyReadTimeout = TimeSpan.FromMinutes(5), RequestPathLoggingMode = OutgoingPathLoggingMode.Structured, LogRequestStart = false, - LogBody = true, - RouteParameterDataClasses = { { "userId", FakeTaxonomy.PrivateData } }, + LogBody = true }; var mockHeadersRedactor = new Mock(); mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), It.IsAny())) .Returns(Redacted); + var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object); - var fakeLogger = new FakeLogger(new FakeLogCollector(Options.Options.Create(new FakeLogCollectorOptions()))); + var fakeLogger = new FakeLogger(); var logger = new HttpClientLogger( fakeLogger, new HttpRequestReader( options, GetHttpRouteFormatter(), + Mock.Of(), headersReader, RequestMetadataContext), new List { testEnricher }, options); @@ -300,8 +301,7 @@ public async Task HttpLoggingHandler_AllOptionsWithLogRequestStart_LogsOutgoingR BodyReadTimeout = TimeSpan.FromMinutes(5), RequestPathLoggingMode = OutgoingPathLoggingMode.Structured, LogRequestStart = true, - LogBody = true, - RouteParameterDataClasses = { { "userId", FakeTaxonomy.PrivateData } }, + LogBody = true }; var fakeLogger = new FakeLogger( @@ -321,6 +321,7 @@ public async Task HttpLoggingHandler_AllOptionsWithLogRequestStart_LogsOutgoingR new HttpRequestReader( options, GetHttpRouteFormatter(), + Mock.Of(), headersReader, RequestMetadataContext), new List { testEnricher }, options); @@ -407,8 +408,7 @@ public async Task HttpLoggingHandler_AllOptionsSendAsyncFailed_LogsRequestInform BodyReadTimeout = TimeSpan.FromMinutes(5), RequestPathLoggingMode = OutgoingPathLoggingMode.Structured, LogRequestStart = false, - LogBody = true, - RouteParameterDataClasses = { { "userId", FakeTaxonomy.PrivateData } }, + LogBody = true }; var fakeLogger = new FakeLogger(new FakeLogCollector(Options.Options.Create(new FakeLogCollectorOptions()))); @@ -426,6 +426,7 @@ public async Task HttpLoggingHandler_AllOptionsSendAsyncFailed_LogsRequestInform new HttpRequestReader( options, GetHttpRouteFormatter(), + Mock.Of(), headersReader, RequestMetadataContext), new List { testEnricher }, options), @@ -503,8 +504,7 @@ public async Task HttpLoggingHandler_ReadResponseThrows_LogsException() BodyReadTimeout = TimeSpan.FromMinutes(5), RequestPathLoggingMode = OutgoingPathLoggingMode.Structured, LogRequestStart = false, - LogBody = true, - RouteParameterDataClasses = { { "userId", FakeTaxonomy.PrivateData } }, + LogBody = true }; var fakeLogger = new FakeLogger( @@ -519,7 +519,7 @@ public async Task HttpLoggingHandler_ReadResponseThrows_LogsException() var exception = new InvalidOperationException("test"); - var actualRequestReader = new HttpRequestReader(options, GetHttpRouteFormatter(), headersReader, RequestMetadataContext); + var actualRequestReader = new HttpRequestReader(options, GetHttpRouteFormatter(), Mock.Of(), headersReader, RequestMetadataContext); var mockedRequestReader = new Mock(); mockedRequestReader .Setup(m => @@ -618,8 +618,7 @@ public async Task HttpLoggingHandler_AllOptionsTransferEncodingIsNotChunked_Logs BodyReadTimeout = TimeSpan.FromMinutes(5), RequestPathLoggingMode = OutgoingPathLoggingMode.Structured, LogRequestStart = false, - LogBody = true, - RouteParameterDataClasses = { { "userId", FakeTaxonomy.PrivateData } }, + LogBody = true }; var fakeLogger = new FakeLogger(new FakeLogCollector(Options.Options.Create(new FakeLogCollectorOptions()))); @@ -635,6 +634,7 @@ public async Task HttpLoggingHandler_AllOptionsTransferEncodingIsNotChunked_Logs new HttpRequestReader( options, GetHttpRouteFormatter(), + Mock.Of(), headersReader, RequestMetadataContext), new List { testEnricher }, options), @@ -676,6 +676,7 @@ public async Task HttpLoggingHandler_WithEnrichers_CallsEnrichMethodExactlyOnce( new HttpRequestReader( options, GetHttpRouteFormatter(), + Mock.Of(), new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object), RequestMetadataContext), new List { enricher1.Object, enricher2.Object }, @@ -718,6 +719,7 @@ public async Task HttpLoggingHandler_WithEnrichersAndLogRequestStart_CallsEnrich new HttpRequestReader( options, GetHttpRouteFormatter(), + Mock.Of(), new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object), RequestMetadataContext), new List { enricher1.Object, enricher2.Object }, @@ -754,7 +756,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(), headersReader, RequestMetadataContext); + var requestReader = new HttpRequestReader(options, GetHttpRouteFormatter(), Mock.Of(), headersReader, RequestMetadataContext); var enrichers = new List { enricher1.Object, enricher2.Object }; using var handler = new TestLoggingHandler( @@ -886,8 +888,7 @@ public async Task HttpLoggingHandler_AllOptionsTransferEncodingChunked_LogsOutgo BodyReadTimeout = TimeSpan.FromMinutes(5), RequestPathLoggingMode = OutgoingPathLoggingMode.Structured, LogRequestStart = false, - LogBody = true, - RouteParameterDataClasses = { { "userId", FakeTaxonomy.PrivateData } }, + LogBody = true }; var fakeLogger = new FakeLogger(new FakeLogCollector(Options.Options.Create(new FakeLogCollectorOptions()))); @@ -903,6 +904,7 @@ public async Task HttpLoggingHandler_AllOptionsTransferEncodingChunked_LogsOutgo new HttpRequestReader( options, GetHttpRouteFormatter(), + Mock.Of(), headersReader, RequestMetadataContext), new List { testEnricher }, options), @@ -944,7 +946,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(), headersReader, RequestMetadataContext); + options, GetHttpRouteFormatter(), Mock.Of(), headersReader, RequestMetadataContext); using var handler = new TestLoggingHandler( new HttpClientLogger(fakeLogger, requestReader, Array.Empty(), options), @@ -993,7 +995,7 @@ public void SyncMethods_ShouldThrow() var options = new LoggingOptions(); var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), Mock.Of()); var requestReader = new HttpRequestReader( - options, GetHttpRouteFormatter(), headersReader, RequestMetadataContext); + options, GetHttpRouteFormatter(), Mock.Of(), headersReader, RequestMetadataContext); var logger = new HttpClientLogger(new FakeLogger(), requestReader, Array.Empty(), options); using var httpRequestMessage = new HttpRequestMessage(); 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 cb3b5cabb2f..9baf3b6a2ff 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpRequestReaderTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpRequestReaderTest.cs @@ -3,8 +3,10 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Mime; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -25,6 +27,7 @@ namespace Microsoft.Extensions.Http.Logging.Test; public class HttpRequestReaderTest { private const string Redacted = "REDACTED"; + private const string RequestedHost = "default-uri.com"; private readonly Fixture _fixture; @@ -38,14 +41,12 @@ public async Task ReadAsync_AllData_ReturnsLogRecord() { var requestContent = _fixture.Create(); var responseContent = _fixture.Create(); - var host = "default-uri.com"; - var plainTextMedia = "text/plain"; var header1 = new KeyValuePair("Header1", "Value1"); var header2 = new KeyValuePair("Header2", "Value2"); var header3 = new KeyValuePair("Header3", "Value3"); var expectedRecord = new LogRecord { - Host = host, + Host = RequestedHost, Method = HttpMethod.Post, Path = TelemetryConstants.Redacted, StatusCode = 200, @@ -59,8 +60,8 @@ public async Task ReadAsync_AllData_ReturnsLogRecord() { RequestHeadersDataClasses = new Dictionary { { header1.Key, FakeTaxonomy.PrivateData }, { header3.Key, FakeTaxonomy.PrivateData } }, ResponseHeadersDataClasses = new Dictionary { { header2.Key, FakeTaxonomy.PrivateData }, { header3.Key, FakeTaxonomy.PrivateData } }, - RequestBodyContentTypes = new HashSet { plainTextMedia }, - ResponseBodyContentTypes = new HashSet { plainTextMedia }, + RequestBodyContentTypes = new HashSet { MediaTypeNames.Text.Plain }, + ResponseBodyContentTypes = new HashSet { MediaTypeNames.Text.Plain }, BodyReadTimeout = TimeSpan.FromSeconds(100000), LogBody = true, }; @@ -74,7 +75,7 @@ public async Task ReadAsync_AllData_ReturnsLogRecord() using var serviceProvider = GetServiceProvider(headersReader, serviceKey); var reader = new HttpRequestReader(serviceProvider, options.ToOptionsMonitor(serviceKey), serviceProvider.GetRequiredService(), - RequestMetadataContext, serviceKey: serviceKey); + serviceProvider.GetRequiredService(), RequestMetadataContext, serviceKey: serviceKey); using var httpRequestMessage = new HttpRequestMessage { @@ -101,15 +102,7 @@ public async Task ReadAsync_AllData_ReturnsLogRecord() await reader.ReadRequestAsync(logRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None).ConfigureAwait(false); await reader.ReadResponseAsync(logRecord, httpResponseMessage, responseHeadersBuffer, CancellationToken.None).ConfigureAwait(false); - logRecord.Should().BeEquivalentTo( - expectedRecord, - o => o - .Excluding(m => m.RequestBody) - .Excluding(m => m.ResponseBody) - .ComparingByMembers()); - - logRecord.RequestBody.Should().BeEquivalentTo(expectedRecord.RequestBody); - logRecord.ResponseBody.Should().BeEquivalentTo(expectedRecord.ResponseBody); + logRecord.Should().BeEquivalentTo(expectedRecord); } [Fact] @@ -144,7 +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(), RequestMetadataContext); + var reader = new HttpRequestReader(serviceProvider, options.ToOptionsMonitor(), serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), RequestMetadataContext); using var httpRequestMessage = new HttpRequestMessage { @@ -165,15 +159,7 @@ public async Task ReadAsync_NoHost_ReturnsLogRecordWithoutHost() await reader.ReadRequestAsync(actualRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None).ConfigureAwait(false); await reader.ReadResponseAsync(actualRecord, httpResponseMessage, responseHeadersBuffer, CancellationToken.None).ConfigureAwait(false); - actualRecord.Should().BeEquivalentTo( - expectedRecord, - o => o - .Excluding(m => m.RequestBody) - .Excluding(m => m.ResponseBody) - .ComparingByMembers()); - - actualRecord.RequestBody.Should().BeEquivalentTo(expectedRecord.RequestBody); - actualRecord.ResponseBody.Should().BeEquivalentTo(expectedRecord.ResponseBody); + actualRecord.Should().BeEquivalentTo(expectedRecord); } [Fact] @@ -181,14 +167,12 @@ public async Task ReadAsync_AllDataWithRequestMetadataSet_ReturnsLogRecord() { var requestContent = _fixture.Create(); var responseContent = _fixture.Create(); - var host = "default-uri.com"; - var plainTextMedia = "text/plain"; var header1 = new KeyValuePair("Header1", "Value1"); var header2 = new KeyValuePair("Header2", "Value2"); var expectedRecord = new LogRecord { - Host = host, + Host = RequestedHost, Method = HttpMethod.Post, Path = "foo/bar/123", StatusCode = 200, @@ -202,8 +186,8 @@ public async Task ReadAsync_AllDataWithRequestMetadataSet_ReturnsLogRecord() { RequestHeadersDataClasses = new Dictionary { { header1.Key, FakeTaxonomy.PrivateData } }, ResponseHeadersDataClasses = new Dictionary { { header2.Key, FakeTaxonomy.PrivateData } }, - RequestBodyContentTypes = new HashSet { plainTextMedia }, - ResponseBodyContentTypes = new HashSet { plainTextMedia }, + RequestBodyContentTypes = new HashSet { MediaTypeNames.Text.Plain }, + ResponseBodyContentTypes = new HashSet { MediaTypeNames.Text.Plain }, BodyReadTimeout = TimeSpan.FromSeconds(10), LogBody = true, }; @@ -217,7 +201,7 @@ public async Task ReadAsync_AllDataWithRequestMetadataSet_ReturnsLogRecord() using var serviceProvider = GetServiceProvider(headersReader); var reader = new HttpRequestReader(serviceProvider, opts.ToOptionsMonitor(), - serviceProvider.GetRequiredService(), RequestMetadataContext); + serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); using var httpRequestMessage = new HttpRequestMessage { @@ -246,14 +230,7 @@ public async Task ReadAsync_AllDataWithRequestMetadataSet_ReturnsLogRecord() await reader.ReadRequestAsync(actualRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None).ConfigureAwait(false); await reader.ReadResponseAsync(actualRecord, httpResponseMessage, responseHeadersBuffer, CancellationToken.None).ConfigureAwait(false); - actualRecord.Should().BeEquivalentTo( - expectedRecord, - o => o - .Excluding(m => m.RequestBody) - .Excluding(m => m.ResponseBody) - .ComparingByMembers()); - actualRecord.RequestBody.Should().BeEquivalentTo(expectedRecord.RequestBody); - actualRecord.ResponseBody.Should().BeEquivalentTo(expectedRecord.ResponseBody); + actualRecord.Should().BeEquivalentTo(expectedRecord); } [Fact] @@ -261,14 +238,12 @@ public async Task ReadAsync_FormatRequestPathDisabled_ReturnsLogRecordWithRoute( { var requestContent = _fixture.Create(); var responseContent = _fixture.Create(); - var host = "default-uri.com"; - var plainTextMedia = "text/plain"; var header1 = new KeyValuePair("Header1", "Value1"); var header2 = new KeyValuePair("Header2", "Value2"); var expectedRecord = new LogRecord { - Host = host, + Host = RequestedHost, Method = HttpMethod.Post, Path = "foo/bar/{userId}", StatusCode = 200, @@ -276,6 +251,7 @@ public async Task ReadAsync_FormatRequestPathDisabled_ReturnsLogRecordWithRoute( ResponseHeaders = [new("Header2", Redacted)], RequestBody = requestContent, ResponseBody = responseContent, + PathParametersCount = 1 }; var opts = new LoggingOptions @@ -284,8 +260,8 @@ public async Task ReadAsync_FormatRequestPathDisabled_ReturnsLogRecordWithRoute( LogBody = true, RequestHeadersDataClasses = new Dictionary { { header1.Key, FakeTaxonomy.PrivateData } }, ResponseHeadersDataClasses = new Dictionary { { header2.Key, FakeTaxonomy.PrivateData } }, - RequestBodyContentTypes = new HashSet { plainTextMedia }, - ResponseBodyContentTypes = new HashSet { plainTextMedia }, + RequestBodyContentTypes = new HashSet { MediaTypeNames.Text.Plain }, + ResponseBodyContentTypes = new HashSet { MediaTypeNames.Text.Plain }, BodyReadTimeout = TimeSpan.FromSeconds(10), RequestPathLoggingMode = OutgoingPathLoggingMode.Structured }; @@ -297,15 +273,15 @@ public async Task ReadAsync_FormatRequestPathDisabled_ReturnsLogRecordWithRoute( .Returns(Redacted); var headersReader = new HttpHeadersReader(opts.ToOptionsMonitor(), mockHeadersRedactor.Object); - using var serviceProvider = GetServiceProvider(headersReader); + using var serviceProvider = GetServiceProvider(headersReader, configureRedaction: x => x.RedactionFormat = Redacted); var reader = new HttpRequestReader(serviceProvider, opts.ToOptionsMonitor(), - serviceProvider.GetRequiredService(), RequestMetadataContext); + serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); using var httpRequestMessage = new HttpRequestMessage { Method = HttpMethod.Post, - RequestUri = new Uri("http://default-uri.com/foo/bar/123"), + RequestUri = new Uri($"http://{RequestedHost}/foo/bar/123"), Content = new StringContent(requestContent, Encoding.UTF8), }; @@ -329,28 +305,22 @@ public async Task ReadAsync_FormatRequestPathDisabled_ReturnsLogRecordWithRoute( await reader.ReadRequestAsync(actualRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None).ConfigureAwait(false); await reader.ReadResponseAsync(actualRecord, httpResponseMessage, responseHeadersBuffer, CancellationToken.None).ConfigureAwait(false); - actualRecord.Should().BeEquivalentTo( - expectedRecord, - o => o - .Excluding(m => m.RequestBody) - .Excluding(m => m.ResponseBody) - .ComparingByMembers()); - actualRecord.RequestBody.Should().BeEquivalentTo(expectedRecord.RequestBody); - actualRecord.ResponseBody.Should().BeEquivalentTo(expectedRecord.ResponseBody); + actualRecord.Should().BeEquivalentTo(expectedRecord, o => o.Excluding(x => x.PathParameters)); + + HttpRouteParameter[] expectedParameters = [new("userId", Redacted, true)]; + actualRecord.PathParameters.Should().NotBeNull().And.Subject.Take(actualRecord.PathParametersCount).Should().BeEquivalentTo(expectedParameters); } [Fact] public async Task ReadAsync_RouteParameterRedactionModeNone_ReturnsLogRecordWithUnredactedRoute() { var requestContent = _fixture.Create(); - var host = "default-uri.com"; - var plainTextMedia = "text/plain"; var header1 = new KeyValuePair("Header1", "Value1"); var header2 = new KeyValuePair("Header2", "Value2"); var expectedRecord = new LogRecord { - Host = host, + Host = RequestedHost, Method = HttpMethod.Post, Path = "/foo/bar/123", RequestHeaders = [new("Header1", Redacted)], @@ -363,7 +333,7 @@ public async Task ReadAsync_RouteParameterRedactionModeNone_ReturnsLogRecordWith LogBody = true, RequestPathParameterRedactionMode = HttpRouteParameterRedactionMode.None, RequestHeadersDataClasses = new Dictionary { { header1.Key, FakeTaxonomy.PrivateData } }, - RequestBodyContentTypes = new HashSet { plainTextMedia }, + RequestBodyContentTypes = new HashSet { MediaTypeNames.Text.Plain }, BodyReadTimeout = TimeSpan.FromSeconds(10), RequestPathLoggingMode = OutgoingPathLoggingMode.Structured }; @@ -378,7 +348,7 @@ public async Task ReadAsync_RouteParameterRedactionModeNone_ReturnsLogRecordWith using var serviceProvider = GetServiceProvider(headersReader); var reader = new HttpRequestReader(serviceProvider, opts.ToOptionsMonitor(), - serviceProvider.GetRequiredService(), RequestMetadataContext); + serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); using var httpRequestMessage = new HttpRequestMessage { @@ -394,12 +364,7 @@ public async Task ReadAsync_RouteParameterRedactionModeNone_ReturnsLogRecordWith var actualRecord = new LogRecord(); await reader.ReadRequestAsync(actualRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None).ConfigureAwait(false); - actualRecord.Should().BeEquivalentTo( - expectedRecord, - o => o - .Excluding(m => m.RequestBody) - .Excluding(m => m.ResponseBody) - .ComparingByMembers()); + actualRecord.Should().BeEquivalentTo(expectedRecord); } [Fact] @@ -407,14 +372,12 @@ public async Task ReadAsync_RequestMetadataRequestNameSetAndRouteMissing_Returns { var requestContent = _fixture.Create(); var responseContent = _fixture.Create(); - var host = "default-uri.com"; - var plainTextMedia = "text/plain"; var header1 = new KeyValuePair("Header1", "Value1"); var header2 = new KeyValuePair("Header2", "Value2"); var expectedRecord = new LogRecord { - Host = host, + Host = RequestedHost, Method = HttpMethod.Post, Path = "TestRequest", StatusCode = 200, @@ -428,8 +391,8 @@ public async Task ReadAsync_RequestMetadataRequestNameSetAndRouteMissing_Returns { RequestHeadersDataClasses = new Dictionary { { header1.Key, FakeTaxonomy.PrivateData } }, ResponseHeadersDataClasses = new Dictionary { { header2.Key, FakeTaxonomy.PrivateData } }, - RequestBodyContentTypes = new HashSet { plainTextMedia }, - ResponseBodyContentTypes = new HashSet { plainTextMedia }, + RequestBodyContentTypes = new HashSet { MediaTypeNames.Text.Plain }, + ResponseBodyContentTypes = new HashSet { MediaTypeNames.Text.Plain }, BodyReadTimeout = TimeSpan.FromSeconds(10), LogBody = true, }; @@ -443,7 +406,7 @@ public async Task ReadAsync_RequestMetadataRequestNameSetAndRouteMissing_Returns using var serviceProvider = GetServiceProvider(headersReader); var reader = new HttpRequestReader(serviceProvider, opts.ToOptionsMonitor(), - serviceProvider.GetRequiredService(), RequestMetadataContext); + serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); using var httpRequestMessage = new HttpRequestMessage { @@ -472,15 +435,7 @@ public async Task ReadAsync_RequestMetadataRequestNameSetAndRouteMissing_Returns await reader.ReadRequestAsync(actualRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None).ConfigureAwait(false); await reader.ReadResponseAsync(actualRecord, httpResponseMessage, responseHeadersBuffer, CancellationToken.None).ConfigureAwait(false); - actualRecord.Should().BeEquivalentTo( - expectedRecord, - o => o - .Excluding(m => m.RequestBody) - .Excluding(m => m.ResponseBody) - .ComparingByMembers()); - - actualRecord.RequestBody.Should().BeEquivalentTo(expectedRecord.RequestBody); - actualRecord.ResponseBody.Should().BeEquivalentTo(expectedRecord.ResponseBody); + actualRecord.Should().BeEquivalentTo(expectedRecord); } [Fact] @@ -488,14 +443,12 @@ public async Task ReadAsync_NoMetadataUsesRedactedString_ReturnsLogRecord() { var requestContent = _fixture.Create(); var responseContent = _fixture.Create(); - var host = "default-uri.com"; - var plainTextMedia = "text/plain"; var header1 = new KeyValuePair("Header1", "Value1"); var header2 = new KeyValuePair("Header2", "Value2"); var expectedRecord = new LogRecord { - Host = host, + Host = RequestedHost, Method = HttpMethod.Post, Path = TelemetryConstants.Redacted, StatusCode = 200, @@ -509,8 +462,8 @@ public async Task ReadAsync_NoMetadataUsesRedactedString_ReturnsLogRecord() { RequestHeadersDataClasses = new Dictionary { { header1.Key, FakeTaxonomy.PrivateData } }, ResponseHeadersDataClasses = new Dictionary { { header2.Key, FakeTaxonomy.PrivateData } }, - RequestBodyContentTypes = new HashSet { plainTextMedia }, - ResponseBodyContentTypes = new HashSet { plainTextMedia }, + RequestBodyContentTypes = new HashSet { MediaTypeNames.Text.Plain }, + ResponseBodyContentTypes = new HashSet { MediaTypeNames.Text.Plain }, BodyReadTimeout = TimeSpan.FromSeconds(10), LogBody = true, }; @@ -524,7 +477,7 @@ public async Task ReadAsync_NoMetadataUsesRedactedString_ReturnsLogRecord() using var serviceProvider = GetServiceProvider(headersReader); var reader = new HttpRequestReader(serviceProvider, opts.ToOptionsMonitor(), - serviceProvider.GetRequiredService(), RequestMetadataContext); + serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); using var httpRequestMessage = new HttpRequestMessage { @@ -549,14 +502,7 @@ public async Task ReadAsync_NoMetadataUsesRedactedString_ReturnsLogRecord() await reader.ReadRequestAsync(actualRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None).ConfigureAwait(false); await reader.ReadResponseAsync(actualRecord, httpResponseMessage, responseHeadersBuffer, CancellationToken.None).ConfigureAwait(false); - actualRecord.Should().BeEquivalentTo( - expectedRecord, - o => o - .Excluding(m => m.RequestBody) - .Excluding(m => m.ResponseBody) - .ComparingByMembers()); - actualRecord.RequestBody.Should().BeEquivalentTo(expectedRecord.RequestBody); - actualRecord.ResponseBody.Should().BeEquivalentTo(expectedRecord.ResponseBody); + actualRecord.Should().BeEquivalentTo(expectedRecord); } [Fact] @@ -564,14 +510,12 @@ public async Task ReadAsync_MetadataWithoutRequestRouteOrNameUsesConstants_Retur { var requestContent = _fixture.Create(); var responseContent = _fixture.Create(); - var host = "default-uri.com"; - var plainTextMedia = "text/plain"; var header1 = new KeyValuePair("Header1", "Value1"); var header2 = new KeyValuePair("Header2", "Value2"); var expectedRecord = new LogRecord { - Host = host, + Host = RequestedHost, Method = HttpMethod.Post, Path = TelemetryConstants.Unknown, StatusCode = 200, @@ -585,8 +529,8 @@ public async Task ReadAsync_MetadataWithoutRequestRouteOrNameUsesConstants_Retur { RequestHeadersDataClasses = new Dictionary { { header1.Key, FakeTaxonomy.PrivateData } }, ResponseHeadersDataClasses = new Dictionary { { header2.Key, FakeTaxonomy.PrivateData } }, - RequestBodyContentTypes = new HashSet { plainTextMedia }, - ResponseBodyContentTypes = new HashSet { plainTextMedia }, + RequestBodyContentTypes = new HashSet { MediaTypeNames.Text.Plain }, + ResponseBodyContentTypes = new HashSet { MediaTypeNames.Text.Plain }, BodyReadTimeout = TimeSpan.FromSeconds(10), LogBody = true, }; @@ -600,7 +544,7 @@ public async Task ReadAsync_MetadataWithoutRequestRouteOrNameUsesConstants_Retur using var serviceProvider = GetServiceProvider(headersReader); var reader = new HttpRequestReader(serviceProvider, opts.ToOptionsMonitor(), - serviceProvider.GetRequiredService(), RequestMetadataContext); + serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), RequestMetadataContext); using var httpRequestMessage = new HttpRequestMessage { @@ -626,18 +570,13 @@ public async Task ReadAsync_MetadataWithoutRequestRouteOrNameUsesConstants_Retur await reader.ReadRequestAsync(actualRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None).ConfigureAwait(false); await reader.ReadResponseAsync(actualRecord, httpResponseMessage, responseHeadersBuffer, CancellationToken.None).ConfigureAwait(false); - actualRecord.Should().BeEquivalentTo( - expectedRecord, - o => o - .Excluding(m => m.RequestBody) - .Excluding(m => m.ResponseBody) - .ComparingByMembers()); - - actualRecord.RequestBody.Should().BeEquivalentTo(expectedRecord.RequestBody); - actualRecord.ResponseBody.Should().BeEquivalentTo(expectedRecord.ResponseBody); + actualRecord.Should().BeEquivalentTo(expectedRecord); } - private static ServiceProvider GetServiceProvider(HttpHeadersReader headersReader, string? serviceKey = null) + private static ServiceProvider GetServiceProvider( + HttpHeadersReader headersReader, + string? serviceKey = null, + Action? configureRedaction = null) { var services = new ServiceCollection(); if (serviceKey is null) @@ -650,7 +589,7 @@ private static ServiceProvider GetServiceProvider(HttpHeadersReader headersReade } return services - .AddFakeRedaction() + .AddFakeRedaction(configureRedaction ?? (_ => { })) .AddHttpRouteProcessor() .BuildServiceProvider(); }