Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proof of concept for extending http logging #48679

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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 HttpLogging.Sample;

internal class SampleHttpLoggingInterceptor : IHttpLoggingInterceptor
{
public void OnRequest(HttpLoggingContext logContext)
{
// Compare to ExcludePathStartsWith
if (!logContext.HttpContext.Request.Path.StartsWithSegments("/api"))
{
logContext.LoggingFields = HttpLoggingFields.None;
}

// Don't enrich if we're not going to log any part of the request
if (!logContext.IsAnyEnabled(HttpLoggingFields.Request))
{
return;
}

if (logContext.TryOverride(HttpLoggingFields.RequestPath))
{
RedactPath(logContext);
}

if (logContext.TryOverride(HttpLoggingFields.RequestHeaders))
{
RedactRequestHeaders(logContext);
}

EnrichRequest(logContext);
}

private void RedactRequestHeaders(HttpLoggingContext logContext)
{
foreach (var header in logContext.HttpContext.Request.Headers)
{
logContext.Add(header.Key, "RedactedHeader"); // TODO: Redact header value
}
}

private void RedactResponseHeaders(HttpLoggingContext logContext)
{
foreach (var header in logContext.HttpContext.Response.Headers)
{
logContext.Add(header.Key, "RedactedHeader"); // TODO: Redact header value
}
}

public void OnResponse(HttpLoggingContext logContext)
{
// Don't enrich if we're not going to log any part of the response
if (!logContext.IsAnyEnabled(HttpLoggingFields.Response))
{
return;
}

if (logContext.TryOverride(HttpLoggingFields.ResponseHeaders))
{
RedactResponseHeaders(logContext);
}

EnrichResponse(logContext);
}

private void EnrichResponse(HttpLoggingContext logContext)
{
logContext.Add("ResponseEnrichment", "Stuff");
}

private void EnrichRequest(HttpLoggingContext logContext)
{
logContext.Add("RequestEnrichment", "Stuff");
}

private void RedactPath(HttpLoggingContext logContext)
{
logContext.Add(nameof(logContext.HttpContext.Request.Path), "RedactedPath");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public void ConfigureServices(IServiceCollection services)
{
logging.LoggingFields = HttpLoggingFields.All;
});
services.AddSingleton<IHttpLoggingInterceptor, SampleHttpLoggingInterceptor>();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
Expand Down
9 changes: 1 addition & 8 deletions src/Middleware/HttpLogging/src/BufferingStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public override int WriteTimeout
set => _innerStream.WriteTimeout = value;
}

public string GetString(Encoding? encoding)
public string GetString(Encoding encoding)
{
try
{
Expand All @@ -57,13 +57,6 @@ public string GetString(Encoding? encoding)
return "";
}

if (encoding == null)
{
// This method is used only for the response body
_logger.UnrecognizedMediaType("response");
return "";
}

// Only place where we are actually using the buffered data.
// update tail here.
_tail.End = _tailBytesBuffered;
Expand Down
111 changes: 111 additions & 0 deletions src/Middleware/HttpLogging/src/HttpLoggingContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// 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.Http;
using Microsoft.Extensions.ObjectPool;

namespace Microsoft.AspNetCore.HttpLogging;

/// <summary>
/// The context used for logging customization callbacks.
/// </summary>
public sealed class HttpLoggingContext : IResettable
{
private HttpContext? _httpContext;

/// <summary>
/// The request context.
/// </summary>
// We'd make this a required constructor parameter but ObjectPool requires a parameterless constructor.
public HttpContext HttpContext
{
get => _httpContext ?? throw new InvalidOperationException("HttpContext was not initialized");
set => _httpContext = value ?? throw new ArgumentNullException(nameof(value));
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// What parts of the request and response to log.
/// </summary>
public HttpLoggingFields LoggingFields { get; set; }

/// <summary>
/// Limits how much of the request body to log.
/// </summary>
public int RequestBodyLogLimit { get; set; }

/// <summary>
/// Limits how much of the response body to log.
/// </summary>
public int ResponseBodyLogLimit { get; set; }
Tratcher marked this conversation as resolved.
Show resolved Hide resolved

internal long StartTimestamp { get; set; }

/// <summary>
/// The parameters to log.
/// </summary>
public IList<KeyValuePair<string, object?>> Parameters { get; } = new List<KeyValuePair<string, object?>>();

/// <summary>
/// Adds a parameter to the log context.
/// </summary>
/// <param name="key">The parameter name.</param>
/// <param name="value">The parameter value.</param>
public void Add(string key, object? value)
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
{
Parameters.Add(new(key, value));
}

/// <summary>
/// Adds the given fields to what's currently enabled in <see cref="LoggingFields"/>.
/// </summary>
/// <param name="fields"></param>
public void Enable(HttpLoggingFields fields)
{
LoggingFields |= fields;
}

/// <summary>
/// Checks if any of the given fields are currently enabled in <see cref="LoggingFields"/>.
/// </summary>
public bool IsAnyEnabled(HttpLoggingFields fields)
{
return (LoggingFields & fields) != HttpLoggingFields.None;
}

/// <summary>
/// Removes the given fields from what's currently enabled in <see cref="LoggingFields"/>.
/// </summary>
/// <param name="fields"></param>
public void Disable(HttpLoggingFields fields)
{
LoggingFields &= ~fields;
}

/// <summary>
/// Checks if the given field is currently enabled in <see cref="LoggingFields"/>
/// and disables it so that a custom log value can be provided instead.
/// </summary>
/// <param name="field">A single field flag to check.</param>
/// <returns>`true` if the field was enabled.</returns>
public bool TryOverride(HttpLoggingFields field)
{
if (LoggingFields.HasFlag(field))
{
Disable(field);
return true;
}

return false;
}

bool IResettable.TryReset()
{
_httpContext = null;
LoggingFields = HttpLoggingFields.None;
RequestBodyLogLimit = 0;
ResponseBodyLogLimit = 0;
StartTimestamp = 0;
Parameters.Clear();
return true;
}
}
10 changes: 8 additions & 2 deletions src/Middleware/HttpLogging/src/HttpLoggingFields.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ public enum HttpLoggingFields : long
/// </summary>
ResponseBody = 0x800,

/// <summary>
/// Log how long it took to process the request and return a response in total milliseconds.
/// </summary>
// TODO: This does not include the time taken to write the response body.
Duration = 0x1000,
Tratcher marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Flag for logging a collection of HTTP Request properties,
/// including <see cref="RequestPath"/>, <see cref="RequestProtocol"/>,
Expand All @@ -160,7 +166,7 @@ public enum HttpLoggingFields : long
/// Flag for logging HTTP Response properties and headers.
/// Includes <see cref="ResponseStatusCode"/> and <see cref="ResponseHeaders"/>
/// </summary>
ResponsePropertiesAndHeaders = ResponseStatusCode | ResponseHeaders,
ResponsePropertiesAndHeaders = ResponseStatusCode | ResponseHeaders | Duration,

/// <summary>
/// Flag for logging the entire HTTP Request.
Expand All @@ -180,7 +186,7 @@ public enum HttpLoggingFields : long
/// Logging the response body has performance implications, as it requires buffering
/// the entire response body up to <see cref="HttpLoggingOptions.ResponseBodyLogLimit"/>.
/// </summary>
Response = ResponseStatusCode | ResponseHeaders | ResponseBody,
Response = ResponseStatusCode | ResponseHeaders | ResponseBody | Duration,

/// <summary>
/// Flag for logging both the HTTP Request and Response.
Expand Down
Loading