diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.cs index 1634453a9906de..e8ed9315016758 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.cs @@ -176,7 +176,7 @@ internal static HttpMethod Normalize(HttpMethod method) method; } - private static HttpMethod? GetKnownMethod(ReadOnlySpan method) + internal static HttpMethod? GetKnownMethod(ReadOnlySpan method) { if (method.Length >= 3) // 3 == smallest known method { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Metrics/HttpMetricsEnrichmentContext.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Metrics/HttpMetricsEnrichmentContext.cs index 77b94fda296313..89a5d1fd1bfacd 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/Metrics/HttpMetricsEnrichmentContext.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Metrics/HttpMetricsEnrichmentContext.cs @@ -108,15 +108,12 @@ public static void AddCallback(HttpRequestMessage request, Action requestDuration, - Counter failedRequests) + Histogram requestDuration) { _request = request; _response = response; @@ -140,16 +137,7 @@ internal void RecordWithEnrichment(HttpRequestMessage request, callback(this); } - if (recordRequestDuration) - { - TimeSpan duration = Stopwatch.GetElapsedTime(startTimestamp, Stopwatch.GetTimestamp()); - requestDuration.Record(duration.TotalSeconds, CollectionsMarshal.AsSpan(_tags)); - } - - if (recordFailedRequests) - { - failedRequests.Add(1, CollectionsMarshal.AsSpan(_tags)); - } + requestDuration.Record(durationTime.TotalSeconds, CollectionsMarshal.AsSpan(_tags)); } finally { 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 cdb722c867f172..02826aa593d29f 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 @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Metrics; using System.Threading; @@ -11,8 +12,7 @@ namespace System.Net.Http.Metrics internal sealed class MetricsHandler : HttpMessageHandlerStage { private readonly HttpMessageHandler _innerHandler; - private readonly UpDownCounter _currentRequests; - private readonly Counter _failedRequests; + private readonly UpDownCounter _activeRequests; private readonly Histogram _requestsDuration; public MetricsHandler(HttpMessageHandler innerHandler, IMeterFactory? meterFactory, out Meter meter) @@ -22,21 +22,18 @@ public MetricsHandler(HttpMessageHandler innerHandler, IMeterFactory? meterFacto meter = meterFactory?.Create("System.Net.Http") ?? SharedMeter.Instance; // Meter has a cache for the instruments it owns - _currentRequests = meter.CreateUpDownCounter( - "http-client-current-requests", + _activeRequests = meter.CreateUpDownCounter( + "http.client.active_requests", description: "Number of outbound HTTP requests that are currently active on the client."); - _failedRequests = meter.CreateCounter( - "http-client-failed-requests", - description: "Number of outbound HTTP requests that have failed."); _requestsDuration = meter.CreateHistogram( - "http-client-request-duration", + "http.client.request.duration", unit: "s", description: "The duration of outbound HTTP requests."); } internal override ValueTask SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { - if (_currentRequests.Enabled || _failedRequests.Enabled || _requestsDuration.Enabled) + if (_activeRequests.Enabled || _requestsDuration.Enabled) { return SendAsyncWithMetrics(request, async, cancellationToken); } @@ -83,67 +80,90 @@ protected override void Dispose(bool disposing) private (long StartTimestamp, bool RecordCurrentRequests) RequestStart(HttpRequestMessage request) { - bool recordCurrentRequests = _currentRequests.Enabled; + bool recordCurrentRequests = _activeRequests.Enabled; long startTimestamp = Stopwatch.GetTimestamp(); if (recordCurrentRequests) { TagList tags = InitializeCommonTags(request); - _currentRequests.Add(1, tags); + _activeRequests.Add(1, tags); } return (startTimestamp, recordCurrentRequests); } - private void RequestStop(HttpRequestMessage request, HttpResponseMessage? response, Exception? exception, long startTimestamp, bool recordCurrentRequsts) + private void RequestStop(HttpRequestMessage request, HttpResponseMessage? response, Exception? exception, long startTimestamp, bool recordCurrentRequests) { TagList tags = InitializeCommonTags(request); - if (recordCurrentRequsts) + if (recordCurrentRequests) { - _currentRequests.Add(-1, tags); + _activeRequests.Add(-1, tags); } - bool recordRequestDuration = _requestsDuration.Enabled; - bool recordFailedRequests = _failedRequests.Enabled && response is null; + if (!_requestsDuration.Enabled) + { + return; + } - HttpMetricsEnrichmentContext? enrichmentContext = null; - if (recordRequestDuration || recordFailedRequests) + if (response is not null) { - if (response is not null) - { - tags.Add("status-code", GetBoxedStatusCode((int)response.StatusCode)); - tags.Add("protocol", GetProtocolName(response.Version)); - } - enrichmentContext = HttpMetricsEnrichmentContext.GetEnrichmentContextForRequest(request); + tags.Add("http.response.status_code", GetBoxedStatusCode((int)response.StatusCode)); + tags.Add("network.protocol.version", GetProtocolVersionString(response.Version)); + } + else + { + Debug.Assert(exception is not null); + tags.Add("http.error.reason", GetErrorReason(exception)); } + TimeSpan durationTime = Stopwatch.GetElapsedTime(startTimestamp, Stopwatch.GetTimestamp()); + HttpMetricsEnrichmentContext? enrichmentContext = HttpMetricsEnrichmentContext.GetEnrichmentContextForRequest(request); if (enrichmentContext is null) { - if (recordRequestDuration) - { - TimeSpan duration = Stopwatch.GetElapsedTime(startTimestamp, Stopwatch.GetTimestamp()); - _requestsDuration.Record(duration.TotalSeconds, tags); - } - - if (recordFailedRequests) - { - _failedRequests.Add(1, tags); - } + _requestsDuration.Record(durationTime.TotalSeconds, tags); } else { - enrichmentContext.RecordWithEnrichment(request, response, exception, startTimestamp, tags, recordRequestDuration, recordFailedRequests, _requestsDuration, _failedRequests); + enrichmentContext.RecordDurationWithEnrichment(request, response, exception, durationTime, tags, _requestsDuration); + } + } + + private static string GetErrorReason(Exception exception) + { + if (exception is OperationCanceledException) + { + return "cancellation"; } + else if (exception is HttpRequestException e) + { + Debug.Assert(Enum.GetValues().Length == 12, "We need to extend the mapping in case new values are added to HttpRequestError."); + return e.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", + _ => "_OTHER" + }; + } + return "_OTHER"; } - private static string GetProtocolName(Version httpVersion) => (httpVersion.Major, httpVersion.Minor) switch + private static string GetProtocolVersionString(Version httpVersion) => (httpVersion.Major, httpVersion.Minor) switch { - (1, 0) => "HTTP/1.0", - (1, 1) => "HTTP/1.1", - (2, 0) => "HTTP/2", - (3, 0) => "HTTP/3", - _ => $"HTTP/{httpVersion.Major}.{httpVersion.Minor}" + (1, 0) => "1.0", + (1, 1) => "1.1", + (2, 0) => "2.0", + (3, 0) => "3.0", + _ => httpVersion.ToString() }; private static TagList InitializeCommonTags(HttpRequestMessage request) @@ -152,19 +172,26 @@ private static TagList InitializeCommonTags(HttpRequestMessage request) if (request.RequestUri is Uri requestUri && requestUri.IsAbsoluteUri) { - tags.Add("scheme", requestUri.Scheme); - tags.Add("host", requestUri.Host); + tags.Add("url.scheme", requestUri.Scheme); + tags.Add("server.address", requestUri.Host); // Add port tag when not the default value for the current scheme if (!requestUri.IsDefaultPort) { - tags.Add("port", requestUri.Port); + tags.Add("server.port", requestUri.Port); } } - tags.Add("method", request.Method.Method); + tags.Add(GetMethodTag(request.Method)); return tags; } + internal static KeyValuePair GetMethodTag(HttpMethod method) + { + // Return canonical names for known methods and "_OTHER" for unknown ones. + HttpMethod? known = HttpMethod.GetKnownMethod(method.Method); + return new KeyValuePair("http.request.method", known?.Method ?? "_OTHER"); + } + private static object[]? s_boxedStatusCodes; private static object GetBoxedStatusCode(int statusCode) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs index 9744d02fc94f85..3b8aaae6605ed0 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs @@ -197,7 +197,7 @@ public async Task SendAsync(HttpRequestMessage request, lon { TimeSpan duration = Stopwatch.GetElapsedTime(queueStartingTimestamp); - _pool.Settings._metrics!.RequestLeftQueue(Pool, duration, versionMajor: 3); + _pool.Settings._metrics!.RequestLeftQueue(request, Pool, duration, versionMajor: 3); if (HttpTelemetry.Log.IsEnabled()) { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs index f44c893f8f53d5..b67ea5ac48d62b 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs @@ -42,28 +42,27 @@ public HttpConnectionBase(HttpConnectionPool pool, IPEndPoint? remoteEndPoint) SocketsHttpHandlerMetrics metrics = pool.Settings._metrics; - if (metrics.CurrentConnections.Enabled || - metrics.IdleConnections.Enabled || - metrics.ConnectionDuration.Enabled) + if (metrics.OpenConnections.Enabled || metrics.ConnectionDuration.Enabled) { // While requests may report HTTP/1.0 as the protocol, we treat all HTTP/1.X connections as HTTP/1.1. string protocol = - this is HttpConnection ? "HTTP/1.1" : - this is Http2Connection ? "HTTP/2" : - "HTTP/3"; + this is HttpConnection ? "1.1" : + this is Http2Connection ? "2.0" : + "3.0"; _connectionMetrics = new ConnectionMetrics( metrics, protocol, pool.IsSecure ? "https" : "http", pool.OriginAuthority.HostValue, - pool.IsDefaultPort ? null : pool.OriginAuthority.Port); + pool.IsDefaultPort ? null : pool.OriginAuthority.Port, + remoteEndPoint?.Address?.ToString()); _connectionMetrics.ConnectionEstablished(); - - MarkConnectionAsIdle(); } + _idleSinceTickCount = _creationTickCount; + if (HttpTelemetry.Log.IsEnabled()) { _httpTelemetryMarkedConnectionAsOpened = true; @@ -97,13 +96,12 @@ public void MarkConnectionAsClosed() public void MarkConnectionAsIdle() { _idleSinceTickCount = Environment.TickCount64; - - _connectionMetrics?.MarkAsIdle(); + _connectionMetrics?.IdleStateChanged(idle: true); } public void MarkConnectionAsNotIdle() { - _connectionMetrics?.MarkAsNotIdle(); + _connectionMetrics?.IdleStateChanged(idle: false); } /// Uses , but first special-cases several known headers for which we can use caching. diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index 5e98fc1cdcad93..cf40ee0fedd8f7 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -1084,7 +1084,7 @@ public async ValueTask SendWithVersionDetectionAndRetryAsyn if (!TryGetPooledHttp2Connection(request, out Http2Connection? connection, out http2ConnectionWaiter) && http2ConnectionWaiter != null) { - connection = await http2ConnectionWaiter.WaitForConnectionAsync(this, async, cancellationToken).ConfigureAwait(false); + connection = await http2ConnectionWaiter.WaitForConnectionAsync(request, this, async, cancellationToken).ConfigureAwait(false); } Debug.Assert(connection is not null || !_http2Enabled); @@ -1116,7 +1116,7 @@ public async ValueTask SendWithVersionDetectionAndRetryAsyn // Use HTTP/1.x. if (!TryGetPooledHttp11Connection(request, async, out HttpConnection? connection, out http11ConnectionWaiter)) { - connection = await http11ConnectionWaiter.WaitForConnectionAsync(this, async, cancellationToken).ConfigureAwait(false); + connection = await http11ConnectionWaiter.WaitForConnectionAsync(request, this, async, cancellationToken).ConfigureAwait(false); } connection.Acquire(); // In case we are doing Windows (i.e. connection-based) auth, we need to ensure that we hold on to this specific connection while auth is underway. @@ -2620,14 +2620,14 @@ private sealed class HttpConnectionWaiter : TaskCompletionSourceWithCancellat // Distinguish connection cancellation that happens because the initiating request is cancelled or completed on a different connection. public bool CancelledByOriginatingRequestCompletion { get; set; } - public ValueTask WaitForConnectionAsync(HttpConnectionPool pool, bool async, CancellationToken requestCancellationToken) + public ValueTask WaitForConnectionAsync(HttpRequestMessage request, HttpConnectionPool pool, bool async, CancellationToken requestCancellationToken) { return HttpTelemetry.Log.IsEnabled() || pool.Settings._metrics!.RequestsQueueDuration.Enabled - ? WaitForConnectionWithTelemetryAsync(pool, async, requestCancellationToken) + ? WaitForConnectionWithTelemetryAsync(request, pool, async, requestCancellationToken) : WaitWithCancellationAsync(async, requestCancellationToken); } - private async ValueTask WaitForConnectionWithTelemetryAsync(HttpConnectionPool pool, bool async, CancellationToken requestCancellationToken) + private async ValueTask WaitForConnectionWithTelemetryAsync(HttpRequestMessage request, HttpConnectionPool pool, bool async, CancellationToken requestCancellationToken) { Debug.Assert(typeof(T) == typeof(HttpConnection) || typeof(T) == typeof(Http2Connection)); @@ -2641,7 +2641,7 @@ private async ValueTask WaitForConnectionWithTelemetryAsync(HttpConnectionPoo TimeSpan duration = Stopwatch.GetElapsedTime(startingTimestamp); int versionMajor = typeof(T) == typeof(HttpConnection) ? 1 : 2; - pool.Settings._metrics!.RequestLeftQueue(pool, duration, versionMajor); + pool.Settings._metrics!.RequestLeftQueue(request, pool, duration, versionMajor); if (HttpTelemetry.Log.IsEnabled()) { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/ConnectionMetrics.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/ConnectionMetrics.cs index d047f70766fd05..a9498cdc948dfb 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/ConnectionMetrics.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/ConnectionMetrics.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Diagnostics; namespace System.Net.Http.Metrics @@ -8,23 +9,23 @@ namespace System.Net.Http.Metrics internal sealed class ConnectionMetrics { private readonly SocketsHttpHandlerMetrics _metrics; - private readonly bool _currentConnectionsEnabled; - private readonly bool _idleConnectionsEnabled; - private readonly object _protocolTag; + private readonly bool _openConnectionsEnabled; + private readonly object _protocolVersionTag; private readonly object _schemeTag; private readonly object _hostTag; private readonly object? _portTag; + private readonly object? _socketAddressTag; private bool _currentlyIdle; - public ConnectionMetrics(SocketsHttpHandlerMetrics metrics, string protocol, string scheme, string host, int? port) + public ConnectionMetrics(SocketsHttpHandlerMetrics metrics, string protocolVersion, string scheme, string host, int? port, string? socketAddress) { _metrics = metrics; - _currentConnectionsEnabled = _metrics.CurrentConnections.Enabled; - _idleConnectionsEnabled = _metrics.IdleConnections.Enabled; - _protocolTag = protocol; + _openConnectionsEnabled = _metrics.OpenConnections.Enabled; + _protocolVersionTag = protocolVersion; _schemeTag = scheme; _hostTag = host; _portTag = port; + _socketAddressTag = socketAddress; } // TagList is a huge struct, so we avoid storing it in a field to reduce the amount we allocate on the heap. @@ -32,56 +33,62 @@ private TagList GetTags() { TagList tags = default; - tags.Add("protocol", _protocolTag); - tags.Add("scheme", _schemeTag); - tags.Add("host", _hostTag); + tags.Add("network.protocol.version", _protocolVersionTag); + tags.Add("url.scheme", _schemeTag); + tags.Add("server.address", _hostTag); if (_portTag is not null) { - tags.Add("port", _portTag); + tags.Add("server.port", _portTag); + } + + if (_socketAddressTag is not null) + { + tags.Add("server.socket.address", _socketAddressTag); } return tags; } + private static KeyValuePair GetStateTag(bool idle) => new KeyValuePair("http.connection.state", idle ? "idle" : "active"); + public void ConnectionEstablished() { - if (_currentConnectionsEnabled) + if (_openConnectionsEnabled) { - _metrics.CurrentConnections.Add(1, GetTags()); + _currentlyIdle = true; + TagList tags = GetTags(); + tags.Add(GetStateTag(idle: true)); + _metrics.OpenConnections.Add(1, tags); } } public void ConnectionClosed(long durationMs) { - MarkAsNotIdle(); - - if (_currentConnectionsEnabled) - { - _metrics.CurrentConnections.Add(-1, GetTags()); - } + TagList tags = GetTags(); if (_metrics.ConnectionDuration.Enabled) { - _metrics.ConnectionDuration.Record(durationMs / 1000d, GetTags()); + _metrics.ConnectionDuration.Record(durationMs / 1000d, tags); } - } - public void MarkAsIdle() - { - if (_idleConnectionsEnabled && !_currentlyIdle) + if (_openConnectionsEnabled) { - _currentlyIdle = true; - _metrics.IdleConnections.Add(1, GetTags()); + tags.Add(GetStateTag(idle: _currentlyIdle)); + _metrics.OpenConnections.Add(-1, tags); } } - public void MarkAsNotIdle() + public void IdleStateChanged(bool idle) { - if (_idleConnectionsEnabled && _currentlyIdle) + if (_openConnectionsEnabled && _currentlyIdle != idle) { - _currentlyIdle = false; - _metrics.IdleConnections.Add(-1, GetTags()); + _currentlyIdle = idle; + TagList tags = GetTags(); + tags.Add(GetStateTag(idle: !idle)); + _metrics.OpenConnections.Add(-1, tags); + tags[tags.Count - 1] = GetStateTag(idle: idle); + _metrics.OpenConnections.Add(1, tags); } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/SocketsHttpHandlerMetrics.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/SocketsHttpHandlerMetrics.cs index 91a4adccc7df9c..d245de835080c4 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/SocketsHttpHandlerMetrics.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/SocketsHttpHandlerMetrics.cs @@ -8,25 +8,21 @@ namespace System.Net.Http.Metrics { internal sealed class SocketsHttpHandlerMetrics(Meter meter) { - public readonly UpDownCounter CurrentConnections = meter.CreateUpDownCounter( - name: "http-client-current-connections", - description: "Number of outbound HTTP connections that are currently active on the client."); - - public readonly UpDownCounter IdleConnections = meter.CreateUpDownCounter( - name: "http-client-current-idle-connections", - description: "Number of outbound HTTP connections that are currently idle on the client."); + public readonly UpDownCounter OpenConnections = meter.CreateUpDownCounter( + name: "http.client.open_connections", + description: "Number of outbound HTTP connections that are currently active or idle on the client."); public readonly Histogram ConnectionDuration = meter.CreateHistogram( - name: "http-client-connection-duration", + name: "http.client.connection.duration", unit: "s", - description: "The duration of outbound HTTP connections."); + description: "The duration of successfully established outbound HTTP connections."); public readonly Histogram RequestsQueueDuration = meter.CreateHistogram( - name: "http-client-requests-queue-duration", + name: "http.client.request.time_in_queue", unit: "s", description: "The amount of time requests spent on a queue waiting for an available connection."); - public void RequestLeftQueue(HttpConnectionPool pool, TimeSpan duration, int versionMajor) + public void RequestLeftQueue(HttpRequestMessage request, HttpConnectionPool pool, TimeSpan duration, int versionMajor) { Debug.Assert(versionMajor is 1 or 2 or 3); @@ -35,21 +31,23 @@ public void RequestLeftQueue(HttpConnectionPool pool, TimeSpan duration, int ver TagList tags = default; // While requests may report HTTP/1.0 as the protocol, we treat all HTTP/1.X connections as HTTP/1.1. - tags.Add("protocol", versionMajor switch + tags.Add("network.protocol.version", versionMajor switch { - 1 => "HTTP/1.1", - 2 => "HTTP/2", - _ => "HTTP/3" + 1 => "1.1", + 2 => "2.0", + _ => "3.0" }); - tags.Add("scheme", pool.IsSecure ? "https" : "http"); - tags.Add("host", pool.OriginAuthority.HostValue); + tags.Add("url.scheme", pool.IsSecure ? "https" : "http"); + tags.Add("server.address", pool.OriginAuthority.HostValue); if (!pool.IsDefaultPort) { - tags.Add("port", pool.OriginAuthority.Port); + tags.Add("server.port", pool.OriginAuthority.Port); } + tags.Add(MetricsHandler.GetMethodTag(request.Method)); + RequestsQueueDuration.Record(duration.TotalSeconds, tags); } } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs index 111957ca1ff089..3b601cf05e1aae 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs @@ -7,6 +7,7 @@ using System.Diagnostics.Metrics; using System.Linq; using System.Net.Http.Metrics; +using System.Net.Sockets; using System.Net.Test.Common; using System.Reflection; using System.Text; @@ -22,20 +23,19 @@ public abstract class HttpMetricsTestBase : HttpClientHandlerTestBase { protected static class InstrumentNames { - public const string RequestDuration = "http-client-request-duration"; - public const string CurrentRequests = "http-client-current-requests"; - public const string FailedRequests = "http-client-failed-requests"; - public const string CurrentConnections = "http-client-current-connections"; + public const string RequestDuration = "http.client.request.duration"; + public const string ActiveRequests = "http.client.active_requests"; + public const string OpenConnections = "http.client.open_connections"; public const string IdleConnections = "http-client-current-idle-connections"; - public const string ConnectionDuration = "http-client-connection-duration"; - public const string RequestsQueueDuration = "http-client-requests-queue-duration"; + public const string ConnectionDuration = "http.client.connection.duration"; + public const string TimeInQueue = "http.client.request.time_in_queue"; } protected HttpMetricsTestBase(ITestOutputHelper output) : base(output) { } - protected static void VerifyOptionalTag(KeyValuePair[] tags, string name, T value) + protected static void VerifyTag(KeyValuePair[] tags, string name, T value) { if (value is null) { @@ -47,64 +47,95 @@ protected static void VerifyOptionalTag(KeyValuePair[] tags, } } - private static void VerifySchemeHostPortTags(KeyValuePair[] tags, Uri uri) + private static void VerifySocketAddress(KeyValuePair[] tags) { - VerifyOptionalTag(tags, "scheme", uri.Scheme); - VerifyOptionalTag(tags, "host", uri.Host); - VerifyOptionalTag(tags, "port", uri.Port); + string ipString = (string)tags.Single(t => t.Key == "server.socket.address").Value; + IPAddress ip = IPAddress.Parse(ipString); + Assert.True(ip.Equals(IPAddress.Loopback.MapToIPv6()) || + ip.Equals(IPAddress.Loopback) || + ip.Equals(IPAddress.IPv6Loopback)); } - protected static void VerifyRequestDuration(Measurement measurement, Uri uri, string? protocol, int? statusCode, string method = "GET") => - VerifyRequestDuration(InstrumentNames.RequestDuration, measurement.Value, measurement.Tags.ToArray(), uri, protocol, statusCode, method); + private static void VerifySchemeHostPortTags(KeyValuePair[] tags, Uri uri) + { + VerifyTag(tags, "url.scheme", uri.Scheme); + VerifyTag(tags, "server.address", uri.Host); + VerifyTag(tags, "server.port", uri.Port); + } - protected static void VerifyRequestDuration(string instrumentName, double measurement, KeyValuePair[] tags, Uri uri, string? protocol, int? statusCode, string method = "GET") + protected static void VerifyRequestDuration(Measurement measurement, + Uri uri, + Version? protocolVersion, + int? statusCode, + string method = "GET", + string[] acceptedErrorReasons = null) => + VerifyRequestDuration(InstrumentNames.RequestDuration, measurement.Value, measurement.Tags.ToArray(), uri, protocolVersion, statusCode, method, acceptedErrorReasons); + + protected static void VerifyRequestDuration(string instrumentName, + double measurement, + KeyValuePair[] tags, + Uri uri, + Version? protocol, + int? statusCode, + string method = "GET", + string[] acceptedErrorReasons = null) { Assert.Equal(InstrumentNames.RequestDuration, instrumentName); Assert.InRange(measurement, double.Epsilon, 60); VerifySchemeHostPortTags(tags, uri); - VerifyOptionalTag(tags, "method", method); - VerifyOptionalTag(tags, "protocol", protocol); - VerifyOptionalTag(tags, "status-code", statusCode); + VerifyTag(tags, "http.request.method", method); + VerifyTag(tags, "network.protocol.version", protocol?.ToString()); + VerifyTag(tags, "http.response.status_code", statusCode); + if (acceptedErrorReasons == null) + { + Assert.DoesNotContain(tags, t => t.Key == "http.error.reason"); + } + else + { + string errorReason = (string)tags.Single(t => t.Key == "http.error.reason").Value; + Assert.Contains(errorReason, acceptedErrorReasons); + } } - protected static void VerifyCurrentRequest(Measurement measurement, long expectedValue, Uri uri) => - VerifyCurrentRequest(InstrumentNames.CurrentRequests, measurement.Value, measurement.Tags.ToArray(), expectedValue, uri); + protected static void VerifyActiveRequests(Measurement measurement, long expectedValue, Uri uri, string method = "GET") => + VerifyActiveRequests(InstrumentNames.ActiveRequests, measurement.Value, measurement.Tags.ToArray(), expectedValue, uri, method); - protected static void VerifyCurrentRequest(string instrumentName, long measurement, KeyValuePair[] tags, long expectedValue, Uri uri) + protected static void VerifyActiveRequests(string instrumentName, long measurement, KeyValuePair[] tags, long expectedValue, Uri uri, string method = "GET") { - Assert.Equal(InstrumentNames.CurrentRequests, instrumentName); + Assert.Equal(InstrumentNames.ActiveRequests, instrumentName); Assert.Equal(expectedValue, measurement); VerifySchemeHostPortTags(tags, uri); + Assert.Equal(method, tags.Single(t => t.Key == "http.request.method").Value); } - protected static void VerifyFailedRequests(Measurement measurement, long expectedValue, Uri uri, string? protocol, int? statusCode, string method = "GET") + protected static void VerifyOpenConnections(string actualName, object measurement, KeyValuePair[] tags, long expectedValue, Uri uri, Version? protocolVersion, string state) { - Assert.Equal(expectedValue, measurement.Value); - - KeyValuePair[] tags = measurement.Tags.ToArray(); - + Assert.Equal(InstrumentNames.OpenConnections, actualName); + Assert.Equal(expectedValue, Assert.IsType(measurement)); VerifySchemeHostPortTags(tags, uri); - - Assert.Equal(method, tags.Single(t => t.Key == "method").Value); - VerifyOptionalTag(tags, "protocol", protocol); - VerifyOptionalTag(tags, "status-code", statusCode); + VerifyTag(tags, "network.protocol.version", protocolVersion.ToString()); + VerifyTag(tags, "http.connection.state", state); + VerifySocketAddress(tags); } - protected static void VerifyConnectionCounter(string expectedName, string actualName, object measurement, KeyValuePair[] tags, long expectedValue, Uri uri, string protocol) + protected static void VerifyConnectionDuration(string instrumentName, object measurement, KeyValuePair[] tags, Uri uri, Version? protocolVersion) { - Assert.Equal(expectedName, actualName); - Assert.Equal(expectedValue, Assert.IsType(measurement)); + Assert.Equal(InstrumentNames.ConnectionDuration, instrumentName); + double value = Assert.IsType(measurement); + Assert.InRange(value, double.Epsilon, 60); VerifySchemeHostPortTags(tags, uri); - VerifyOptionalTag(tags, "protocol", protocol); + VerifyTag(tags, "network.protocol.version", protocolVersion.ToString()); + VerifySocketAddress(tags); } - protected static void VerifyConnectionDuration(string expectedName, string instrumentName, object measurement, KeyValuePair[] tags, Uri uri, string protocol) + protected static void VerifyTimeInQueue(string instrumentName, object measurement, KeyValuePair[] tags, Uri uri, Version? protocolVersion, string method = "GET") { - Assert.Equal(expectedName, instrumentName); + Assert.Equal(InstrumentNames.TimeInQueue, instrumentName); double value = Assert.IsType(measurement); Assert.InRange(value, double.Epsilon, 60); VerifySchemeHostPortTags(tags, uri); - VerifyOptionalTag(tags, "protocol", protocol); + VerifyTag(tags, "network.protocol.version", protocolVersion.ToString()); + VerifyTag(tags, "http.request.method", method); } protected static async Task WaitForEnvironmentTicksToAdvance() @@ -154,7 +185,20 @@ public InstrumentRecorder(IMeterFactory meterFactory, string instrumentName) public void Dispose() => _meterListener.Dispose(); } - protected record RecordedCounter(string InstrumentName, object Value, KeyValuePair[] Tags); + protected record RecordedCounter(string InstrumentName, object Value, KeyValuePair[] Tags) + { + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append($"{InstrumentName}={Value} ["); + for (int i = 0; i < Tags.Length - 1; i++) + { + sb.Append($"{Tags[i].Key}={Tags[i].Value}, "); + } + sb.Append($"{Tags.Last().Key}={Tags.Last().Value}]"); + return sb.ToString(); + } + } protected sealed class MultiInstrumentRecorder : IDisposable { @@ -205,51 +249,67 @@ public HttpMetricsTest(ITestOutputHelper output) : base(output) } [Fact] - public Task CurrentRequests_Success_Recorded() + public Task ActiveRequests_Success_Recorded() { return LoopbackServerFactory.CreateClientAndServerAsync(async uri => { using HttpMessageInvoker client = CreateHttpMessageInvoker(); - using InstrumentRecorder recorder = SetupInstrumentRecorder(InstrumentNames.CurrentRequests); + using InstrumentRecorder recorder = SetupInstrumentRecorder(InstrumentNames.ActiveRequests); using HttpRequestMessage request = new(HttpMethod.Get, uri) { Version = UseVersion }; HttpResponseMessage response = await SendAsync(client, request); response.Dispose(); // Make sure disposal doesn't interfere with recording by enforcing early disposal. Assert.Collection(recorder.GetMeasurements(), - m => VerifyCurrentRequest(m, 1, uri), - m => VerifyCurrentRequest(m, -1, uri)); + m => VerifyActiveRequests(m, 1, uri), + m => VerifyActiveRequests(m, -1, uri)); }, async server => { await server.AcceptConnectionSendResponseAndCloseAsync(); }); } - [Fact] - [OuterLoop("Uses Task.Delay")] - public async Task CurrentRequests_InstrumentEnabledAfterSending_NotRecorded() + [ConditionalFact(typeof(SocketsHttpHandler), nameof(SocketsHttpHandler.IsSupported))] + public async Task ActiveRequests_InstrumentEnabledAfterSending_NotRecorded() { - SemaphoreSlim instrumentEnabledSemaphore = new SemaphoreSlim(0); + if (UseVersion == HttpVersion.Version30) + { + return; // This test depends on ConnectCallback. + } + + TaskCompletionSource connectionStarted = new TaskCompletionSource(); await LoopbackServerFactory.CreateClientAndServerAsync(async uri => { using HttpMessageInvoker client = CreateHttpMessageInvoker(); + GetUnderlyingSocketsHttpHandler(Handler).ConnectCallback = async (ctx, cancellationToken) => + { + connectionStarted.SetResult(); + Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; + try + { + await socket.ConnectAsync(ctx.DnsEndPoint, cancellationToken); + return new NetworkStream(socket, ownsSocket: true); + } + catch + { + socket.Dispose(); + throw; + } + }; // Enable recording request-duration to test the path with metrics enabled. using InstrumentRecorder unrelatedRecorder = SetupInstrumentRecorder(InstrumentNames.RequestDuration); using HttpRequestMessage request = new(HttpMethod.Get, uri) { Version = UseVersion }; - Task clientTask = SendAsync(client, request); - await Task.Delay(100); - using InstrumentRecorder recorder = new(Handler.MeterFactory, InstrumentNames.CurrentRequests); - instrumentEnabledSemaphore.Release(); - + Task clientTask = Task.Run(() => SendAsync(client, request)); + await connectionStarted.Task; + using InstrumentRecorder recorder = new(Handler.MeterFactory, InstrumentNames.ActiveRequests); using HttpResponseMessage response = await clientTask; Assert.Empty(recorder.GetMeasurements()); }, async server => { - await instrumentEnabledSemaphore.WaitAsync(); await server.AcceptConnectionSendResponseAndCloseAsync(); }); } @@ -268,7 +328,7 @@ public Task RequestDuration_Success_Recorded(string method, HttpStatusCode statu using HttpResponseMessage response = await SendAsync(client, request); Measurement m = recorder.GetMeasurements().Single(); - VerifyRequestDuration(m, uri, ExpectedProtocolString, (int)statusCode, method); + VerifyRequestDuration(m, uri, UseVersion, (int)statusCode, method); }, async server => { @@ -293,7 +353,7 @@ public Task RequestDuration_CustomTags_Recorded() using HttpResponseMessage response = await SendAsync(client, request); Measurement m = recorder.GetMeasurements().Single(); - VerifyRequestDuration(m, uri, ExpectedProtocolString, 200); + VerifyRequestDuration(m, uri, UseVersion, 200); Assert.Equal("/test", m.Tags.ToArray().Single(t => t.Key == "route").Value); }, async server => @@ -346,7 +406,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => using HttpResponseMessage response = await SendAsync(client, request); Measurement m = recorder.GetMeasurements().Single(); - VerifyRequestDuration(m, uri, ExpectedProtocolString, 200); + VerifyRequestDuration(m, uri, UseVersion, 200); Assert.Equal("/test", m.Tags.ToArray().Single(t => t.Key == "route").Value); Assert.Equal("observed!", m.Tags.ToArray().Single(t => t.Key == "observed?").Value); @@ -399,7 +459,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => using HttpResponseMessage response = await client.SendAsync(TestAsync, request, completionOption); Measurement m = recorder.GetMeasurements().Single(); - VerifyRequestDuration(m, uri, ExpectedProtocolString, 200); ; + VerifyRequestDuration(m, uri, UseVersion, 200); ; Assert.Equal("before!", m.Tags.ToArray().Single(t => t.Key == "before").Value); }, async server => { @@ -423,14 +483,14 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => } [ConditionalFact(nameof(SupportsSeparateHttpSpansForRedirects))] - public Task CurrentRequests_Redirect_RecordedForEachHttpSpan() + public Task ActiveRequests_Redirect_RecordedForEachHttpSpan() { return LoopbackServerFactory.CreateServerAsync((originalServer, originalUri) => { return LoopbackServerFactory.CreateServerAsync(async (redirectServer, redirectUri) => { using HttpMessageInvoker client = CreateHttpMessageInvoker(); - using InstrumentRecorder recorder = SetupInstrumentRecorder(InstrumentNames.CurrentRequests); + using InstrumentRecorder recorder = SetupInstrumentRecorder(InstrumentNames.ActiveRequests); using HttpRequestMessage request = new(HttpMethod.Get, originalUri) { Version = UseVersion }; Task clientTask = SendAsync(client, request); @@ -445,14 +505,60 @@ public Task CurrentRequests_Redirect_RecordedForEachHttpSpan() await clientTask; Assert.Collection(recorder.GetMeasurements(), - m => VerifyCurrentRequest(m, 1, originalUri), - m => VerifyCurrentRequest(m, -1, originalUri), - m => VerifyCurrentRequest(m, 1, redirectUri), - m => VerifyCurrentRequest(m, -1, redirectUri)); + m => VerifyActiveRequests(m, 1, originalUri), + m => VerifyActiveRequests(m, -1, originalUri), + m => VerifyActiveRequests(m, 1, redirectUri), + m => VerifyActiveRequests(m, -1, redirectUri)); }); }); } + public static TheoryData MethodData = new TheoryData() + { + { "GET", "GET" }, + { "get", "GET" }, + { "PUT", "PUT" }, + { "Put", "PUT" }, + { "POST", "POST" }, + { "pOst", "POST" }, + { "delete", "DELETE" }, + { "head", "HEAD" }, + { "options", "OPTIONS" }, + { "trace", "TRACE" }, + { "patch", "PATCH" }, + { "connect", "CONNECT" }, + { "g3t", "_OTHER" }, + }; + + [Theory] + [PlatformSpecific(~TestPlatforms.Browser)] // BrowserHttpHandler supports only a limited set of methods. + [MemberData(nameof(MethodData))] + public async Task RequestMetrics_EmitNormalizedMethodTags(string method, string expectedMethodTag) + { + await LoopbackServerFactory.CreateClientAndServerAsync(async uri => + { + using HttpMessageInvoker client = CreateHttpMessageInvoker(); + using InstrumentRecorder requestDuration = SetupInstrumentRecorder(InstrumentNames.RequestDuration); + using InstrumentRecorder activeRequests = SetupInstrumentRecorder(InstrumentNames.ActiveRequests); + using InstrumentRecorder timeInQueue = SetupInstrumentRecorder(InstrumentNames.TimeInQueue); + + using HttpRequestMessage request = new(new HttpMethod(method), uri) { Version = UseVersion }; + if (expectedMethodTag == "CONNECT") + { + request.Headers.Host = "localhost"; + } + + using HttpResponseMessage response = await client.SendAsync(TestAsync, request); + + Assert.All(requestDuration.GetMeasurements(), m => VerifyTag(m.Tags.ToArray(), "http.request.method", expectedMethodTag)); + Assert.All(activeRequests.GetMeasurements(), m => VerifyTag(m.Tags.ToArray(), "http.request.method", expectedMethodTag)); + Assert.All(timeInQueue.GetMeasurements(), m => VerifyTag(m.Tags.ToArray(), "http.request.method", expectedMethodTag)); + }, async server => + { + await server.AcceptConnectionSendResponseAndCloseAsync(); + }); + } + [ConditionalFact(typeof(SocketsHttpHandler), nameof(SocketsHttpHandler.IsSupported))] public async Task AllSocketsHttpHandlerCounters_Success_Recorded() { @@ -478,33 +584,43 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => clientDisposedTcs.SetResult(); Action requestsQueueDuration = m => - VerifyConnectionDuration(InstrumentNames.RequestsQueueDuration, m.InstrumentName, m.Value, m.Tags, uri, ExpectedProtocolString); - + VerifyTimeInQueue(m.InstrumentName, m.Value, m.Tags, uri, UseVersion); Action connectionNoLongerIdle = m => - VerifyConnectionCounter(InstrumentNames.IdleConnections, m.InstrumentName, m.Value, m.Tags, -1, uri, ExpectedProtocolString); + VerifyOpenConnections(m.InstrumentName, m.Value, m.Tags, -1, uri, UseVersion, "idle"); + Action connectionIsActive = m => + VerifyOpenConnections(m.InstrumentName, m.Value, m.Tags, 1, uri, UseVersion, "active"); Action check1 = requestsQueueDuration; Action check2 = connectionNoLongerIdle; + Action check3 = connectionIsActive; if (UseVersion.Major > 1) { - // With HTTP/2 and HTTP/3, the IdleConnections counter is emitted before RequestsQueueDuration. + // With HTTP/2 and HTTP/3, the idle state change is emitted before RequestsQueueDuration. check1 = connectionNoLongerIdle; - check2 = requestsQueueDuration; + check2 = connectionIsActive; + check3 = requestsQueueDuration; } - Assert.Collection(recorder.GetMeasurements(), - m => VerifyCurrentRequest(m.InstrumentName, (long)m.Value, m.Tags, 1, uri), - m => VerifyConnectionCounter(InstrumentNames.CurrentConnections, m.InstrumentName, m.Value, m.Tags, 1, uri, ExpectedProtocolString), - m => VerifyConnectionCounter(InstrumentNames.IdleConnections, m.InstrumentName, m.Value, m.Tags, 1, uri, ExpectedProtocolString), - check1, // requestsQueueDuration and connectionNoLongerIdle in the appropriate order. + IReadOnlyList measurements = recorder.GetMeasurements(); + foreach (RecordedCounter m in measurements) + { + _output.WriteLine(m.ToString()); + } + + Assert.Collection(measurements, + m => VerifyActiveRequests(m.InstrumentName, (long)m.Value, m.Tags, 1, uri), + m => VerifyOpenConnections(m.InstrumentName, m.Value, m.Tags, 1, uri, UseVersion, "idle"), + check1, // requestsQueueDuration, connectionNoLongerIdle, connectionIsActive in the appropriate order. check2, - m => VerifyConnectionCounter(InstrumentNames.IdleConnections, m.InstrumentName, m.Value, m.Tags, 1, uri, ExpectedProtocolString), - m => VerifyCurrentRequest(m.InstrumentName, (long)m.Value, m.Tags, -1, uri), - m => VerifyRequestDuration(m.InstrumentName, (double)m.Value, m.Tags, uri, ExpectedProtocolString, 200), - m => VerifyConnectionCounter(InstrumentNames.IdleConnections, m.InstrumentName, m.Value, m.Tags, -1, uri, ExpectedProtocolString), - m => VerifyConnectionCounter(InstrumentNames.CurrentConnections, m.InstrumentName, m.Value, m.Tags, -1, uri, ExpectedProtocolString), - m => VerifyConnectionDuration(InstrumentNames.ConnectionDuration, m.InstrumentName, m.Value, m.Tags, uri, ExpectedProtocolString)); + check3, + m => VerifyOpenConnections(m.InstrumentName, m.Value, m.Tags, -1, uri, UseVersion, "active"), + m => VerifyOpenConnections(m.InstrumentName, m.Value, m.Tags, 1, uri, UseVersion, "idle"), + + m => VerifyActiveRequests(m.InstrumentName, (long)m.Value, m.Tags, -1, uri), + m => VerifyRequestDuration(m.InstrumentName, (double)m.Value, m.Tags, uri, UseVersion, 200), + m => VerifyConnectionDuration(m.InstrumentName, m.Value, m.Tags, uri, UseVersion), + m => VerifyOpenConnections(m.InstrumentName, m.Value, m.Tags, -1, uri, UseVersion, "idle")); }, async server => { @@ -547,14 +663,6 @@ protected InstrumentRecorder SetupInstrumentRecorder(string instrumentName return new InstrumentRecorder(_meterFactory, instrumentName); } - protected string ExpectedProtocolString => (UseVersion.Major, UseVersion.Minor) switch - { - (1, 1) => "HTTP/1.1", - (2, 0) => "HTTP/2", - (3, 0) => "HTTP/3", - _ => throw new Exception("Unknown version.") - }; - protected sealed class EnrichmentHandler : DelegatingHandler { public EnrichmentHandler(HttpMessageHandler innerHandler) : base(innerHandler) @@ -606,7 +714,7 @@ await Assert.ThrowsAsync(async () => } Measurement m = recorder.GetMeasurements().Single(); - VerifyRequestDuration(m, uri, ExpectedProtocolString, 200); ; + VerifyRequestDuration(m, uri, UseVersion, 200); Assert.Equal("before!", m.Tags.ToArray().Single(t => t.Key == "before").Value); }, server => server.HandleRequestAsync(headers: new[] { @@ -616,13 +724,13 @@ await Assert.ThrowsAsync(async () => [Fact] [SkipOnPlatform(TestPlatforms.Browser, "Browser is relaxed about validating HTTP headers")] - public async Task FailedRequests_ConnectionClosedWhileReceivingHeaders_Recorded() + public async Task RequestDuration_ConnectionClosedWhileReceivingHeaders_Recorded() { using CancellationTokenSource cancelServerCts = new CancellationTokenSource(); await LoopbackServerFactory.CreateClientAndServerAsync(async uri => { using HttpMessageInvoker client = CreateHttpMessageInvoker(); - using InstrumentRecorder recorder = SetupInstrumentRecorder(InstrumentNames.FailedRequests); + using InstrumentRecorder recorder = SetupInstrumentRecorder(InstrumentNames.RequestDuration); using HttpRequestMessage request = new(HttpMethod.Get, uri) { Version = UseVersion }; Exception ex = await Assert.ThrowsAnyAsync(async () => @@ -634,8 +742,8 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => cancelServerCts.Cancel(); Assert.True(ex is HttpRequestException or TaskCanceledException); - Measurement m = recorder.GetMeasurements().Single(); - VerifyFailedRequests(m, 1, uri, null, null); + Measurement m = recorder.GetMeasurements().Single(); + VerifyRequestDuration(m, uri, null, null, acceptedErrorReasons: new[] { "_OTHER", "cancellation", "response_ended" }); }, async server => { try @@ -681,21 +789,20 @@ await server.AcceptConnectionAsync(async connection => { await connection.ReadRequestHeaderAndSendResponseAsync(); } - }); if (malformedResponse) { await Assert.ThrowsAsync(() => clientTask); Measurement m = recorder.GetMeasurements().Single(); - VerifyRequestDuration(m, server.Address, null, null); // Protocol is not logged. + VerifyRequestDuration(m, server.Address, null, null, acceptedErrorReasons: new[] { "response_ended" }); } else { using HttpResponseMessage response = await clientTask; Measurement m = recorder.GetMeasurements().Single(); - VerifyRequestDuration(m, server.Address, "HTTP/1.1", 200); + VerifyRequestDuration(m, server.Address, HttpVersion.Version11, 200); } }, new LoopbackServer.Options() { UseSsl = true }); @@ -753,10 +860,10 @@ public Task RequestDuration_Redirect_RecordedForEachHttpSpan() Assert.Collection(recorder.GetMeasurements(), m0 => { - VerifyRequestDuration(m0, originalUri, $"HTTP/1.1", (int)HttpStatusCode.Redirect); + VerifyRequestDuration(m0, originalUri, HttpVersion.Version11, (int)HttpStatusCode.Redirect); }, m1 => { - VerifyRequestDuration(m1, redirectUri, $"HTTP/2", (int)HttpStatusCode.OK); + VerifyRequestDuration(m1, redirectUri, HttpVersion.Version20, (int)HttpStatusCode.OK); }); }, options: new GenericLoopbackOptions() { UseSsl = true }); @@ -786,33 +893,7 @@ await Assert.ThrowsAsync(async () => }); Measurement m = recorder.GetMeasurements().Single(); - VerifyRequestDuration(m, server.Address, null, null); // Protocol is not recorded - } - - [Fact] - public async Task FailedRequests_ProtocolError_Recorded() - { - using Http2LoopbackServer server = Http2LoopbackServer.CreateServer(); - using HttpMessageInvoker client = CreateHttpMessageInvoker(); - using InstrumentRecorder recorder = SetupInstrumentRecorder(InstrumentNames.FailedRequests); - - using HttpRequestMessage request = new(HttpMethod.Get, server.Address) { Version = HttpVersion.Version20 }; - Task sendTask = SendAsync(client, request); - - Http2LoopbackConnection connection = await server.EstablishConnectionAsync(); - int streamId = await connection.ReadRequestHeaderAsync(); - - // Send a reset stream frame so that the stream moves to a terminal state. - RstStreamFrame resetStream = new RstStreamFrame(FrameFlags.None, (int)ProtocolErrors.INTERNAL_ERROR, streamId); - await connection.WriteFrameAsync(resetStream); - - await Assert.ThrowsAsync(async () => - { - using HttpResponseMessage response = await sendTask; - }); - - Measurement m = recorder.GetMeasurements().Single(); - VerifyFailedRequests(m, 1, server.Address, null, null); + VerifyRequestDuration(m, server.Address, null, null, acceptedErrorReasons: new[] { "http_protocol_error" }); } } @@ -850,7 +931,7 @@ public HttpMetricsTest_DefaultMeter(ITestOutputHelper output) : base(output) } [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public void CurrentRequests_Success_Recorded() + public void ActiveRequests_Success_Recorded() { RemoteExecutor.Invoke(static async Task () => { @@ -858,15 +939,15 @@ public void CurrentRequests_Success_Recorded() await test.LoopbackServerFactory.CreateClientAndServerAsync(async uri => { using HttpClient client = test.CreateHttpClient(); - using InstrumentRecorder recorder = new InstrumentRecorder(InstrumentNames.CurrentRequests); + using InstrumentRecorder recorder = new InstrumentRecorder(InstrumentNames.ActiveRequests); using HttpRequestMessage request = new(HttpMethod.Get, uri) { Version = test.UseVersion }; HttpResponseMessage response = await client.SendAsync(request); response.Dispose(); // Make sure disposal doesn't interfere with recording by enforcing early disposal. Assert.Collection(recorder.GetMeasurements(), - m => VerifyCurrentRequest(m, 1, uri), - m => VerifyCurrentRequest(m, -1, uri)); + m => VerifyActiveRequests(m, 1, uri), + m => VerifyActiveRequests(m, -1, uri)); }, async server => { await server.AcceptConnectionSendResponseAndCloseAsync(); @@ -898,18 +979,21 @@ await test.LoopbackServerFactory.CreateClientAndServerAsync(async uri => await WaitForEnvironmentTicksToAdvance(); } + Version version = HttpVersion.Version11; Assert.Collection(recorder.GetMeasurements(), - m => VerifyCurrentRequest(m.InstrumentName, (long)m.Value, m.Tags, 1, uri), - m => VerifyConnectionCounter(InstrumentNames.CurrentConnections, m.InstrumentName, m.Value, m.Tags, 1, uri, "HTTP/1.1"), - m => VerifyConnectionCounter(InstrumentNames.IdleConnections, m.InstrumentName, m.Value, m.Tags, 1, uri, "HTTP/1.1"), - m => VerifyConnectionDuration(InstrumentNames.RequestsQueueDuration, m.InstrumentName, m.Value, m.Tags, uri, "HTTP/1.1"), - m => VerifyConnectionCounter(InstrumentNames.IdleConnections, m.InstrumentName, m.Value, m.Tags, -1, uri, "HTTP/1.1"), - m => VerifyConnectionCounter(InstrumentNames.IdleConnections, m.InstrumentName, m.Value, m.Tags, 1, uri, "HTTP/1.1"), - m => VerifyCurrentRequest(m.InstrumentName, (long)m.Value, m.Tags, -1, uri), - m => VerifyRequestDuration(m.InstrumentName, (double)m.Value, m.Tags, uri, "HTTP/1.1", 200), - m => VerifyConnectionCounter(InstrumentNames.IdleConnections, m.InstrumentName, m.Value, m.Tags, -1, uri, "HTTP/1.1"), - m => VerifyConnectionCounter(InstrumentNames.CurrentConnections, m.InstrumentName, m.Value, m.Tags, -1, uri, "HTTP/1.1"), - m => VerifyConnectionDuration(InstrumentNames.ConnectionDuration, m.InstrumentName, m.Value, m.Tags, uri, "HTTP/1.1")); + m => VerifyActiveRequests(m.InstrumentName, (long)m.Value, m.Tags, 1, uri), + m => VerifyOpenConnections(m.InstrumentName, m.Value, m.Tags, 1, uri, version, "idle"), + m => VerifyTimeInQueue(m.InstrumentName, m.Value, m.Tags, uri, version), + + m => VerifyOpenConnections(m.InstrumentName, m.Value, m.Tags, -1, uri, version, "idle"), + m => VerifyOpenConnections(m.InstrumentName, m.Value, m.Tags, 1, uri, version, "active"), + m => VerifyOpenConnections(m.InstrumentName, m.Value, m.Tags, -1, uri, version, "active"), + m => VerifyOpenConnections(m.InstrumentName, m.Value, m.Tags, 1, uri, version, "idle"), + + m => VerifyActiveRequests(m.InstrumentName, (long)m.Value, m.Tags, -1, uri), + m => VerifyRequestDuration(m.InstrumentName, (double)m.Value, m.Tags, uri, version, 200), + m => VerifyConnectionDuration(m.InstrumentName, m.Value, m.Tags, uri, version), + m => VerifyOpenConnections(m.InstrumentName, m.Value, m.Tags, -1, uri, version, "idle")); }, async server => { @@ -939,7 +1023,7 @@ await test.LoopbackServerFactory.CreateClientAndServerAsync(async uri => using HttpResponseMessage response = await client.SendAsync(request); Measurement m = recorder.GetMeasurements().Single(); - VerifyRequestDuration(m, uri, "HTTP/1.1", (int)HttpStatusCode.OK, "GET"); + VerifyRequestDuration(m, uri, HttpVersion.Version11, (int)HttpStatusCode.OK, "GET"); }, async server => { await server.AcceptConnectionSendResponseAndCloseAsync(HttpStatusCode.OK);