Skip to content

Add disable HTTP metrics endpoint metadata #56036

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 3 commits into from
Jul 12, 2024
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
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the flag interface for? Relatedly, if there's only one concrete implementation, can we just use that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using interfaces for endpoint metadata is the normal pattern. The only implementation is an attribute. Internally, builder methods that add this metadata to the endpoint just add the attribute.

{
}
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
21 changes: 21 additions & 0 deletions src/Http/Http.Extensions/src/DisableHttpMetricsAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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 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)]
[DebuggerDisplay("{ToString(),nq}")]
public sealed class DisableHttpMetricsAttribute : Attribute, IDisableHttpMetricsMetadata
{
/// <inheritdoc/>
public override string ToString()
{
return "DisableHttpMetrics";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public EndpointDescriptionAttribute(string description)
/// <inheritdoc />
public string Description { get; }

/// <inheritdoc/>>
/// <inheritdoc/>
public override string ToString()
{
return $"Description: {Description ?? "(null)"}";
Expand Down
2 changes: 1 addition & 1 deletion src/Http/Http.Extensions/src/EndpointSummaryAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public EndpointSummaryAttribute(string summary)
/// <inheritdoc />
public string Summary { get; }

/// <inheritdoc/>>
/// <inheritdoc/>
public override string ToString()
{
return DebuggerHelpers.GetDebugText(nameof(Summary), Summary);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// 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
{
private static readonly DisableHttpMetricsAttribute _disableHttpMetricsAttribute = new DisableHttpMetricsAttribute();

/// <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(_disableHttpMetricsAttribute));
return builder;
}
}
5 changes: 5 additions & 0 deletions src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
#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
override Microsoft.AspNetCore.Http.DisableHttpMetricsAttribute.ToString() -> string!
static Microsoft.AspNetCore.Builder.HttpMetricsEndpointConventionBuilderExtensions.DisableHttpMetrics<TBuilder>(this TBuilder builder) -> TBuilder
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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.Builder;
using Microsoft.AspNetCore.Http.Metadata;

namespace Microsoft.AspNetCore.Http.Extensions.Tests;

public partial class HttpMetricsEndpointConventionBuilderExtensionsTests
{
[Fact]
public void DisableHttpMetrics_AddsMetadata()
{
var builder = new TestEndointConventionBuilder();
builder.DisableHttpMetrics();

Assert.IsAssignableFrom<IDisableHttpMetricsMetadata>(Assert.Single(builder.Metadata));
}

private sealed class TestEndointConventionBuilder : EndpointBuilder, IEndpointConventionBuilder
{
public void Add(Action<EndpointBuilder> convention)
{
convention(this);
}

public override Endpoint Build() => throw new NotImplementedException();
}
}
Loading
Loading