diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index c5658692b0dd..0d045ee520d8 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -148,15 +148,11 @@ public void RequestEnd(HttpContext httpContext, Exception? exception, HostingApp httpContext.Request.Host, route, httpContext.Response.StatusCode, + reachedPipelineEnd, exception, customTags, startTimestamp, currentTimestamp); - - if (reachedPipelineEnd) - { - _metrics.UnhandledRequest(); - } } if (reachedPipelineEnd) diff --git a/src/Hosting/Hosting/src/Internal/HostingMetrics.cs b/src/Hosting/Hosting/src/Internal/HostingMetrics.cs index 3fd09d9bac3e..3eca109f9e0f 100644 --- a/src/Hosting/Hosting/src/Internal/HostingMetrics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingMetrics.cs @@ -3,6 +3,7 @@ using System.Collections.Frozen; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using Microsoft.AspNetCore.Http; @@ -13,26 +14,22 @@ internal sealed class HostingMetrics : IDisposable public const string MeterName = "Microsoft.AspNetCore.Hosting"; private readonly Meter _meter; - private readonly UpDownCounter _currentRequestsCounter; + private readonly UpDownCounter _activeRequestsCounter; private readonly Histogram _requestDuration; - private readonly Counter _unhandledRequestsCounter; public HostingMetrics(IMeterFactory meterFactory) { _meter = meterFactory.Create(MeterName); - _currentRequestsCounter = _meter.CreateUpDownCounter( - "http-server-current-requests", + _activeRequestsCounter = _meter.CreateUpDownCounter( + "http.server.active_requests", + unit: "{request}", description: "Number of HTTP requests that are currently active on the server."); _requestDuration = _meter.CreateHistogram( - "http-server-request-duration", + "http.server.request.duration", unit: "s", - description: "The duration of HTTP requests on the server."); - - _unhandledRequestsCounter = _meter.CreateCounter( - "http-server-unhandled-requests", - description: "Number of HTTP requests that reached the end of the middleware pipeline without being handled by application code."); + description: "Measures the duration of inbound HTTP requests."); } // Note: Calling code checks whether counter is enabled. @@ -41,35 +38,43 @@ public void RequestStart(bool isHttps, string scheme, string method, HostString // Tags must match request end. var tags = new TagList(); InitializeRequestTags(ref tags, isHttps, scheme, method, host); - _currentRequestsCounter.Add(1, tags); + _activeRequestsCounter.Add(1, tags); } - public void RequestEnd(string protocol, bool isHttps, string scheme, string method, HostString host, string? route, int statusCode, Exception? exception, List>? customTags, long startTimestamp, long currentTimestamp) + public void RequestEnd(string protocol, bool isHttps, string scheme, string method, HostString host, string? route, int statusCode, bool unhandledRequest, Exception? exception, List>? customTags, long startTimestamp, long currentTimestamp) { var tags = new TagList(); InitializeRequestTags(ref tags, isHttps, scheme, method, host); // Tags must match request start. - if (_currentRequestsCounter.Enabled) + if (_activeRequestsCounter.Enabled) { - _currentRequestsCounter.Add(-1, tags); + _activeRequestsCounter.Add(-1, tags); } if (_requestDuration.Enabled) { - tags.Add("protocol", protocol); + tags.Add("network.protocol.name", "http"); + if (TryGetHttpVersion(protocol, out var httpVersion)) + { + tags.Add("network.protocol.version", httpVersion); + } + if (unhandledRequest) + { + tags.Add("aspnetcore.request.is_unhandled", true); + } // Add information gathered during request. - tags.Add("status-code", GetBoxedStatusCode(statusCode)); + tags.Add("http.response.status_code", GetBoxedStatusCode(statusCode)); if (route != null) { - tags.Add("route", route); + tags.Add("http.route", route); } // This exception is only present if there is an unhandled exception. - // An exception caught by ExceptionHandlerMiddleware and DeveloperExceptionMiddleware isn't thrown to here. Instead, those middleware add exception-name to custom tags. + // An exception caught by ExceptionHandlerMiddleware and DeveloperExceptionMiddleware isn't thrown to here. Instead, those middleware add exception.type to custom tags. if (exception != null) { - tags.Add("exception-name", exception.GetType().FullName); + tags.Add("exception.type", exception.GetType().FullName); } if (customTags != null) { @@ -84,25 +89,25 @@ public void RequestEnd(string protocol, bool isHttps, string scheme, string meth } } - public void UnhandledRequest() - { - _unhandledRequestsCounter.Add(1); - } - public void Dispose() { _meter.Dispose(); } - public bool IsEnabled() => _currentRequestsCounter.Enabled || _requestDuration.Enabled || _unhandledRequestsCounter.Enabled; + public bool IsEnabled() => _activeRequestsCounter.Enabled || _requestDuration.Enabled; private static void InitializeRequestTags(ref TagList tags, bool isHttps, string scheme, string method, HostString host) { - tags.Add("scheme", scheme); - tags.Add("method", method); + tags.Add("url.scheme", scheme); + tags.Add("http.request.method", ResolveHttpMethod(method)); + + _ = isHttps; + _ = host; + // TODO: Support configuration for enabling host header annotations + /* if (host.HasValue) { - tags.Add("host", host.Host); + tags.Add("server.address", host.Host); // Port is parsed each time it's accessed. Store part in local variable. if (host.Port is { } port) @@ -110,10 +115,11 @@ private static void InitializeRequestTags(ref TagList tags, bool isHttps, string // Add port tag when not the default value for the current scheme if ((isHttps && port != 443) || (!isHttps && port != 80)) { - tags.Add("port", port); + tags.Add("server.port", port); } } } + */ } // Status Codes listed at http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml @@ -197,4 +203,61 @@ private static object GetBoxedStatusCode(int statusCode) return statusCode; } + + private static readonly FrozenDictionary KnownMethods = FrozenDictionary.ToFrozenDictionary(new[] + { + KeyValuePair.Create(HttpMethods.Connect, HttpMethods.Connect), + KeyValuePair.Create(HttpMethods.Delete, HttpMethods.Delete), + KeyValuePair.Create(HttpMethods.Get, HttpMethods.Get), + KeyValuePair.Create(HttpMethods.Head, HttpMethods.Head), + KeyValuePair.Create(HttpMethods.Options, HttpMethods.Options), + KeyValuePair.Create(HttpMethods.Patch, HttpMethods.Patch), + KeyValuePair.Create(HttpMethods.Post, HttpMethods.Post), + KeyValuePair.Create(HttpMethods.Put, HttpMethods.Put), + KeyValuePair.Create(HttpMethods.Trace, HttpMethods.Trace) + }, StringComparer.OrdinalIgnoreCase); + + private static string ResolveHttpMethod(string method) + { + // TODO: Support configuration for configuring known methods + if (KnownMethods.TryGetValue(method, out var result)) + { + // KnownMethods ignores case. Use the value returned by the dictionary to have a consistent case. + return result; + } + return "_OTHER"; + } + + private static bool TryGetHttpVersion(string protocol, [NotNullWhen(true)] out string? version) + { + if (HttpProtocol.IsHttp11(protocol)) + { + version = "1.1"; + return true; + } + if (HttpProtocol.IsHttp2(protocol)) + { + // HTTP/2 only has one version. + version = "2"; + return true; + } + if (HttpProtocol.IsHttp3(protocol)) + { + // HTTP/3 only has one version. + version = "3"; + return true; + } + if (HttpProtocol.IsHttp10(protocol)) + { + version = "1.0"; + return true; + } + if (HttpProtocol.IsHttp09(protocol)) + { + version = "0.9"; + return true; + } + version = null; + return false; + } } diff --git a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs index 8a4db4e13b95..08465dd8547d 100644 --- a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs +++ b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs @@ -53,10 +53,10 @@ public async Task EventCountersAndMetricsValues() var hostingApplication1 = CreateApplication(out var features1, eventSource: hostingEventSource, meterFactory: testMeterFactory1); var hostingApplication2 = CreateApplication(out var features2, eventSource: hostingEventSource, meterFactory: testMeterFactory2); - using var currentRequestsRecorder1 = new MetricCollector(testMeterFactory1, HostingMetrics.MeterName, "http-server-current-requests"); - using var currentRequestsRecorder2 = new MetricCollector(testMeterFactory2, HostingMetrics.MeterName, "http-server-current-requests"); - using var requestDurationRecorder1 = new MetricCollector(testMeterFactory1, HostingMetrics.MeterName, "http-server-request-duration"); - using var requestDurationRecorder2 = new MetricCollector(testMeterFactory2, HostingMetrics.MeterName, "http-server-request-duration"); + using var activeRequestsCollector1 = new MetricCollector(testMeterFactory1, HostingMetrics.MeterName, "http.server.active_requests"); + using var activeRequestsCollector2 = new MetricCollector(testMeterFactory2, HostingMetrics.MeterName, "http.server.active_requests"); + using var requestDurationCollector1 = new MetricCollector(testMeterFactory1, HostingMetrics.MeterName, "http.server.request.duration"); + using var requestDurationCollector2 = new MetricCollector(testMeterFactory2, HostingMetrics.MeterName, "http.server.request.duration"); // Act/Assert 1 var context1 = hostingApplication1.CreateContext(features1); @@ -75,15 +75,15 @@ public async Task EventCountersAndMetricsValues() Assert.Equal(0, await currentRequestValues.FirstOrDefault(v => v == 0)); Assert.Equal(0, await failedRequestValues.FirstOrDefault(v => v == 0)); - Assert.Collection(currentRequestsRecorder1.GetMeasurementSnapshot(), + Assert.Collection(activeRequestsCollector1.GetMeasurementSnapshot(), m => Assert.Equal(1, m.Value), m => Assert.Equal(-1, m.Value)); - Assert.Collection(currentRequestsRecorder2.GetMeasurementSnapshot(), + Assert.Collection(activeRequestsCollector2.GetMeasurementSnapshot(), m => Assert.Equal(1, m.Value), m => Assert.Equal(-1, m.Value)); - Assert.Collection(requestDurationRecorder1.GetMeasurementSnapshot(), + Assert.Collection(requestDurationCollector1.GetMeasurementSnapshot(), m => Assert.True(m.Value > 0)); - Assert.Collection(requestDurationRecorder2.GetMeasurementSnapshot(), + Assert.Collection(requestDurationCollector2.GetMeasurementSnapshot(), m => Assert.True(m.Value > 0)); // Act/Assert 2 @@ -106,20 +106,20 @@ public async Task EventCountersAndMetricsValues() Assert.Equal(0, await currentRequestValues.FirstOrDefault(v => v == 0)); Assert.Equal(2, await failedRequestValues.FirstOrDefault(v => v == 2)); - Assert.Collection(currentRequestsRecorder1.GetMeasurementSnapshot(), + Assert.Collection(activeRequestsCollector1.GetMeasurementSnapshot(), 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(currentRequestsRecorder2.GetMeasurementSnapshot(), + Assert.Collection(activeRequestsCollector2.GetMeasurementSnapshot(), 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(requestDurationRecorder1.GetMeasurementSnapshot(), + Assert.Collection(requestDurationCollector1.GetMeasurementSnapshot(), m => Assert.True(m.Value > 0), m => Assert.True(m.Value > 0)); - Assert.Collection(requestDurationRecorder2.GetMeasurementSnapshot(), + Assert.Collection(requestDurationCollector2.GetMeasurementSnapshot(), m => Assert.True(m.Value > 0), m => Assert.True(m.Value > 0)); } diff --git a/src/Hosting/Hosting/test/HostingMetricsTests.cs b/src/Hosting/Hosting/test/HostingMetricsTests.cs index 33598ec8abce..18cd4f54c54a 100644 --- a/src/Hosting/Hosting/test/HostingMetricsTests.cs +++ b/src/Hosting/Hosting/test/HostingMetricsTests.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Telemetry.Testing.Metering; @@ -26,9 +25,8 @@ public void MultipleRequests() var httpContext = new DefaultHttpContext(); var meter = meterFactory.Meters.Single(); - using var requestDurationCollector = new MetricCollector(meterFactory, HostingMetrics.MeterName, "http-server-request-duration"); - using var currentRequestsCollector = new MetricCollector(meterFactory, HostingMetrics.MeterName, "http-server-current-requests"); - using var unhandledRequestsCollector = new MetricCollector(meterFactory, HostingMetrics.MeterName, "http-server-unhandled-requests"); + using var requestDurationCollector = new MetricCollector(meterFactory, HostingMetrics.MeterName, "http.server.request.duration"); + using var activeRequestsCollector = new MetricCollector(meterFactory, HostingMetrics.MeterName, "http.server.active_requests"); // Act/Assert Assert.Equal(HostingMetrics.MeterName, meter.Name); @@ -40,11 +38,11 @@ public void MultipleRequests() context1.HttpContext.Response.StatusCode = StatusCodes.Status200OK; hostingApplication.DisposeContext(context1, null); - Assert.Collection(currentRequestsCollector.GetMeasurementSnapshot(), + Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(), m => Assert.Equal(1, m.Value), m => Assert.Equal(-1, m.Value)); Assert.Collection(requestDurationCollector.GetMeasurementSnapshot(), - m => AssertRequestDuration(m, HttpProtocol.Http11, StatusCodes.Status200OK)); + m => AssertRequestDuration(m, "1.1", StatusCodes.Status200OK)); // Request 2 (after failure) httpContext.Request.Protocol = HttpProtocol.Http2; @@ -52,14 +50,14 @@ public void MultipleRequests() context2.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; hostingApplication.DisposeContext(context2, new InvalidOperationException("Test error")); - Assert.Collection(currentRequestsCollector.GetMeasurementSnapshot(), + Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(), 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(requestDurationCollector.GetMeasurementSnapshot(), - m => AssertRequestDuration(m, HttpProtocol.Http11, StatusCodes.Status200OK), - m => AssertRequestDuration(m, HttpProtocol.Http2, StatusCodes.Status500InternalServerError, exceptionName: "System.InvalidOperationException")); + m => AssertRequestDuration(m, "1.1", StatusCodes.Status200OK), + m => AssertRequestDuration(m, "2", StatusCodes.Status500InternalServerError, exceptionName: "System.InvalidOperationException")); // Request 3 httpContext.Request.Protocol = HttpProtocol.Http3; @@ -67,19 +65,19 @@ public void MultipleRequests() context3.HttpContext.Items["__RequestUnhandled"] = true; context3.HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; - Assert.Collection(currentRequestsCollector.GetMeasurementSnapshot(), + Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(), 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(requestDurationCollector.GetMeasurementSnapshot(), - m => AssertRequestDuration(m, HttpProtocol.Http11, StatusCodes.Status200OK), - m => AssertRequestDuration(m, HttpProtocol.Http2, StatusCodes.Status500InternalServerError, exceptionName: "System.InvalidOperationException")); + m => AssertRequestDuration(m, "1.1", StatusCodes.Status200OK), + m => AssertRequestDuration(m, "2", StatusCodes.Status500InternalServerError, exceptionName: "System.InvalidOperationException")); hostingApplication.DisposeContext(context3, null); - Assert.Collection(currentRequestsCollector.GetMeasurementSnapshot(), + Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(), m => Assert.Equal(1, m.Value), m => Assert.Equal(-1, m.Value), m => Assert.Equal(1, m.Value), @@ -87,24 +85,31 @@ public void MultipleRequests() m => Assert.Equal(1, m.Value), m => Assert.Equal(-1, m.Value)); Assert.Collection(requestDurationCollector.GetMeasurementSnapshot(), - m => AssertRequestDuration(m, HttpProtocol.Http11, StatusCodes.Status200OK), - m => AssertRequestDuration(m, HttpProtocol.Http2, StatusCodes.Status500InternalServerError, exceptionName: "System.InvalidOperationException"), - m => AssertRequestDuration(m, HttpProtocol.Http3, StatusCodes.Status404NotFound)); - Assert.Collection(unhandledRequestsCollector.GetMeasurementSnapshot(), - m => Assert.Equal(1, m.Value)); + m => AssertRequestDuration(m, "1.1", StatusCodes.Status200OK), + m => AssertRequestDuration(m, "2", StatusCodes.Status500InternalServerError, exceptionName: "System.InvalidOperationException"), + m => AssertRequestDuration(m, "3", StatusCodes.Status404NotFound, unhandledRequest: true)); - static void AssertRequestDuration(CollectedMeasurement measurement, string protocol, int statusCode, string exceptionName = null) + static void AssertRequestDuration(CollectedMeasurement measurement, string httpVersion, int statusCode, string exceptionName = null, bool? unhandledRequest = null) { Assert.True(measurement.Value > 0); - Assert.Equal(protocol, (string)measurement.Tags["protocol"]); - Assert.Equal(statusCode, (int)measurement.Tags["status-code"]); + Assert.Equal("http", (string)measurement.Tags["network.protocol.name"]); + Assert.Equal(httpVersion, (string)measurement.Tags["network.protocol.version"]); + Assert.Equal(statusCode, (int)measurement.Tags["http.response.status_code"]); if (exceptionName == null) { - Assert.DoesNotContain(measurement.Tags.ToArray(), t => t.Key == "exception-name"); + Assert.False(measurement.Tags.ContainsKey("exception.type")); + } + else + { + Assert.Equal(exceptionName, (string)measurement.Tags["exception.type"]); + } + if (unhandledRequest ?? false) + { + Assert.True((bool)measurement.Tags["aspnetcore.request.is_unhandled"]); } else { - Assert.Equal(exceptionName, (string)measurement.Tags.ToArray().Single(t => t.Key == "exception-name").Value); + Assert.False(measurement.Tags.ContainsKey("aspnetcore.request.is_unhandled")); } } } @@ -133,8 +138,8 @@ public async Task StartListeningDuringRequest_NotMeasured() await syncPoint.WaitForSyncPoint().DefaultTimeout(); - using var requestDurationCollector = new MetricCollector(meterFactory, HostingMetrics.MeterName, "http-server-request-duration"); - using var currentRequestsCollector = new MetricCollector(meterFactory, HostingMetrics.MeterName, "http-server-current-requests"); + using var requestDurationCollector = new MetricCollector(meterFactory, HostingMetrics.MeterName, "http.server.request.duration"); + using var currentRequestsCollector = new MetricCollector(meterFactory, HostingMetrics.MeterName, "http.server.active_requests"); context1.HttpContext.Response.StatusCode = StatusCodes.Status200OK; syncPoint.Continue(); @@ -155,8 +160,8 @@ public void IHttpMetricsTagsFeatureNotUsedFromFeatureCollection() var httpContext = new DefaultHttpContext(); var meter = meterFactory.Meters.Single(); - using var requestDurationCollector = new MetricCollector(meterFactory, HostingMetrics.MeterName, "http-server-request-duration"); - using var currentRequestsCollector = new MetricCollector(meterFactory, HostingMetrics.MeterName, "http-server-current-requests"); + using var requestDurationCollector = new MetricCollector(meterFactory, HostingMetrics.MeterName, "http.server.request.duration"); + using var currentRequestsCollector = new MetricCollector(meterFactory, HostingMetrics.MeterName, "http.server.active_requests"); // Act/Assert Assert.Equal(HostingMetrics.MeterName, meter.Name); diff --git a/src/Http/Routing/src/RoutingMetrics.cs b/src/Http/Routing/src/RoutingMetrics.cs index fd0f72a97aab..c8400d9f7f68 100644 --- a/src/Http/Routing/src/RoutingMetrics.cs +++ b/src/Http/Routing/src/RoutingMetrics.cs @@ -10,33 +10,31 @@ internal sealed class RoutingMetrics public const string MeterName = "Microsoft.AspNetCore.Routing"; private readonly Meter _meter; - private readonly Counter _matchSuccessCounter; - private readonly Counter _matchFailureCounter; + private readonly Counter _matchAttemptsCounter; public RoutingMetrics(IMeterFactory meterFactory) { _meter = meterFactory.Create(MeterName); - _matchSuccessCounter = _meter.CreateCounter( - "routing-match-success", - description: "Number of requests that successfully matched to an endpoint."); - - _matchFailureCounter = _meter.CreateCounter( - "routing-match-failure", - description: "Number of requests that failed to match to an endpoint. An unmatched request may be handled by later middleware, such as the static files or authentication middleware."); + _matchAttemptsCounter = _meter.CreateCounter( + "aspnetcore.routing.match_attempts", + unit: "{match_attempt}", + description: "Number of requests that were attempted to be matched to an endpoint."); } - public bool MatchSuccessCounterEnabled => _matchSuccessCounter.Enabled; + public bool MatchSuccessCounterEnabled => _matchAttemptsCounter.Enabled; public void MatchSuccess(string route, bool isFallback) { - _matchSuccessCounter.Add(1, - new KeyValuePair("route", route), - new KeyValuePair("fallback", isFallback)); + _matchAttemptsCounter.Add(1, + new KeyValuePair("http.route", route), + new KeyValuePair("aspnetcore.routing.match_status", "success"), + new KeyValuePair("aspnetcore.routing.is_fallback", isFallback)); } public void MatchFailure() { - _matchFailureCounter.Add(1); + _matchAttemptsCounter.Add(1, + new KeyValuePair("aspnetcore.routing.match_status", "failure")); } } diff --git a/src/Http/Routing/test/UnitTests/RoutingMetricsTests.cs b/src/Http/Routing/test/UnitTests/RoutingMetricsTests.cs index e8338013317b..18ed872ce99a 100644 --- a/src/Http/Routing/test/UnitTests/RoutingMetricsTests.cs +++ b/src/Http/Routing/test/UnitTests/RoutingMetricsTests.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.AspNetCore.Testing; -using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -35,8 +34,7 @@ public async Task Match_Success() var httpContext = new DefaultHttpContext(); var meter = meterFactory.Meters.Single(); - using var routingMatchSuccessCollector = new MetricCollector(meterFactory, RoutingMetrics.MeterName, "routing-match-success"); - using var routingMatchFailureCollector = new MetricCollector(meterFactory, RoutingMetrics.MeterName, "routing-match-failure"); + using var routingMatchAttemptsCollector = new MetricCollector(meterFactory, RoutingMetrics.MeterName, "aspnetcore.routing.match_attempts"); // Act await middleware.Invoke(httpContext); @@ -45,9 +43,8 @@ public async Task Match_Success() Assert.Equal(RoutingMetrics.MeterName, meter.Name); Assert.Null(meter.Version); - Assert.Collection(routingMatchSuccessCollector.GetMeasurementSnapshot(), + Assert.Collection(routingMatchAttemptsCollector.GetMeasurementSnapshot(), m => AssertSuccess(m, "/{hi}", fallback: false)); - Assert.Empty(routingMatchFailureCollector.GetMeasurementSnapshot()); } [Theory] @@ -71,8 +68,7 @@ public async Task Match_SuccessFallback_SetTagIfPresent(bool hasFallbackMetadata var httpContext = new DefaultHttpContext(); var meter = meterFactory.Meters.Single(); - using var routingMatchSuccessCollector = new MetricCollector(meterFactory, RoutingMetrics.MeterName, "routing-match-success"); - using var routingMatchFailureCollector = new MetricCollector(meterFactory, RoutingMetrics.MeterName, "routing-match-failure"); + using var routingMatchAttemptsCollector = new MetricCollector(meterFactory, RoutingMetrics.MeterName, "aspnetcore.routing.match_attempts"); // Act await middleware.Invoke(httpContext); @@ -81,9 +77,8 @@ public async Task Match_SuccessFallback_SetTagIfPresent(bool hasFallbackMetadata Assert.Equal(RoutingMetrics.MeterName, meter.Name); Assert.Null(meter.Version); - Assert.Collection(routingMatchSuccessCollector.GetMeasurementSnapshot(), + Assert.Collection(routingMatchAttemptsCollector.GetMeasurementSnapshot(), m => AssertSuccess(m, "/{hi}", fallback: hasFallbackMetadata)); - Assert.Empty(routingMatchFailureCollector.GetMeasurementSnapshot()); } [Fact] @@ -100,8 +95,7 @@ public async Task Match_Success_MissingRoute() var httpContext = new DefaultHttpContext(); var meter = meterFactory.Meters.Single(); - using var routingMatchSuccessCollector = new MetricCollector(meterFactory, RoutingMetrics.MeterName, "routing-match-success"); - using var routingMatchFailureCollector = new MetricCollector(meterFactory, RoutingMetrics.MeterName, "routing-match-failure"); + using var routingMatchAttemptsCollector = new MetricCollector(meterFactory, RoutingMetrics.MeterName, "aspnetcore.routing.match_attempts"); // Act await middleware.Invoke(httpContext); @@ -110,9 +104,8 @@ public async Task Match_Success_MissingRoute() Assert.Equal(RoutingMetrics.MeterName, meter.Name); Assert.Null(meter.Version); - Assert.Collection(routingMatchSuccessCollector.GetMeasurementSnapshot(), + Assert.Collection(routingMatchAttemptsCollector.GetMeasurementSnapshot(), m => AssertSuccess(m, "(missing)", fallback: false)); - Assert.Empty(routingMatchFailureCollector.GetMeasurementSnapshot()); } [Fact] @@ -126,8 +119,7 @@ public async Task Match_Failure() var httpContext = new DefaultHttpContext(); var meter = meterFactory.Meters.Single(); - using var routingMatchSuccessCollector = new MetricCollector(meterFactory, RoutingMetrics.MeterName, "routing-match-success"); - using var routingMatchFailureCollector = new MetricCollector(meterFactory, RoutingMetrics.MeterName, "routing-match-failure"); + using var routingMatchAttemptsCollector = new MetricCollector(meterFactory, RoutingMetrics.MeterName, "aspnetcore.routing.match_attempts"); // Act await middleware.Invoke(httpContext); @@ -136,16 +128,22 @@ public async Task Match_Failure() Assert.Equal(RoutingMetrics.MeterName, meter.Name); Assert.Null(meter.Version); - Assert.Empty(routingMatchSuccessCollector.GetMeasurementSnapshot()); - Assert.Collection(routingMatchFailureCollector.GetMeasurementSnapshot(), - m => Assert.Equal(1, m.Value)); + Assert.Collection(routingMatchAttemptsCollector.GetMeasurementSnapshot(), + m => AssertFailure(m)); + } + + private void AssertFailure(CollectedMeasurement measurement) + { + Assert.Equal(1, measurement.Value); + Assert.Equal("failure", (string)measurement.Tags["aspnetcore.routing.match_status"]); } private void AssertSuccess(CollectedMeasurement measurement, string route, bool fallback) { Assert.Equal(1, measurement.Value); - Assert.Equal(route, (string)measurement.Tags["route"]); - Assert.Equal(fallback, (bool)measurement.Tags["fallback"]); + Assert.Equal("success", (string)measurement.Tags["aspnetcore.routing.match_status"]); + Assert.Equal(route, (string)measurement.Tags["http.route"]); + Assert.Equal(fallback, (bool)measurement.Tags["aspnetcore.routing.is_fallback"]); } private EndpointRoutingMiddleware CreateMiddleware( diff --git a/src/Middleware/Diagnostics/src/DiagnosticsMetrics.cs b/src/Middleware/Diagnostics/src/DiagnosticsMetrics.cs index f77ab2050e4f..7280bd5190a3 100644 --- a/src/Middleware/Diagnostics/src/DiagnosticsMetrics.cs +++ b/src/Middleware/Diagnostics/src/DiagnosticsMetrics.cs @@ -22,7 +22,8 @@ public DiagnosticsMetrics(IMeterFactory meterFactory) _meter = meterFactory.Create(MeterName); _handlerExceptionCounter = _meter.CreateCounter( - "diagnostics-handler-exception", + "aspnetcore.diagnostics.exceptions", + unit: "{exception}", description: "Number of exceptions caught by exception handling middleware."); } @@ -38,14 +39,31 @@ public void RequestException(string exceptionName, ExceptionResult result, strin private void RequestExceptionCore(string exceptionName, ExceptionResult result, string? handler) { var tags = new TagList(); - tags.Add("exception-name", exceptionName); - tags.Add("result", result.ToString()); + tags.Add("exception.type", exceptionName); + tags.Add("aspnetcore.diagnostics.exception.result", GetExceptionResult(result)); if (handler != null) { - tags.Add("handler", handler); + tags.Add("aspnetcore.diagnostics.handler.type", handler); } _handlerExceptionCounter.Add(1, tags); } + + private static string GetExceptionResult(ExceptionResult result) + { + switch (result) + { + case ExceptionResult.Skipped: + return "skipped"; + case ExceptionResult.Handled: + return "handled"; + case ExceptionResult.Unhandled: + return "unhandled"; + case ExceptionResult.Aborted: + return "aborted"; + default: + throw new InvalidOperationException("Unexpected value: " + result); + } + } } internal enum ExceptionResult diff --git a/src/Middleware/Diagnostics/src/DiagnosticsTelemetry.cs b/src/Middleware/Diagnostics/src/DiagnosticsTelemetry.cs index 28e0db455fd9..5a319af79c6e 100644 --- a/src/Middleware/Diagnostics/src/DiagnosticsTelemetry.cs +++ b/src/Middleware/Diagnostics/src/DiagnosticsTelemetry.cs @@ -15,7 +15,7 @@ public static void ReportUnhandledException(ILogger logger, HttpContext context, if (context.Features.Get() is { } tagsFeature) { - tagsFeature.Tags.Add(new KeyValuePair("exception-name", ex.GetType().FullName)); + tagsFeature.Tags.Add(new KeyValuePair("exception.type", ex.GetType().FullName)); } } } diff --git a/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs b/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs index dc80c53efb6a..92e456adcb38 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs @@ -540,8 +540,8 @@ public async Task UnhandledError_ExceptionNameTagAdded() { // Arrange var meterFactory = new TestMeterFactory(); - using var requestDurationCollector = new MetricCollector(meterFactory, "Microsoft.AspNetCore.Hosting", "http-server-request-duration"); - using var requestExceptionCollector = new MetricCollector(meterFactory, DiagnosticsMetrics.MeterName, "diagnostics-handler-exception"); + using var requestDurationCollector = new MetricCollector(meterFactory, "Microsoft.AspNetCore.Hosting", "http.server.request.duration"); + using var requestExceptionCollector = new MetricCollector(meterFactory, DiagnosticsMetrics.MeterName, "aspnetcore.diagnostics.exceptions"); using var host = new HostBuilder() .ConfigureServices(s => @@ -579,25 +579,25 @@ public async Task UnhandledError_ExceptionNameTagAdded() m => { Assert.True(m.Value > 0); - Assert.Equal(500, (int)m.Tags.ToArray().Single(t => t.Key == "status-code").Value); - Assert.Equal("System.Exception", (string)m.Tags.ToArray().Single(t => t.Key == "exception-name").Value); + Assert.Equal(500, (int)m.Tags["http.response.status_code"]); + Assert.Equal("System.Exception", (string)m.Tags["exception.type"]); }); Assert.Collection(requestExceptionCollector.GetMeasurementSnapshot(), - m => AssertRequestException(m, "System.Exception", "Unhandled")); + m => AssertRequestException(m, "System.Exception", "unhandled")); } private static void AssertRequestException(CollectedMeasurement measurement, string exceptionName, string result, string handler = null) { Assert.Equal(1, measurement.Value); - Assert.Equal(exceptionName, (string)measurement.Tags["exception-name"]); - Assert.Equal(result, measurement.Tags["result"].ToString()); + Assert.Equal(exceptionName, (string)measurement.Tags["exception.type"]); + Assert.Equal(result, measurement.Tags["aspnetcore.diagnostics.exception.result"].ToString()); if (handler == null) { - Assert.False(measurement.Tags.ContainsKey("handler")); + Assert.False(measurement.Tags.ContainsKey("aspnetcore.diagnostics.handler.type")); } else { - Assert.Equal(handler, (string)measurement.Tags["handler"]); + Assert.Equal(handler, (string)measurement.Tags["aspnetcore.diagnostics.handler.type"]); } } diff --git a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs index 0a0b0fcf9c29..fd0c791cd287 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs @@ -211,7 +211,7 @@ public async Task Metrics_NoExceptionThrown() var middleware = CreateMiddleware(_ => Task.CompletedTask, optionsAccessor, exceptionHandlers, meterFactory); var meter = meterFactory.Meters.Single(); - using var diagnosticsRequestExceptionCollector = new MetricCollector(meterFactory, DiagnosticsMetrics.MeterName, "diagnostics-handler-exception"); + using var diagnosticsRequestExceptionCollector = new MetricCollector(meterFactory, DiagnosticsMetrics.MeterName, "aspnetcore.diagnostics.exceptions"); // Act await middleware.Invoke(httpContext); @@ -234,14 +234,14 @@ public async Task Metrics_ExceptionThrown_Handled_Reported() var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, exceptionHandlers, meterFactory); var meter = meterFactory.Meters.Single(); - using var diagnosticsRequestExceptionCollector = new MetricCollector(meterFactory, DiagnosticsMetrics.MeterName, "diagnostics-handler-exception"); + using var diagnosticsRequestExceptionCollector = new MetricCollector(meterFactory, DiagnosticsMetrics.MeterName, "aspnetcore.diagnostics.exceptions"); // Act await middleware.Invoke(httpContext); // Assert Assert.Collection(diagnosticsRequestExceptionCollector.GetMeasurementSnapshot(), - m => AssertRequestException(m, "System.InvalidOperationException", "Handled", typeof(TestExceptionHandler).FullName)); + m => AssertRequestException(m, "System.InvalidOperationException", "handled", typeof(TestExceptionHandler).FullName)); } [Fact] @@ -256,14 +256,14 @@ public async Task Metrics_ExceptionThrown_ResponseStarted_Skipped() var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, exceptionHandlers, meterFactory); var meter = meterFactory.Meters.Single(); - using var diagnosticsRequestExceptionCollector = new MetricCollector(meterFactory, DiagnosticsMetrics.MeterName, "diagnostics-handler-exception"); + using var diagnosticsRequestExceptionCollector = new MetricCollector(meterFactory, DiagnosticsMetrics.MeterName, "aspnetcore.diagnostics.exceptions"); // Act await Assert.ThrowsAsync(() => middleware.Invoke(httpContext)); // Assert Assert.Collection(diagnosticsRequestExceptionCollector.GetMeasurementSnapshot(), - m => AssertRequestException(m, "System.InvalidOperationException", "Skipped")); + m => AssertRequestException(m, "System.InvalidOperationException", "skipped")); } private sealed class TestResponseFeature : HttpResponseFeature @@ -281,14 +281,14 @@ public async Task Metrics_ExceptionThrown_DefaultSettings_Handled_Reported() var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, meterFactory: meterFactory); var meter = meterFactory.Meters.Single(); - using var diagnosticsRequestExceptionCollector = new MetricCollector(meterFactory, DiagnosticsMetrics.MeterName, "diagnostics-handler-exception"); + using var diagnosticsRequestExceptionCollector = new MetricCollector(meterFactory, DiagnosticsMetrics.MeterName, "aspnetcore.diagnostics.exceptions"); // Act await middleware.Invoke(httpContext); // Assert Assert.Collection(diagnosticsRequestExceptionCollector.GetMeasurementSnapshot(), - m => AssertRequestException(m, "System.InvalidOperationException", "Handled", null)); + m => AssertRequestException(m, "System.InvalidOperationException", "handled", null)); } [Fact] @@ -305,28 +305,28 @@ public async Task Metrics_ExceptionThrown_Unhandled_Reported() var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, meterFactory: meterFactory); var meter = meterFactory.Meters.Single(); - using var diagnosticsRequestExceptionCollector = new MetricCollector(meterFactory, DiagnosticsMetrics.MeterName, "diagnostics-handler-exception"); + using var diagnosticsRequestExceptionCollector = new MetricCollector(meterFactory, DiagnosticsMetrics.MeterName, "aspnetcore.diagnostics.exceptions"); // Act await Assert.ThrowsAsync(() => middleware.Invoke(httpContext)); // Assert Assert.Collection(diagnosticsRequestExceptionCollector.GetMeasurementSnapshot(), - m => AssertRequestException(m, "System.InvalidOperationException", "Unhandled")); + m => AssertRequestException(m, "System.InvalidOperationException", "unhandled")); } private static void AssertRequestException(CollectedMeasurement measurement, string exceptionName, string result, string handler = null) { Assert.Equal(1, measurement.Value); - Assert.Equal(exceptionName, (string)measurement.Tags["exception-name"]); - Assert.Equal(result, measurement.Tags["result"].ToString()); + Assert.Equal(exceptionName, (string)measurement.Tags["exception.type"]); + Assert.Equal(result, measurement.Tags["aspnetcore.diagnostics.exception.result"].ToString()); if (handler == null) { - Assert.False(measurement.Tags.ContainsKey("handler")); + Assert.False(measurement.Tags.ContainsKey("aspnetcore.diagnostics.handler.type")); } else { - Assert.Equal(handler, (string)measurement.Tags["handler"]); + Assert.Equal(handler, (string)measurement.Tags["aspnetcore.diagnostics.handler.type"]); } } diff --git a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs index ec2eea50d192..93431bee80ef 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs @@ -917,7 +917,7 @@ public async Task UnhandledError_ExceptionNameTagAdded() { // Arrange var meterFactory = new TestMeterFactory(); - using var instrumentCollector = new MetricCollector(meterFactory, "Microsoft.AspNetCore.Hosting", "http-server-request-duration"); + using var instrumentCollector = new MetricCollector(meterFactory, "Microsoft.AspNetCore.Hosting", "http.server.request.duration"); using var host = new HostBuilder() .ConfigureServices(s => @@ -961,8 +961,8 @@ public async Task UnhandledError_ExceptionNameTagAdded() m => { Assert.True(m.Value > 0); - Assert.Equal(404, (int)m.Tags.ToArray().Single(t => t.Key == "status-code").Value); - Assert.Equal("System.Exception", (string)m.Tags.ToArray().Single(t => t.Key == "exception-name").Value); + Assert.Equal(404, (int)m.Tags["http.response.status_code"]); + Assert.Equal("System.Exception", (string)m.Tags["exception.type"]); }); } } diff --git a/src/Middleware/Middleware.slnf b/src/Middleware/Middleware.slnf index fd9d1ba17db3..2c64128c9144 100644 --- a/src/Middleware/Middleware.slnf +++ b/src/Middleware/Middleware.slnf @@ -16,6 +16,8 @@ "src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj", "src\\FileProviders\\Embedded\\src\\Microsoft.Extensions.FileProviders.Embedded.csproj", "src\\FileProviders\\Manifest.MSBuildTask\\src\\Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.csproj", + "src\\HealthChecks\\Abstractions\\src\\Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj", + "src\\HealthChecks\\HealthChecks\\src\\Microsoft.Extensions.Diagnostics.HealthChecks.csproj", "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj", "src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj", "src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj", @@ -28,6 +30,7 @@ "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", + "src\\Http\\Http.Results\\src\\Microsoft.AspNetCore.Http.Results.csproj", "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj",