File: Latency\Internal\HttpLatencyLogEnricher.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.
 
using System;
using System.Text;
using Microsoft.AspNetCore.Diagnostics.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.Enrichment;
using Microsoft.Extensions.Diagnostics.Latency;
using Microsoft.Extensions.Http.Diagnostics;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Shared.Pools;
 
namespace Microsoft.AspNetCore.Diagnostics.Latency;
 
/// <summary>
/// Enriches incoming HTTP request logs with latency information captured in the request's <see cref="ILatencyContext"/>.
/// </summary>
internal sealed class HttpLatencyLogEnricher : IHttpLogEnricher
{
    internal const string DataVersion = "v1.0";
 
    private static readonly ObjectPool<StringBuilder> _builderPool = PoolFactory.SharedStringBuilderPool;
 
    public void Enrich(IEnrichmentTagCollector collector, HttpContext httpContext)
    {
        var latencyContext = httpContext.RequestServices.GetService<ILatencyContext>();
 
        if (latencyContext != null)
        {
            StringBuilder stringBuilder = _builderPool.Get();
            try
            {
                _ = stringBuilder.Append(DataVersion);
                _ = stringBuilder.Append(',');
                AppendClientName(httpContext.Request, stringBuilder);
                _ = stringBuilder.Append(',');
                FormatLatencyData(stringBuilder, latencyContext.LatencyData);
                collector.Add("LatencyInfo", stringBuilder.ToString());
            }
            finally
            {
                _builderPool.Return(stringBuilder);
            }
        }
    }
 
    private static void AppendClientName(HttpRequest request, StringBuilder stringBuilder)
    {
        if (request.Headers.TryGetValue(TelemetryConstants.ClientApplicationNameHeader, out var values))
        {
            _ = stringBuilder.Append(values[0]);
        }
    }
 
    private static void FormatLatencyData(StringBuilder sb, LatencyData latencyData)
    {
        const int MillisecondsPerSecond = 1000;
 
        // Append tags
        AppendSpanEscapingSlash(sb, latencyData.Tags, a => a.Name);
        _ = sb.Append(',');
        AppendSpanEscapingSlash(sb, latencyData.Tags, a => a.Value);
        _ = sb.Append(',');
 
        // Append checkpoints
        AppendSpanEscapingSlash(sb, latencyData.Checkpoints, a => a.Name);
        _ = sb.Append(',');
        AppendSpan(sb, latencyData.Checkpoints, a => (long)Math.Round(((double)a.Elapsed / a.Frequency) * MillisecondsPerSecond));
        _ = sb.Append(',');
 
        // Append measures
        AppendSpanEscapingSlash(sb, latencyData.Measures, a => a.Name);
        _ = sb.Append(',');
        AppendSpan(sb, latencyData.Measures, a => a.Value);
        _ = sb.Append(',');
 
        // Append duration
        _ = sb.Append((long)Math.Round(((double)latencyData.DurationTimestamp / latencyData.DurationTimestampFrequency) * MillisecondsPerSecond));
    }
 
    private static void AppendSpanEscapingSlash<TX>(StringBuilder sb, ReadOnlySpan<TX> span, Func<TX, string> select)
    {
        for (int i = 0; i < span.Length; i++)
        {
            var selectedValue = select(span[i]).AsSpan();
            for (int s = 0; s < selectedValue.Length; s++)
            {
                _ = sb.Append(selectedValue[s] == '/' ? '_' : selectedValue[s]);
            }
 
            _ = sb.Append('/');
        }
    }
 
    private static void AppendSpan<TX, TY>(StringBuilder sb, ReadOnlySpan<TX> span, Func<TX, TY> apply)
    {
        for (int i = 0; i < span.Length; i++)
        {
            _ = sb.Append(apply(span[i]));
            _ = sb.Append('/');
        }
    }
}