diff --git a/src/Middleware/HttpLogging/src/HttpLoggingAttribute.cs b/src/Middleware/HttpLogging/src/HttpLoggingAttribute.cs new file mode 100644 index 000000000000..9761063552b0 --- /dev/null +++ b/src/Middleware/HttpLogging/src/HttpLoggingAttribute.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.HttpLogging; + +/// +/// Metadata that provides endpoint-specific settings for the HttpLogging middleware. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] +public sealed class HttpLoggingAttribute : Attribute +{ + /// + /// Initializes an instance of the class. + /// + /// Specifies what fields to log for the endpoint. + /// Specifies the maximum number of bytes to be logged for the request body. A value of -1 means use the default setting in . + /// Specifies the maximum number of bytes to be logged for the response body. A value of -1 means use the default setting in . + /// Thrown when or is less than -1. + public HttpLoggingAttribute(HttpLoggingFields loggingFields, int requestBodyLogLimit = -1, int responseBodyLogLimit = -1) + { + LoggingFields = loggingFields; + + ArgumentOutOfRangeException.ThrowIfLessThan(requestBodyLogLimit, -1); + ArgumentOutOfRangeException.ThrowIfLessThan(responseBodyLogLimit, -1); + + RequestBodyLogLimit = requestBodyLogLimit; + ResponseBodyLogLimit = responseBodyLogLimit; + } + + /// + /// Specifies what fields to log. + /// + public HttpLoggingFields LoggingFields { get; } + + /// + /// Specifies the maximum number of bytes to be logged for the request body. + /// + public int RequestBodyLogLimit { get; } + + /// + /// Specifies the maximum number of bytes to be logged for the response body. + /// + public int ResponseBodyLogLimit { get; } +} diff --git a/src/Middleware/HttpLogging/src/HttpLoggingEndpointConventionBuilderExtensions.cs b/src/Middleware/HttpLogging/src/HttpLoggingEndpointConventionBuilderExtensions.cs new file mode 100644 index 000000000000..8d914503ec9a --- /dev/null +++ b/src/Middleware/HttpLogging/src/HttpLoggingEndpointConventionBuilderExtensions.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.HttpLogging; + +namespace Microsoft.AspNetCore.Builder; + +/// +/// HttpLogging middleware extension methods for . +/// +public static class HttpLoggingEndpointConventionBuilderExtensions +{ + /// + /// Adds endpoint specific settings for the HttpLogging middleware. + /// + /// The type of endpoint convention builder. + /// The endpoint convention builder. + /// The to apply to this endpoint. + /// Sets the for this endpoint. A value of -1 means use the default setting in . + /// Sets the for this endpoint. A value of -1 means use the default setting in . + /// The original convention builder parameter. + /// Thrown when or is less than -1. + public static TBuilder WithHttpLogging(this TBuilder builder, HttpLoggingFields loggingFields, int requestBodyLogLimit = -1, int responseBodyLogLimit = -1) where TBuilder : IEndpointConventionBuilder + { + // Construct outside build.Add lambda to allow exceptions to be thrown immediately + var metadata = new HttpLoggingAttribute(loggingFields, requestBodyLogLimit, responseBodyLogLimit); + + builder.Add(endpointBuilder => + { + endpointBuilder.Metadata.Add(metadata); + }); + return builder; + } +} diff --git a/src/Middleware/HttpLogging/src/HttpLoggingMiddleware.cs b/src/Middleware/HttpLogging/src/HttpLoggingMiddleware.cs index 452c83c4dec1..05d0b92f7996 100644 --- a/src/Middleware/HttpLogging/src/HttpLoggingMiddleware.cs +++ b/src/Middleware/HttpLogging/src/HttpLoggingMiddleware.cs @@ -59,44 +59,47 @@ private async Task InvokeInternal(HttpContext context) RequestBufferingStream? requestBufferingStream = null; Stream? originalBody = null; - if ((HttpLoggingFields.Request & options.LoggingFields) != HttpLoggingFields.None) + var loggingAttribute = context.GetEndpoint()?.Metadata.GetMetadata(); + var loggingFields = loggingAttribute?.LoggingFields ?? options.LoggingFields; + + if ((HttpLoggingFields.Request & loggingFields) != HttpLoggingFields.None) { var request = context.Request; var list = new List>( request.Headers.Count + DefaultRequestFieldsMinusHeaders); - if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestProtocol)) + if (loggingFields.HasFlag(HttpLoggingFields.RequestProtocol)) { AddToList(list, nameof(request.Protocol), request.Protocol); } - if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestMethod)) + if (loggingFields.HasFlag(HttpLoggingFields.RequestMethod)) { AddToList(list, nameof(request.Method), request.Method); } - if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestScheme)) + if (loggingFields.HasFlag(HttpLoggingFields.RequestScheme)) { AddToList(list, nameof(request.Scheme), request.Scheme); } - if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestPath)) + if (loggingFields.HasFlag(HttpLoggingFields.RequestPath)) { AddToList(list, nameof(request.PathBase), request.PathBase); AddToList(list, nameof(request.Path), request.Path); } - if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestQuery)) + if (loggingFields.HasFlag(HttpLoggingFields.RequestQuery)) { AddToList(list, nameof(request.QueryString), request.QueryString.Value); } - if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestHeaders)) + if (loggingFields.HasFlag(HttpLoggingFields.RequestHeaders)) { FilterHeaders(list, request.Headers, options._internalRequestHeaders); } - if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestBody)) + if (loggingFields.HasFlag(HttpLoggingFields.RequestBody)) { if (request.ContentType is null) { @@ -106,10 +109,16 @@ private async Task InvokeInternal(HttpContext context) options.MediaTypeOptions.MediaTypeStates, out var encoding)) { + var requestBodyLogLimit = options.RequestBodyLogLimit; + if (loggingAttribute?.RequestBodyLogLimit is int) + { + requestBodyLogLimit = loggingAttribute.RequestBodyLogLimit; + } + originalBody = request.Body; requestBufferingStream = new RequestBufferingStream( request.Body, - options.RequestBodyLogLimit, + requestBodyLogLimit, _logger, encoding); request.Body = requestBufferingStream; @@ -135,29 +144,36 @@ private async Task InvokeInternal(HttpContext context) { var response = context.Response; - if (options.LoggingFields.HasFlag(HttpLoggingFields.ResponseStatusCode) || options.LoggingFields.HasFlag(HttpLoggingFields.ResponseHeaders)) + if (loggingFields.HasFlag(HttpLoggingFields.ResponseStatusCode) || loggingFields.HasFlag(HttpLoggingFields.ResponseHeaders)) { originalUpgradeFeature = context.Features.Get(); if (originalUpgradeFeature != null && originalUpgradeFeature.IsUpgradableRequest) { - loggableUpgradeFeature = new UpgradeFeatureLoggingDecorator(originalUpgradeFeature, response, options, _logger); + loggableUpgradeFeature = new UpgradeFeatureLoggingDecorator(originalUpgradeFeature, response, options._internalResponseHeaders, loggingFields, _logger); context.Features.Set(loggableUpgradeFeature); } } - if (options.LoggingFields.HasFlag(HttpLoggingFields.ResponseBody)) + if (loggingFields.HasFlag(HttpLoggingFields.ResponseBody)) { originalBodyFeature = context.Features.Get()!; + var responseBodyLogLimit = options.ResponseBodyLogLimit; + if (loggingAttribute?.ResponseBodyLogLimit is int) + { + responseBodyLogLimit = loggingAttribute.ResponseBodyLogLimit; + } + // TODO pool these. responseBufferingStream = new ResponseBufferingStream(originalBodyFeature, - options.ResponseBodyLogLimit, + responseBodyLogLimit, _logger, context, options.MediaTypeOptions.MediaTypeStates, - options); + options._internalResponseHeaders, + loggingFields); response.Body = responseBufferingStream; context.Features.Set(responseBufferingStream); } @@ -174,7 +190,7 @@ private async Task InvokeInternal(HttpContext context) if (ResponseHeadersNotYetWritten(responseBufferingStream, loggableUpgradeFeature)) { // No body, not an upgradable request or request not upgraded, write headers here. - LogResponseHeaders(response, options, _logger); + LogResponseHeaders(response, loggingFields, options._internalResponseHeaders, _logger); } if (responseBufferingStream != null) @@ -216,7 +232,7 @@ private static bool ResponseHeadersNotYetWritten(ResponseBufferingStream? respon private static bool BodyNotYetWritten(ResponseBufferingStream? responseBufferingStream) { - return responseBufferingStream == null || responseBufferingStream.FirstWrite == false; + return responseBufferingStream == null || responseBufferingStream.HeadersWritten == false; } private static bool NotUpgradeableRequestOrRequestNotUpgraded(UpgradeFeatureLoggingDecorator? upgradeFeatureLogging) @@ -229,19 +245,19 @@ private static void AddToList(List> list, string k list.Add(new KeyValuePair(key, value)); } - public static void LogResponseHeaders(HttpResponse response, HttpLoggingOptions options, ILogger logger) + public static void LogResponseHeaders(HttpResponse response, HttpLoggingFields loggingFields, HashSet allowedResponseHeaders, ILogger logger) { var list = new List>( response.Headers.Count + DefaultResponseFieldsMinusHeaders); - if (options.LoggingFields.HasFlag(HttpLoggingFields.ResponseStatusCode)) + if (loggingFields.HasFlag(HttpLoggingFields.ResponseStatusCode)) { list.Add(new KeyValuePair(nameof(response.StatusCode), response.StatusCode)); } - if (options.LoggingFields.HasFlag(HttpLoggingFields.ResponseHeaders)) + if (loggingFields.HasFlag(HttpLoggingFields.ResponseHeaders)) { - FilterHeaders(list, response.Headers, options._internalResponseHeaders); + FilterHeaders(list, response.Headers, allowedResponseHeaders); } if (list.Count > 0) diff --git a/src/Middleware/HttpLogging/src/PublicAPI.Unshipped.txt b/src/Middleware/HttpLogging/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..d2f14db0a9ab 100644 --- a/src/Middleware/HttpLogging/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/HttpLogging/src/PublicAPI.Unshipped.txt @@ -1 +1,8 @@ #nullable enable +Microsoft.AspNetCore.Builder.HttpLoggingEndpointConventionBuilderExtensions +Microsoft.AspNetCore.HttpLogging.HttpLoggingAttribute +Microsoft.AspNetCore.HttpLogging.HttpLoggingAttribute.HttpLoggingAttribute(Microsoft.AspNetCore.HttpLogging.HttpLoggingFields loggingFields, int requestBodyLogLimit = -1, int responseBodyLogLimit = -1) -> void +Microsoft.AspNetCore.HttpLogging.HttpLoggingAttribute.LoggingFields.get -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields +Microsoft.AspNetCore.HttpLogging.HttpLoggingAttribute.RequestBodyLogLimit.get -> int +Microsoft.AspNetCore.HttpLogging.HttpLoggingAttribute.ResponseBodyLogLimit.get -> int +static Microsoft.AspNetCore.Builder.HttpLoggingEndpointConventionBuilderExtensions.WithHttpLogging(this TBuilder builder, Microsoft.AspNetCore.HttpLogging.HttpLoggingFields loggingFields, int requestBodyLogLimit = -1, int responseBodyLogLimit = -1) -> TBuilder diff --git a/src/Middleware/HttpLogging/src/ResponseBufferingStream.cs b/src/Middleware/HttpLogging/src/ResponseBufferingStream.cs index ffd348255c79..628801dfe77d 100644 --- a/src/Middleware/HttpLogging/src/ResponseBufferingStream.cs +++ b/src/Middleware/HttpLogging/src/ResponseBufferingStream.cs @@ -22,7 +22,8 @@ internal sealed class ResponseBufferingStream : BufferingStream, IHttpResponseBo private readonly HttpContext _context; private readonly List _encodings; - private readonly HttpLoggingOptions _options; + private readonly HashSet _allowedResponseHeaders; + private readonly HttpLoggingFields _loggingFields; private Encoding? _encoding; private static readonly StreamPipeWriterOptions _pipeWriterOptions = new StreamPipeWriterOptions(leaveOpen: true); @@ -32,7 +33,8 @@ internal ResponseBufferingStream(IHttpResponseBodyFeature innerBodyFeature, ILogger logger, HttpContext context, List encodings, - HttpLoggingOptions options) + HashSet allowedResponseHeaders, + HttpLoggingFields loggingFields) : base(innerBodyFeature.Stream, logger) { _innerBodyFeature = innerBodyFeature; @@ -40,10 +42,11 @@ internal ResponseBufferingStream(IHttpResponseBodyFeature innerBodyFeature, _limit = limit; _context = context; _encodings = encodings; - _options = options; + _allowedResponseHeaders = allowedResponseHeaders; + _loggingFields = loggingFields; } - public bool FirstWrite { get; private set; } + public bool HeadersWritten { get; private set; } public Stream Stream => this; @@ -68,24 +71,7 @@ public override void EndWrite(IAsyncResult asyncResult) public override void Write(ReadOnlySpan span) { - var remaining = _limit - _bytesBuffered; - var innerCount = Math.Min(remaining, span.Length); - - OnFirstWrite(); - - if (innerCount > 0) - { - if (span.Slice(0, innerCount).TryCopyTo(_tailMemory.Span)) - { - _tailBytesBuffered += innerCount; - _bytesBuffered += innerCount; - _tailMemory = _tailMemory.Slice(innerCount); - } - else - { - BuffersExtensions.Write(this, span.Slice(0, innerCount)); - } - } + CommonWrite(span); _innerStream.Write(span); } @@ -96,39 +82,44 @@ public override async Task WriteAsync(byte[] buffer, int offset, int count, Canc } public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + CommonWrite(buffer.Span); + + await _innerStream.WriteAsync(buffer, cancellationToken); + } + + private void CommonWrite(ReadOnlySpan span) { var remaining = _limit - _bytesBuffered; - var innerCount = Math.Min(remaining, buffer.Length); + var innerCount = Math.Min(remaining, span.Length); OnFirstWrite(); if (innerCount > 0) { - if (_tailMemory.Length - innerCount > 0) + var slice = span.Slice(0, innerCount); + if (slice.TryCopyTo(_tailMemory.Span)) { - buffer.Slice(0, innerCount).CopyTo(_tailMemory); _tailBytesBuffered += innerCount; _bytesBuffered += innerCount; _tailMemory = _tailMemory.Slice(innerCount); } else { - BuffersExtensions.Write(this, buffer.Span); + BuffersExtensions.Write(this, slice); } } - - await _innerStream.WriteAsync(buffer, cancellationToken); } private void OnFirstWrite() { - if (!FirstWrite) + if (!HeadersWritten) { // Log headers as first write occurs (headers locked now) - HttpLoggingMiddleware.LogResponseHeaders(_context.Response, _options, _logger); + HttpLoggingMiddleware.LogResponseHeaders(_context.Response, _loggingFields, _allowedResponseHeaders, _logger); MediaTypeHelpers.TryGetEncodingForMediaType(_context.Response.ContentType, _encodings, out _encoding); - FirstWrite = true; + HeadersWritten = true; } } diff --git a/src/Middleware/HttpLogging/src/UpgradeFeatureLoggingDecorator.cs b/src/Middleware/HttpLogging/src/UpgradeFeatureLoggingDecorator.cs index dd181b1616ce..b1162b77b85b 100644 --- a/src/Middleware/HttpLogging/src/UpgradeFeatureLoggingDecorator.cs +++ b/src/Middleware/HttpLogging/src/UpgradeFeatureLoggingDecorator.cs @@ -11,17 +11,19 @@ internal sealed class UpgradeFeatureLoggingDecorator : IHttpUpgradeFeature { private readonly IHttpUpgradeFeature _innerUpgradeFeature; private readonly HttpResponse _response; - private readonly HttpLoggingOptions _options; + private readonly HashSet _allowedResponseHeaders; private readonly ILogger _logger; + private readonly HttpLoggingFields _loggingFields; private bool _isUpgraded; - public UpgradeFeatureLoggingDecorator(IHttpUpgradeFeature innerUpgradeFeature, HttpResponse response, HttpLoggingOptions options, ILogger logger) + public UpgradeFeatureLoggingDecorator(IHttpUpgradeFeature innerUpgradeFeature, HttpResponse response, HashSet allowedResponseHeaders, HttpLoggingFields loggingFields, ILogger logger) { _innerUpgradeFeature = innerUpgradeFeature ?? throw new ArgumentNullException(nameof(innerUpgradeFeature)); _response = response ?? throw new ArgumentNullException(nameof(response)); - _options = options ?? throw new ArgumentNullException(nameof(options)); + _allowedResponseHeaders = allowedResponseHeaders ?? throw new ArgumentNullException(nameof(allowedResponseHeaders)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _loggingFields = loggingFields; } public bool IsUpgradableRequest => _innerUpgradeFeature.IsUpgradableRequest; @@ -34,7 +36,7 @@ public async Task UpgradeAsync() _isUpgraded = true; - HttpLoggingMiddleware.LogResponseHeaders(_response, _options, _logger); + HttpLoggingMiddleware.LogResponseHeaders(_response, _loggingFields, _allowedResponseHeaders, _logger); return upgradeStream; } diff --git a/src/Middleware/HttpLogging/test/HttpLoggingEndpointConventionBuilderTests.cs b/src/Middleware/HttpLogging/test/HttpLoggingEndpointConventionBuilderTests.cs new file mode 100644 index 000000000000..45d65df5a822 --- /dev/null +++ b/src/Middleware/HttpLogging/test/HttpLoggingEndpointConventionBuilderTests.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.HttpLogging.Tests; + +public class HttpLoggingEndpointConventionBuilderTests +{ + [Fact] + public void WithHttpLogging_SetsMetadata() + { + // Arrange + var testConventionBuilder = new TestEndpointConventionBuilder(); + var loggingFields = HttpLoggingFields.RequestScheme | HttpLoggingFields.RequestPath; + var requestBodyLogLimit = 22; + var responseBodyLogLimit = 94; + + // Act + testConventionBuilder.WithHttpLogging(loggingFields, requestBodyLogLimit, responseBodyLogLimit); + + // Assert + var httpLogingAttribute = Assert.Single(testConventionBuilder.Conventions); + + var endpointModel = new TestEndpointBuilder(); + httpLogingAttribute(endpointModel); + var endpoint = endpointModel.Build(); + + var metadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(metadata); + Assert.Equal(requestBodyLogLimit, metadata.RequestBodyLogLimit); + Assert.Equal(responseBodyLogLimit, metadata.ResponseBodyLogLimit); + Assert.Equal(loggingFields, metadata.LoggingFields); + } + + [Fact] + public void WithHttpLogging_ThrowsForInvalidLimits() + { + // Arrange + var testConventionBuilder = new TestEndpointConventionBuilder(); + + // Act & Assert + var ex = Assert.Throws(() => + testConventionBuilder.WithHttpLogging(HttpLoggingFields.None, requestBodyLogLimit: -2)); + Assert.Equal("requestBodyLogLimit", ex.ParamName); + + ex = Assert.Throws(() => + testConventionBuilder.WithHttpLogging(HttpLoggingFields.None, responseBodyLogLimit: -2)); + Assert.Equal("responseBodyLogLimit", ex.ParamName); + } +} + +internal class TestEndpointBuilder : EndpointBuilder +{ + public override Endpoint Build() + { + return new Endpoint(RequestDelegate, new EndpointMetadataCollection(Metadata), DisplayName); + } +} + +internal class TestEndpointConventionBuilder : IEndpointConventionBuilder +{ + public IList> Conventions { get; } = new List>(); + + public void Add(Action convention) + { + Conventions.Add(convention); + } + + public TestEndpointConventionBuilder ApplyToEndpoint(EndpointBuilder endpoint) + { + foreach (var convention in Conventions) + { + convention(endpoint); + } + + return this; + } +} diff --git a/src/Middleware/HttpLogging/test/HttpLoggingMiddlewareTests.cs b/src/Middleware/HttpLogging/test/HttpLoggingMiddlewareTests.cs index b80d0db1e92c..c31bc608a82a 100644 --- a/src/Middleware/HttpLogging/test/HttpLoggingMiddlewareTests.cs +++ b/src/Middleware/HttpLogging/test/HttpLoggingMiddlewareTests.cs @@ -1,10 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net.Http; using System.Text; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; @@ -1115,10 +1121,209 @@ public async Task OriginalUpgradeFeatureIsRestoredBeforeMiddlewareCompletes(Http Assert.False(httpContext.Features.Get() is UpgradeFeatureLoggingDecorator); } + [Fact] + public async Task HttpLoggingAttributeWithLessOptionsAppliesToEndpoint() + { + var app = CreateApp(); + await app.StartAsync(); + + using var server = app.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/attr_responseonly")); + + var filteredLogs = TestSink.Writes.Where(w => w.LoggerName.Contains("HttpLogging")); + Assert.DoesNotContain(filteredLogs, w => w.Message.Contains("Request")); + Assert.Contains(filteredLogs, w => w.Message.Contains("StatusCode: 200")); + } + + [Fact] + public async Task HttpLoggingAttributeWithMoreOptionsAppliesToEndpoint() + { + var app = CreateApp(defaultFields: HttpLoggingFields.None); + await app.StartAsync(); + + using var server = app.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/attr_responseandrequest")); + + var filteredLogs = TestSink.Writes.Where(w => w.LoggerName.Contains("HttpLogging")); + Assert.Contains(filteredLogs, w => w.Message.Contains("Request")); + Assert.Contains(filteredLogs, w => w.Message.Contains("StatusCode: 200")); + } + + [Fact] + public async Task HttpLoggingAttributeCanRestrictHeaderOutputOnEndpoint() + { + var app = CreateApp(); + await app.StartAsync(); + + using var server = app.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/attr_restrictedheaders")); + + var filteredLogs = TestSink.Writes.Where(w => w.LoggerName.Contains("HttpLogging")); + Assert.DoesNotContain(filteredLogs, w => w.Message.Contains("Scheme")); + Assert.DoesNotContain(filteredLogs, w => w.Message.Contains("StatusCode: 200")); + } + + [Fact] + public async Task HttpLoggingAttributeCanModifyRequestAndResponseSizeOnEndpoint() + { + var app = CreateApp(); + await app.StartAsync(); + + using var server = app.GetTestServer(); + var client = server.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Get, "/attr_restrictedsize") { Content = new ReadOnlyMemoryContent("from request"u8.ToArray()) }; + request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("text/plain"); + var initialResponse = await client.SendAsync(request); + + var filteredLogs = TestSink.Writes.Where(w => w.LoggerName.Contains("HttpLogging")); + Assert.Contains(filteredLogs, w => w.Message.Equals("RequestBody: fro")); + Assert.Contains(filteredLogs, w => w.Message.Equals("ResponseBody: testin")); + } + + [Fact] + public async Task HttpLoggingExtensionWithLessOptionsAppliesToEndpoint() + { + var app = CreateApp(); + await app.StartAsync(); + + using var server = app.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/ext_responseonly")); + + var filteredLogs = TestSink.Writes.Where(w => w.LoggerName.Contains("HttpLogging")); + Assert.DoesNotContain(filteredLogs, w => w.Message.Contains("Request")); + Assert.Contains(filteredLogs, w => w.Message.Contains("StatusCode: 200")); + } + + [Fact] + public async Task HttpLoggingExtensionWithMoreOptionsAppliesToEndpoint() + { + var app = CreateApp(defaultFields: HttpLoggingFields.None); + await app.StartAsync(); + + using var server = app.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/ext_responseandrequest")); + + var filteredLogs = TestSink.Writes.Where(w => w.LoggerName.Contains("HttpLogging")); + Assert.Contains(filteredLogs, w => w.Message.Contains("Request")); + Assert.Contains(filteredLogs, w => w.Message.Contains("StatusCode: 200")); + } + + [Fact] + public async Task HttpLoggingExtensionCanRestrictHeaderOutputOnEndpoint() + { + var app = CreateApp(); + await app.StartAsync(); + + using var server = app.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/ext_restrictedheaders")); + + var filteredLogs = TestSink.Writes.Where(w => w.LoggerName.Contains("HttpLogging")); + Assert.DoesNotContain(filteredLogs, w => w.Message.Contains("Scheme")); + Assert.DoesNotContain(filteredLogs, w => w.Message.Contains("StatusCode: 200")); + } + + [Fact] + public async Task HttpLoggingExtensionCanModifyRequestAndResponseSizeOnEndpoint() + { + var app = CreateApp(); + await app.StartAsync(); + + using var server = app.GetTestServer(); + var client = server.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Get, "/ext_restrictedsize") { Content = new ReadOnlyMemoryContent("from request"u8.ToArray()) }; + request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("text/plain"); + var initialResponse = await client.SendAsync(request); + + var filteredLogs = TestSink.Writes.Where(w => w.LoggerName.Contains("HttpLogging")); + Assert.Contains(filteredLogs, w => w.Message.Equals("RequestBody: fro")); + Assert.Contains(filteredLogs, w => w.Message.Equals("ResponseBody: testin")); + } + private IOptionsMonitor CreateOptionsAccessor() { var options = new HttpLoggingOptions(); var optionsAccessor = Mock.Of>(o => o.CurrentValue == options); return optionsAccessor; } + + private IHost CreateApp(HttpLoggingFields defaultFields = HttpLoggingFields.All) + { + var builder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .ConfigureServices(services => + { + services.AddRouting(); + services.AddHttpLogging(o => + { + o.LoggingFields = defaultFields; + }); + services.AddSingleton(LoggerFactory); + }) + .Configure(app => + { + app.UseRouting(); + app.UseHttpLogging(); + app.UseEndpoints(endpoint => + { + endpoint.MapGet("/attr_responseonly", [HttpLogging(HttpLoggingFields.Response)] async (HttpContext c) => + { + await c.Request.Body.ReadAsync(new byte[100]); + return "testing"; + }); + + endpoint.MapGet("/ext_responseonly", async (HttpContext c) => + { + await c.Request.Body.ReadAsync(new byte[100]); + return "testing"; + }).WithHttpLogging(HttpLoggingFields.Response); + + endpoint.MapGet("/attr_responseandrequest", [HttpLogging(HttpLoggingFields.Request | HttpLoggingFields.Response)] async (HttpContext c) => + { + await c.Request.Body.ReadAsync(new byte[100]); + return "testing"; + }); + + endpoint.MapGet("/ext_responseandrequest", async(HttpContext c) => + { + await c.Request.Body.ReadAsync(new byte[100]); + return "testing"; + }).WithHttpLogging(HttpLoggingFields.Request | HttpLoggingFields.Response); + + endpoint.MapGet("/attr_restrictedheaders", [HttpLogging((HttpLoggingFields.Request & ~HttpLoggingFields.RequestScheme) | (HttpLoggingFields.Response & ~HttpLoggingFields.ResponseStatusCode))] async (HttpContext c) => + { + await c.Request.Body.ReadAsync(new byte[100]); + return "testing"; + }); + + endpoint.MapGet("/ext_restrictedheaders", async (HttpContext c) => + { + await c.Request.Body.ReadAsync(new byte[100]); + return "testing"; + }).WithHttpLogging((HttpLoggingFields.Request & ~HttpLoggingFields.RequestScheme) | (HttpLoggingFields.Response & ~HttpLoggingFields.ResponseStatusCode)); + + endpoint.MapGet("/attr_restrictedsize", [HttpLogging(HttpLoggingFields.Request | HttpLoggingFields.Response, requestBodyLogLimit: 3, responseBodyLogLimit: 6)] async (HttpContext c) => + { + await c.Request.Body.ReadAsync(new byte[100]); + return "testing"; + }); + + endpoint.MapGet("/ext_restrictedsize", async (HttpContext c) => + { + await c.Request.Body.ReadAsync(new byte[100]); + return "testing"; + }).WithHttpLogging(HttpLoggingFields.Request | HttpLoggingFields.Response, requestBodyLogLimit: 3, responseBodyLogLimit: 6); + }); + }); + }); + return builder.Build(); + } } diff --git a/src/Middleware/HttpLogging/test/Microsoft.AspNetCore.HttpLogging.Tests.csproj b/src/Middleware/HttpLogging/test/Microsoft.AspNetCore.HttpLogging.Tests.csproj index 17c52be2ca50..6204ea459146 100644 --- a/src/Middleware/HttpLogging/test/Microsoft.AspNetCore.HttpLogging.Tests.csproj +++ b/src/Middleware/HttpLogging/test/Microsoft.AspNetCore.HttpLogging.Tests.csproj @@ -7,6 +7,7 @@ +