Skip to content

Commit

Permalink
Add disable HTTP metrics endpoint metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK committed Jul 9, 2024
1 parent 1313a4d commit 67924e5
Show file tree
Hide file tree
Showing 13 changed files with 252 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,12 @@ public void RequestEnd(HttpContext httpContext, Exception? exception, HostingApp

if (context.MetricsEnabled)
{
Debug.Assert(context.MetricsTagsFeature != null, "MetricsTagsFeature should be set if MetricsEnabled is true.");

var endpoint = HttpExtensions.GetOriginalEndpoint(httpContext);
var disableHttpRequestDurationMetric = endpoint?.Metadata.GetMetadata<IDisableHttpMetricsMetadata>() != null || context.MetricsTagsFeature.MetricsDisabled;
var route = endpoint?.Metadata.GetMetadata<IRouteDiagnosticsMetadata>()?.Route;

Debug.Assert(context.MetricsTagsFeature != null, "MetricsTagsFeature should be set if MetricsEnabled is true.");

_metrics.RequestEnd(
context.MetricsTagsFeature.Protocol!,
context.MetricsTagsFeature.Scheme!,
Expand All @@ -169,7 +170,8 @@ public void RequestEnd(HttpContext httpContext, Exception? exception, HostingApp
exception,
context.MetricsTagsFeature.TagsList,
startTimestamp,
currentTimestamp);
currentTimestamp,
disableHttpRequestDurationMetric);
}

if (reachedPipelineEnd)
Expand Down
4 changes: 2 additions & 2 deletions src/Hosting/Hosting/src/Internal/HostingMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public void RequestStart(string scheme, string method)
_activeRequestsCounter.Add(1, tags);
}

public void RequestEnd(string protocol, string scheme, string method, string? route, int statusCode, bool unhandledRequest, Exception? exception, List<KeyValuePair<string, object?>>? customTags, long startTimestamp, long currentTimestamp)
public void RequestEnd(string protocol, string scheme, string method, string? route, int statusCode, bool unhandledRequest, Exception? exception, List<KeyValuePair<string, object?>>? customTags, long startTimestamp, long currentTimestamp, bool disableHttpRequestDurationMetric)
{
var tags = new TagList();
InitializeRequestTags(ref tags, scheme, method);
Expand All @@ -53,7 +53,7 @@ public void RequestEnd(string protocol, string scheme, string method, string? ro
_activeRequestsCounter.Add(-1, tags);
}

if (_requestDuration.Enabled)
if (!disableHttpRequestDurationMetric && _requestDuration.Enabled)
{
if (TryGetHttpVersion(protocol, out var httpVersion))
{
Expand Down
2 changes: 2 additions & 0 deletions src/Hosting/Hosting/src/Internal/HttpMetricsTagsFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Microsoft.AspNetCore.Hosting;
internal sealed class HttpMetricsTagsFeature : IHttpMetricsTagsFeature
{
ICollection<KeyValuePair<string, object?>> IHttpMetricsTagsFeature.Tags => TagsList;
public bool MetricsDisabled { get; set; }

public List<KeyValuePair<string, object?>> TagsList { get; } = new List<KeyValuePair<string, object?>>();

Expand All @@ -20,6 +21,7 @@ internal sealed class HttpMetricsTagsFeature : IHttpMetricsTagsFeature
public void Reset()
{
TagsList.Clear();
MetricsDisabled = false;

Method = null;
Scheme = null;
Expand Down
179 changes: 178 additions & 1 deletion src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
using System.Diagnostics.Metrics;
using System.Diagnostics.Tracing;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Diagnostics.Metrics;
Expand Down Expand Up @@ -228,7 +230,6 @@ public void Metrics_RequestChanges_OriginalValuesUsed()

var testMeterFactory = new TestMeterFactory();
using var activeRequestsCollector = new MetricCollector<long>(testMeterFactory, HostingMetrics.MeterName, "http.server.active_requests");
using var requestDurationCollector = new MetricCollector<double>(testMeterFactory, HostingMetrics.MeterName, "http.server.request.duration");

// Act
var hostingApplication = CreateApplication(out var features, eventSource: hostingEventSource, meterFactory: testMeterFactory, configure: c =>
Expand Down Expand Up @@ -279,6 +280,182 @@ public void Metrics_RequestChanges_OriginalValuesUsed()
Assert.Null(context.MetricsTagsFeature.Protocol);
}

[Fact]
public void Metrics_Route_RouteTagReported()
{
// Arrange
var hostingEventSource = new HostingEventSource(Guid.NewGuid().ToString());

var testMeterFactory = new TestMeterFactory();
using var activeRequestsCollector = new MetricCollector<long>(testMeterFactory, HostingMetrics.MeterName, "http.server.active_requests");
using var requestDurationCollector = new MetricCollector<double>(testMeterFactory, HostingMetrics.MeterName, "http.server.request.duration");

// Act
var hostingApplication = CreateApplication(out var features, eventSource: hostingEventSource, meterFactory: testMeterFactory, configure: c =>
{
c.Request.Protocol = "1.1";
c.Request.Scheme = "http";
c.Request.Method = "POST";
c.Request.Host = new HostString("localhost");
c.Request.Path = "/hello";
c.Request.ContentType = "text/plain";
c.Request.ContentLength = 1024;
});
var context = hostingApplication.CreateContext(features);

Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(),
m =>
{
Assert.Equal(1, m.Value);
Assert.Equal("http", m.Tags["url.scheme"]);
Assert.Equal("POST", m.Tags["http.request.method"]);
});

context.HttpContext.SetEndpoint(new Endpoint(
c => Task.CompletedTask,
new EndpointMetadataCollection(new TestRouteDiagnosticsMetadata()),
"Test endpoint"));

hostingApplication.DisposeContext(context, null);

// Assert
Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(),
m =>
{
Assert.Equal(1, m.Value);
Assert.Equal("http", m.Tags["url.scheme"]);
Assert.Equal("POST", m.Tags["http.request.method"]);
},
m =>
{
Assert.Equal(-1, m.Value);
Assert.Equal("http", m.Tags["url.scheme"]);
Assert.Equal("POST", m.Tags["http.request.method"]);
});
Assert.Collection(requestDurationCollector.GetMeasurementSnapshot(),
m =>
{
Assert.True(m.Value > 0);
Assert.Equal("hello/{name}", m.Tags["http.route"]);
});
}

[Fact]
public void Metrics_DisableHttpMetricsWithMetadata_NoMetrics()
{
// Arrange
var hostingEventSource = new HostingEventSource(Guid.NewGuid().ToString());

var testMeterFactory = new TestMeterFactory();
using var activeRequestsCollector = new MetricCollector<long>(testMeterFactory, HostingMetrics.MeterName, "http.server.active_requests");
using var requestDurationCollector = new MetricCollector<double>(testMeterFactory, HostingMetrics.MeterName, "http.server.request.duration");

// Act
var hostingApplication = CreateApplication(out var features, eventSource: hostingEventSource, meterFactory: testMeterFactory, configure: c =>
{
c.Request.Protocol = "1.1";
c.Request.Scheme = "http";
c.Request.Method = "POST";
c.Request.Host = new HostString("localhost");
c.Request.Path = "/hello";
c.Request.ContentType = "text/plain";
c.Request.ContentLength = 1024;
});
var context = hostingApplication.CreateContext(features);

Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(),
m =>
{
Assert.Equal(1, m.Value);
Assert.Equal("http", m.Tags["url.scheme"]);
Assert.Equal("POST", m.Tags["http.request.method"]);
});

context.HttpContext.SetEndpoint(new Endpoint(
c => Task.CompletedTask,
new EndpointMetadataCollection(new TestRouteDiagnosticsMetadata(), new DisableHttpMetricsAttribute()),
"Test endpoint"));

hostingApplication.DisposeContext(context, null);

// Assert
Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(),
m =>
{
Assert.Equal(1, m.Value);
Assert.Equal("http", m.Tags["url.scheme"]);
Assert.Equal("POST", m.Tags["http.request.method"]);
},
m =>
{
Assert.Equal(-1, m.Value);
Assert.Equal("http", m.Tags["url.scheme"]);
Assert.Equal("POST", m.Tags["http.request.method"]);
});
Assert.Empty(requestDurationCollector.GetMeasurementSnapshot());
}

[Fact]
public void Metrics_DisableHttpMetricsWithFeature_NoMetrics()
{
// Arrange
var hostingEventSource = new HostingEventSource(Guid.NewGuid().ToString());

var testMeterFactory = new TestMeterFactory();
using var activeRequestsCollector = new MetricCollector<long>(testMeterFactory, HostingMetrics.MeterName, "http.server.active_requests");
using var requestDurationCollector = new MetricCollector<double>(testMeterFactory, HostingMetrics.MeterName, "http.server.request.duration");

// Act
var hostingApplication = CreateApplication(out var features, eventSource: hostingEventSource, meterFactory: testMeterFactory, configure: c =>
{
c.Request.Protocol = "1.1";
c.Request.Scheme = "http";
c.Request.Method = "POST";
c.Request.Host = new HostString("localhost");
c.Request.Path = "/hello";
c.Request.ContentType = "text/plain";
c.Request.ContentLength = 1024;
});
var context = hostingApplication.CreateContext(features);

Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(),
m =>
{
Assert.Equal(1, m.Value);
Assert.Equal("http", m.Tags["url.scheme"]);
Assert.Equal("POST", m.Tags["http.request.method"]);
});

context.HttpContext.Features.Get<IHttpMetricsTagsFeature>().MetricsDisabled = true;

// Assert 1
Assert.True(context.MetricsTagsFeature.MetricsDisabled);

hostingApplication.DisposeContext(context, null);

// Assert 2
Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(),
m =>
{
Assert.Equal(1, m.Value);
Assert.Equal("http", m.Tags["url.scheme"]);
Assert.Equal("POST", m.Tags["http.request.method"]);
},
m =>
{
Assert.Equal(-1, m.Value);
Assert.Equal("http", m.Tags["url.scheme"]);
Assert.Equal("POST", m.Tags["http.request.method"]);
});
Assert.Empty(requestDurationCollector.GetMeasurementSnapshot());
Assert.False(context.MetricsTagsFeature.MetricsDisabled);
}

private sealed class TestRouteDiagnosticsMetadata : IRouteDiagnosticsMetadata
{
public string Route { get; } = "hello/{name}";
}

[Fact]
public void DisposeContextDoesNotThrowWhenContextScopeIsNull()
{
Expand Down
1 change: 1 addition & 0 deletions src/Hosting/Hosting/test/HostingMetricsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ public void IHttpMetricsTagsFeatureNotUsedFromFeatureCollection()
private sealed class TestHttpMetricsTagsFeature : IHttpMetricsTagsFeature
{
public ICollection<KeyValuePair<string, object>> Tags { get; } = new Collection<KeyValuePair<string, object>>();
public bool MetricsDisabled { get; set; }
}

private static HostingApplication CreateApplication(IHttpContextFactory httpContextFactory = null, bool useHttpContextAccessor = false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// 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.Http.Metadata;

/// <summary>
/// A marker interface which can be used to identify metadata that disables HTTP request duration metrics.
/// </summary>
public interface IDisableHttpMetricsMetadata
{
}
1 change: 1 addition & 0 deletions src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ Microsoft.AspNetCore.Http.HostString.HostString(string? value) -> void
*REMOVED*Microsoft.AspNetCore.Http.HostString.Value.get -> string!
Microsoft.AspNetCore.Http.HostString.Value.get -> string?
Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, string![]!>>! errors) -> void
Microsoft.AspNetCore.Http.Metadata.IDisableHttpMetricsMetadata
14 changes: 14 additions & 0 deletions src/Http/Http.Extensions/src/DisableHttpMetricsAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http.Metadata;

namespace Microsoft.AspNetCore.Http;

/// <summary>
/// Specifies that HTTP request duration metrics is disabled for an endpoint.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public sealed class DisableHttpMetricsAttribute : Attribute, IDisableHttpMetricsMetadata
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Builder;

/// <summary>
/// HTTP metrics extension methods for <see cref="IEndpointConventionBuilder"/>.
/// </summary>
public static class HttpMetricsEndpointConventionBuilderExtensions
{
/// <summary>
/// Specifies that HTTP request duration metrics is disabled for an endpoint.
/// </summary>
/// <typeparam name="TBuilder">The type of endpoint convention builder.</typeparam>
/// <param name="builder">The endpoint convention builder.</param>
/// <returns>The original convention builder parameter.</returns>
public static TBuilder DisableHttpMetrics<TBuilder>(this TBuilder builder) where TBuilder : IEndpointConventionBuilder
{
builder.Add(b => b.Metadata.Add(new DisableHttpMetricsAttribute()));
return builder;
}
}
4 changes: 4 additions & 0 deletions src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
#nullable enable
Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, string![]!>>! errors) -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions)
Microsoft.AspNetCore.Builder.HttpMetricsEndpointConventionBuilderExtensions
Microsoft.AspNetCore.Http.DisableHttpMetricsAttribute
Microsoft.AspNetCore.Http.DisableHttpMetricsAttribute.DisableHttpMetricsAttribute() -> void
static Microsoft.AspNetCore.Builder.HttpMetricsEndpointConventionBuilderExtensions.DisableHttpMetrics<TBuilder>(this TBuilder builder) -> TBuilder
8 changes: 7 additions & 1 deletion src/Http/Http.Features/src/IHttpMetricsTagsFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@
namespace Microsoft.AspNetCore.Http.Features;

/// <summary>
/// Provides access to tags added to the metrics HTTP request counter. This feature isn't set if the counter isn't enabled.
/// Provides access to tags added to HTTP request duration metrics. This feature isn't set if the counter isn't enabled.
/// </summary>
public interface IHttpMetricsTagsFeature
{
/// <summary>
/// Gets the tag collection.
/// </summary>
ICollection<KeyValuePair<string, object?>> Tags { get; }

// MetricsDisabled was added after the initial release of this interface and is intentionally a DIM property.
/// <summary>
/// Gets or sets a flag that disables recording HTTP request duration metrics for the current HTTP request.
/// </summary>
public bool MetricsDisabled { get; set; }
}
2 changes: 2 additions & 0 deletions src/Http/Http.Features/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.AspNetCore.Http.Features.IHttpMetricsTagsFeature.MetricsDisabled.get -> bool
Microsoft.AspNetCore.Http.Features.IHttpMetricsTagsFeature.MetricsDisabled.set -> void
2 changes: 1 addition & 1 deletion src/Http/Routing/src/EndpointNameAttribute.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.

using Microsoft.AspNetCore.Http;
Expand Down

0 comments on commit 67924e5

Please sign in to comment.