Skip to content

Commit

Permalink
Add metrics to rate limiting (dotnet#47758)
Browse files Browse the repository at this point in the history
Co-authored-by: Stephen Halter <halter73@gmail.com>
  • Loading branch information
JamesNK and halter73 authored May 3, 2023
1 parent 42d14c4 commit d78d2a0
Show file tree
Hide file tree
Showing 16 changed files with 816 additions and 50 deletions.
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
}
}
22 changes: 22 additions & 0 deletions src/Middleware/RateLimiting/src/MetricsContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.RateLimiting;

internal readonly struct MetricsContext
{
public readonly string? PolicyName;
public readonly string? Method;
public readonly string? Route;
public readonly bool CurrentLeaseRequestsCounterEnabled;
public readonly bool CurrentRequestsQueuedCounterEnabled;

public MetricsContext(string? policyName, string? method, string? route, bool currentLeaseRequestsCounterEnabled, bool currentRequestsQueuedCounterEnabled)
{
PolicyName = policyName;
Method = method;
Route = route;
CurrentLeaseRequestsCounterEnabled = currentLeaseRequestsCounterEnabled;
CurrentRequestsQueuedCounterEnabled = currentRequestsQueuedCounterEnabled;
}
}
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
177 changes: 177 additions & 0 deletions src/Middleware/RateLimiting/src/RateLimitingMetrics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// 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> _currentLeasedRequestsCounter;
private readonly Histogram<double> _leasedRequestDurationCounter;
private readonly UpDownCounter<long> _currentQueuedRequestsCounter;
private readonly Histogram<double> _queuedRequestDurationCounter;
private readonly Counter<long> _leaseFailedRequestsCounter;

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

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

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

_currentQueuedRequestsCounter = _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 bool CurrentLeasedRequestsCounterEnabled => _currentLeasedRequestsCounter.Enabled;
public bool CurrentQueuedRequestsCounterEnabled => _currentQueuedRequestsCounter.Enabled;

public void LeaseFailed(in MetricsContext metricsContext, RequestRejectionReason reason)
{
if (_leaseFailedRequestsCounter.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("reason", reason.ToString());
_leaseFailedRequestsCounter.Add(1, tags);
}

public void LeaseStart(in MetricsContext metricsContext)
{
if (metricsContext.CurrentLeaseRequestsCounterEnabled)
{
LeaseStartCore(metricsContext);
}
}

[MethodImpl(MethodImplOptions.NoInlining)]
public void LeaseStartCore(in MetricsContext metricsContext)
{
var tags = new TagList();
InitializeRateLimitingTags(ref tags, metricsContext);
_currentLeasedRequestsCounter.Add(1, tags);
}

public void LeaseEnd(in MetricsContext metricsContext, long startTimestamp, long currentTimestamp)
{
if (metricsContext.CurrentLeaseRequestsCounterEnabled || _leasedRequestDurationCounter.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.CurrentLeaseRequestsCounterEnabled)
{
_currentLeasedRequestsCounter.Add(-1, tags);
}

if (_leasedRequestDurationCounter.Enabled)
{
var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp);
_leasedRequestDurationCounter.Record(duration.TotalSeconds, tags);
}
}

public void QueueStart(in MetricsContext metricsContext)
{
if (metricsContext.CurrentRequestsQueuedCounterEnabled)
{
QueueStartCore(metricsContext);
}
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void QueueStartCore(in MetricsContext metricsContext)
{
var tags = new TagList();
InitializeRateLimitingTags(ref tags, metricsContext);
_currentQueuedRequestsCounter.Add(1, tags);
}

public void QueueEnd(in MetricsContext metricsContext, RequestRejectionReason? reason, long startTimestamp, long currentTimestamp)
{
if (metricsContext.CurrentRequestsQueuedCounterEnabled || _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.CurrentRequestsQueuedCounterEnabled)
{
_currentQueuedRequestsCounter.Add(-1, tags);
}

if (_queuedRequestDurationCounter.Enabled)
{
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, in MetricsContext metricsContext)
{
if (metricsContext.PolicyName is not null)
{
tags.Add("policy", metricsContext.PolicyName);
}
if (metricsContext.Method is not null)
{
tags.Add("method", metricsContext.Method);
}
if (metricsContext.Route is not null)
{
tags.Add("route", metricsContext.Route);
}
}
}
Loading

0 comments on commit d78d2a0

Please sign in to comment.