File: Logging\HttpLoggingRedactionInterceptor.cs
Web Access
Project: src\src\Libraries\Microsoft.AspNetCore.Diagnostics.Middleware\Microsoft.AspNetCore.Diagnostics.Middleware.csproj (Microsoft.AspNetCore.Diagnostics.Middleware)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#if NET8_0_OR_GREATER
 
using System;
using System.Buffers;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpLogging;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Compliance.Classification;
using Microsoft.Extensions.Compliance.Redaction;
using Microsoft.Extensions.Http.Diagnostics;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
 
namespace Microsoft.AspNetCore.Diagnostics.Logging;
 
internal sealed class HttpLoggingRedactionInterceptor : IHttpLoggingInterceptor
{
    private readonly IHttpLogEnricher[] _enrichers;
    private readonly IncomingPathLoggingMode _requestPathLogMode;
    private readonly HttpRouteParameterRedactionMode _parameterRedactionMode;
    private readonly ILogger<HttpLoggingRedactionInterceptor> _logger;
    private readonly IHttpRouteParser _httpRouteParser;
    private readonly IHttpRouteFormatter _httpRouteFormatter;
    private readonly IIncomingHttpRouteUtility _httpRouteUtility;
    private readonly HeaderReader _requestHeadersReader;
    private readonly HeaderReader _responseHeadersReader;
    private readonly string[] _excludePathStartsWith;
    private readonly FrozenDictionary<string, DataClassification> _parametersToRedactMap;
 
    public HttpLoggingRedactionInterceptor(
        IOptions<LoggingRedactionOptions> options,
        ILogger<HttpLoggingRedactionInterceptor> logger,
        IEnumerable<IHttpLogEnricher> httpLogEnrichers,
        IHttpRouteParser httpRouteParser,
        IHttpRouteFormatter httpRouteFormatter,
        IRedactorProvider redactorProvider,
        IIncomingHttpRouteUtility httpRouteUtility)
    {
        var optionsValue = options.Value;
        _logger = logger;
        _enrichers = httpLogEnrichers.ToArray();
        _httpRouteParser = httpRouteParser;
        _httpRouteFormatter = httpRouteFormatter;
        _httpRouteUtility = httpRouteUtility;
 
        _parametersToRedactMap = optionsValue.RouteParameterDataClasses.ToFrozenDictionary(StringComparer.Ordinal);
 
        _requestPathLogMode = EnsureRequestPathLoggingModeIsValid(optionsValue.RequestPathLoggingMode);
        _parameterRedactionMode = optionsValue.RequestPathParameterRedactionMode;
 
        _requestHeadersReader = new(optionsValue.RequestHeadersDataClasses, redactorProvider, HttpLoggingTagNames.RequestHeaderPrefix);
        _responseHeadersReader = new(optionsValue.ResponseHeadersDataClasses, redactorProvider, HttpLoggingTagNames.ResponseHeaderPrefix);
 
        _excludePathStartsWith = optionsValue.ExcludePathStartsWith.ToArray();
    }
 
    public ValueTask OnRequestAsync(HttpLoggingInterceptorContext logContext)
    {
        var context = logContext.HttpContext;
        var request = context.Request;
        if (_excludePathStartsWith.Length != 0 && ShouldExcludePath(context.Request.Path.Value!))
        {
            logContext.LoggingFields = HttpLoggingFields.None;
            return default;
        }
 
        // Don't redact if we're not going to log any part of the request
        if (!logContext.IsAnyEnabled(HttpLoggingFields.RequestPropertiesAndHeaders))
        {
            return default;
        }
 
        // Always included, redaction will filter it out of the headers by default.
        logContext.AddParameter(HttpLoggingTagNames.Host, context.Request.Host.Value);
 
        if (logContext.TryDisable(HttpLoggingFields.RequestPath))
        {
            string path = TelemetryConstants.Unknown;
 
            if (_parameterRedactionMode != HttpRouteParameterRedactionMode.None)
            {
                var endpoint = context.GetEndpoint() as RouteEndpoint;
 
                if (endpoint?.RoutePattern.RawText != null)
                {
                    var httpRoute = endpoint.RoutePattern.RawText;
                    var paramsToRedact = _httpRouteUtility.GetSensitiveParameters(httpRoute, request, _parametersToRedactMap);
 
                    var routeSegments = _httpRouteParser.ParseRoute(httpRoute);
 
                    if (_requestPathLogMode == IncomingPathLoggingMode.Formatted)
                    {
                        path = _httpRouteFormatter.Format(in routeSegments, request.Path, _parameterRedactionMode, paramsToRedact);
                    }
                    else
                    {
                        // Case when logging mode is IncomingPathLoggingMode.Structured
                        path = httpRoute;
                        var routeParams = ArrayPool<HttpRouteParameter>.Shared.Rent(routeSegments.ParameterCount);
 
                        // Setting this value right away to be able to return it back to pool in a callee's "finally" block:
                        if (_httpRouteParser.TryExtractParameters(request.Path, in routeSegments, _parameterRedactionMode, paramsToRedact, ref routeParams))
                        {
                            for (var i = 0; i < routeSegments.ParameterCount; i++)
                            {
                                logContext.AddParameter(routeParams[i].Name, routeParams[i].Value);
                            }
                        }
                    }
                }
            }
            else if (request.Path.HasValue)
            {
                path = request.Path.Value!;
            }
 
            logContext.AddParameter(nameof(request.Path), path);
        }
 
        if (logContext.TryDisable(HttpLoggingFields.RequestHeaders))
        {
            _requestHeadersReader.Read(context.Request.Headers, logContext.Parameters);
        }
 
        return default;
    }
 
    public ValueTask OnResponseAsync(HttpLoggingInterceptorContext logContext)
    {
        var context = logContext.HttpContext;
 
        if (logContext.TryDisable(HttpLoggingFields.ResponseHeaders))
        {
            _responseHeadersReader.Read(context.Response.Headers, logContext.Parameters);
        }
 
        // Don't enrich if we're not going to log any part of the response
        if (_enrichers.Length == 0
            || (!logContext.IsAnyEnabled(HttpLoggingFields.Response) && logContext.Parameters.Count == 0))
        {
            return default;
        }
 
        var loggerMessageState = LoggerMessageHelper.ThreadLocalState;
 
        try
        {
            foreach (var enricher in _enrichers)
            {
#pragma warning disable CA1031 // Do not catch general exception types
                try
                {
                    enricher.Enrich(loggerMessageState, context);
                }
                catch (Exception ex)
                {
                    _logger.EnricherFailed(ex, enricher.GetType().Name);
                }
#pragma warning restore CA1031 // Do not catch general exception types
            }
 
            foreach (var pair in loggerMessageState)
            {
                logContext.Parameters.Add(pair);
            }
        }
        finally
        {
            loggerMessageState.Clear();
        }
 
        return default;
    }
 
    private static IncomingPathLoggingMode EnsureRequestPathLoggingModeIsValid(IncomingPathLoggingMode mode)
        => mode switch
        {
            IncomingPathLoggingMode.Structured or IncomingPathLoggingMode.Formatted => mode,
            _ => throw new InvalidOperationException($"Unsupported value '{mode}' for enum type '{nameof(IncomingPathLoggingMode)}'"),
        };
 
    private bool ShouldExcludePath(string path)
    {
        foreach (var excludedPath in _excludePathStartsWith)
        {
            if (path.StartsWith(excludedPath, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }
        }
 
        return false;
    }
}
 
#endif