Skip to content

Add metrics to rate limiting #47758

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,6 @@
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>

<ItemGroup>
<Compile Include="$(SharedSourceRoot)Metrics\**\*.cs" LinkBase="Metrics" />
</ItemGroup>

<!-- Temporary hack to make prototype Metrics DI integration types available -->
<!-- TODO: Remove when Metrics DI intergration package is available https://github.com/dotnet/aspnetcore/issues/47618 -->
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.AspNetCore.Hosting" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Hosting.Tests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Core" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Core.Tests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Tests" />
<InternalsVisibleTo Include="InMemory.FunctionalTests" />
<InternalsVisibleTo Include="Sockets.BindTests" />
<InternalsVisibleTo Include="Sockets.FunctionalTests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Connections" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Connections.Tests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.SignalR" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Diagnostics.Tests" />
</ItemGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" />
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,29 @@ Microsoft.AspNetCore.Http.HttpResponse</Description>
</Compile>
</ItemGroup>

<!-- Temporary hack to make prototype Metrics DI integration types available -->
<!-- TODO: Remove when Metrics DI intergration package is available https://github.com/dotnet/aspnetcore/issues/47618 -->
<ItemGroup>
<Compile Include="$(SharedSourceRoot)Metrics\**\*.cs" LinkBase="Metrics" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.AspNetCore.Hosting" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Hosting.Tests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Core" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Core.Tests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Tests" />
<InternalsVisibleTo Include="InMemory.FunctionalTests" />
<InternalsVisibleTo Include="Sockets.BindTests" />
<InternalsVisibleTo Include="Sockets.FunctionalTests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Connections" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Connections.Tests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.SignalR" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Diagnostics.Tests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.RateLimiting" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.RateLimiting.Tests" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Abstractions.Tests" />
</ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/Middleware/RateLimiting/src/LeaseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ internal enum RequestRejectionReason
EndpointLimiter,
GlobalLimiter,
RequestCanceled
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Resources = Microsoft.AspNetCore.RateLimiting.Resources;

namespace Microsoft.AspNetCore.Builder;

Expand All @@ -20,6 +22,8 @@ public static IApplicationBuilder UseRateLimiter(this IApplicationBuilder app)
{
ArgumentNullException.ThrowIfNull(app);

VerifyServicesAreRegistered(app);

return app.UseMiddleware<RateLimitingMiddleware>();
}

Expand All @@ -34,6 +38,19 @@ public static IApplicationBuilder UseRateLimiter(this IApplicationBuilder app, R
ArgumentNullException.ThrowIfNull(app);
ArgumentNullException.ThrowIfNull(options);

VerifyServicesAreRegistered(app);

return app.UseMiddleware<RateLimitingMiddleware>(Options.Create(options));
}

private static void VerifyServicesAreRegistered(IApplicationBuilder app)
{
var serviceProviderIsService = app.ApplicationServices.GetService<IServiceProviderIsService>();
if (serviceProviderIsService != null && !serviceProviderIsService.IsService(typeof(RateLimitingMetrics)))
{
throw new InvalidOperationException(Resources.FormatUnableToFindServices(
nameof(IServiceCollection),
nameof(RateLimiterServiceCollectionExtensions.AddRateLimiter)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public static IServiceCollection AddRateLimiter(this IServiceCollection services
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configureOptions);

services.AddMetrics();
services.AddSingleton<RateLimitingMetrics>();
services.Configure(configureOptions);
return services;
}
Expand Down
161 changes: 161 additions & 0 deletions src/Middleware/RateLimiting/src/RateLimitingMetrics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// 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.Extensions.Metrics;

namespace Microsoft.AspNetCore.RateLimiting;

internal sealed class RateLimitingMetrics : IDisposable
{
public const string MeterName = "Microsoft.AspNetCore.RateLimiting";

private readonly Meter _meter;
private readonly UpDownCounter<long> _currentLeaseRequestsCounter;
private readonly Histogram<double> _leaseRequestDurationCounter;
private readonly UpDownCounter<long> _currentRequestsQueuedCounter;
private readonly Histogram<double> _queuedRequestDurationCounter;
private readonly Counter<long> _leaseFailedRequestsCounter;

public RateLimitingMetrics(IMeterFactory meterFactory)
{
_meter = meterFactory.CreateMeter(MeterName);

_currentLeaseRequestsCounter = _meter.CreateUpDownCounter<long>(
"current-leased-requests",
description: "Number of HTTP requests that are currently active on the server that hold a rate limiting lease.");

_leaseRequestDurationCounter = _meter.CreateHistogram<double>(
"leased-request-duration",
unit: "s",
description: "The duration of rate limiting leases held by HTTP requests on the server.");

_currentRequestsQueuedCounter = _meter.CreateUpDownCounter<long>(
"current-queued-requests",
description: "Number of HTTP requests that are currently queued, waiting to acquire a rate limiting lease.");

_queuedRequestDurationCounter = _meter.CreateHistogram<double>(
"queued-request-duration",
unit: "s",
description: "The duration of HTTP requests in a queue, waiting to acquire a rate limiting lease.");

_leaseFailedRequestsCounter = _meter.CreateCounter<long>(
"lease-failed-requests",
description: "Number of HTTP requests that failed 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(string? policyName, string? method, string? route, RequestRejectionReason reason)
{
if (_leaseFailedRequestsCounter.Enabled)
{
LeaseFailedCore(policyName, method, route, reason);
}
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void LeaseFailedCore(string? policyName, string? method, string? route, RequestRejectionReason reason)
{
var tags = new TagList();
InitializeRateLimitingTags(ref tags, policyName, method, route);
tags.Add("reason", reason.ToString());
_leaseFailedRequestsCounter.Add(1, tags);
}

public void LeaseStart(string? policyName, string? method, string? route)
{
if (_currentLeaseRequestsCounter.Enabled)
{
LeaseStartCore(policyName, method, route);
}
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void LeaseStartCore(string? policyName, string? method, string? route)
{
var tags = new TagList();
InitializeRateLimitingTags(ref tags, policyName, method, route);
_currentLeaseRequestsCounter.Add(1, tags);
}

public void LeaseEnd(string? policyName, string? method, string? route, long startTimestamp, long currentTimestamp)
{
if (_currentLeaseRequestsCounter.Enabled || _leaseRequestDurationCounter.Enabled)
{
LeaseEndCore(policyName, method, route, startTimestamp, currentTimestamp);
}
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void LeaseEndCore(string? policyName, string? method, string? route, long startTimestamp, long currentTimestamp)
{
var tags = new TagList();
InitializeRateLimitingTags(ref tags, policyName, method, route);

_currentLeaseRequestsCounter.Add(-1, tags);

var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp);
_leaseRequestDurationCounter.Record(duration.TotalSeconds, tags);
}

public void QueueStart(string? policyName, string? method, string? route)
{
if (_currentRequestsQueuedCounter.Enabled)
{
QueueStartCore(policyName, method, route);
}
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void QueueStartCore(string? policyName, string? method, string? route)
{
var tags = new TagList();
InitializeRateLimitingTags(ref tags, policyName, method, route);
_currentRequestsQueuedCounter.Add(1, tags);
}

public void QueueEnd(string? policyName, string? method, string? route, RequestRejectionReason? reason, long startTimestamp, long currentTimestamp)
{
if (_currentRequestsQueuedCounter.Enabled || _queuedRequestDurationCounter.Enabled)
{
QueueEndCore(policyName, method, route, reason, startTimestamp, currentTimestamp);
}
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void QueueEndCore(string? policyName, string? method, string? route, RequestRejectionReason? reason, long startTimestamp, long currentTimestamp)
{
var tags = new TagList();
InitializeRateLimitingTags(ref tags, policyName, method, route);
_currentRequestsQueuedCounter.Add(-1, tags);

if (reason != null)
{
tags.Add("reason", reason.Value.ToString());
}
var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp);
_queuedRequestDurationCounter.Record(duration.TotalSeconds, tags);
}

public void Dispose()
{
_meter.Dispose();
}

private static void InitializeRateLimitingTags(ref TagList tags, string? policyName, string? method, string? route)
{
if (policyName is not null)
{
tags.Add("policy", policyName);
}
if (method is not null)
{
tags.Add("method", method);
}
if (route is not null)
{
tags.Add("route", route);
}
}
}
Loading