Skip to content

Commit b4811e3

Browse files
Make HttpLogging middleware endpoint aware (#47595)
1 parent 64dd055 commit b4811e3

9 files changed

+435
-55
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.HttpLogging;
5+
6+
/// <summary>
7+
/// Metadata that provides endpoint-specific settings for the HttpLogging middleware.
8+
/// </summary>
9+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
10+
public sealed class HttpLoggingAttribute : Attribute
11+
{
12+
/// <summary>
13+
/// Initializes an instance of the <see cref="HttpLoggingAttribute"/> class.
14+
/// </summary>
15+
/// <param name="loggingFields">Specifies what fields to log for the endpoint.</param>
16+
/// <param name="requestBodyLogLimit">Specifies the maximum number of bytes to be logged for the request body. A value of <c>-1</c> means use the default setting in <see cref="HttpLoggingOptions.RequestBodyLogLimit"/>.</param>
17+
/// <param name="responseBodyLogLimit">Specifies the maximum number of bytes to be logged for the response body. A value of <c>-1</c> means use the default setting in <see cref="HttpLoggingOptions.ResponseBodyLogLimit"/>.</param>
18+
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="requestBodyLogLimit"/> or <paramref name="responseBodyLogLimit"/> is less than <c>-1</c>.</exception>
19+
public HttpLoggingAttribute(HttpLoggingFields loggingFields, int requestBodyLogLimit = -1, int responseBodyLogLimit = -1)
20+
{
21+
LoggingFields = loggingFields;
22+
23+
ArgumentOutOfRangeException.ThrowIfLessThan(requestBodyLogLimit, -1);
24+
ArgumentOutOfRangeException.ThrowIfLessThan(responseBodyLogLimit, -1);
25+
26+
RequestBodyLogLimit = requestBodyLogLimit;
27+
ResponseBodyLogLimit = responseBodyLogLimit;
28+
}
29+
30+
/// <summary>
31+
/// Specifies what fields to log.
32+
/// </summary>
33+
public HttpLoggingFields LoggingFields { get; }
34+
35+
/// <summary>
36+
/// Specifies the maximum number of bytes to be logged for the request body.
37+
/// </summary>
38+
public int RequestBodyLogLimit { get; }
39+
40+
/// <summary>
41+
/// Specifies the maximum number of bytes to be logged for the response body.
42+
/// </summary>
43+
public int ResponseBodyLogLimit { get; }
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.HttpLogging;
5+
6+
namespace Microsoft.AspNetCore.Builder;
7+
8+
/// <summary>
9+
/// HttpLogging middleware extension methods for <see cref="IEndpointConventionBuilder"/>.
10+
/// </summary>
11+
public static class HttpLoggingEndpointConventionBuilderExtensions
12+
{
13+
/// <summary>
14+
/// Adds endpoint specific settings for the HttpLogging middleware.
15+
/// </summary>
16+
/// <typeparam name="TBuilder">The type of endpoint convention builder.</typeparam>
17+
/// <param name="builder">The endpoint convention builder.</param>
18+
/// <param name="loggingFields">The <see cref="HttpLoggingFields"/> to apply to this endpoint.</param>
19+
/// <param name="requestBodyLogLimit">Sets the <see cref="HttpLoggingOptions.RequestBodyLogLimit"/> for this endpoint. A value of <c>-1</c> means use the default setting in <see cref="HttpLoggingOptions.RequestBodyLogLimit"/>.</param>
20+
/// <param name="responseBodyLogLimit">Sets the <see cref="HttpLoggingOptions.ResponseBodyLogLimit"/> for this endpoint. A value of <c>-1</c> means use the default setting in <see cref="HttpLoggingOptions.ResponseBodyLogLimit"/>.</param>
21+
/// <returns>The original convention builder parameter.</returns>
22+
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="requestBodyLogLimit"/> or <paramref name="responseBodyLogLimit"/> is less than <c>-1</c>.</exception>
23+
public static TBuilder WithHttpLogging<TBuilder>(this TBuilder builder, HttpLoggingFields loggingFields, int requestBodyLogLimit = -1, int responseBodyLogLimit = -1) where TBuilder : IEndpointConventionBuilder
24+
{
25+
// Construct outside build.Add lambda to allow exceptions to be thrown immediately
26+
var metadata = new HttpLoggingAttribute(loggingFields, requestBodyLogLimit, responseBodyLogLimit);
27+
28+
builder.Add(endpointBuilder =>
29+
{
30+
endpointBuilder.Metadata.Add(metadata);
31+
});
32+
return builder;
33+
}
34+
}

src/Middleware/HttpLogging/src/HttpLoggingMiddleware.cs

+36-20
Original file line numberDiff line numberDiff line change
@@ -59,44 +59,47 @@ private async Task InvokeInternal(HttpContext context)
5959
RequestBufferingStream? requestBufferingStream = null;
6060
Stream? originalBody = null;
6161

62-
if ((HttpLoggingFields.Request & options.LoggingFields) != HttpLoggingFields.None)
62+
var loggingAttribute = context.GetEndpoint()?.Metadata.GetMetadata<HttpLoggingAttribute>();
63+
var loggingFields = loggingAttribute?.LoggingFields ?? options.LoggingFields;
64+
65+
if ((HttpLoggingFields.Request & loggingFields) != HttpLoggingFields.None)
6366
{
6467
var request = context.Request;
6568
var list = new List<KeyValuePair<string, object?>>(
6669
request.Headers.Count + DefaultRequestFieldsMinusHeaders);
6770

68-
if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestProtocol))
71+
if (loggingFields.HasFlag(HttpLoggingFields.RequestProtocol))
6972
{
7073
AddToList(list, nameof(request.Protocol), request.Protocol);
7174
}
7275

73-
if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestMethod))
76+
if (loggingFields.HasFlag(HttpLoggingFields.RequestMethod))
7477
{
7578
AddToList(list, nameof(request.Method), request.Method);
7679
}
7780

78-
if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestScheme))
81+
if (loggingFields.HasFlag(HttpLoggingFields.RequestScheme))
7982
{
8083
AddToList(list, nameof(request.Scheme), request.Scheme);
8184
}
8285

83-
if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestPath))
86+
if (loggingFields.HasFlag(HttpLoggingFields.RequestPath))
8487
{
8588
AddToList(list, nameof(request.PathBase), request.PathBase);
8689
AddToList(list, nameof(request.Path), request.Path);
8790
}
8891

89-
if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestQuery))
92+
if (loggingFields.HasFlag(HttpLoggingFields.RequestQuery))
9093
{
9194
AddToList(list, nameof(request.QueryString), request.QueryString.Value);
9295
}
9396

94-
if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestHeaders))
97+
if (loggingFields.HasFlag(HttpLoggingFields.RequestHeaders))
9598
{
9699
FilterHeaders(list, request.Headers, options._internalRequestHeaders);
97100
}
98101

99-
if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestBody))
102+
if (loggingFields.HasFlag(HttpLoggingFields.RequestBody))
100103
{
101104
if (request.ContentType is null)
102105
{
@@ -106,10 +109,16 @@ private async Task InvokeInternal(HttpContext context)
106109
options.MediaTypeOptions.MediaTypeStates,
107110
out var encoding))
108111
{
112+
var requestBodyLogLimit = options.RequestBodyLogLimit;
113+
if (loggingAttribute?.RequestBodyLogLimit is int)
114+
{
115+
requestBodyLogLimit = loggingAttribute.RequestBodyLogLimit;
116+
}
117+
109118
originalBody = request.Body;
110119
requestBufferingStream = new RequestBufferingStream(
111120
request.Body,
112-
options.RequestBodyLogLimit,
121+
requestBodyLogLimit,
113122
_logger,
114123
encoding);
115124
request.Body = requestBufferingStream;
@@ -135,29 +144,36 @@ private async Task InvokeInternal(HttpContext context)
135144
{
136145
var response = context.Response;
137146

138-
if (options.LoggingFields.HasFlag(HttpLoggingFields.ResponseStatusCode) || options.LoggingFields.HasFlag(HttpLoggingFields.ResponseHeaders))
147+
if (loggingFields.HasFlag(HttpLoggingFields.ResponseStatusCode) || loggingFields.HasFlag(HttpLoggingFields.ResponseHeaders))
139148
{
140149
originalUpgradeFeature = context.Features.Get<IHttpUpgradeFeature>();
141150

142151
if (originalUpgradeFeature != null && originalUpgradeFeature.IsUpgradableRequest)
143152
{
144-
loggableUpgradeFeature = new UpgradeFeatureLoggingDecorator(originalUpgradeFeature, response, options, _logger);
153+
loggableUpgradeFeature = new UpgradeFeatureLoggingDecorator(originalUpgradeFeature, response, options._internalResponseHeaders, loggingFields, _logger);
145154

146155
context.Features.Set<IHttpUpgradeFeature>(loggableUpgradeFeature);
147156
}
148157
}
149158

150-
if (options.LoggingFields.HasFlag(HttpLoggingFields.ResponseBody))
159+
if (loggingFields.HasFlag(HttpLoggingFields.ResponseBody))
151160
{
152161
originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>()!;
153162

163+
var responseBodyLogLimit = options.ResponseBodyLogLimit;
164+
if (loggingAttribute?.ResponseBodyLogLimit is int)
165+
{
166+
responseBodyLogLimit = loggingAttribute.ResponseBodyLogLimit;
167+
}
168+
154169
// TODO pool these.
155170
responseBufferingStream = new ResponseBufferingStream(originalBodyFeature,
156-
options.ResponseBodyLogLimit,
171+
responseBodyLogLimit,
157172
_logger,
158173
context,
159174
options.MediaTypeOptions.MediaTypeStates,
160-
options);
175+
options._internalResponseHeaders,
176+
loggingFields);
161177
response.Body = responseBufferingStream;
162178
context.Features.Set<IHttpResponseBodyFeature>(responseBufferingStream);
163179
}
@@ -174,7 +190,7 @@ private async Task InvokeInternal(HttpContext context)
174190
if (ResponseHeadersNotYetWritten(responseBufferingStream, loggableUpgradeFeature))
175191
{
176192
// No body, not an upgradable request or request not upgraded, write headers here.
177-
LogResponseHeaders(response, options, _logger);
193+
LogResponseHeaders(response, loggingFields, options._internalResponseHeaders, _logger);
178194
}
179195

180196
if (responseBufferingStream != null)
@@ -216,7 +232,7 @@ private static bool ResponseHeadersNotYetWritten(ResponseBufferingStream? respon
216232

217233
private static bool BodyNotYetWritten(ResponseBufferingStream? responseBufferingStream)
218234
{
219-
return responseBufferingStream == null || responseBufferingStream.FirstWrite == false;
235+
return responseBufferingStream == null || responseBufferingStream.HeadersWritten == false;
220236
}
221237

222238
private static bool NotUpgradeableRequestOrRequestNotUpgraded(UpgradeFeatureLoggingDecorator? upgradeFeatureLogging)
@@ -229,19 +245,19 @@ private static void AddToList(List<KeyValuePair<string, object?>> list, string k
229245
list.Add(new KeyValuePair<string, object?>(key, value));
230246
}
231247

232-
public static void LogResponseHeaders(HttpResponse response, HttpLoggingOptions options, ILogger logger)
248+
public static void LogResponseHeaders(HttpResponse response, HttpLoggingFields loggingFields, HashSet<string> allowedResponseHeaders, ILogger logger)
233249
{
234250
var list = new List<KeyValuePair<string, object?>>(
235251
response.Headers.Count + DefaultResponseFieldsMinusHeaders);
236252

237-
if (options.LoggingFields.HasFlag(HttpLoggingFields.ResponseStatusCode))
253+
if (loggingFields.HasFlag(HttpLoggingFields.ResponseStatusCode))
238254
{
239255
list.Add(new KeyValuePair<string, object?>(nameof(response.StatusCode), response.StatusCode));
240256
}
241257

242-
if (options.LoggingFields.HasFlag(HttpLoggingFields.ResponseHeaders))
258+
if (loggingFields.HasFlag(HttpLoggingFields.ResponseHeaders))
243259
{
244-
FilterHeaders(list, response.Headers, options._internalResponseHeaders);
260+
FilterHeaders(list, response.Headers, allowedResponseHeaders);
245261
}
246262

247263
if (list.Count > 0)
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Builder.HttpLoggingEndpointConventionBuilderExtensions
3+
Microsoft.AspNetCore.HttpLogging.HttpLoggingAttribute
4+
Microsoft.AspNetCore.HttpLogging.HttpLoggingAttribute.HttpLoggingAttribute(Microsoft.AspNetCore.HttpLogging.HttpLoggingFields loggingFields, int requestBodyLogLimit = -1, int responseBodyLogLimit = -1) -> void
5+
Microsoft.AspNetCore.HttpLogging.HttpLoggingAttribute.LoggingFields.get -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
6+
Microsoft.AspNetCore.HttpLogging.HttpLoggingAttribute.RequestBodyLogLimit.get -> int
7+
Microsoft.AspNetCore.HttpLogging.HttpLoggingAttribute.ResponseBodyLogLimit.get -> int
8+
static Microsoft.AspNetCore.Builder.HttpLoggingEndpointConventionBuilderExtensions.WithHttpLogging<TBuilder>(this TBuilder builder, Microsoft.AspNetCore.HttpLogging.HttpLoggingFields loggingFields, int requestBodyLogLimit = -1, int responseBodyLogLimit = -1) -> TBuilder

src/Middleware/HttpLogging/src/ResponseBufferingStream.cs

+22-31
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ internal sealed class ResponseBufferingStream : BufferingStream, IHttpResponseBo
2222

2323
private readonly HttpContext _context;
2424
private readonly List<MediaTypeState> _encodings;
25-
private readonly HttpLoggingOptions _options;
25+
private readonly HashSet<string> _allowedResponseHeaders;
26+
private readonly HttpLoggingFields _loggingFields;
2627
private Encoding? _encoding;
2728

2829
private static readonly StreamPipeWriterOptions _pipeWriterOptions = new StreamPipeWriterOptions(leaveOpen: true);
@@ -32,18 +33,20 @@ internal ResponseBufferingStream(IHttpResponseBodyFeature innerBodyFeature,
3233
ILogger logger,
3334
HttpContext context,
3435
List<MediaTypeState> encodings,
35-
HttpLoggingOptions options)
36+
HashSet<string> allowedResponseHeaders,
37+
HttpLoggingFields loggingFields)
3638
: base(innerBodyFeature.Stream, logger)
3739
{
3840
_innerBodyFeature = innerBodyFeature;
3941
_innerStream = innerBodyFeature.Stream;
4042
_limit = limit;
4143
_context = context;
4244
_encodings = encodings;
43-
_options = options;
45+
_allowedResponseHeaders = allowedResponseHeaders;
46+
_loggingFields = loggingFields;
4447
}
4548

46-
public bool FirstWrite { get; private set; }
49+
public bool HeadersWritten { get; private set; }
4750

4851
public Stream Stream => this;
4952

@@ -68,24 +71,7 @@ public override void EndWrite(IAsyncResult asyncResult)
6871

6972
public override void Write(ReadOnlySpan<byte> span)
7073
{
71-
var remaining = _limit - _bytesBuffered;
72-
var innerCount = Math.Min(remaining, span.Length);
73-
74-
OnFirstWrite();
75-
76-
if (innerCount > 0)
77-
{
78-
if (span.Slice(0, innerCount).TryCopyTo(_tailMemory.Span))
79-
{
80-
_tailBytesBuffered += innerCount;
81-
_bytesBuffered += innerCount;
82-
_tailMemory = _tailMemory.Slice(innerCount);
83-
}
84-
else
85-
{
86-
BuffersExtensions.Write(this, span.Slice(0, innerCount));
87-
}
88-
}
74+
CommonWrite(span);
8975

9076
_innerStream.Write(span);
9177
}
@@ -96,39 +82,44 @@ public override async Task WriteAsync(byte[] buffer, int offset, int count, Canc
9682
}
9783

9884
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
85+
{
86+
CommonWrite(buffer.Span);
87+
88+
await _innerStream.WriteAsync(buffer, cancellationToken);
89+
}
90+
91+
private void CommonWrite(ReadOnlySpan<byte> span)
9992
{
10093
var remaining = _limit - _bytesBuffered;
101-
var innerCount = Math.Min(remaining, buffer.Length);
94+
var innerCount = Math.Min(remaining, span.Length);
10295

10396
OnFirstWrite();
10497

10598
if (innerCount > 0)
10699
{
107-
if (_tailMemory.Length - innerCount > 0)
100+
var slice = span.Slice(0, innerCount);
101+
if (slice.TryCopyTo(_tailMemory.Span))
108102
{
109-
buffer.Slice(0, innerCount).CopyTo(_tailMemory);
110103
_tailBytesBuffered += innerCount;
111104
_bytesBuffered += innerCount;
112105
_tailMemory = _tailMemory.Slice(innerCount);
113106
}
114107
else
115108
{
116-
BuffersExtensions.Write(this, buffer.Span);
109+
BuffersExtensions.Write(this, slice);
117110
}
118111
}
119-
120-
await _innerStream.WriteAsync(buffer, cancellationToken);
121112
}
122113

123114
private void OnFirstWrite()
124115
{
125-
if (!FirstWrite)
116+
if (!HeadersWritten)
126117
{
127118
// Log headers as first write occurs (headers locked now)
128-
HttpLoggingMiddleware.LogResponseHeaders(_context.Response, _options, _logger);
119+
HttpLoggingMiddleware.LogResponseHeaders(_context.Response, _loggingFields, _allowedResponseHeaders, _logger);
129120

130121
MediaTypeHelpers.TryGetEncodingForMediaType(_context.Response.ContentType, _encodings, out _encoding);
131-
FirstWrite = true;
122+
HeadersWritten = true;
132123
}
133124
}
134125

src/Middleware/HttpLogging/src/UpgradeFeatureLoggingDecorator.cs

+6-4
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,19 @@ internal sealed class UpgradeFeatureLoggingDecorator : IHttpUpgradeFeature
1111
{
1212
private readonly IHttpUpgradeFeature _innerUpgradeFeature;
1313
private readonly HttpResponse _response;
14-
private readonly HttpLoggingOptions _options;
14+
private readonly HashSet<string> _allowedResponseHeaders;
1515
private readonly ILogger _logger;
16+
private readonly HttpLoggingFields _loggingFields;
1617

1718
private bool _isUpgraded;
1819

19-
public UpgradeFeatureLoggingDecorator(IHttpUpgradeFeature innerUpgradeFeature, HttpResponse response, HttpLoggingOptions options, ILogger logger)
20+
public UpgradeFeatureLoggingDecorator(IHttpUpgradeFeature innerUpgradeFeature, HttpResponse response, HashSet<string> allowedResponseHeaders, HttpLoggingFields loggingFields, ILogger logger)
2021
{
2122
_innerUpgradeFeature = innerUpgradeFeature ?? throw new ArgumentNullException(nameof(innerUpgradeFeature));
2223
_response = response ?? throw new ArgumentNullException(nameof(response));
23-
_options = options ?? throw new ArgumentNullException(nameof(options));
24+
_allowedResponseHeaders = allowedResponseHeaders ?? throw new ArgumentNullException(nameof(allowedResponseHeaders));
2425
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
26+
_loggingFields = loggingFields;
2527
}
2628

2729
public bool IsUpgradableRequest => _innerUpgradeFeature.IsUpgradableRequest;
@@ -34,7 +36,7 @@ public async Task<Stream> UpgradeAsync()
3436

3537
_isUpgraded = true;
3638

37-
HttpLoggingMiddleware.LogResponseHeaders(_response, _options, _logger);
39+
HttpLoggingMiddleware.LogResponseHeaders(_response, _loggingFields, _allowedResponseHeaders, _logger);
3840

3941
return upgradeStream;
4042
}

0 commit comments

Comments
 (0)