File: Latency\RequestLatencyTelemetryMiddleware.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.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.AmbientMetadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.Latency;
using Microsoft.Extensions.Http.Diagnostics;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Microsoft.Shared.Diagnostics;
using Microsoft.Shared.Pools;
 
namespace Microsoft.AspNetCore.Diagnostics.Latency;
 
/// <summary>
/// Middleware that manages latency context for requests.
/// </summary>
internal sealed class RequestLatencyTelemetryMiddleware
{
    private static readonly ObjectPool<List<Task>> _exporterTaskPool = PoolFactory.CreateListPool<Task>();
    private static readonly ObjectPool<CancellationTokenSource> _cancellationTokenSourcePool = PoolFactory.CreateCancellationTokenSourcePool();
 
    private readonly TimeSpan _exportTimeout;
    private readonly string _applicationName;
    private readonly ILatencyDataExporter[] _latencyDataExporters;
    private readonly RequestDelegate _next;
 
    public RequestLatencyTelemetryMiddleware(
        RequestDelegate next,
        IOptions<RequestLatencyTelemetryOptions> options,
        IEnumerable<ILatencyDataExporter> latencyDataExporters,
        IOptions<ApplicationMetadata>? appMetdata = null)
    {
        _exportTimeout = options.Value.LatencyDataExportTimeout;
        _latencyDataExporters = latencyDataExporters.ToArray();
        _applicationName = string.Empty;
        _next = Throw.IfNull(next);
 
        if (appMetdata != null)
        {
            _applicationName = appMetdata.Value.ApplicationName;
        }
    }
 
    /// <summary>
    /// Request handling method.
    /// </summary>
    /// <param name="context">The <see cref="HttpContext"/> for the current request.</param>
    /// <returns>A <see cref="Task"/> that represents the execution of this middleware.</returns>
    public Task InvokeAsync(HttpContext context)
    {
        var latencyContext = context.RequestServices.GetRequiredService<ILatencyContext>();
 
        if (!string.IsNullOrEmpty(_applicationName))
        {
            context.Response.OnStarting(ctx =>
            {
                var httpContext = (HttpContext)ctx;
 
                // Set server name header to the current one.
                httpContext.Response.Headers[TelemetryConstants.ServerApplicationNameHeader] = _applicationName;
 
                return Task.CompletedTask;
            }, context);
        }
 
        context.Response.OnCompleted(async l =>
        {
            var latencyContext = l as ILatencyContext;
            latencyContext!.Freeze();
            await ExportAsync(latencyContext.LatencyData).ConfigureAwait(false);
        }, latencyContext);
 
        return _next.Invoke(context);
    }
 
    [SuppressMessage("Resilience", "EA0014:The async method doesn't support cancellation", Justification = "The time limit is enforced inside of the method")]
    private async Task ExportAsync(LatencyData latencyData)
    {
        var tokenSource = _cancellationTokenSourcePool.Get();
        tokenSource.CancelAfter(_exportTimeout);
 
        List<Task> exports = _exporterTaskPool.Get();
        foreach (ILatencyDataExporter latencyDataExporter in _latencyDataExporters)
        {
            exports.Add(latencyDataExporter.ExportAsync(latencyData, tokenSource.Token));
        }
 
        await Task.WhenAll(exports).ConfigureAwait(false);
 
        _exporterTaskPool.Return(exports);
        _cancellationTokenSourcePool.Return(tokenSource);
    }
}