Skip to content

Commit 639952a

Browse files
authored
Add disable HTTP metrics endpoint metadata (#56036)
1 parent 7498cea commit 639952a

23 files changed

+307
-15
lines changed

src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,12 @@ public void RequestEnd(HttpContext httpContext, Exception? exception, HostingApp
154154

155155
if (context.MetricsEnabled)
156156
{
157+
Debug.Assert(context.MetricsTagsFeature != null, "MetricsTagsFeature should be set if MetricsEnabled is true.");
158+
157159
var endpoint = HttpExtensions.GetOriginalEndpoint(httpContext);
160+
var disableHttpRequestDurationMetric = endpoint?.Metadata.GetMetadata<IDisableHttpMetricsMetadata>() != null || context.MetricsTagsFeature.MetricsDisabled;
158161
var route = endpoint?.Metadata.GetMetadata<IRouteDiagnosticsMetadata>()?.Route;
159162

160-
Debug.Assert(context.MetricsTagsFeature != null, "MetricsTagsFeature should be set if MetricsEnabled is true.");
161-
162163
_metrics.RequestEnd(
163164
context.MetricsTagsFeature.Protocol!,
164165
context.MetricsTagsFeature.Scheme!,
@@ -169,7 +170,8 @@ public void RequestEnd(HttpContext httpContext, Exception? exception, HostingApp
169170
exception,
170171
context.MetricsTagsFeature.TagsList,
171172
startTimestamp,
172-
currentTimestamp);
173+
currentTimestamp,
174+
disableHttpRequestDurationMetric);
173175
}
174176

175177
if (reachedPipelineEnd)

src/Hosting/Hosting/src/Internal/HostingMetrics.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public void RequestStart(string scheme, string method)
4242
_activeRequestsCounter.Add(1, tags);
4343
}
4444

45-
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)
45+
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)
4646
{
4747
var tags = new TagList();
4848
InitializeRequestTags(ref tags, scheme, method);
@@ -53,7 +53,7 @@ public void RequestEnd(string protocol, string scheme, string method, string? ro
5353
_activeRequestsCounter.Add(-1, tags);
5454
}
5555

56-
if (_requestDuration.Enabled)
56+
if (!disableHttpRequestDurationMetric && _requestDuration.Enabled)
5757
{
5858
if (TryGetHttpVersion(protocol, out var httpVersion))
5959
{

src/Hosting/Hosting/src/Internal/HttpMetricsTagsFeature.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace Microsoft.AspNetCore.Hosting;
88
internal sealed class HttpMetricsTagsFeature : IHttpMetricsTagsFeature
99
{
1010
ICollection<KeyValuePair<string, object?>> IHttpMetricsTagsFeature.Tags => TagsList;
11+
public bool MetricsDisabled { get; set; }
1112

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

@@ -20,6 +21,7 @@ internal sealed class HttpMetricsTagsFeature : IHttpMetricsTagsFeature
2021
public void Reset()
2122
{
2223
TagsList.Clear();
24+
MetricsDisabled = false;
2325

2426
Method = null;
2527
Scheme = null;

src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs

Lines changed: 178 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
using System.Diagnostics.Metrics;
66
using System.Diagnostics.Tracing;
77
using System.Reflection;
8+
using Microsoft.AspNetCore.Builder;
89
using Microsoft.AspNetCore.Http;
910
using Microsoft.AspNetCore.Http.Features;
11+
using Microsoft.AspNetCore.Http.Metadata;
1012
using Microsoft.AspNetCore.Internal;
1113
using Microsoft.AspNetCore.InternalTesting;
1214
using Microsoft.Extensions.Diagnostics.Metrics;
@@ -228,7 +230,6 @@ public void Metrics_RequestChanges_OriginalValuesUsed()
228230

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

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

283+
[Fact]
284+
public void Metrics_Route_RouteTagReported()
285+
{
286+
// Arrange
287+
var hostingEventSource = new HostingEventSource(Guid.NewGuid().ToString());
288+
289+
var testMeterFactory = new TestMeterFactory();
290+
using var activeRequestsCollector = new MetricCollector<long>(testMeterFactory, HostingMetrics.MeterName, "http.server.active_requests");
291+
using var requestDurationCollector = new MetricCollector<double>(testMeterFactory, HostingMetrics.MeterName, "http.server.request.duration");
292+
293+
// Act
294+
var hostingApplication = CreateApplication(out var features, eventSource: hostingEventSource, meterFactory: testMeterFactory, configure: c =>
295+
{
296+
c.Request.Protocol = "1.1";
297+
c.Request.Scheme = "http";
298+
c.Request.Method = "POST";
299+
c.Request.Host = new HostString("localhost");
300+
c.Request.Path = "/hello";
301+
c.Request.ContentType = "text/plain";
302+
c.Request.ContentLength = 1024;
303+
});
304+
var context = hostingApplication.CreateContext(features);
305+
306+
Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(),
307+
m =>
308+
{
309+
Assert.Equal(1, m.Value);
310+
Assert.Equal("http", m.Tags["url.scheme"]);
311+
Assert.Equal("POST", m.Tags["http.request.method"]);
312+
});
313+
314+
context.HttpContext.SetEndpoint(new Endpoint(
315+
c => Task.CompletedTask,
316+
new EndpointMetadataCollection(new TestRouteDiagnosticsMetadata()),
317+
"Test endpoint"));
318+
319+
hostingApplication.DisposeContext(context, null);
320+
321+
// Assert
322+
Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(),
323+
m =>
324+
{
325+
Assert.Equal(1, m.Value);
326+
Assert.Equal("http", m.Tags["url.scheme"]);
327+
Assert.Equal("POST", m.Tags["http.request.method"]);
328+
},
329+
m =>
330+
{
331+
Assert.Equal(-1, m.Value);
332+
Assert.Equal("http", m.Tags["url.scheme"]);
333+
Assert.Equal("POST", m.Tags["http.request.method"]);
334+
});
335+
Assert.Collection(requestDurationCollector.GetMeasurementSnapshot(),
336+
m =>
337+
{
338+
Assert.True(m.Value > 0);
339+
Assert.Equal("hello/{name}", m.Tags["http.route"]);
340+
});
341+
}
342+
343+
[Fact]
344+
public void Metrics_DisableHttpMetricsWithMetadata_NoMetrics()
345+
{
346+
// Arrange
347+
var hostingEventSource = new HostingEventSource(Guid.NewGuid().ToString());
348+
349+
var testMeterFactory = new TestMeterFactory();
350+
using var activeRequestsCollector = new MetricCollector<long>(testMeterFactory, HostingMetrics.MeterName, "http.server.active_requests");
351+
using var requestDurationCollector = new MetricCollector<double>(testMeterFactory, HostingMetrics.MeterName, "http.server.request.duration");
352+
353+
// Act
354+
var hostingApplication = CreateApplication(out var features, eventSource: hostingEventSource, meterFactory: testMeterFactory, configure: c =>
355+
{
356+
c.Request.Protocol = "1.1";
357+
c.Request.Scheme = "http";
358+
c.Request.Method = "POST";
359+
c.Request.Host = new HostString("localhost");
360+
c.Request.Path = "/hello";
361+
c.Request.ContentType = "text/plain";
362+
c.Request.ContentLength = 1024;
363+
});
364+
var context = hostingApplication.CreateContext(features);
365+
366+
Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(),
367+
m =>
368+
{
369+
Assert.Equal(1, m.Value);
370+
Assert.Equal("http", m.Tags["url.scheme"]);
371+
Assert.Equal("POST", m.Tags["http.request.method"]);
372+
});
373+
374+
context.HttpContext.SetEndpoint(new Endpoint(
375+
c => Task.CompletedTask,
376+
new EndpointMetadataCollection(new TestRouteDiagnosticsMetadata(), new DisableHttpMetricsAttribute()),
377+
"Test endpoint"));
378+
379+
hostingApplication.DisposeContext(context, null);
380+
381+
// Assert
382+
Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(),
383+
m =>
384+
{
385+
Assert.Equal(1, m.Value);
386+
Assert.Equal("http", m.Tags["url.scheme"]);
387+
Assert.Equal("POST", m.Tags["http.request.method"]);
388+
},
389+
m =>
390+
{
391+
Assert.Equal(-1, m.Value);
392+
Assert.Equal("http", m.Tags["url.scheme"]);
393+
Assert.Equal("POST", m.Tags["http.request.method"]);
394+
});
395+
Assert.Empty(requestDurationCollector.GetMeasurementSnapshot());
396+
}
397+
398+
[Fact]
399+
public void Metrics_DisableHttpMetricsWithFeature_NoMetrics()
400+
{
401+
// Arrange
402+
var hostingEventSource = new HostingEventSource(Guid.NewGuid().ToString());
403+
404+
var testMeterFactory = new TestMeterFactory();
405+
using var activeRequestsCollector = new MetricCollector<long>(testMeterFactory, HostingMetrics.MeterName, "http.server.active_requests");
406+
using var requestDurationCollector = new MetricCollector<double>(testMeterFactory, HostingMetrics.MeterName, "http.server.request.duration");
407+
408+
// Act
409+
var hostingApplication = CreateApplication(out var features, eventSource: hostingEventSource, meterFactory: testMeterFactory, configure: c =>
410+
{
411+
c.Request.Protocol = "1.1";
412+
c.Request.Scheme = "http";
413+
c.Request.Method = "POST";
414+
c.Request.Host = new HostString("localhost");
415+
c.Request.Path = "/hello";
416+
c.Request.ContentType = "text/plain";
417+
c.Request.ContentLength = 1024;
418+
});
419+
var context = hostingApplication.CreateContext(features);
420+
421+
Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(),
422+
m =>
423+
{
424+
Assert.Equal(1, m.Value);
425+
Assert.Equal("http", m.Tags["url.scheme"]);
426+
Assert.Equal("POST", m.Tags["http.request.method"]);
427+
});
428+
429+
context.HttpContext.Features.Get<IHttpMetricsTagsFeature>().MetricsDisabled = true;
430+
431+
// Assert 1
432+
Assert.True(context.MetricsTagsFeature.MetricsDisabled);
433+
434+
hostingApplication.DisposeContext(context, null);
435+
436+
// Assert 2
437+
Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(),
438+
m =>
439+
{
440+
Assert.Equal(1, m.Value);
441+
Assert.Equal("http", m.Tags["url.scheme"]);
442+
Assert.Equal("POST", m.Tags["http.request.method"]);
443+
},
444+
m =>
445+
{
446+
Assert.Equal(-1, m.Value);
447+
Assert.Equal("http", m.Tags["url.scheme"]);
448+
Assert.Equal("POST", m.Tags["http.request.method"]);
449+
});
450+
Assert.Empty(requestDurationCollector.GetMeasurementSnapshot());
451+
Assert.False(context.MetricsTagsFeature.MetricsDisabled);
452+
}
453+
454+
private sealed class TestRouteDiagnosticsMetadata : IRouteDiagnosticsMetadata
455+
{
456+
public string Route { get; } = "hello/{name}";
457+
}
458+
282459
[Fact]
283460
public void DisposeContextDoesNotThrowWhenContextScopeIsNull()
284461
{

src/Hosting/Hosting/test/HostingMetricsTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ public void IHttpMetricsTagsFeatureNotUsedFromFeatureCollection()
180180
private sealed class TestHttpMetricsTagsFeature : IHttpMetricsTagsFeature
181181
{
182182
public ICollection<KeyValuePair<string, object>> Tags { get; } = new Collection<KeyValuePair<string, object>>();
183+
public bool MetricsDisabled { get; set; }
183184
}
184185

185186
private static HostingApplication CreateApplication(IHttpContextFactory httpContextFactory = null, bool useHttpContextAccessor = false,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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.Http.Metadata;
5+
6+
/// <summary>
7+
/// A marker interface which can be used to identify metadata that disables HTTP request duration metrics.
8+
/// </summary>
9+
public interface IDisableHttpMetricsMetadata
10+
{
11+
}

src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ Microsoft.AspNetCore.Http.HostString.HostString(string? value) -> void
44
*REMOVED*Microsoft.AspNetCore.Http.HostString.Value.get -> string!
55
Microsoft.AspNetCore.Http.HostString.Value.get -> string?
66
Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, string![]!>>! errors) -> void
7+
Microsoft.AspNetCore.Http.Metadata.IDisableHttpMetricsMetadata
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 Microsoft.AspNetCore.Http.Metadata;
6+
7+
namespace Microsoft.AspNetCore.Http;
8+
9+
/// <summary>
10+
/// Specifies that HTTP request duration metrics is disabled for an endpoint.
11+
/// </summary>
12+
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
13+
[DebuggerDisplay("{ToString(),nq}")]
14+
public sealed class DisableHttpMetricsAttribute : Attribute, IDisableHttpMetricsMetadata
15+
{
16+
/// <inheritdoc/>
17+
public override string ToString()
18+
{
19+
return "DisableHttpMetrics";
20+
}
21+
}

src/Http/Http.Extensions/src/EndpointDescriptionAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public EndpointDescriptionAttribute(string description)
2929
/// <inheritdoc />
3030
public string Description { get; }
3131

32-
/// <inheritdoc/>>
32+
/// <inheritdoc/>
3333
public override string ToString()
3434
{
3535
return $"Description: {Description ?? "(null)"}";

src/Http/Http.Extensions/src/EndpointSummaryAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public EndpointSummaryAttribute(string summary)
2626
/// <inheritdoc />
2727
public string Summary { get; }
2828

29-
/// <inheritdoc/>>
29+
/// <inheritdoc/>
3030
public override string ToString()
3131
{
3232
return DebuggerHelpers.GetDebugText(nameof(Summary), Summary);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 Microsoft.AspNetCore.Http;
5+
6+
namespace Microsoft.AspNetCore.Builder;
7+
8+
/// <summary>
9+
/// HTTP metrics extension methods for <see cref="IEndpointConventionBuilder"/>.
10+
/// </summary>
11+
public static class HttpMetricsEndpointConventionBuilderExtensions
12+
{
13+
private static readonly DisableHttpMetricsAttribute _disableHttpMetricsAttribute = new DisableHttpMetricsAttribute();
14+
15+
/// <summary>
16+
/// Specifies that HTTP request duration metrics is disabled for an endpoint.
17+
/// </summary>
18+
/// <typeparam name="TBuilder">The type of endpoint convention builder.</typeparam>
19+
/// <param name="builder">The endpoint convention builder.</param>
20+
/// <returns>The original convention builder parameter.</returns>
21+
public static TBuilder DisableHttpMetrics<TBuilder>(this TBuilder builder) where TBuilder : IEndpointConventionBuilder
22+
{
23+
builder.Add(b => b.Metadata.Add(_disableHttpMetricsAttribute));
24+
return builder;
25+
}
26+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
#nullable enable
22
Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, string![]!>>! errors) -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions)
3+
Microsoft.AspNetCore.Builder.HttpMetricsEndpointConventionBuilderExtensions
4+
Microsoft.AspNetCore.Http.DisableHttpMetricsAttribute
5+
Microsoft.AspNetCore.Http.DisableHttpMetricsAttribute.DisableHttpMetricsAttribute() -> void
6+
override Microsoft.AspNetCore.Http.DisableHttpMetricsAttribute.ToString() -> string!
7+
static Microsoft.AspNetCore.Builder.HttpMetricsEndpointConventionBuilderExtensions.DisableHttpMetrics<TBuilder>(this TBuilder builder) -> TBuilder
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 Microsoft.AspNetCore.Builder;
5+
using Microsoft.AspNetCore.Http.Metadata;
6+
7+
namespace Microsoft.AspNetCore.Http.Extensions.Tests;
8+
9+
public partial class HttpMetricsEndpointConventionBuilderExtensionsTests
10+
{
11+
[Fact]
12+
public void DisableHttpMetrics_AddsMetadata()
13+
{
14+
var builder = new TestEndointConventionBuilder();
15+
builder.DisableHttpMetrics();
16+
17+
Assert.IsAssignableFrom<IDisableHttpMetricsMetadata>(Assert.Single(builder.Metadata));
18+
}
19+
20+
private sealed class TestEndointConventionBuilder : EndpointBuilder, IEndpointConventionBuilder
21+
{
22+
public void Add(Action<EndpointBuilder> convention)
23+
{
24+
convention(this);
25+
}
26+
27+
public override Endpoint Build() => throw new NotImplementedException();
28+
}
29+
}

0 commit comments

Comments
 (0)