File: Logging\Internal\Log.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.Http.Diagnostics\Microsoft.Extensions.Http.Diagnostics.csproj (Microsoft.Extensions.Http.Diagnostics)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.Extensions.Http.Logging.Internal;
 
/// <summary>
/// Logs <see cref="HttpRequestMessage"/>, <see cref="HttpResponseMessage"/> and the exceptions due to errors of request/response.
/// </summary>
[SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", Justification = "Event ID's.")]
internal static partial class Log
{
    internal const string OriginalFormat = "{OriginalFormat}";
 
    private const int MinimalPropertyCount = 4;
 
    private const string RequestReadErrorMessage =
        "An error occurred while reading the request data to fill the logger context for request: " +
        $"{{{HttpClientLoggingTagNames.Method}}} {{{HttpClientLoggingTagNames.Host}}}/{{{HttpClientLoggingTagNames.Path}}}";
 
    private const string ResponseReadErrorMessage =
        "An error occurred while reading the response data to fill the logger context for request: " +
        $"{{{HttpClientLoggingTagNames.Method}}} {{{HttpClientLoggingTagNames.Host}}}/{{{HttpClientLoggingTagNames.Path}}}";
 
    private const string LoggerContextMissingMessage =
        $"The logger couldn't read its context for {{RequestState}} request: {{{HttpClientLoggingTagNames.Method}}} {{{HttpClientLoggingTagNames.Host}}}";
 
    private const string EnrichmentErrorMessage =
        "An error occurred in enricher '{Enricher}' while enriching the logger context for request: " +
        $"{{{HttpClientLoggingTagNames.Method}}} {{{HttpClientLoggingTagNames.Host}}}/{{{HttpClientLoggingTagNames.Path}}}";
 
    private static readonly Func<LoggerMessageState, Exception?, string> _originalFormatValueFmtFunc = OriginalFormatValueFmt;
 
    public static void OutgoingRequest(ILogger logger, LogLevel level, LogRecord record)
    {
        OutgoingRequest(logger, level, 1, nameof(OutgoingRequest), record);
    }
 
    public static void OutgoingRequestError(ILogger logger, LogRecord record, Exception exception)
    {
        OutgoingRequest(logger, LogLevel.Error, 2, nameof(OutgoingRequestError), record, exception);
    }
 
    [LoggerMessage(LogLevel.Error, RequestReadErrorMessage)]
    public static partial void RequestReadError(
        ILogger logger,
        Exception exception,
        [TagName(HttpClientLoggingTagNames.Method)] HttpMethod method,
        [TagName(HttpClientLoggingTagNames.Host)] string? host,
        [TagName(HttpClientLoggingTagNames.Path)] string? path);
 
    [LoggerMessage(LogLevel.Error, ResponseReadErrorMessage)]
    public static partial void ResponseReadError(
        ILogger logger,
        Exception exception,
        [TagName(HttpClientLoggingTagNames.Method)] HttpMethod method,
        [TagName(HttpClientLoggingTagNames.Host)] string host,
        [TagName(HttpClientLoggingTagNames.Path)] string path);
 
    [LoggerMessage(LogLevel.Error, LoggerContextMissingMessage)]
    public static partial void LoggerContextMissing(
        ILogger logger,
        Exception? exception,
        string requestState,
        [TagName(HttpClientLoggingTagNames.Method)] HttpMethod method,
        [TagName(HttpClientLoggingTagNames.Host)] string? host);
 
    [LoggerMessage(LogLevel.Error, EnrichmentErrorMessage)]
    public static partial void EnrichmentError(
        ILogger logger,
        Exception exception,
        string? enricher,
        [TagName(HttpClientLoggingTagNames.Method)] HttpMethod method,
        [TagName(HttpClientLoggingTagNames.Host)] string host,
        [TagName(HttpClientLoggingTagNames.Path)] string path);
 
    // Using the code below instead of generated logging method because we have a custom formatter and custom tag keys for headers.
    private static void OutgoingRequest(
        ILogger logger, LogLevel level, int eventId, string eventName, LogRecord record, Exception? exception = null)
    {
        if (!logger.IsEnabled(level))
        {
            return;
        }
 
        // EnrichmentTags is null when we log request's start:
        var loggerMessageState = record.EnrichmentTags ?? LoggerMessageHelper.ThreadLocalState;
 
        var statusCodePropertyCount = record.StatusCode.HasValue ? 1 : 0;
        var requestHeadersCount = record.RequestHeaders?.Count ?? 0;
        var responseHeadersCount = record.ResponseHeaders?.Count ?? 0;
 
        var spaceToReserve = MinimalPropertyCount + statusCodePropertyCount + requestHeadersCount + responseHeadersCount +
            record.PathParametersCount + (record.RequestBody is null ? 0 : 1) + (record.ResponseBody is null ? 0 : 1);
 
        var index = loggerMessageState.ReserveTagSpace(spaceToReserve);
        loggerMessageState.TagArray[index++] = new(HttpClientLoggingTagNames.Method, record.Method);
        loggerMessageState.TagArray[index++] = new(HttpClientLoggingTagNames.Host, record.Host);
        loggerMessageState.TagArray[index++] = new(HttpClientLoggingTagNames.Path, record.Path);
        loggerMessageState.TagArray[index++] = new(HttpClientLoggingTagNames.Duration, record.Duration);
 
        if (record.StatusCode.HasValue)
        {
            loggerMessageState.TagArray[index++] = new(HttpClientLoggingTagNames.StatusCode, record.StatusCode.Value);
        }
 
        if (requestHeadersCount > 0)
        {
            loggerMessageState.AddRequestHeaders(record.RequestHeaders!, ref index);
        }
 
        if (responseHeadersCount > 0)
        {
            loggerMessageState.AddResponseHeaders(record.ResponseHeaders!, ref index);
        }
 
        if (record.PathParameters is not null)
        {
            loggerMessageState.AddPathParameters(record.PathParameters, record.PathParametersCount, ref index);
        }
 
        if (record.RequestBody is not null)
        {
            loggerMessageState.TagArray[index++] = new(HttpClientLoggingTagNames.RequestBody, record.RequestBody);
        }
 
        if (record.ResponseBody is not null)
        {
            loggerMessageState.TagArray[index] = new(HttpClientLoggingTagNames.ResponseBody, record.ResponseBody);
        }
 
        logger.Log(
            level,
            new(eventId, eventName),
            loggerMessageState,
            exception,
            _originalFormatValueFmtFunc);
 
        if (record.EnrichmentTags is null)
        {
            loggerMessageState.Clear();
        }
    }
 
    private static string OriginalFormatValueFmt(LoggerMessageState request, Exception? _)
    {
        int startIndex = FindStartIndex(request);
        var httpMethod = request[startIndex].Value;
        var httpHost = request[startIndex + 1].Value;
        var httpPath = request[startIndex + 2].Value;
        return FormattableString.Invariant($"{httpMethod} {httpHost}/{httpPath}");
    }
 
    private static int FindStartIndex(LoggerMessageState request)
    {
        int startIndex = 0;
 
        foreach (var kvp in request)
        {
            if (kvp.Key == HttpClientLoggingTagNames.Method)
            {
                break;
            }
 
            startIndex++;
        }
 
        return startIndex;
    }
}