Skip to content

Commit 67924e5

Browse files
committed
Add disable HTTP metrics endpoint metadata
1 parent 1313a4d commit 67924e5

13 files changed

+252
-8
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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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.Metadata;
5+
6+
namespace Microsoft.AspNetCore.Http;
7+
8+
/// <summary>
9+
/// Specifies that HTTP request duration metrics is disabled for an endpoint.
10+
/// </summary>
11+
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
12+
public sealed class DisableHttpMetricsAttribute : Attribute, IDisableHttpMetricsMetadata
13+
{
14+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
/// <summary>
14+
/// Specifies that HTTP request duration metrics is disabled for an endpoint.
15+
/// </summary>
16+
/// <typeparam name="TBuilder">The type of endpoint convention builder.</typeparam>
17+
/// <param name="builder">The endpoint convention builder.</param>
18+
/// <returns>The original convention builder parameter.</returns>
19+
public static TBuilder DisableHttpMetrics<TBuilder>(this TBuilder builder) where TBuilder : IEndpointConventionBuilder
20+
{
21+
builder.Add(b => b.Metadata.Add(new DisableHttpMetricsAttribute()));
22+
return builder;
23+
}
24+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
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+
static Microsoft.AspNetCore.Builder.HttpMetricsEndpointConventionBuilderExtensions.DisableHttpMetrics<TBuilder>(this TBuilder builder) -> TBuilder

src/Http/Http.Features/src/IHttpMetricsTagsFeature.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@
44
namespace Microsoft.AspNetCore.Http.Features;
55

66
/// <summary>
7-
/// Provides access to tags added to the metrics HTTP request counter. This feature isn't set if the counter isn't enabled.
7+
/// Provides access to tags added to HTTP request duration metrics. This feature isn't set if the counter isn't enabled.
88
/// </summary>
99
public interface IHttpMetricsTagsFeature
1010
{
1111
/// <summary>
1212
/// Gets the tag collection.
1313
/// </summary>
1414
ICollection<KeyValuePair<string, object?>> Tags { get; }
15+
16+
// MetricsDisabled was added after the initial release of this interface and is intentionally a DIM property.
17+
/// <summary>
18+
/// Gets or sets a flag that disables recording HTTP request duration metrics for the current HTTP request.
19+
/// </summary>
20+
public bool MetricsDisabled { get; set; }
1521
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Http.Features.IHttpMetricsTagsFeature.MetricsDisabled.get -> bool
3+
Microsoft.AspNetCore.Http.Features.IHttpMetricsTagsFeature.MetricsDisabled.set -> void

src/Http/Routing/src/EndpointNameAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.AspNetCore.Http;

0 commit comments

Comments
 (0)