diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/HttpLoggingRedactionInterceptor.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/HttpLoggingRedactionInterceptor.cs index 050f0bb092d..31a253f68db 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/HttpLoggingRedactionInterceptor.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/HttpLoggingRedactionInterceptor.cs @@ -33,6 +33,7 @@ internal sealed class HttpLoggingRedactionInterceptor : IHttpLoggingInterceptor private readonly HeaderReader _responseHeadersReader; private readonly string[] _excludePathStartsWith; private readonly FrozenDictionary _parametersToRedactMap; + private readonly bool _includeUnmatchedRoutes; public HttpLoggingRedactionInterceptor( IOptions options, @@ -59,6 +60,7 @@ public HttpLoggingRedactionInterceptor( _responseHeadersReader = new(optionsValue.ResponseHeadersDataClasses, redactorProvider, HttpLoggingTagNames.ResponseHeaderPrefix); _excludePathStartsWith = optionsValue.ExcludePathStartsWith.ToArray(); + _includeUnmatchedRoutes = optionsValue.IncludeUnmatchedRoutes; } public ValueTask OnRequestAsync(HttpLoggingInterceptorContext logContext) @@ -115,6 +117,10 @@ public ValueTask OnRequestAsync(HttpLoggingInterceptorContext logContext) } } } + else if (_includeUnmatchedRoutes) + { + path = context.Request.Path.ToString(); + } } else if (request.Path.HasValue) { diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/LoggingRedactionOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/LoggingRedactionOptions.cs index 0f6f1324e97..d2cb34ac547 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/LoggingRedactionOptions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/LoggingRedactionOptions.cs @@ -100,6 +100,15 @@ public class LoggingRedactionOptions #pragma warning disable CA2227 // Collection properties should be read only public ISet ExcludePathStartsWith { get; set; } = new HashSet(StringComparer.OrdinalIgnoreCase); #pragma warning restore CA2227 // Collection properties should be read only + + /// + /// Gets or sets a value indicating whether to report unmatched routes. + /// + /// + /// If set to true, instead of logging unknown value for path attribute it will log whole path of routes not identified by ASP.NET Routing. + /// + /// Defaults to . + public bool IncludeUnmatchedRoutes { get; set; } } #endif diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs index 869b0716e17..01a962f2f90 100644 --- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs @@ -230,7 +230,7 @@ await RunAsync( const string Content = "Client: hello!"; using var content = new StringContent(Content, null, requestContentType); - using var response = await client.PostAsync("/", content).ConfigureAwait(false); + using var response = await client.PostAsync("/myroute/123", content).ConfigureAwait(false); Assert.True(response.IsSuccessStatusCode); await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout); @@ -267,6 +267,48 @@ await RunAsync( }); } + [Fact] + public async Task HttpLogging_WhenIncludeUnmatchedRoutes_LogRequestPath() + { + await RunAsync( + LogLevel.Information, + services => services.AddHttpLogging(x => + { + x.MediaTypeOptions.Clear(); + x.MediaTypeOptions.AddText("text/*"); + x.LoggingFields |= HttpLoggingFields.RequestBody; + }).AddHttpLoggingRedaction(options => options.IncludeUnmatchedRoutes = true), + async (logCollector, client) => + { + const string Content = "Client: hello!"; + + using var content = new StringContent(Content, null, MediaTypeNames.Text.Html); + using var response = await client.PostAsync("/myroute/123", content).ConfigureAwait(false); + Assert.True(response.IsSuccessStatusCode); + + await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout); + + Assert.Equal(1, logCollector.Count); + Assert.Null(logCollector.LatestRecord.Exception); + Assert.Equal(LogLevel.Information, logCollector.LatestRecord.Level); + Assert.Equal(LoggingCategory, logCollector.LatestRecord.Category); + + var responseStatus = ((int)response.StatusCode).ToInvariantString(); + var state = logCollector.LatestRecord.StructuredState!; + + Assert.DoesNotContain(state, x => x.Key == HttpLoggingTagNames.ResponseBody); + Assert.DoesNotContain(state, x => x.Key.StartsWith(HttpLoggingTagNames.RequestHeaderPrefix)); + Assert.DoesNotContain(state, x => x.Key.StartsWith(HttpLoggingTagNames.ResponseHeaderPrefix)); + Assert.Single(state, x => x.Key == HttpLoggingTagNames.Host && !string.IsNullOrEmpty(x.Value)); + Assert.Single(state, x => x.Key == HttpLoggingTagNames.Path && x.Value == "/myroute/123"); + Assert.Single(state, x => x.Key == HttpLoggingTagNames.StatusCode && x.Value == responseStatus); + Assert.Single(state, x => x.Key == HttpLoggingTagNames.Method && x.Value == HttpMethod.Post.ToString()); + Assert.Single(state, x => x.Key == HttpLoggingTagNames.Duration && + x.Value != null && + int.Parse(x.Value, CultureInfo.InvariantCulture) == SlashRouteProcessingTimeMs); + }); + } + [Fact] public async Task HttpLogging_WhenLogLevelInfo_LogRequestStart() {