File: RateLimitingMetrics.cs
Web Access
Project: src\src\Middleware\RateLimiting\src\Microsoft.AspNetCore.RateLimiting.csproj (Microsoft.AspNetCore.RateLimiting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Http;
 
namespace Microsoft.AspNetCore.RateLimiting;
 
internal sealed class RateLimitingMetrics : IDisposable
{
    public const string MeterName = "Microsoft.AspNetCore.RateLimiting";
 
    private readonly Meter _meter;
    private readonly UpDownCounter<long> _activeRequestLeasesCounter;
    private readonly Histogram<double> _requestLeaseDurationCounter;
    private readonly UpDownCounter<long> _queuedRequestsCounter;
    private readonly Histogram<double> _queuedRequestDurationCounter;
    private readonly Counter<long> _requestsCounter;
 
    public RateLimitingMetrics(IMeterFactory meterFactory)
    {
        _meter = meterFactory.Create(MeterName);
 
        _activeRequestLeasesCounter = _meter.CreateUpDownCounter<long>(
            "aspnetcore.rate_limiting.active_request_leases",
            unit: "{request}",
            description: "Number of HTTP requests that are currently active on the server that hold a rate limiting lease.");
 
        _requestLeaseDurationCounter = _meter.CreateHistogram<double>(
            "aspnetcore.rate_limiting.request_lease.duration",
            unit: "s",
            description: "The duration of rate limiting leases held by HTTP requests on the server.",
            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
 
        _queuedRequestsCounter = _meter.CreateUpDownCounter<long>(
            "aspnetcore.rate_limiting.queued_requests",
            unit: "{request}",
            description: "Number of HTTP requests that are currently queued, waiting to acquire a rate limiting lease.");
 
        _queuedRequestDurationCounter = _meter.CreateHistogram<double>(
            "aspnetcore.rate_limiting.request.time_in_queue",
            unit: "s",
            description: "The duration of HTTP requests in a queue, waiting to acquire a rate limiting lease.",
            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
 
        _requestsCounter = _meter.CreateCounter<long>(
            "aspnetcore.rate_limiting.requests",
            unit: "{request}",
            description: "Number of requests that tried to acquire a rate limiting lease. Requests could be rejected by global or endpoint rate limiting policies. Or the request could be canceled while waiting for the lease.");
    }
 
    public void LeaseFailed(in MetricsContext metricsContext, RequestRejectionReason reason)
    {
        if (_requestsCounter.Enabled)
        {
            LeaseFailedCore(metricsContext, reason);
        }
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private void LeaseFailedCore(in MetricsContext metricsContext, RequestRejectionReason reason)
    {
        var tags = new TagList();
        InitializeRateLimitingTags(ref tags, metricsContext);
        tags.Add("aspnetcore.rate_limiting.result", GetResult(reason));
        _requestsCounter.Add(1, tags);
    }
 
    public void LeaseStart(in MetricsContext metricsContext)
    {
        if (metricsContext.CurrentLeasedRequestsCounterEnabled)
        {
            LeaseStartCore(metricsContext);
        }
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    public void LeaseStartCore(in MetricsContext metricsContext)
    {
        var tags = new TagList();
        InitializeRateLimitingTags(ref tags, metricsContext);
        _activeRequestLeasesCounter.Add(1, tags);
    }
 
    public void LeaseEnd(in MetricsContext metricsContext, long startTimestamp, long currentTimestamp)
    {
        if (metricsContext.CurrentLeasedRequestsCounterEnabled || _requestLeaseDurationCounter.Enabled || _requestsCounter.Enabled)
        {
            LeaseEndCore(metricsContext, startTimestamp, currentTimestamp);
        }
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private void LeaseEndCore(in MetricsContext metricsContext, long startTimestamp, long currentTimestamp)
    {
        var tags = new TagList();
        InitializeRateLimitingTags(ref tags, metricsContext);
 
        if (metricsContext.CurrentLeasedRequestsCounterEnabled)
        {
            _activeRequestLeasesCounter.Add(-1, tags);
        }
 
        if (_requestLeaseDurationCounter.Enabled)
        {
            var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp);
            _requestLeaseDurationCounter.Record(duration.TotalSeconds, tags);
        }
 
        if (_requestsCounter.Enabled)
        {
            // This modifies the shared tags list so must be the last usage in the method.
            tags.Add("aspnetcore.rate_limiting.result", "acquired");
            _requestsCounter.Add(1, tags);
        }
    }
 
    public void QueueStart(in MetricsContext metricsContext)
    {
        if (metricsContext.CurrentQueuedRequestsCounterEnabled)
        {
            QueueStartCore(metricsContext);
        }
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private void QueueStartCore(in MetricsContext metricsContext)
    {
        var tags = new TagList();
        InitializeRateLimitingTags(ref tags, metricsContext);
        _queuedRequestsCounter.Add(1, tags);
    }
 
    public void QueueEnd(in MetricsContext metricsContext, RequestRejectionReason? reason, long startTimestamp, long currentTimestamp)
    {
        if (metricsContext.CurrentQueuedRequestsCounterEnabled || _queuedRequestDurationCounter.Enabled)
        {
            QueueEndCore(metricsContext, reason, startTimestamp, currentTimestamp);
        }
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private void QueueEndCore(in MetricsContext metricsContext, RequestRejectionReason? reason, long startTimestamp, long currentTimestamp)
    {
        var tags = new TagList();
        InitializeRateLimitingTags(ref tags, metricsContext);
 
        if (metricsContext.CurrentQueuedRequestsCounterEnabled)
        {
            _queuedRequestsCounter.Add(-1, tags);
        }
 
        if (_queuedRequestDurationCounter.Enabled)
        {
            tags.Add("aspnetcore.rate_limiting.result", reason != null ? GetResult(reason.Value) : "acquired");
            var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp);
            _queuedRequestDurationCounter.Record(duration.TotalSeconds, tags);
        }
    }
 
    public void Dispose()
    {
        _meter.Dispose();
    }
 
    private static void InitializeRateLimitingTags(ref TagList tags, in MetricsContext metricsContext)
    {
        if (metricsContext.PolicyName is not null)
        {
            tags.Add("aspnetcore.rate_limiting.policy", metricsContext.PolicyName);
        }
    }
 
    private static string GetResult(RequestRejectionReason reason)
    {
        return reason switch
        {
            RequestRejectionReason.EndpointLimiter => "endpoint_limiter",
            RequestRejectionReason.GlobalLimiter => "global_limiter",
            RequestRejectionReason.RequestCanceled => "request_canceled",
            _ => throw new InvalidOperationException("Unexpected value: " + reason)
        };
    }
 
    public MetricsContext CreateContext(string? policyName)
    {
        return new MetricsContext(policyName, _activeRequestLeasesCounter.Enabled, _queuedRequestsCounter.Enabled);
    }
}