Skip to content

Commit d78d2a0

Browse files
JamesNKhalter73
andauthored
Add metrics to rate limiting (#47758)
Co-authored-by: Stephen Halter <[email protected]>
1 parent 42d14c4 commit d78d2a0

16 files changed

+816
-50
lines changed

src/Hosting/Abstractions/src/Microsoft.AspNetCore.Hosting.Abstractions.csproj

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,6 @@
1111
<IsTrimmable>true</IsTrimmable>
1212
</PropertyGroup>
1313

14-
<ItemGroup>
15-
<Compile Include="$(SharedSourceRoot)Metrics\**\*.cs" LinkBase="Metrics" />
16-
</ItemGroup>
17-
18-
<!-- Temporary hack to make prototype Metrics DI integration types available -->
19-
<!-- TODO: Remove when Metrics DI intergration package is available https://github.com/dotnet/aspnetcore/issues/47618 -->
20-
<ItemGroup>
21-
<InternalsVisibleTo Include="Microsoft.AspNetCore.Hosting" />
22-
<InternalsVisibleTo Include="Microsoft.AspNetCore.Hosting.Tests" />
23-
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Core" />
24-
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Core.Tests" />
25-
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Tests" />
26-
<InternalsVisibleTo Include="InMemory.FunctionalTests" />
27-
<InternalsVisibleTo Include="Sockets.BindTests" />
28-
<InternalsVisibleTo Include="Sockets.FunctionalTests" />
29-
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks" />
30-
<InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Connections" />
31-
<InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Connections.Tests" />
32-
<InternalsVisibleTo Include="Microsoft.AspNetCore.SignalR" />
33-
<InternalsVisibleTo Include="Microsoft.AspNetCore.Diagnostics.Tests" />
34-
</ItemGroup>
35-
3614
<ItemGroup>
3715
<Reference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" />
3816
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />

src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,29 @@ Microsoft.AspNetCore.Http.HttpResponse</Description>
5353
</Compile>
5454
</ItemGroup>
5555

56+
<!-- Temporary hack to make prototype Metrics DI integration types available -->
57+
<!-- TODO: Remove when Metrics DI intergration package is available https://github.com/dotnet/aspnetcore/issues/47618 -->
58+
<ItemGroup>
59+
<Compile Include="$(SharedSourceRoot)Metrics\**\*.cs" LinkBase="Metrics" />
60+
</ItemGroup>
61+
<ItemGroup>
62+
<InternalsVisibleTo Include="Microsoft.AspNetCore.Hosting" />
63+
<InternalsVisibleTo Include="Microsoft.AspNetCore.Hosting.Tests" />
64+
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Core" />
65+
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Core.Tests" />
66+
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Tests" />
67+
<InternalsVisibleTo Include="InMemory.FunctionalTests" />
68+
<InternalsVisibleTo Include="Sockets.BindTests" />
69+
<InternalsVisibleTo Include="Sockets.FunctionalTests" />
70+
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks" />
71+
<InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Connections" />
72+
<InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Connections.Tests" />
73+
<InternalsVisibleTo Include="Microsoft.AspNetCore.SignalR" />
74+
<InternalsVisibleTo Include="Microsoft.AspNetCore.Diagnostics.Tests" />
75+
<InternalsVisibleTo Include="Microsoft.AspNetCore.RateLimiting" />
76+
<InternalsVisibleTo Include="Microsoft.AspNetCore.RateLimiting.Tests" />
77+
</ItemGroup>
78+
5679
<ItemGroup>
5780
<InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Abstractions.Tests" />
5881
</ItemGroup>

src/Middleware/RateLimiting/src/LeaseContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@ internal enum RequestRejectionReason
2222
EndpointLimiter,
2323
GlobalLimiter,
2424
RequestCanceled
25-
}
25+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.RateLimiting;
5+
6+
internal readonly struct MetricsContext
7+
{
8+
public readonly string? PolicyName;
9+
public readonly string? Method;
10+
public readonly string? Route;
11+
public readonly bool CurrentLeaseRequestsCounterEnabled;
12+
public readonly bool CurrentRequestsQueuedCounterEnabled;
13+
14+
public MetricsContext(string? policyName, string? method, string? route, bool currentLeaseRequestsCounterEnabled, bool currentRequestsQueuedCounterEnabled)
15+
{
16+
PolicyName = policyName;
17+
Method = method;
18+
Route = route;
19+
CurrentLeaseRequestsCounterEnabled = currentLeaseRequestsCounterEnabled;
20+
CurrentRequestsQueuedCounterEnabled = currentRequestsQueuedCounterEnabled;
21+
}
22+
}

src/Middleware/RateLimiting/src/RateLimiterApplicationBuilderExtensions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.AspNetCore.RateLimiting;
5+
using Microsoft.Extensions.DependencyInjection;
56
using Microsoft.Extensions.Options;
7+
using Resources = Microsoft.AspNetCore.RateLimiting.Resources;
68

79
namespace Microsoft.AspNetCore.Builder;
810

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

25+
VerifyServicesAreRegistered(app);
26+
2327
return app.UseMiddleware<RateLimitingMiddleware>();
2428
}
2529

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

41+
VerifyServicesAreRegistered(app);
42+
3743
return app.UseMiddleware<RateLimitingMiddleware>(Options.Create(options));
3844
}
45+
46+
private static void VerifyServicesAreRegistered(IApplicationBuilder app)
47+
{
48+
var serviceProviderIsService = app.ApplicationServices.GetService<IServiceProviderIsService>();
49+
if (serviceProviderIsService != null && !serviceProviderIsService.IsService(typeof(RateLimitingMetrics)))
50+
{
51+
throw new InvalidOperationException(Resources.FormatUnableToFindServices(
52+
nameof(IServiceCollection),
53+
nameof(RateLimiterServiceCollectionExtensions.AddRateLimiter)));
54+
}
55+
}
3956
}

src/Middleware/RateLimiting/src/RateLimiterServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public static IServiceCollection AddRateLimiter(this IServiceCollection services
2222
ArgumentNullException.ThrowIfNull(services);
2323
ArgumentNullException.ThrowIfNull(configureOptions);
2424

25+
services.AddMetrics();
26+
services.AddSingleton<RateLimitingMetrics>();
2527
services.Configure(configureOptions);
2628
return services;
2729
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using System.Diagnostics.Metrics;
6+
using System.Runtime.CompilerServices;
7+
using Microsoft.Extensions.Metrics;
8+
9+
namespace Microsoft.AspNetCore.RateLimiting;
10+
11+
internal sealed class RateLimitingMetrics : IDisposable
12+
{
13+
public const string MeterName = "Microsoft.AspNetCore.RateLimiting";
14+
15+
private readonly Meter _meter;
16+
private readonly UpDownCounter<long> _currentLeasedRequestsCounter;
17+
private readonly Histogram<double> _leasedRequestDurationCounter;
18+
private readonly UpDownCounter<long> _currentQueuedRequestsCounter;
19+
private readonly Histogram<double> _queuedRequestDurationCounter;
20+
private readonly Counter<long> _leaseFailedRequestsCounter;
21+
22+
public RateLimitingMetrics(IMeterFactory meterFactory)
23+
{
24+
_meter = meterFactory.CreateMeter(MeterName);
25+
26+
_currentLeasedRequestsCounter = _meter.CreateUpDownCounter<long>(
27+
"current-leased-requests",
28+
description: "Number of HTTP requests that are currently active on the server that hold a rate limiting lease.");
29+
30+
_leasedRequestDurationCounter = _meter.CreateHistogram<double>(
31+
"leased-request-duration",
32+
unit: "s",
33+
description: "The duration of rate limiting leases held by HTTP requests on the server.");
34+
35+
_currentQueuedRequestsCounter = _meter.CreateUpDownCounter<long>(
36+
"current-queued-requests",
37+
description: "Number of HTTP requests that are currently queued, waiting to acquire a rate limiting lease.");
38+
39+
_queuedRequestDurationCounter = _meter.CreateHistogram<double>(
40+
"queued-request-duration",
41+
unit: "s",
42+
description: "The duration of HTTP requests in a queue, waiting to acquire a rate limiting lease.");
43+
44+
_leaseFailedRequestsCounter = _meter.CreateCounter<long>(
45+
"lease-failed-requests",
46+
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.");
47+
}
48+
49+
public bool CurrentLeasedRequestsCounterEnabled => _currentLeasedRequestsCounter.Enabled;
50+
public bool CurrentQueuedRequestsCounterEnabled => _currentQueuedRequestsCounter.Enabled;
51+
52+
public void LeaseFailed(in MetricsContext metricsContext, RequestRejectionReason reason)
53+
{
54+
if (_leaseFailedRequestsCounter.Enabled)
55+
{
56+
LeaseFailedCore(metricsContext, reason);
57+
}
58+
}
59+
60+
[MethodImpl(MethodImplOptions.NoInlining)]
61+
private void LeaseFailedCore(in MetricsContext metricsContext, RequestRejectionReason reason)
62+
{
63+
var tags = new TagList();
64+
InitializeRateLimitingTags(ref tags, metricsContext);
65+
tags.Add("reason", reason.ToString());
66+
_leaseFailedRequestsCounter.Add(1, tags);
67+
}
68+
69+
public void LeaseStart(in MetricsContext metricsContext)
70+
{
71+
if (metricsContext.CurrentLeaseRequestsCounterEnabled)
72+
{
73+
LeaseStartCore(metricsContext);
74+
}
75+
}
76+
77+
[MethodImpl(MethodImplOptions.NoInlining)]
78+
public void LeaseStartCore(in MetricsContext metricsContext)
79+
{
80+
var tags = new TagList();
81+
InitializeRateLimitingTags(ref tags, metricsContext);
82+
_currentLeasedRequestsCounter.Add(1, tags);
83+
}
84+
85+
public void LeaseEnd(in MetricsContext metricsContext, long startTimestamp, long currentTimestamp)
86+
{
87+
if (metricsContext.CurrentLeaseRequestsCounterEnabled || _leasedRequestDurationCounter.Enabled)
88+
{
89+
LeaseEndCore(metricsContext, startTimestamp, currentTimestamp);
90+
}
91+
}
92+
93+
[MethodImpl(MethodImplOptions.NoInlining)]
94+
private void LeaseEndCore(in MetricsContext metricsContext, long startTimestamp, long currentTimestamp)
95+
{
96+
var tags = new TagList();
97+
InitializeRateLimitingTags(ref tags, metricsContext);
98+
99+
if (metricsContext.CurrentLeaseRequestsCounterEnabled)
100+
{
101+
_currentLeasedRequestsCounter.Add(-1, tags);
102+
}
103+
104+
if (_leasedRequestDurationCounter.Enabled)
105+
{
106+
var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp);
107+
_leasedRequestDurationCounter.Record(duration.TotalSeconds, tags);
108+
}
109+
}
110+
111+
public void QueueStart(in MetricsContext metricsContext)
112+
{
113+
if (metricsContext.CurrentRequestsQueuedCounterEnabled)
114+
{
115+
QueueStartCore(metricsContext);
116+
}
117+
}
118+
119+
[MethodImpl(MethodImplOptions.NoInlining)]
120+
private void QueueStartCore(in MetricsContext metricsContext)
121+
{
122+
var tags = new TagList();
123+
InitializeRateLimitingTags(ref tags, metricsContext);
124+
_currentQueuedRequestsCounter.Add(1, tags);
125+
}
126+
127+
public void QueueEnd(in MetricsContext metricsContext, RequestRejectionReason? reason, long startTimestamp, long currentTimestamp)
128+
{
129+
if (metricsContext.CurrentRequestsQueuedCounterEnabled || _queuedRequestDurationCounter.Enabled)
130+
{
131+
QueueEndCore(metricsContext, reason, startTimestamp, currentTimestamp);
132+
}
133+
}
134+
135+
[MethodImpl(MethodImplOptions.NoInlining)]
136+
private void QueueEndCore(in MetricsContext metricsContext, RequestRejectionReason? reason, long startTimestamp, long currentTimestamp)
137+
{
138+
var tags = new TagList();
139+
InitializeRateLimitingTags(ref tags, metricsContext);
140+
141+
if (metricsContext.CurrentRequestsQueuedCounterEnabled)
142+
{
143+
_currentQueuedRequestsCounter.Add(-1, tags);
144+
}
145+
146+
if (_queuedRequestDurationCounter.Enabled)
147+
{
148+
if (reason != null)
149+
{
150+
tags.Add("reason", reason.Value.ToString());
151+
}
152+
var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp);
153+
_queuedRequestDurationCounter.Record(duration.TotalSeconds, tags);
154+
}
155+
}
156+
157+
public void Dispose()
158+
{
159+
_meter.Dispose();
160+
}
161+
162+
private static void InitializeRateLimitingTags(ref TagList tags, in MetricsContext metricsContext)
163+
{
164+
if (metricsContext.PolicyName is not null)
165+
{
166+
tags.Add("policy", metricsContext.PolicyName);
167+
}
168+
if (metricsContext.Method is not null)
169+
{
170+
tags.Add("method", metricsContext.Method);
171+
}
172+
if (metricsContext.Route is not null)
173+
{
174+
tags.Add("route", metricsContext.Route);
175+
}
176+
}
177+
}

0 commit comments

Comments
 (0)