diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Metrics/MetricsHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Metrics/MetricsHandler.cs index 3c34e43011938e..1f353f167150e5 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/Metrics/MetricsHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Metrics/MetricsHandler.cs @@ -112,11 +112,12 @@ private void RequestStop(HttpRequestMessage request, HttpResponseMessage? respon tags.Add("http.response.status_code", GetBoxedStatusCode((int)response.StatusCode)); tags.Add("network.protocol.version", GetProtocolVersionString(response.Version)); } - else + + if (TryGetErrorType(response, exception, out string? errorType)) { - Debug.Assert(exception is not null); - tags.Add("http.error.reason", GetErrorReason(exception)); + tags.Add("error.type", errorType); } + TimeSpan durationTime = Stopwatch.GetElapsedTime(startTimestamp, Stopwatch.GetTimestamp()); HttpMetricsEnrichmentContext? enrichmentContext = HttpMetricsEnrichmentContext.GetEnrichmentContextForRequest(request); @@ -130,37 +131,47 @@ private void RequestStop(HttpRequestMessage request, HttpResponseMessage? respon } } - private static string GetErrorReason(Exception exception) + private static bool TryGetErrorType(HttpResponseMessage? response, Exception? exception, out string? errorType) { - if (exception is HttpRequestException e) + if (response is not null) { - Debug.Assert(Enum.GetValues().Length == 12, "We need to extend the mapping in case new values are added to HttpRequestError."); + int statusCode = (int)response.StatusCode; - string? errorReason = e.HttpRequestError switch + // In case the status code indicates a client or a server error, return the string representation of the status code. + // See the paragraph Status and the definition of 'error.type' in + // https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-spans.md + if (statusCode >= 400 && statusCode <= 599) { - HttpRequestError.NameResolutionError => "name_resolution_error", - HttpRequestError.ConnectionError => "connection_error", - HttpRequestError.SecureConnectionError => "secure_connection_error", - HttpRequestError.HttpProtocolError => "http_protocol_error", - HttpRequestError.ExtendedConnectNotSupported => "extended_connect_not_supported", - HttpRequestError.VersionNegotiationError => "version_negotiation_error", - HttpRequestError.UserAuthenticationError => "user_authentication_error", - HttpRequestError.ProxyTunnelError => "proxy_tunnel_error", - HttpRequestError.InvalidResponse => "invalid_response", - HttpRequestError.ResponseEnded => "response_ended", - HttpRequestError.ConfigurationLimitExceeded => "configuration_limit_exceeded", - - // Fall back to the exception type name (including for HttpRequestError.Unknown). - _ => null - }; - - if (errorReason is not null) - { - return errorReason; + errorType = GetErrorStatusCodeString(statusCode); + return true; } } - return exception.GetType().Name; + if (exception is null) + { + errorType = null; + return false; + } + + Debug.Assert(Enum.GetValues().Length == 12, "We need to extend the mapping in case new values are added to HttpRequestError."); + errorType = (exception as HttpRequestException)?.HttpRequestError switch + { + HttpRequestError.NameResolutionError => "name_resolution_error", + HttpRequestError.ConnectionError => "connection_error", + HttpRequestError.SecureConnectionError => "secure_connection_error", + HttpRequestError.HttpProtocolError => "http_protocol_error", + HttpRequestError.ExtendedConnectNotSupported => "extended_connect_not_supported", + HttpRequestError.VersionNegotiationError => "version_negotiation_error", + HttpRequestError.UserAuthenticationError => "user_authentication_error", + HttpRequestError.ProxyTunnelError => "proxy_tunnel_error", + HttpRequestError.InvalidResponse => "invalid_response", + HttpRequestError.ResponseEnded => "response_ended", + HttpRequestError.ConfigurationLimitExceeded => "configuration_limit_exceeded", + + // Fall back to the exception type name in case of HttpRequestError.Unknown or when exception is not an HttpRequestException. + _ => exception.GetType().Name + }; + return true; } private static string GetProtocolVersionString(Version httpVersion) => (httpVersion.Major, httpVersion.Minor) switch @@ -199,6 +210,7 @@ private static TagList InitializeCommonTags(HttpRequestMessage request) } private static object[]? s_boxedStatusCodes; + private static string[]? s_statusCodeStrings; private static object GetBoxedStatusCode(int statusCode) { @@ -209,6 +221,17 @@ private static object GetBoxedStatusCode(int statusCode) : statusCode; } + private static string GetErrorStatusCodeString(int statusCode) + { + Debug.Assert(statusCode >= 400 && statusCode <= 599); + + string[] strings = LazyInitializer.EnsureInitialized(ref s_statusCodeStrings, static () => new string[200]); + int index = statusCode - 400; + return (uint)index < (uint)strings.Length + ? strings[index] ??= statusCode.ToString() + : statusCode.ToString(); + } + private sealed class SharedMeter : Meter { public static Meter Instance { get; } = new SharedMeter(); diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs index da36366246f415..15bb4bcbe2bb97 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs @@ -75,8 +75,8 @@ protected static void VerifyRequestDuration(Measurement measurement, Version? protocolVersion = null, int? statusCode = null, string method = "GET", - string[] acceptedErrorReasons = null) => - VerifyRequestDuration(InstrumentNames.RequestDuration, measurement.Value, measurement.Tags.ToArray(), uri, protocolVersion, statusCode, method, acceptedErrorReasons); + string[] acceptedErrorTypes = null) => + VerifyRequestDuration(InstrumentNames.RequestDuration, measurement.Value, measurement.Tags.ToArray(), uri, protocolVersion, statusCode, method, acceptedErrorTypes); protected static void VerifyRequestDuration(string instrumentName, double measurement, @@ -85,7 +85,7 @@ protected static void VerifyRequestDuration(string instrumentName, Version? protocolVersion, int? statusCode, string method = "GET", - string[] acceptedErrorReasons = null) + string[] acceptedErrorTypes = null) { Assert.Equal(InstrumentNames.RequestDuration, instrumentName); Assert.InRange(measurement, double.Epsilon, 60); @@ -93,14 +93,14 @@ protected static void VerifyRequestDuration(string instrumentName, VerifyTag(tags, "http.request.method", method); VerifyTag(tags, "network.protocol.version", GetVersionString(protocolVersion)); VerifyTag(tags, "http.response.status_code", statusCode); - if (acceptedErrorReasons == null) + if (acceptedErrorTypes == null) { - Assert.DoesNotContain(tags, t => t.Key == "http.error.reason"); + Assert.DoesNotContain(tags, t => t.Key == "error.type"); } else { - string errorReason = (string)tags.Single(t => t.Key == "http.error.reason").Value; - Assert.Contains(errorReason, acceptedErrorReasons); + string errorReason = (string)tags.Single(t => t.Key == "error.type").Value; + Assert.Contains(errorReason, acceptedErrorTypes); } } @@ -659,7 +659,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => : [nameof(TaskCanceledException), nameof(OperationCanceledException)]; Measurement m = Assert.Single(recorder.GetMeasurements()); - VerifyRequestDuration(m, uri, acceptedErrorReasons: expectedExceptionTypes); + VerifyRequestDuration(m, uri, acceptedErrorTypes: expectedExceptionTypes); clientCompleted.SetResult(); }, @@ -703,7 +703,7 @@ public async Task RequestDuration_ConnectionError_LogsExpectedErrorReason() _output.WriteLine($"Client exception: {ex}"); Measurement m = Assert.Single(recorder.GetMeasurements()); - VerifyRequestDuration(m, uri, acceptedErrorReasons: ["connection_error"]); + VerifyRequestDuration(m, uri, acceptedErrorTypes: ["connection_error"]); } protected override void Dispose(bool disposing) @@ -793,6 +793,29 @@ await Assert.ThrowsAsync(async () => }, content: "x")); } + [Theory] + [InlineData(400)] + [InlineData(404)] + [InlineData(599)] + public Task RequestDuration_ErrorStatus_ErrorTypeRecorded(int statusCode) + { + return LoopbackServerFactory.CreateClientAndServerAsync(async uri => + { + using HttpMessageInvoker client = CreateHttpMessageInvoker(); + using InstrumentRecorder recorder = SetupInstrumentRecorder(InstrumentNames.RequestDuration); + using HttpRequestMessage request = new(HttpMethod.Get, uri) { Version = UseVersion }; + + using HttpResponseMessage response = await SendAsync(client, request); + + Measurement m = Assert.Single(recorder.GetMeasurements()); + VerifyRequestDuration(m, uri, UseVersion, statusCode, "GET", acceptedErrorTypes: new[] { $"{statusCode}" }); + + }, async server => + { + await server.AcceptConnectionSendResponseAndCloseAsync(statusCode: (HttpStatusCode)statusCode); + }); + } + [Fact] [SkipOnPlatform(TestPlatforms.Browser, "Browser is relaxed about validating HTTP headers")] public async Task RequestDuration_ConnectionClosedWhileReceivingHeaders_Recorded() @@ -814,7 +837,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => Assert.True(ex is HttpRequestException or TaskCanceledException); Measurement m = Assert.Single(recorder.GetMeasurements()); - VerifyRequestDuration(m, uri, acceptedErrorReasons: [nameof(TaskCanceledException), "response_ended"]); + VerifyRequestDuration(m, uri, acceptedErrorTypes: [nameof(TaskCanceledException), "response_ended"]); }, async server => { try @@ -869,7 +892,7 @@ await server.AcceptConnectionAsync(async connection => { await Assert.ThrowsAsync(() => clientTask); Measurement m = Assert.Single(recorder.GetMeasurements()); - VerifyRequestDuration(m, server.Address, acceptedErrorReasons: ["response_ended"]); + VerifyRequestDuration(m, server.Address, acceptedErrorTypes: ["response_ended"]); } else { @@ -967,7 +990,7 @@ await Assert.ThrowsAsync(async () => }); Measurement m = Assert.Single(recorder.GetMeasurements()); - VerifyRequestDuration(m, server.Address, acceptedErrorReasons: ["http_protocol_error"]); + VerifyRequestDuration(m, server.Address, acceptedErrorTypes: ["http_protocol_error"]); } }