Skip to content

Skip counter measurement when start listening while processing #48045

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 9 commits into from
May 18, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
122 changes: 0 additions & 122 deletions src/Hosting/Hosting/test/HostingApplicationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,128 +20,6 @@ namespace Microsoft.AspNetCore.Hosting.Tests;

public class HostingApplicationTests
{
[Fact]
public void Metrics()
{
// Arrange
var meterFactory = new TestMeterFactory();
var meterRegistry = new TestMeterRegistry(meterFactory.Meters);
var hostingApplication = CreateApplication(meterFactory: meterFactory);
var httpContext = new DefaultHttpContext();
var meter = meterFactory.Meters.Single();

using var requestDurationRecorder = new InstrumentRecorder<double>(meterRegistry, HostingMetrics.MeterName, "request-duration");
using var currentRequestsRecorder = new InstrumentRecorder<long>(meterRegistry, HostingMetrics.MeterName, "current-requests");

// Act/Assert
Assert.Equal(HostingMetrics.MeterName, meter.Name);
Assert.Null(meter.Version);

// Request 1 (after success)
httpContext.Request.Protocol = HttpProtocol.Http11;
var context1 = hostingApplication.CreateContext(httpContext.Features);
context1.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
hostingApplication.DisposeContext(context1, null);

Assert.Collection(currentRequestsRecorder.GetMeasurements(),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value));
Assert.Collection(requestDurationRecorder.GetMeasurements(),
m => AssertRequestDuration(m, HttpProtocol.Http11, StatusCodes.Status200OK));

// Request 2 (after failure)
httpContext.Request.Protocol = HttpProtocol.Http2;
var context2 = hostingApplication.CreateContext(httpContext.Features);
context2.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
hostingApplication.DisposeContext(context2, new InvalidOperationException("Test error"));

Assert.Collection(currentRequestsRecorder.GetMeasurements(),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value));
Assert.Collection(requestDurationRecorder.GetMeasurements(),
m => AssertRequestDuration(m, HttpProtocol.Http11, StatusCodes.Status200OK),
m => AssertRequestDuration(m, HttpProtocol.Http2, StatusCodes.Status500InternalServerError, exceptionName: "System.InvalidOperationException"));

// Request 3
httpContext.Request.Protocol = HttpProtocol.Http3;
var context3 = hostingApplication.CreateContext(httpContext.Features);
context3.HttpContext.Response.StatusCode = StatusCodes.Status200OK;

Assert.Collection(currentRequestsRecorder.GetMeasurements(),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value),
m => Assert.Equal(1, m.Value));
Assert.Collection(requestDurationRecorder.GetMeasurements(),
m => AssertRequestDuration(m, HttpProtocol.Http11, StatusCodes.Status200OK),
m => AssertRequestDuration(m, HttpProtocol.Http2, StatusCodes.Status500InternalServerError, exceptionName: "System.InvalidOperationException"));

hostingApplication.DisposeContext(context3, null);

Assert.Collection(currentRequestsRecorder.GetMeasurements(),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value));
Assert.Collection(requestDurationRecorder.GetMeasurements(),
m => AssertRequestDuration(m, HttpProtocol.Http11, StatusCodes.Status200OK),
m => AssertRequestDuration(m, HttpProtocol.Http2, StatusCodes.Status500InternalServerError, exceptionName: "System.InvalidOperationException"),
m => AssertRequestDuration(m, HttpProtocol.Http3, StatusCodes.Status200OK));

static void AssertRequestDuration(Measurement<double> measurement, string protocol, int statusCode, string exceptionName = null)
{
Assert.True(measurement.Value > 0);
Assert.Equal(protocol, (string)measurement.Tags.ToArray().Single(t => t.Key == "protocol").Value);
Assert.Equal(statusCode, (int)measurement.Tags.ToArray().Single(t => t.Key == "status-code").Value);
if (exceptionName == null)
{
Assert.DoesNotContain(measurement.Tags.ToArray(), t => t.Key == "exception-name");
}
else
{
Assert.Equal(exceptionName, (string)measurement.Tags.ToArray().Single(t => t.Key == "exception-name").Value);
}
}
}

[Fact]
public void IHttpMetricsTagsFeatureNotUsedFromFeatureCollection()
{
// Arrange
var meterFactory = new TestMeterFactory();
var meterRegistry = new TestMeterRegistry(meterFactory.Meters);
var hostingApplication = CreateApplication(meterFactory: meterFactory);
var httpContext = new DefaultHttpContext();
var meter = meterFactory.Meters.Single();

using var requestDurationRecorder = new InstrumentRecorder<double>(meterRegistry, HostingMetrics.MeterName, "request-duration");
using var currentRequestsRecorder = new InstrumentRecorder<long>(meterRegistry, HostingMetrics.MeterName, "current-requests");

// Act/Assert
Assert.Equal(HostingMetrics.MeterName, meter.Name);
Assert.Null(meter.Version);

// This feature will be overidden by hosting. Hosting is the owner of the feature and is resposible for setting it.
var overridenFeature = new TestHttpMetricsTagsFeature();
httpContext.Features.Set<IHttpMetricsTagsFeature>(overridenFeature);

var context = hostingApplication.CreateContext(httpContext.Features);
var contextFeature = httpContext.Features.Get<IHttpMetricsTagsFeature>();

Assert.NotNull(contextFeature);
Assert.NotEqual(overridenFeature, contextFeature);
}

private sealed class TestHttpMetricsTagsFeature : IHttpMetricsTagsFeature
{
public ICollection<KeyValuePair<string, object>> Tags { get; } = new Collection<KeyValuePair<string, object>>();
}

[Fact]
public void DisposeContextDoesNotClearHttpContextIfDefaultHttpContextFactoryUsed()
{
Expand Down
204 changes: 204 additions & 0 deletions src/Hosting/Hosting/test/HostingMetricsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Metrics;

namespace Microsoft.AspNetCore.Hosting.Tests;

public class HostingMetricsTests
{
[Fact]
public void MultipleRequests()
{
// Arrange
var meterFactory = new TestMeterFactory();
var meterRegistry = new TestMeterRegistry(meterFactory.Meters);
var hostingApplication = CreateApplication(meterFactory: meterFactory);
var httpContext = new DefaultHttpContext();
var meter = meterFactory.Meters.Single();

using var requestDurationRecorder = new InstrumentRecorder<double>(meterRegistry, HostingMetrics.MeterName, "request-duration");
using var currentRequestsRecorder = new InstrumentRecorder<long>(meterRegistry, HostingMetrics.MeterName, "current-requests");

// Act/Assert
Assert.Equal(HostingMetrics.MeterName, meter.Name);
Assert.Null(meter.Version);

// Request 1 (after success)
httpContext.Request.Protocol = HttpProtocol.Http11;
var context1 = hostingApplication.CreateContext(httpContext.Features);
context1.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
hostingApplication.DisposeContext(context1, null);

Assert.Collection(currentRequestsRecorder.GetMeasurements(),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value));
Assert.Collection(requestDurationRecorder.GetMeasurements(),
m => AssertRequestDuration(m, HttpProtocol.Http11, StatusCodes.Status200OK));

// Request 2 (after failure)
httpContext.Request.Protocol = HttpProtocol.Http2;
var context2 = hostingApplication.CreateContext(httpContext.Features);
context2.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
hostingApplication.DisposeContext(context2, new InvalidOperationException("Test error"));

Assert.Collection(currentRequestsRecorder.GetMeasurements(),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value));
Assert.Collection(requestDurationRecorder.GetMeasurements(),
m => AssertRequestDuration(m, HttpProtocol.Http11, StatusCodes.Status200OK),
m => AssertRequestDuration(m, HttpProtocol.Http2, StatusCodes.Status500InternalServerError, exceptionName: "System.InvalidOperationException"));

// Request 3
httpContext.Request.Protocol = HttpProtocol.Http3;
var context3 = hostingApplication.CreateContext(httpContext.Features);
context3.HttpContext.Response.StatusCode = StatusCodes.Status200OK;

Assert.Collection(currentRequestsRecorder.GetMeasurements(),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value),
m => Assert.Equal(1, m.Value));
Assert.Collection(requestDurationRecorder.GetMeasurements(),
m => AssertRequestDuration(m, HttpProtocol.Http11, StatusCodes.Status200OK),
m => AssertRequestDuration(m, HttpProtocol.Http2, StatusCodes.Status500InternalServerError, exceptionName: "System.InvalidOperationException"));

hostingApplication.DisposeContext(context3, null);

Assert.Collection(currentRequestsRecorder.GetMeasurements(),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value));
Assert.Collection(requestDurationRecorder.GetMeasurements(),
m => AssertRequestDuration(m, HttpProtocol.Http11, StatusCodes.Status200OK),
m => AssertRequestDuration(m, HttpProtocol.Http2, StatusCodes.Status500InternalServerError, exceptionName: "System.InvalidOperationException"),
m => AssertRequestDuration(m, HttpProtocol.Http3, StatusCodes.Status200OK));

static void AssertRequestDuration(Measurement<double> measurement, string protocol, int statusCode, string exceptionName = null)
{
Assert.True(measurement.Value > 0);
Assert.Equal(protocol, (string)measurement.Tags.ToArray().Single(t => t.Key == "protocol").Value);
Assert.Equal(statusCode, (int)measurement.Tags.ToArray().Single(t => t.Key == "status-code").Value);
if (exceptionName == null)
{
Assert.DoesNotContain(measurement.Tags.ToArray(), t => t.Key == "exception-name");
}
else
{
Assert.Equal(exceptionName, (string)measurement.Tags.ToArray().Single(t => t.Key == "exception-name").Value);
}
}
}

[Fact]
public async Task StartListeningDuringRequest_NotMeasured()
{
// Arrange
var syncPoint = new SyncPoint();
var meterFactory = new TestMeterFactory();
var meterRegistry = new TestMeterRegistry(meterFactory.Meters);
var hostingApplication = CreateApplication(meterFactory: meterFactory, requestDelegate: async ctx =>
{
await syncPoint.WaitToContinue();
});
var httpContext = new DefaultHttpContext();
var meter = meterFactory.Meters.Single();

// Act/Assert
Assert.Equal(HostingMetrics.MeterName, meter.Name);
Assert.Null(meter.Version);

// Request 1 (after success)
httpContext.Request.Protocol = HttpProtocol.Http11;
var context1 = hostingApplication.CreateContext(httpContext.Features);
var processRequestTask = hostingApplication.ProcessRequestAsync(context1);

await syncPoint.WaitForSyncPoint().DefaultTimeout();

using var requestDurationRecorder = new InstrumentRecorder<double>(meterRegistry, HostingMetrics.MeterName, "request-duration");
using var currentRequestsRecorder = new InstrumentRecorder<long>(meterRegistry, HostingMetrics.MeterName, "current-requests");
context1.HttpContext.Response.StatusCode = StatusCodes.Status200OK;

syncPoint.Continue();
await processRequestTask.DefaultTimeout();

hostingApplication.DisposeContext(context1, null);

Assert.Empty(currentRequestsRecorder.GetMeasurements());
Assert.Empty(requestDurationRecorder.GetMeasurements());
}

[Fact]
public void IHttpMetricsTagsFeatureNotUsedFromFeatureCollection()
{
// Arrange
var meterFactory = new TestMeterFactory();
var meterRegistry = new TestMeterRegistry(meterFactory.Meters);
var hostingApplication = CreateApplication(meterFactory: meterFactory);
var httpContext = new DefaultHttpContext();
var meter = meterFactory.Meters.Single();

using var requestDurationRecorder = new InstrumentRecorder<double>(meterRegistry, HostingMetrics.MeterName, "request-duration");
using var currentRequestsRecorder = new InstrumentRecorder<long>(meterRegistry, HostingMetrics.MeterName, "current-requests");

// Act/Assert
Assert.Equal(HostingMetrics.MeterName, meter.Name);
Assert.Null(meter.Version);

// This feature will be overidden by hosting. Hosting is the owner of the feature and is resposible for setting it.
var overridenFeature = new TestHttpMetricsTagsFeature();
httpContext.Features.Set<IHttpMetricsTagsFeature>(overridenFeature);

var context = hostingApplication.CreateContext(httpContext.Features);
var contextFeature = httpContext.Features.Get<IHttpMetricsTagsFeature>();

Assert.NotNull(contextFeature);
Assert.NotEqual(overridenFeature, contextFeature);
}

private sealed class TestHttpMetricsTagsFeature : IHttpMetricsTagsFeature
{
public ICollection<KeyValuePair<string, object>> Tags { get; } = new Collection<KeyValuePair<string, object>>();
}

private static HostingApplication CreateApplication(IHttpContextFactory httpContextFactory = null, bool useHttpContextAccessor = false,
ActivitySource activitySource = null, IMeterFactory meterFactory = null, RequestDelegate requestDelegate = null)
{
var services = new ServiceCollection();
services.AddOptions();
if (useHttpContextAccessor)
{
services.AddHttpContextAccessor();
}

httpContextFactory ??= new DefaultHttpContextFactory(services.BuildServiceProvider());
requestDelegate ??= ctx => Task.CompletedTask;

var hostingApplication = new HostingApplication(
requestDelegate,
NullLogger.Instance,
new DiagnosticListener("Microsoft.AspNetCore"),
activitySource ?? new ActivitySource("Microsoft.AspNetCore"),
DistributedContextPropagator.CreateDefaultPropagator(),
httpContextFactory,
HostingEventSource.Log,
new HostingMetrics(meterFactory ?? new TestMeterFactory()));

return hostingApplication;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<ItemGroup>
<Compile Include="$(SharedSourceRoot)EventSource.Testing\TestEventListener.cs" />
<Compile Include="$(SharedSourceRoot)EventSource.Testing\TestCounterListener.cs" />
<Compile Include="$(SharedSourceRoot)SyncPoint\SyncPoint.cs" />
<Content Include="testroot\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
<Content Include="Microsoft.AspNetCore.Hosting.StaticWebAssets.xml" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
Expand Down
12 changes: 6 additions & 6 deletions src/Middleware/RateLimiting/src/MetricsContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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;
Expand All @@ -8,15 +8,15 @@ 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 readonly bool CurrentLeasedRequestsCounterEnabled;
public readonly bool CurrentQueuedRequestsCounterEnabled;

public MetricsContext(string? policyName, string? method, string? route, bool currentLeaseRequestsCounterEnabled, bool currentRequestsQueuedCounterEnabled)
public MetricsContext(string? policyName, string? method, string? route, bool currentLeasedRequestsCounterEnabled, bool currentQueuedRequestsCounterEnabled)
{
PolicyName = policyName;
Method = method;
Route = route;
CurrentLeaseRequestsCounterEnabled = currentLeaseRequestsCounterEnabled;
CurrentRequestsQueuedCounterEnabled = currentRequestsQueuedCounterEnabled;
CurrentLeasedRequestsCounterEnabled = currentLeasedRequestsCounterEnabled;
CurrentQueuedRequestsCounterEnabled = currentQueuedRequestsCounterEnabled;
}
}
Loading