File: RateLimitingMetricsTests.cs
Web Access
Project: src\src\Middleware\RateLimiting\test\Microsoft.AspNetCore.RateLimiting.Tests.csproj (Microsoft.AspNetCore.RateLimiting.Tests)
// 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.Metrics;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.Metrics;
using Microsoft.Extensions.Diagnostics.Metrics.Testing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
 
namespace Microsoft.AspNetCore.RateLimiting;
 
public class RateLimitingMetricsTests
{
    [Fact]
    public async Task Metrics_Rejected()
    {
        // Arrange
        var meterFactory = new TestMeterFactory();
 
        var options = CreateOptionsAccessor();
        options.Value.GlobalLimiter = new TestPartitionedRateLimiter<HttpContext>(new TestRateLimiter(false));
 
        var middleware = CreateTestRateLimitingMiddleware(options, meterFactory: meterFactory);
        var meter = meterFactory.Meters.Single();
 
        var context = new DefaultHttpContext();
 
        using var leaseRequestDurationCollector = new MetricCollector<double>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.request_lease.duration");
        using var currentLeaseRequestsCollector = new MetricCollector<long>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.active_request_leases");
        using var currentRequestsQueuedCollector = new MetricCollector<long>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.queued_requests");
        using var queuedRequestDurationCollector = new MetricCollector<double>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.request.time_in_queue");
        using var rateLimitingRequestsCollector = new MetricCollector<long>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.requests");
 
        // Act
        await middleware.Invoke(context).DefaultTimeout();
 
        // Assert
        Assert.Equal(StatusCodes.Status503ServiceUnavailable, context.Response.StatusCode);
 
        Assert.Empty(currentLeaseRequestsCollector.GetMeasurementSnapshot());
        Assert.Empty(leaseRequestDurationCollector.GetMeasurementSnapshot());
        Assert.Empty(currentRequestsQueuedCollector.GetMeasurementSnapshot());
        Assert.Empty(queuedRequestDurationCollector.GetMeasurementSnapshot());
        Assert.Collection(rateLimitingRequestsCollector.GetMeasurementSnapshot(),
            m =>
            {
                Assert.Equal(1, m.Value);
                Assert.Equal("global_limiter", (string)m.Tags["aspnetcore.rate_limiting.result"]);
            });
    }
 
    [Fact]
    public async Task Metrics_Success()
    {
        // Arrange
        var syncPoint = new SyncPoint();
 
        var meterFactory = new TestMeterFactory();
 
        var options = CreateOptionsAccessor();
        options.Value.GlobalLimiter = new TestPartitionedRateLimiter<HttpContext>(new TestRateLimiter(true));
 
        var middleware = CreateTestRateLimitingMiddleware(
            options,
            meterFactory: meterFactory,
            next: async c =>
            {
                await syncPoint.WaitToContinue();
            });
        var meter = meterFactory.Meters.Single();
 
        var context = new DefaultHttpContext();
        context.Request.Method = "GET";
 
        using var leaseRequestDurationCollector = new MetricCollector<double>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.request_lease.duration");
        using var currentLeaseRequestsCollector = new MetricCollector<long>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.active_request_leases");
        using var currentRequestsQueuedCollector = new MetricCollector<long>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.queued_requests");
        using var queuedRequestDurationCollector = new MetricCollector<double>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.request.time_in_queue");
        using var rateLimitingRequestsCollector = new MetricCollector<long>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.requests");
 
        // Act
        var middlewareTask = middleware.Invoke(context);
 
        await syncPoint.WaitForSyncPoint().DefaultTimeout();
 
        Assert.Collection(currentLeaseRequestsCollector.GetMeasurementSnapshot(),
            m => AssertCounter(m, 1, null));
        Assert.Empty(leaseRequestDurationCollector.GetMeasurementSnapshot());
 
        syncPoint.Continue();
 
        await middlewareTask.DefaultTimeout();
 
        // Assert
        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
 
        Assert.Collection(currentLeaseRequestsCollector.GetMeasurementSnapshot(),
            m => AssertCounter(m, 1, null),
            m => AssertCounter(m, -1, null));
        Assert.Collection(leaseRequestDurationCollector.GetMeasurementSnapshot(),
            m => AssertDuration(m, null));
        Assert.Empty(currentRequestsQueuedCollector.GetMeasurementSnapshot());
        Assert.Empty(queuedRequestDurationCollector.GetMeasurementSnapshot());
        Assert.Collection(rateLimitingRequestsCollector.GetMeasurementSnapshot(),
            m =>
            {
                Assert.Equal("acquired", (string)m.Tags["aspnetcore.rate_limiting.result"]);
            });
    }
 
    [Fact]
    public async Task Metrics_ListenInMiddleOfRequest_CurrentLeasesNotDecreased()
    {
        // Arrange
        var syncPoint = new SyncPoint();
 
        var meterFactory = new TestMeterFactory();
 
        var options = CreateOptionsAccessor();
        options.Value.GlobalLimiter = new TestPartitionedRateLimiter<HttpContext>(new TestRateLimiter(true));
 
        var middleware = CreateTestRateLimitingMiddleware(
            options,
            meterFactory: meterFactory,
            next: async c =>
            {
                await syncPoint.WaitToContinue();
            });
        var meter = meterFactory.Meters.Single();
 
        var context = new DefaultHttpContext();
        context.Request.Method = "GET";
 
        // Act
        var middlewareTask = middleware.Invoke(context);
 
        await syncPoint.WaitForSyncPoint().DefaultTimeout();
 
        using var leaseRequestDurationCollector = new MetricCollector<double>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.request_lease.duration");
        using var currentLeaseRequestsCollector = new MetricCollector<long>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.active_request_leases");
        using var currentRequestsQueuedCollector = new MetricCollector<long>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.queued_requests");
        using var queuedRequestDurationCollector = new MetricCollector<double>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.request.time_in_queue");
        using var rateLimitingRequestsCollector = new MetricCollector<long>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.requests");
 
        syncPoint.Continue();
 
        await middlewareTask.DefaultTimeout();
 
        // Assert
        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
 
        Assert.Empty(currentLeaseRequestsCollector.GetMeasurementSnapshot());
        Assert.Collection(leaseRequestDurationCollector.GetMeasurementSnapshot(),
            m => AssertDuration(m, null));
    }
 
    [Fact]
    public async Task Metrics_Queued()
    {
        // Arrange
        var syncPoint = new SyncPoint();
 
        var meterFactory = new TestMeterFactory();
 
        var services = new ServiceCollection();
 
        services.AddRateLimiter(_ => _
            .AddConcurrencyLimiter(policyName: "concurrencyPolicy", options =>
            {
                options.PermitLimit = 1;
                options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
                options.QueueLimit = 1;
            }));
        var serviceProvider = services.BuildServiceProvider();
 
        var middleware = CreateTestRateLimitingMiddleware(
            serviceProvider.GetRequiredService<IOptions<RateLimiterOptions>>(),
            meterFactory: meterFactory,
            next: async c =>
            {
                await syncPoint.WaitToContinue();
            },
            serviceProvider: serviceProvider);
        var meter = meterFactory.Meters.Single();
 
        var routeEndpointBuilder = new RouteEndpointBuilder(c => Task.CompletedTask, RoutePatternFactory.Parse("/"), 0);
        routeEndpointBuilder.Metadata.Add(new EnableRateLimitingAttribute("concurrencyPolicy"));
        var endpoint = routeEndpointBuilder.Build();
 
        using var leaseRequestDurationCollector = new MetricCollector<double>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.request_lease.duration");
        using var currentLeaseRequestsCollector = new MetricCollector<long>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.active_request_leases");
        using var currentRequestsQueuedCollector = new MetricCollector<long>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.queued_requests");
        using var queuedRequestDurationCollector = new MetricCollector<double>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.request.time_in_queue");
        using var rateLimitingRequestsCollector = new MetricCollector<long>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.requests");
 
        // Act
        var context1 = new DefaultHttpContext();
        context1.Request.Method = "GET";
        context1.SetEndpoint(endpoint);
        var middlewareTask1 = middleware.Invoke(context1);
 
        // Wait for first request to reach server and block it.
        await syncPoint.WaitForSyncPoint().DefaultTimeout();
 
        var context2 = new DefaultHttpContext();
        context2.Request.Method = "GET";
        context2.SetEndpoint(endpoint);
        var middlewareTask2 = middleware.Invoke(context1);
 
        // Assert second request is queued.
        Assert.Collection(currentRequestsQueuedCollector.GetMeasurementSnapshot(),
            m => AssertCounter(m, 1, "concurrencyPolicy"));
        Assert.Empty(queuedRequestDurationCollector.GetMeasurementSnapshot());
 
        // Allow both requests to finish.
        syncPoint.Continue();
 
        await middlewareTask1.DefaultTimeout();
        await middlewareTask2.DefaultTimeout();
 
        Assert.Collection(currentRequestsQueuedCollector.GetMeasurementSnapshot(),
            m => AssertCounter(m, 1, "concurrencyPolicy"),
            m => AssertCounter(m, -1, "concurrencyPolicy"));
        Assert.Collection(queuedRequestDurationCollector.GetMeasurementSnapshot(),
            m =>
            {
                AssertDuration(m, "concurrencyPolicy");
                Assert.Equal("acquired", (string)m.Tags["aspnetcore.rate_limiting.result"]);
            });
    }
 
    [Fact]
    public async Task Metrics_ListenInMiddleOfQueued_CurrentQueueNotDecreased()
    {
        // Arrange
        var syncPoint = new SyncPoint();
 
        var meterFactory = new TestMeterFactory();
 
        var services = new ServiceCollection();
 
        services.AddRateLimiter(_ => _
            .AddConcurrencyLimiter(policyName: "concurrencyPolicy", options =>
            {
                options.PermitLimit = 1;
                options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
                options.QueueLimit = 1;
            }));
        var serviceProvider = services.BuildServiceProvider();
 
        var middleware = CreateTestRateLimitingMiddleware(
            serviceProvider.GetRequiredService<IOptions<RateLimiterOptions>>(),
            meterFactory: meterFactory,
            next: async c =>
            {
                await syncPoint.WaitToContinue();
            },
            serviceProvider: serviceProvider);
        var meter = meterFactory.Meters.Single();
 
        var routeEndpointBuilder = new RouteEndpointBuilder(c => Task.CompletedTask, RoutePatternFactory.Parse("/"), 0);
        routeEndpointBuilder.Metadata.Add(new EnableRateLimitingAttribute("concurrencyPolicy"));
        var endpoint = routeEndpointBuilder.Build();
 
        // Act
        var context1 = new DefaultHttpContext();
        context1.Request.Method = "GET";
        context1.SetEndpoint(endpoint);
        var middlewareTask1 = middleware.Invoke(context1);
 
        // Wait for first request to reach server and block it.
        await syncPoint.WaitForSyncPoint().DefaultTimeout();
 
        var context2 = new DefaultHttpContext();
        context2.Request.Method = "GET";
        context2.SetEndpoint(endpoint);
        var middlewareTask2 = middleware.Invoke(context1);
 
        // Start listening while the second request is queued.
 
        using var leaseRequestDurationCollector = new MetricCollector<double>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.request_lease.duration");
        using var currentLeaseRequestsCollector = new MetricCollector<long>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.active_request_leases");
        using var currentRequestsQueuedCollector = new MetricCollector<long>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.queued_requests");
        using var queuedRequestDurationCollector = new MetricCollector<double>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.request.time_in_queue");
        using var rateLimitingRequestsCollector = new MetricCollector<long>(meterFactory, RateLimitingMetrics.MeterName, "aspnetcore.rate_limiting.requests");
 
        Assert.Empty(currentRequestsQueuedCollector.GetMeasurementSnapshot());
        Assert.Empty(queuedRequestDurationCollector.GetMeasurementSnapshot());
 
        // Allow both requests to finish.
        syncPoint.Continue();
 
        await middlewareTask1.DefaultTimeout();
        await middlewareTask2.DefaultTimeout();
 
        Assert.Empty(currentRequestsQueuedCollector.GetMeasurementSnapshot());
        Assert.Collection(queuedRequestDurationCollector.GetMeasurementSnapshot(),
            m => AssertDuration(m, "concurrencyPolicy"));
    }
 
    private static void AssertCounter(CollectedMeasurement<long> measurement, long value, string policy)
    {
        Assert.Equal(value, measurement.Value);
        AssertTag(measurement.Tags, "aspnetcore.rate_limiting.policy", policy);
    }
 
    private static void AssertDuration(CollectedMeasurement<double> measurement, string policy)
    {
        Assert.True(measurement.Value > 0);
        AssertTag(measurement.Tags, "aspnetcore.rate_limiting.policy", policy);
    }
 
    private static void AssertTag<T>(IReadOnlyDictionary<string, object> tags, string tagName, T expected)
    {
        if (expected == null)
        {
            Assert.False(tags.ContainsKey(tagName));
        }
        else
        {
            Assert.Equal(expected, (T)tags[tagName]);
        }
    }
 
    private RateLimitingMiddleware CreateTestRateLimitingMiddleware(IOptions<RateLimiterOptions> options, ILogger<RateLimitingMiddleware> logger = null, IServiceProvider serviceProvider = null, IMeterFactory meterFactory = null, RequestDelegate next = null)
    {
        next ??= c => Task.CompletedTask;
        return new RateLimitingMiddleware(
            next,
            logger ?? new NullLoggerFactory().CreateLogger<RateLimitingMiddleware>(),
            options,
            serviceProvider ?? Mock.Of<IServiceProvider>(),
            new RateLimitingMetrics(meterFactory ?? new TestMeterFactory()));
    }
 
    private IOptions<RateLimiterOptions> CreateOptionsAccessor() => Options.Create(new RateLimiterOptions());
}