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 @@
+