diff --git a/eng/Packages/General.props b/eng/Packages/General.props
index 64c438de5b0..1117bb0c7e8 100644
--- a/eng/Packages/General.props
+++ b/eng/Packages/General.props
@@ -19,6 +19,7 @@
+
diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HelperExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HelperExtensions.cs
new file mode 100644
index 00000000000..c3998d964d2
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HelperExtensions.cs
@@ -0,0 +1,51 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace Microsoft.Extensions.Http.Telemetry.Metering;
+
+internal static class HelperExtensions
+{
+ internal static HttpStatusCode GetStatusCode(this Exception ex)
+ {
+ if (ex is TaskCanceledException)
+ {
+ return HttpStatusCode.GatewayTimeout;
+ }
+ else if (ex is HttpRequestException exception)
+ {
+#if NET5_0_OR_GREATER
+ return exception.StatusCode ?? HttpStatusCode.ServiceUnavailable;
+#else
+ return HttpStatusCode.ServiceUnavailable;
+#endif
+ }
+ else
+ {
+ return HttpStatusCode.InternalServerError;
+ }
+ }
+
+ ///
+ /// Classifies the result of the HTTP response.
+ ///
+ /// An to categorize the status code of HTTP response.
+ /// type of HTTP response and its .
+ internal static HttpRequestResultType GetResultCategory(this HttpStatusCode statusCode)
+ {
+ if (statusCode >= HttpStatusCode.Continue && statusCode < HttpStatusCode.BadRequest)
+ {
+ return HttpRequestResultType.Success;
+ }
+ else if (statusCode >= HttpStatusCode.BadRequest && statusCode < HttpStatusCode.InternalServerError)
+ {
+ return HttpRequestResultType.ExpectedFailure;
+ }
+
+ return HttpRequestResultType.Failure;
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpClientMeteringExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpClientMeteringExtensions.cs
index 3212b6c54e1..7927428efaf 100644
--- a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpClientMeteringExtensions.cs
+++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpClientMeteringExtensions.cs
@@ -2,9 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
+#if !NETFRAMEWORK
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Http.Telemetry.Metering.Internal;
+#endif
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Http.Telemetry;
using Microsoft.Extensions.Telemetry.Internal;
using Microsoft.Extensions.Telemetry.Metering;
using Microsoft.Shared.Collections;
@@ -18,11 +22,50 @@ namespace Microsoft.Extensions.Http.Telemetry.Metering;
///
public static class HttpClientMeteringExtensions
{
+#if !NETFRAMEWORK
///
- /// Adds a to collect and emit metrics for outgoing requests from all http clients.
+ /// Adds Http client diagnostics listener to capture metrics for requests from all http clients.
///
///
- /// This extension configures outgoing request metrics auto collection globally for all http clients.
+ /// This extension configures outgoing request metrics auto collection
+ /// globally for all http clients regardless of how the http client is created.
+ ///
+ /// The .
+ ///
+ /// instance for chaining.
+ ///
+ [Experimental]
+ public static IServiceCollection AddHttpClientMeteringForAllHttpClients(this IServiceCollection services)
+ {
+ _ = Throw.IfNull(services);
+
+ _ = services
+ .RegisterMetering()
+ .AddOutgoingRequestContext();
+
+ services.TryAddSingleton(sp =>
+ {
+ var meter = sp.GetRequiredService>();
+ var outgoingRequestMetricEnrichers = sp.GetService>().EmptyIfNull();
+ var requestMetadataContext = sp.GetService();
+ var downstreamDependencyMetadataManager = sp.GetService();
+ return new HttpMeteringHandler(meter, outgoingRequestMetricEnrichers, requestMetadataContext, downstreamDependencyMetadataManager);
+ });
+
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddActivatedSingleton();
+
+ return services;
+ }
+#endif
+
+ ///
+ /// Adds a to collect and emit metrics for outgoing requests from all http clients created using IHttpClientFactory.
+ ///
+ ///
+ /// This extension configures outgoing request metrics auto collection
+ /// for all http clients created using IHttpClientFactory.
///
/// The .
///
diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpMeteringHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpMeteringHandler.cs
index abe1dd510ab..ba946b7198b 100644
--- a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpMeteringHandler.cs
+++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpMeteringHandler.cs
@@ -27,9 +27,12 @@ namespace Microsoft.Extensions.Http.Telemetry.Metering;
///
public class HttpMeteringHandler : DelegatingHandler
{
+#pragma warning disable CA2213 // Disposable fields should be disposed
+ internal readonly Meter Meter;
+#pragma warning restore CA2213 // Disposable fields should be disposed
internal TimeProvider TimeProvider = TimeProvider.System;
- private const int StandardDimensionsCount = 4;
+ private const int StandardDimensionsCount = 5;
private const int MaxCustomDimensionsCount = 14;
private static readonly RequestMetadata _fallbackMetadata = new();
@@ -60,7 +63,7 @@ internal HttpMeteringHandler(
IOutgoingRequestContext? requestMetadataContext,
IDownstreamDependencyMetadataManager? downstreamDependencyMetadataManager = null)
{
- _ = Throw.IfNull(meter);
+ Meter = Throw.IfNull(meter);
_ = Throw.IfNull(enrichers);
_requestEnrichers = enrichers.ToArray();
@@ -84,7 +87,8 @@ internal HttpMeteringHandler(
Metric.ReqHost,
Metric.DependencyName,
Metric.ReqName,
- Metric.RspResultCode
+ Metric.RspResultCode,
+ Metric.RspResultCategory
};
for (int i = 0; i < _requestEnrichers.Length; i++)
@@ -104,39 +108,12 @@ internal HttpMeteringHandler(
_downstreamDependencyMetadataManager = downstreamDependencyMetadataManager;
}
- internal static string GetHostName(HttpRequestMessage request) => string.IsNullOrWhiteSpace(request.RequestUri?.Host) ? TelemetryConstants.Unknown : request.RequestUri!.Host;
+ internal static string GetHostName(HttpRequestMessage request)
+ => string.IsNullOrWhiteSpace(request.RequestUri?.Host)
+ ? TelemetryConstants.Unknown
+ : request.RequestUri!.Host;
- ///
- /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation.
- ///
- /// The HTTP request message to send to the server.
- /// A cancellation token to cancel operation.
- ///
- /// The task object representing the asynchronous operation.
- ///
- protected override async Task SendAsync(
- HttpRequestMessage request,
- CancellationToken cancellationToken)
- {
- _ = Throw.IfNull(request);
-
- var timestamp = TimeProvider.GetTimestamp();
-
- try
- {
- var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
- OnRequestEnd(request, timestamp, response.StatusCode);
- return response;
- }
- catch
- {
- // This will not catch a response that returns 4xx, 5xx, etc. but will only catch when base.SendAsync() fails.
- OnRequestEnd(request, timestamp, HttpStatusCode.InternalServerError);
- throw;
- }
- }
-
- private void OnRequestEnd(HttpRequestMessage request, long timestamp, HttpStatusCode statusCode)
+ internal void OnRequestEnd(HttpRequestMessage request, long elapsedMilliseconds, HttpStatusCode statusCode)
{
var requestMetadata = request.GetRequestMetadata() ??
_requestMetadataContext?.RequestMetadata ??
@@ -145,15 +122,16 @@ private void OnRequestEnd(HttpRequestMessage request, long timestamp, HttpStatus
var dependencyName = requestMetadata.DependencyName;
var requestName = $"{request.Method} {requestMetadata.GetRequestName()}";
var hostName = GetHostName(request);
- var duration = (long)TimeProvider.GetElapsedTime(timestamp, TimeProvider.GetTimestamp()).TotalMilliseconds;
-
+ var duration = elapsedMilliseconds;
+ var result = statusCode.GetResultCategory();
var tagList = new TagList
{
new(Metric.ReqHost, hostName),
new(Metric.DependencyName, dependencyName),
new(Metric.ReqName, requestName),
- new(Metric.RspResultCode, (int)statusCode)
- };
+ new(Metric.RspResultCode, (int)statusCode),
+ new(Metric.RspResultCategory, result.ToInvariantString())
+ };
// keep default case fast by avoiding allocations
if (_enrichersCount == 0)
@@ -183,4 +161,44 @@ private void OnRequestEnd(HttpRequestMessage request, long timestamp, HttpStatus
}
}
}
+
+ ///
+ /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation.
+ ///
+ /// The HTTP request message to send to the server.
+ /// A cancellation token to cancel operation.
+ ///
+ /// The task object representing the asynchronous operation.
+ ///
+ protected override async Task SendAsync(
+ HttpRequestMessage request,
+ CancellationToken cancellationToken)
+ {
+ _ = Throw.IfNull(request);
+
+#if !NETFRAMEWORK
+ if (HttpClientMeteringListener.UsingDiagnosticsSource)
+ {
+ return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ }
+#endif
+
+ var start = TimeProvider.GetTimestamp();
+
+ try
+ {
+ var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ var elapsed = TimeProvider.GetTimestamp() - start;
+ long ms = (long)Math.Round(((double)elapsed / TimeProvider.System.TimestampFrequency) * 1000);
+ OnRequestEnd(request, ms, response.StatusCode);
+ return response;
+ }
+ catch (Exception ex)
+ {
+ var elapsed = TimeProvider.GetTimestamp() - start;
+ long ms = (long)Math.Round(((double)elapsed / TimeProvider.System.TimestampFrequency) * 1000);
+ OnRequestEnd(request, ms, ex.GetStatusCode());
+ throw;
+ }
+ }
}
diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpRequestResultType.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpRequestResultType.cs
new file mode 100644
index 00000000000..d6c33a18376
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpRequestResultType.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.EnumStrings;
+
+namespace Microsoft.Extensions.Http.Telemetry.Metering;
+
+///
+/// Statuses for classifying http request result.
+///
+[Experimental]
+[EnumStrings]
+public enum HttpRequestResultType
+{
+ ///
+ /// The status code of the http request indicates that the request is successful.
+ ///
+ Success,
+
+ ///
+ /// The status code of the http request indicates that this request did not succeed and to be treated as failure.
+ ///
+ Failure,
+
+ ///
+ /// The status code of the http request indicates that the request did not succeed but has failed with an error which is expected and acceptable for this request.
+ ///
+ ///
+ /// Expected failures are generally excluded from availability calculations i.e. they are neither
+ /// treated as success nor as failures for availability calculation.
+ ///
+ ExpectedFailure,
+}
diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/HttpClientDiagnosticObserver.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/HttpClientDiagnosticObserver.cs
new file mode 100644
index 00000000000..4920add805d
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/HttpClientDiagnosticObserver.cs
@@ -0,0 +1,45 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if !NETFRAMEWORK
+using System;
+using System.Diagnostics;
+
+namespace Microsoft.Extensions.Http.Telemetry.Metering.Internal;
+
+internal sealed class HttpClientDiagnosticObserver : IObserver, IDisposable
+{
+ private const string ListenerName = "HttpHandlerDiagnosticListener";
+ private readonly HttpClientRequestAdapter _httpClientRequestAdapter;
+ private IDisposable? _observer;
+
+ public HttpClientDiagnosticObserver(HttpClientRequestAdapter httpClientRequestAdapter)
+ {
+ _httpClientRequestAdapter = httpClientRequestAdapter;
+ }
+
+ public void OnNext(DiagnosticListener value)
+ {
+ if (value.Name == ListenerName)
+ {
+ _observer?.Dispose();
+ _observer = value.SubscribeWithAdapter(_httpClientRequestAdapter);
+ }
+ }
+
+ public void OnCompleted()
+ {
+ // Method intentionally left empty.
+ }
+
+ public void OnError(Exception error)
+ {
+ // Method intentionally left empty.
+ }
+
+ public void Dispose()
+ {
+ _observer?.Dispose();
+ }
+}
+#endif
diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/HttpClientMeteringListener.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/HttpClientMeteringListener.cs
new file mode 100644
index 00000000000..31e738d9cd4
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/HttpClientMeteringListener.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if !NETFRAMEWORK
+using System;
+using System.Diagnostics;
+
+namespace Microsoft.Extensions.Http.Telemetry.Metering.Internal;
+
+internal sealed class HttpClientMeteringListener : IDisposable
+{
+ internal static bool UsingDiagnosticsSource { get; set; }
+
+ private readonly IDisposable _listener;
+
+ public HttpClientMeteringListener(HttpClientDiagnosticObserver httpClientDiagnosticObserver)
+ {
+ UsingDiagnosticsSource = true;
+ _listener = DiagnosticListener.AllListeners.Subscribe(httpClientDiagnosticObserver);
+ }
+
+ public void Dispose()
+ {
+ _listener.Dispose();
+ }
+}
+#endif
diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/HttpClientRequestAdapter.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/HttpClientRequestAdapter.cs
new file mode 100644
index 00000000000..ac6d9476198
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/HttpClientRequestAdapter.cs
@@ -0,0 +1,101 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if !NETFRAMEWORK
+using System;
+#if !NET5_0_OR_GREATER
+using System.Collections.Generic;
+#endif
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DiagnosticAdapter;
+
+namespace Microsoft.Extensions.Http.Telemetry.Metering.Internal;
+
+#pragma warning disable R9A013 // This class has virtual members and can't be sealed.
+internal class HttpClientRequestAdapter
+#pragma warning restore R9A013 // This class has virtual members and can't be sealed.
+{
+ internal TimeProvider TimeProvider = TimeProvider.System;
+
+ private const string RequestStartTimeKey = "requestStartTimeTicks";
+
+#if NET5_0_OR_GREATER
+ private static readonly HttpRequestOptionsKey _requestStartTimeOptionsKey = new(RequestStartTimeKey);
+#endif
+
+ private readonly HttpMeteringHandler _httpMeteringHandler;
+
+ public HttpClientRequestAdapter(HttpMeteringHandler httpMeteringHandler)
+ {
+ _httpMeteringHandler = httpMeteringHandler;
+ }
+
+ [DiagnosticName("System.Net.Http.HttpRequestOut")]
+ public virtual void HttpClientListenerSubscribed(HttpRequestMessage request)
+ {
+ // This won't be invoked. This is needed just to add subscription for top level namespace,
+ // because the http handler diagnostics listener check for this subscription to be present
+ // before emitting request start or end events.
+ }
+
+ [DiagnosticName("System.Net.Http.HttpRequestOut.Start")]
+ public virtual void OnRequestStart(HttpRequestMessage request)
+ {
+#if NET5_0_OR_GREATER
+ request.Options.Set(_requestStartTimeOptionsKey, TimeProvider.GetUtcNow());
+#else
+ request.Properties[RequestStartTimeKey] = TimeProvider.GetUtcNow();
+#endif
+ }
+
+ [DiagnosticName("System.Net.Http.HttpRequestOut.Stop")]
+ public virtual void OnRequestStop(HttpResponseMessage? response, HttpRequestMessage request, TaskStatus requestTaskStatus)
+ {
+ if (requestTaskStatus == TaskStatus.Faulted)
+ {
+ // TaskStatus is faulted in case of any exceptions (except operation cancelled)
+ // Metrics emission will be handled as part of the exception event in this case.
+ return;
+ }
+
+ long durationInMs = GetRequestDuration(request);
+ HttpStatusCode statusCode;
+ if (response == null)
+ {
+ statusCode = requestTaskStatus == TaskStatus.Canceled ? HttpStatusCode.GatewayTimeout : HttpStatusCode.InternalServerError;
+ }
+ else
+ {
+ statusCode = response.StatusCode;
+ }
+
+ _httpMeteringHandler.OnRequestEnd(request, durationInMs, statusCode);
+ }
+
+ [DiagnosticName("System.Net.Http.Exception")]
+ public virtual void OnRequestException(Exception exception, HttpRequestMessage request)
+ {
+ long durationInMs = GetRequestDuration(request);
+ _httpMeteringHandler.OnRequestEnd(request, durationInMs, exception.GetStatusCode());
+ }
+
+ private long GetRequestDuration(HttpRequestMessage request)
+ {
+ long durationInMs = 0;
+
+#if NET5_0_OR_GREATER
+ if (request.Options.TryGetValue(_requestStartTimeOptionsKey, out var startTime))
+#else
+ if (request.Properties.TryGetValue(RequestStartTimeKey, out var startTimeObject) &&
+ startTimeObject is DateTimeOffset startTime)
+#endif
+ {
+ durationInMs = (long)(TimeProvider.GetUtcNow() - startTime).TotalMilliseconds;
+ }
+
+ return durationInMs;
+ }
+}
+#endif
diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/Metric.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/Metric.cs
index 6409d250efc..5ed625565e2 100644
--- a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/Metric.cs
+++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/Metric.cs
@@ -30,15 +30,24 @@ internal static partial class Metric
///
///
/// This is the status code returned by the target dependency service. In case of exceptions, when
- /// no status code is available, this will be set to InternalServerError i.e. 500.
+ /// no status code is available, this will be set to 500.
///
internal const string RspResultCode = "rsp_resultCode";
+ ///
+ /// The response status category for the outgoing request.
+ ///
+ ///
+ /// This is the response cantegory returned by the target dependency service. It will return one of 3
+ /// statuses: success, failure or expectedfailure.
+ ///
+ internal const string RspResultCategory = "rsp_resultCategory";
+
///
/// Creates a new histogram instrument for an outgoing HTTP request.
///
/// Meter object.
///
- [Histogram(ReqHost, DependencyName, ReqName, RspResultCode, Name = OutgoingRequestMetricName)]
+ [Histogram(ReqHost, DependencyName, ReqName, RspResultCode, RspResultCategory, Name = OutgoingRequestMetricName)]
public static partial OutgoingMetric CreateHistogram(Meter meter);
}
diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Microsoft.Extensions.Http.Telemetry.csproj b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Microsoft.Extensions.Http.Telemetry.csproj
index 737f31fcdd8..8af37499861 100644
--- a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Microsoft.Extensions.Http.Telemetry.csproj
+++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Microsoft.Extensions.Http.Telemetry.csproj
@@ -6,6 +6,7 @@
+ true
true
true
true
@@ -34,10 +35,12 @@
+
+
diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/HttpMeteringHandlerTests.Ext.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/HttpMeteringHandlerTests.Ext.cs
new file mode 100644
index 00000000000..70b2ee0f6d1
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/HttpMeteringHandlerTests.Ext.cs
@@ -0,0 +1,136 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Http.Telemetry.Metering.Internal;
+using Microsoft.Extensions.Http.Telemetry.Metering.Test.Internal;
+using Microsoft.Extensions.Telemetry;
+using Microsoft.Extensions.Telemetry.Metering;
+using Microsoft.Extensions.Telemetry.Testing.Metering;
+using Xunit;
+
+namespace Microsoft.Extensions.Http.Telemetry.Metering.Test;
+
+#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
+public sealed partial class HttpMeteringHandlerTests : IDisposable
+{
+ [Fact]
+ public void SendAsync_expectedFailure_EmptyOutgoingRequestMetricEnricher()
+ {
+ using var meter = new Meter();
+ using var metricCollector = new MetricCollector(meter);
+ using var client = CreateClientWithHandler(meter);
+
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _expectedFailureUri);
+
+ using var _ = client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token).Result;
+
+ var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
+ Assert.NotNull(latest);
+ Assert.Equal("www.example-expectedfailure.com", latest.GetDimension(Metric.ReqHost));
+ Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(Metric.DependencyName));
+ Assert.Equal($"GET {TelemetryConstants.Unknown}", latest.GetDimension(Metric.ReqName));
+ Assert.Equal(400, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.ExpectedFailure.ToInvariantString(), latest.GetDimension(Metric.RspResultCategory));
+ }
+
+ [Fact]
+ public async Task SendAsync_TaskCanceledException()
+ {
+ using var meter = new Meter();
+ using var metricCollector = new MetricCollector(meter);
+ using var client = CreateClientWithHandler(meter);
+
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _failure1Uri);
+
+ await Assert.ThrowsAsync(async () => await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token));
+
+ var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
+ Assert.NotNull(latest);
+ Assert.Equal("www.example-failure1.com", latest.GetDimension(Metric.ReqHost));
+ Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(Metric.DependencyName));
+ Assert.Equal($"POST {TelemetryConstants.Unknown}", latest.GetDimension(Metric.ReqName));
+ Assert.Equal((int)HttpStatusCode.GatewayTimeout, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.Failure.ToInvariantString(), latest.GetDimension(Metric.RspResultCategory));
+ }
+
+ [Fact]
+ public async Task SendAsync_InvalidOperationException()
+ {
+ using var meter = new Meter();
+ using var metricCollector = new MetricCollector(meter);
+ using var client = CreateClientWithHandler(meter);
+
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _failure2Uri);
+
+ await Assert.ThrowsAsync(async () => await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token));
+
+ var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
+ Assert.NotNull(latest);
+ Assert.Equal("www.example-failure2.com", latest.GetDimension(Metric.ReqHost));
+ Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(Metric.DependencyName));
+ Assert.Equal($"POST {TelemetryConstants.Unknown}", latest.GetDimension(Metric.ReqName));
+ Assert.Equal((int)HttpStatusCode.InternalServerError, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.Failure.ToInvariantString(), latest.GetDimension(Metric.RspResultCategory));
+ }
+
+ [Fact]
+ public async Task SendAsync_Exception_OutgoingRequestMetricEnricherOnly()
+ {
+ using var meter = new Meter();
+ using var metricCollector = new MetricCollector(meter);
+ using var client = CreateClientWithHandler(meter,
+ new List
+ {
+ new TestEnricher(1),
+ });
+
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _failureUri);
+ httpRequestMessage.SetRequestMetadata(new RequestMetadata
+ {
+ DependencyName = "failure_service",
+ RequestRoute = "/foo/failure",
+ RequestName = "TestRequestName"
+ });
+
+ await Assert.ThrowsAsync(async () => await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token));
+
+ var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
+ Assert.NotNull(latest);
+ Assert.Equal("www.example-failure.com", latest.GetDimension(Metric.ReqHost));
+ Assert.Equal("failure_service", latest.GetDimension(Metric.DependencyName));
+ Assert.Equal("POST TestRequestName", latest.GetDimension(Metric.ReqName));
+ Assert.Equal((int)HttpStatusCode.ServiceUnavailable, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.Failure.ToInvariantString(), latest.GetDimension(Metric.RspResultCategory));
+ Assert.Equal("test_value_1", latest.GetDimension("test_property_1"));
+ }
+
+ [Fact]
+ public async Task SendAsync_HttpRequestException()
+ {
+ using var meter = new Meter();
+ using var metricCollector = new MetricCollector(meter);
+ using var client = CreateClientWithHandler(meter);
+
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _failure3Uri);
+
+ await Assert.ThrowsAsync(async () => await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token));
+
+ var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
+ Assert.NotNull(latest);
+ Assert.Equal("www.example-failure3.com", latest.GetDimension(Metric.ReqHost));
+ Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(Metric.DependencyName));
+ Assert.Equal($"POST {TelemetryConstants.Unknown}", latest.GetDimension(Metric.ReqName));
+#if NET6_0_OR_GREATER
+ Assert.Equal((int)HttpStatusCode.BadGateway, latest.GetDimension(Metric.RspResultCode));
+#else
+ Assert.Equal((int)HttpStatusCode.ServiceUnavailable, latest.GetDimension(Metric.RspResultCode));
+#endif
+ Assert.Equal(HttpRequestResultType.Failure.ToInvariantString(), latest.GetDimension(Metric.RspResultCategory));
+ }
+#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
+}
diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/HttpMeteringHandlerTests.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/HttpMeteringHandlerTests.cs
index e8e7576ff0f..ae110356624 100644
--- a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/HttpMeteringHandlerTests.cs
+++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/HttpMeteringHandlerTests.cs
@@ -3,11 +3,21 @@
using System;
using System.Collections.Generic;
+#if !NETFRAMEWORK
+using System.Diagnostics;
+#endif
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Metrics;
+using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
+#if !NETFRAMEWORK
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Hosting.Testing;
+#endif
using Microsoft.Extensions.Http.Telemetry.Metering.Internal;
using Microsoft.Extensions.Http.Telemetry.Metering.Test.Internal;
using Microsoft.Extensions.Telemetry;
@@ -19,15 +29,22 @@
using Xunit;
namespace Microsoft.Extensions.Http.Telemetry.Metering.Test;
+
#pragma warning disable CA2000 // Not necessary to dispose all resources in test class.
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
-public sealed class HttpMeteringHandlerTests : IDisposable
+
+public sealed partial class HttpMeteringHandlerTests : IDisposable
{
private const long DefaultClockAdvanceMs = 200;
private const string DelayPropertyName = nameof(DelayPropertyName);
private static readonly Uri _failureUri = new("https://www.example-failure.com/foo?bar");
- private static readonly Uri _successfullUri = new("https://www.example-success.com/foo?bar");
+ private static readonly Uri _failure1Uri = new("https://www.example-failure1.com/foo?bar");
+ private static readonly Uri _failure2Uri = new("https://www.example-failure2.com/foo?bar");
+ private static readonly Uri _failure3Uri = new("https://www.example-failure3.com/foo?bar");
+ private static readonly Uri _internalServerErrorUri = new("https://www.example-failure.com/internalServererror");
+ private static readonly Uri _expectedFailureUri = new("https://www.example-expectedfailure.com/foo?bar");
+ private static readonly Uri _successfulUri = new("https://www.example-success.com/foo?bar");
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly FakeTimeProvider _fakeTimeProvider = new();
@@ -45,17 +62,73 @@ public void Dispose()
_cancellationTokenSource.Dispose();
}
+ [Fact]
+ public void Handler_DoesntDisposeUnderlyingMeter_WhenMeterAndDisposeFalsePassed()
+ {
+ using var meter = new Meter();
+ using var handler = new OverriddenHttpMeteringHandler(meter, Array.Empty());
+ var disposed = CheckHandlerUnderlyingMeterDisposed(counterName =>
+ {
+ var counter = meter.CreateCounter(counterName);
+ counter.Add(100_500);
+ handler.ExternalDispose(false);
+ });
+
+ Assert.False(disposed, "The underlying Meter in the handler should not be disposed");
+ }
+
+ [Fact]
+ public void Handler_DoesntDisposeUnderlyingMeter_WhenMeterPassed()
+ {
+ using var meter = new Meter();
+ var disposed = CheckHandlerUnderlyingMeterDisposed(counterName =>
+ {
+ using var handler = new HttpMeteringHandler(meter, Array.Empty());
+ var counter = meter.CreateCounter(counterName);
+ counter.Add(100_500);
+ });
+
+ Assert.False(disposed, "The underlying Meter in the handler should not be disposed");
+ }
+
+ private static bool CheckHandlerUnderlyingMeterDisposed(Action testAction)
+ {
+ var counterName = Guid.NewGuid().ToString();
+ using var meterListener = new MeterListener();
+ meterListener.InstrumentPublished += (instrument, listener) =>
+ {
+ if (instrument.Name == counterName)
+ {
+ listener.EnableMeasurementEvents(instrument);
+ }
+ };
+
+ var disposed = false;
+ meterListener.MeasurementsCompleted += (instrument, _) =>
+ {
+ if (instrument.Name == counterName)
+ {
+ disposed = true;
+ }
+ };
+
+ meterListener.Start();
+ testAction(counterName);
+
+ return disposed;
+ }
+
[Fact]
public void SendAsync_Success_NoNamesSet()
{
using var meter = new Meter();
using var metricCollector = new MetricCollector(meter);
- var client = CreateClientWithHandler(meter);
+ using var client = CreateClientWithHandler(meter);
- var httpRequestMessage = new HttpRequestMessage(HttpMethod.Put, _successfullUri);
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Put, _successfulUri);
- _ = client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token).Result;
+ using var _ = client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token).Result;
var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
Assert.NotNull(latest);
@@ -63,6 +136,7 @@ public void SendAsync_Success_NoNamesSet()
Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(Metric.DependencyName));
Assert.Equal($"PUT {TelemetryConstants.Unknown}", latest.GetDimension(Metric.ReqName));
Assert.Equal(201, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.Success.ToString(), latest.GetDimension(Metric.RspResultCategory));
}
[Fact]
@@ -70,16 +144,16 @@ public void SendAsync_Success()
{
using var meter = new Meter();
using var metricCollector = new MetricCollector(meter);
- var client = CreateClientWithHandler(meter);
+ using var client = CreateClientWithHandler(meter);
- var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfullUri);
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfulUri);
httpRequestMessage.SetRequestMetadata(new RequestMetadata
{
DependencyName = "success_service",
RequestRoute = "/foo"
});
- _ = client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token).Result;
+ using var _ = client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token).Result;
var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
Assert.NotNull(latest);
@@ -87,27 +161,29 @@ public void SendAsync_Success()
Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName));
Assert.Equal($"GET /foo", latest.GetDimension(Metric.ReqName));
Assert.Equal(201, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.Success.ToString(), latest.GetDimension(Metric.RspResultCategory));
}
[Fact]
- public void PerfStopwatch_ReturnsTotalMiliseconds_InsteadOfFraction()
+ public void PerfStopwatch_ReturnsTotalMilliseconds_InsteadOfFraction()
{
const long TimeAdvanceMs = 1500L; // We need to use any value greater than 1000 (1 second)
const string ServiceName = "success_service";
using var meter = new Meter();
using var metricCollector = new MetricCollector(meter);
- var client = CreateClientWithHandler(meter);
+ using var client = CreateClientWithHandler(meter);
- var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfullUri);
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfulUri);
httpRequestMessage.SetRequestMetadata(new RequestMetadata
{
DependencyName = ServiceName,
RequestRoute = "/foo"
});
+
_clockAdvanceMs = TimeAdvanceMs;
- _ = client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token).Result;
+ using var _ = client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token).Result;
var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
@@ -116,6 +192,7 @@ public void PerfStopwatch_ReturnsTotalMiliseconds_InsteadOfFraction()
Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName));
Assert.Equal("GET /foo", latest.GetDimension(Metric.ReqName));
Assert.Equal(201, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.Success.ToString(), latest.GetDimension(Metric.RspResultCategory));
}
[Fact]
@@ -123,9 +200,9 @@ public async Task SendAsync_Exception()
{
using var meter = new Meter();
using var metricCollector = new MetricCollector(meter);
- var client = CreateClientWithHandler(meter);
+ using var client = CreateClientWithHandler(meter);
- var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _failureUri);
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _failureUri);
httpRequestMessage.SetRequestMetadata(new RequestMetadata
{
DependencyName = "failure_service",
@@ -133,7 +210,10 @@ public async Task SendAsync_Exception()
RequestName = "TestRequestName"
});
- await Assert.ThrowsAsync(async () => await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token));
+ await Assert.ThrowsAsync(async () =>
+ {
+ using var _ = await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token);
+ });
var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
@@ -141,7 +221,8 @@ public async Task SendAsync_Exception()
Assert.Equal("www.example-failure.com", latest.GetDimension(Metric.ReqHost));
Assert.Equal("failure_service", latest.GetDimension(Metric.DependencyName));
Assert.Equal("POST TestRequestName", latest.GetDimension(Metric.ReqName));
- Assert.Equal(500, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal((int)HttpStatusCode.ServiceUnavailable, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.Failure.ToInvariantString(), latest.GetDimension(Metric.RspResultCategory));
}
[Fact]
@@ -150,7 +231,7 @@ public void SendAsync_SetReqMetadata_OnAsyncContext_Success()
using var meter = new Meter();
using var metricCollector = new MetricCollector(meter);
var requestMetadataContextMock = new Mock();
- var client = CreateClientWithHandler(meter, requestMetadataContextMock.Object);
+ using var client = CreateClientWithHandler(meter, requestMetadataContextMock.Object);
var requestMetadata = new RequestMetadata
{
@@ -159,7 +240,7 @@ public void SendAsync_SetReqMetadata_OnAsyncContext_Success()
};
requestMetadataContextMock.Setup(m => m.RequestMetadata).Returns(requestMetadata);
- _ = client.GetAsync(_successfullUri, _cancellationTokenSource.Token).Result;
+ using var _ = client.GetAsync(_successfulUri, _cancellationTokenSource.Token).Result;
var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
@@ -168,10 +249,11 @@ public void SendAsync_SetReqMetadata_OnAsyncContext_Success()
Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName));
Assert.Equal("GET /foo", latest.GetDimension(Metric.ReqName));
Assert.Equal(201, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.Success.ToString(), latest.GetDimension(Metric.RspResultCategory));
}
[Fact]
- public void PerfStopwatch_SetReqMetadata_OnAsyncContext_ReturnsTotalMiliseconds_InsteadOfFraction()
+ public void PerfStopwatch_SetReqMetadata_OnAsyncContext_ReturnsTotalMilliseconds_InsteadOfFraction()
{
const long TimeAdvanceMs = 1500L; // We need to use any value greater than 1000 (1 second)
const string ServiceName = "success_service";
@@ -179,7 +261,7 @@ public void PerfStopwatch_SetReqMetadata_OnAsyncContext_ReturnsTotalMiliseconds_
using var meter = new Meter();
using var metricCollector = new MetricCollector(meter);
var requestMetadataContextMock = new Mock();
- var client = CreateClientWithHandler(meter, requestMetadataContextMock.Object);
+ using var client = CreateClientWithHandler(meter, requestMetadataContextMock.Object);
var requestMetadata = new RequestMetadata
{
@@ -191,7 +273,7 @@ public void PerfStopwatch_SetReqMetadata_OnAsyncContext_ReturnsTotalMiliseconds_
_clockAdvanceMs = TimeAdvanceMs;
- _ = client.GetAsync(_successfullUri, _cancellationTokenSource.Token).Result;
+ using var _ = client.GetAsync(_successfulUri, _cancellationTokenSource.Token).Result;
var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
@@ -200,6 +282,7 @@ public void PerfStopwatch_SetReqMetadata_OnAsyncContext_ReturnsTotalMiliseconds_
Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName));
Assert.Equal("GET /foo", latest.GetDimension(Metric.ReqName));
Assert.Equal(201, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.Success.ToString(), latest.GetDimension(Metric.RspResultCategory));
}
[Fact]
@@ -208,7 +291,7 @@ public async Task SendAsync_SetReqMetadata_OnAsyncContext_Exception()
using var meter = new Meter();
using var metricCollector = new MetricCollector(meter);
var requestMetadataContextMock = new Mock();
- var client = CreateClientWithHandler(meter, requestMetadataContextMock.Object);
+ using var client = CreateClientWithHandler(meter, requestMetadataContextMock.Object);
var requestMetadata = new RequestMetadata
{
@@ -216,10 +299,14 @@ public async Task SendAsync_SetReqMetadata_OnAsyncContext_Exception()
RequestRoute = "/foo/failure",
RequestName = "TestRequestName"
};
+
requestMetadataContextMock.Setup(m => m.RequestMetadata).Returns(requestMetadata);
- var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _failureUri);
- await Assert.ThrowsAsync(async () => await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token));
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _failureUri);
+ await Assert.ThrowsAsync(async () =>
+ {
+ using var _ = await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token);
+ });
var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
@@ -227,7 +314,8 @@ public async Task SendAsync_SetReqMetadata_OnAsyncContext_Exception()
Assert.Equal("www.example-failure.com", latest.GetDimension(Metric.ReqHost));
Assert.Equal("failure_service", latest.GetDimension(Metric.DependencyName));
Assert.Equal("POST TestRequestName", latest.GetDimension(Metric.ReqName));
- Assert.Equal(500, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal((int)HttpStatusCode.ServiceUnavailable, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.Failure.ToInvariantString(), latest.GetDimension(Metric.RspResultCategory));
}
[Fact]
@@ -236,16 +324,19 @@ public void SendAsync_WithDownstreamDependencyMetadata_OnAsyncContext_Success()
using var meter = new Meter();
using var metricCollector = new MetricCollector(meter);
var downstreamDependencyMetadataManagerMock = new Mock();
- var client = CreateClientWithHandler(meter, downstreamDependencyMetadataManager: downstreamDependencyMetadataManagerMock.Object);
+ using var client = CreateClientWithHandler(meter, downstreamDependencyMetadataManager: downstreamDependencyMetadataManagerMock.Object);
var requestMetadata = new RequestMetadata
{
DependencyName = "success_service",
RequestRoute = "/foo"
};
- downstreamDependencyMetadataManagerMock.Setup(m => m.GetRequestMetadata(It.IsAny())).Returns(requestMetadata);
- _ = client.GetAsync(_successfullUri, _cancellationTokenSource.Token).Result;
+ downstreamDependencyMetadataManagerMock
+ .Setup(m => m.GetRequestMetadata(It.IsAny()))
+ .Returns(requestMetadata);
+
+ using var _ = client.GetAsync(_successfulUri, _cancellationTokenSource.Token).Result;
var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
@@ -254,10 +345,11 @@ public void SendAsync_WithDownstreamDependencyMetadata_OnAsyncContext_Success()
Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName));
Assert.Equal("GET /foo", latest.GetDimension(Metric.ReqName));
Assert.Equal(201, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.Success.ToString(), latest.GetDimension(Metric.RspResultCategory));
}
[Fact]
- public void PerfStopwatch_WithDownstreamDependencyMetadata_OnAsyncContext_ReturnsTotalMiliseconds_InsteadOfFraction()
+ public void PerfStopwatch_WithDownstreamDependencyMetadata_OnAsyncContext_ReturnsTotalMilliseconds_InsteadOfFraction()
{
const long TimeAdvanceMs = 1500L; // We need to use any value greater than 1000 (1 second)
const string ServiceName = "success_service";
@@ -265,7 +357,7 @@ public void PerfStopwatch_WithDownstreamDependencyMetadata_OnAsyncContext_Return
using var meter = new Meter();
using var metricCollector = new MetricCollector(meter);
var downstreamDependencyMetadataManagerMock = new Mock();
- var client = CreateClientWithHandler(meter, downstreamDependencyMetadataManager: downstreamDependencyMetadataManagerMock.Object);
+ using var client = CreateClientWithHandler(meter, downstreamDependencyMetadataManager: downstreamDependencyMetadataManagerMock.Object);
var requestMetadata = new RequestMetadata
{
@@ -277,7 +369,7 @@ public void PerfStopwatch_WithDownstreamDependencyMetadata_OnAsyncContext_Return
_clockAdvanceMs = TimeAdvanceMs;
- _ = client.GetAsync(_successfullUri, _cancellationTokenSource.Token).Result;
+ using var _ = client.GetAsync(_successfulUri, _cancellationTokenSource.Token).Result;
var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
@@ -286,6 +378,7 @@ public void PerfStopwatch_WithDownstreamDependencyMetadata_OnAsyncContext_Return
Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName));
Assert.Equal("GET /foo", latest.GetDimension(Metric.ReqName));
Assert.Equal(201, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.Success.ToString(), latest.GetDimension(Metric.RspResultCategory));
}
[Fact]
@@ -294,7 +387,7 @@ public async Task SendAsync_WithDownstreamDependencyMetadata_OnAsyncContext_Exce
using var meter = new Meter();
using var metricCollector = new MetricCollector(meter);
var dependencyDataManagerMock = new Mock();
- var client = CreateClientWithHandler(meter, downstreamDependencyMetadataManager: dependencyDataManagerMock.Object);
+ using var client = CreateClientWithHandler(meter, downstreamDependencyMetadataManager: dependencyDataManagerMock.Object);
var requestMetadata = new RequestMetadata
{
@@ -304,8 +397,11 @@ public async Task SendAsync_WithDownstreamDependencyMetadata_OnAsyncContext_Exce
};
dependencyDataManagerMock.Setup(m => m.GetRequestMetadata(It.IsAny())).Returns(requestMetadata);
- var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _failureUri);
- await Assert.ThrowsAsync(async () => await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token));
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _failureUri);
+ await Assert.ThrowsAsync(async () =>
+ {
+ using var _ = await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token);
+ });
var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
@@ -313,13 +409,15 @@ public async Task SendAsync_WithDownstreamDependencyMetadata_OnAsyncContext_Exce
Assert.Equal("www.example-failure.com", latest.GetDimension(Metric.ReqHost));
Assert.Equal("failure_service", latest.GetDimension(Metric.DependencyName));
Assert.Equal("POST TestRequestName", latest.GetDimension(Metric.ReqName));
- Assert.Equal(500, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal((int)HttpStatusCode.ServiceUnavailable, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.Failure.ToInvariantString(), latest.GetDimension(Metric.RspResultCategory));
}
[Fact]
public void GetHostName_Returns_Unknown_For_Empty_RequestUri()
{
- var c = HttpMeteringHandler.GetHostName(new HttpRequestMessage());
+ using var requestMessage = new HttpRequestMessage();
+ var c = HttpMeteringHandler.GetHostName(requestMessage);
Assert.Equal(TelemetryConstants.Unknown, c);
}
@@ -331,19 +429,19 @@ public async Task SendAsync_MultiEnrich()
{
using var meter = new Meter();
using var metricCollector = new MetricCollector(meter);
- var client = CreateClientWithHandler(meter, new List
+ using var client = CreateClientWithHandler(meter, new List
{
new TestEnricher(i),
});
- var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfullUri);
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfulUri);
httpRequestMessage.SetRequestMetadata(new RequestMetadata
{
DependencyName = "success_service",
RequestRoute = "/foo"
});
- _ = await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token);
+ using var _ = await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token);
var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
@@ -352,6 +450,7 @@ public async Task SendAsync_MultiEnrich()
Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName));
Assert.Equal("GET /foo", latest.GetDimension(Metric.ReqName));
Assert.Equal(201, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.Success.ToString(), latest.GetDimension(Metric.RspResultCategory));
for (int j = 0; j < i; j++)
{
@@ -361,30 +460,27 @@ public async Task SendAsync_MultiEnrich()
}
[Fact]
- public async Task SendAsync_MultiEnrich_UsingIMeter()
+ public async Task SendAsync_MultiEnrich_UsingMeter()
{
for (int i = 1; i <= 14; i++)
{
using var meter = new Meter();
using var metricCollector = new MetricCollector(meter);
- var handler = new HttpMeteringHandler(meter, new List
- {
- new TestEnricher(i),
- })
+ using var handler = new HttpMeteringHandler(meter, new[] { new TestEnricher(i) })
{
InnerHandler = new TestHandlerStub(InnerHandlerFunction)
};
- var client = new System.Net.Http.HttpClient(handler);
+ using var client = new System.Net.Http.HttpClient(handler);
- var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfullUri);
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfulUri);
httpRequestMessage.SetRequestMetadata(new RequestMetadata
{
DependencyName = "success_service",
RequestRoute = "/foo"
});
- _ = await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token);
+ using var _ = await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token);
var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
@@ -406,20 +502,20 @@ public async Task InvokeAsync_HttpMeteringHandler_MultipleEnrichers()
{
using var meter = new Meter();
using var metricCollector = new MetricCollector(meter);
- var client = CreateClientWithHandler(meter, new List
+ using var client = CreateClientWithHandler(meter, new List
{
new TestEnricher(2),
new TestEnricher(2, "2"),
});
- var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfullUri);
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfulUri);
httpRequestMessage.SetRequestMetadata(new RequestMetadata
{
DependencyName = "success_service",
RequestRoute = "/foo"
});
- _ = await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token);
+ using var _ = await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token);
var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
Assert.NotNull(latest);
@@ -427,6 +523,7 @@ public async Task InvokeAsync_HttpMeteringHandler_MultipleEnrichers()
Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName));
Assert.Equal("GET /foo", latest.GetDimension(Metric.ReqName));
Assert.Equal(201, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.Success.ToString(), latest.GetDimension(Metric.RspResultCategory));
Assert.Equal("test_value_1", latest.GetDimension("test_property_1"));
Assert.Equal("test_value_2", latest.GetDimension("test_property_2"));
Assert.Equal("test_value_21", latest.GetDimension("test_property_21"));
@@ -438,19 +535,19 @@ public async Task InvokeAsync_HttpMeteringHandler_PropertyBagEdgeCase()
{
using var meter = new Meter();
using var metricCollector = new MetricCollector(meter);
- var client = CreateClientWithHandler(meter, new List
+ using var client = CreateClientWithHandler(meter, new List
{
new PropertyBagEdgeCaseEnricher(),
});
- var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfullUri);
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfulUri);
httpRequestMessage.SetRequestMetadata(new RequestMetadata
{
DependencyName = "success_service",
RequestRoute = "/foo"
});
- _ = await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token);
+ using var _ = await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token);
var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
Assert.NotNull(latest);
@@ -458,6 +555,7 @@ public async Task InvokeAsync_HttpMeteringHandler_PropertyBagEdgeCase()
Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName));
Assert.Equal("GET /foo", latest.GetDimension(Metric.ReqName));
Assert.Equal(201, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.Success.ToString(), latest.GetDimension(Metric.RspResultCategory));
Assert.Equal("test_val", latest.GetDimension("non_null_object_property"));
}
@@ -466,10 +564,12 @@ public void HttpMeteringHandler_Fail_16DEnrich()
{
using var meter = new Meter();
Assert.Throws(() =>
- CreateClientWithHandler(meter, new List
+ {
+ using var _ = CreateClientWithHandler(meter, new List
{
- new TestEnricher(16)
- }));
+ new TestEnricher(16)
+ });
+ });
}
[Fact]
@@ -477,11 +577,13 @@ public void HttpMeteringHandler_Fail_RepeatCustomDimensions()
{
using var meter = new Meter();
Assert.Throws(() =>
- CreateClientWithHandler(meter, new List
+ {
+ using var _ = CreateClientWithHandler(meter, new List
{
- new TestEnricher(1),
- new TestEnricher(1),
- }));
+ new TestEnricher(1),
+ new TestEnricher(1),
+ });
+ });
}
[Fact]
@@ -489,10 +591,12 @@ public void HttpMeteringHandler_Fail_RepeatDefaultDimensions()
{
using var meter = new Meter();
Assert.Throws(() =>
- CreateClientWithHandler(meter, new List
+ {
+ using var _ = CreateClientWithHandler(meter, new List
{
- new SameDefaultDimEnricher(),
- }));
+ new SameDefaultDimEnricher(),
+ });
+ });
}
[Fact]
@@ -520,15 +624,10 @@ public void ServiceCollection_AddMultipleOutgoingRequestEnrichersSuccessfully()
using var provider = services.BuildServiceProvider();
var enrichersCollection = provider.GetServices();
- var enricherCount = 0;
- foreach (var enricher in enrichersCollection)
- {
- enricherCount++;
- }
-
- Assert.Equal(2, enricherCount);
+ Assert.Equal(2, enrichersCollection.Count());
}
+ [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Handler will be disposed with HttpClient")]
private System.Net.Http.HttpClient CreateClientWithHandler(
Meter meter,
IEnumerable outgoingRequestMetricEnrichers)
@@ -542,6 +641,7 @@ private System.Net.Http.HttpClient CreateClientWithHandler(
return client;
}
+ [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Handler will be disposed with HttpClient")]
private System.Net.Http.HttpClient CreateClientWithHandler(
Meter meter,
IOutgoingRequestContext? requestMetadataContext = null,
@@ -550,7 +650,7 @@ private System.Net.Http.HttpClient CreateClientWithHandler(
var handler = new HttpMeteringHandler(meter, Array.Empty(), requestMetadataContext, downstreamDependencyMetadataManager)
{
InnerHandler = new TestHandlerStub(InnerHandlerFunction),
- TimeProvider = _fakeTimeProvider
+ TimeProvider = _fakeTimeProvider,
};
var client = new System.Net.Http.HttpClient(handler);
@@ -565,8 +665,37 @@ private Task InnerHandlerFunction(HttpRequestMessage reques
{
throw new HttpRequestException("Something went wrong");
}
+ else if (request.RequestUri == _failure1Uri)
+ {
+ throw new TaskCanceledException("Timeout");
+ }
+ else if (request.RequestUri == _failure2Uri)
+ {
+ throw new InvalidOperationException("Invalid Operation");
+ }
+ else if (request.RequestUri == _failure3Uri)
+ {
+#if NET6_0_OR_GREATER
+ throw new HttpRequestException("Something went wrong", null, HttpStatusCode.BadGateway);
+#else
+ throw new HttpRequestException("Something went wrong");
+#endif
+ }
+
+ HttpResponseMessage response;
+ if (request.RequestUri == _expectedFailureUri)
+ {
+ response = new HttpResponseMessage { StatusCode = HttpStatusCode.BadRequest };
+ }
+ else if (request.RequestUri == _internalServerErrorUri)
+ {
+ response = new HttpResponseMessage { StatusCode = HttpStatusCode.InternalServerError };
+ }
+ else
+ {
+ response = new HttpResponseMessage { StatusCode = HttpStatusCode.Created };
+ }
- var response = new HttpResponseMessage { StatusCode = HttpStatusCode.Created };
return Task.FromResult(response);
}
@@ -595,7 +724,7 @@ public static void AddHttpClientMetering_CreatesClientSuccessfully()
var httpClientFactory = sp.GetRequiredService();
- using var httpClient = httpClientFactory?.CreateClient();
+ using var httpClient = httpClientFactory.CreateClient();
Assert.NotNull(httpClient);
}
@@ -663,16 +792,13 @@ public static void AddDefaultHttpClientMetering_RequestMetadataSetSuccessfully()
DependencyName = "success_service",
RequestRoute = "/foo"
};
-
requestMetadataContext?.SetRequestMetadata(requestMetadata);
-
Assert.NotNull(requestMetadataContext);
}
[Fact]
public static async Task AddDefaultHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt()
{
- var dependencyMetadata = new TestDownstreamDependencyMetadata();
var downstreamDependencyMetadataManagerMock = new Mock();
downstreamDependencyMetadataManagerMock
.Setup(m => m.GetRequestMetadata(It.IsAny()))
@@ -691,13 +817,13 @@ public static async Task AddDefaultHttpClientMetering_WithDownstreamDependencyMe
.GetRequiredService()
.CreateClient(nameof(AddDefaultHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt));
- _ = await client.GetAsync("https://contoso.com");
+ using var _ = await client.GetAsync("https://www.bing.com");
downstreamDependencyMetadataManagerMock.Verify(m => m.GetRequestMetadata(It.IsAny()), Times.Once);
}
[Fact]
- public static async Task AddtHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt()
+ public static async Task AddHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt()
{
var downstreamDependencyMetadataManagerMock = new Mock();
downstreamDependencyMetadataManagerMock
@@ -706,7 +832,7 @@ public static async Task AddtHttpClientMetering_WithDownstreamDependencyMetadata
using var sp = new ServiceCollection()
.RegisterMetering()
- .AddHttpClient(nameof(AddtHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt))
+ .AddHttpClient(nameof(AddHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt))
.AddHttpClientMetering()
.Services
.AddDownstreamDependencyMetadata()
@@ -716,11 +842,334 @@ public static async Task AddtHttpClientMetering_WithDownstreamDependencyMetadata
var client = sp
.GetRequiredService()
- .CreateClient(nameof(AddtHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt));
+ .CreateClient(nameof(AddHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt));
- _ = await client.GetAsync("https://contoso.com");
+ using var _ = await client.GetAsync("https://www.bing.com");
downstreamDependencyMetadataManagerMock.Verify(m => m.GetRequestMetadata(It.IsAny()), Times.Once);
}
+
+#if !NETFRAMEWORK
+ [Fact]
+ public static void AddHttpClientMeteringForAllHttpClients_NullServiceCollection_Throws()
+ {
+ IServiceCollection services = null!;
+ Assert.Throws(() => services.AddHttpClientMeteringForAllHttpClients());
+ }
+
+ [Fact]
+ public static void AddHttpClientMeteringForAllHttpClients_WithDownstreamDependencyMetadata_UsesIt()
+ {
+ var downstreamDependencyMetadataManagerMock = new Mock();
+ downstreamDependencyMetadataManagerMock
+ .Setup(m => m.GetRequestMetadata(It.IsAny()))
+ .Returns(It.IsAny());
+
+ using var host = FakeHost.CreateBuilder()
+ .ConfigureServices((_, services) => services
+ .RegisterMetering()
+ .AddHttpClient()
+ .AddHttpClientMeteringForAllHttpClients()
+ .AddDownstreamDependencyMetadata()
+ .AddSingleton(downstreamDependencyMetadataManagerMock.Object))
+ .Build();
+
+ host.Start();
+
+ var client = host.Services
+ .GetRequiredService()
+ .CreateClient(nameof(AddDefaultHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt));
+
+ using var _ = client.GetAsync("https://www.bing.com").Result;
+ HttpClientMeteringListener.UsingDiagnosticsSource = false;
+
+ downstreamDependencyMetadataManagerMock.Verify(m => m.GetRequestMetadata(It.IsAny()), Times.AtLeastOnce);
+ HttpClientMeteringListener.UsingDiagnosticsSource = false;
+ }
+
+ [Fact]
+ public static void AddHttpClientMeteringForAllHttpClients_EmitMetrics_OnException()
+ {
+ var downstreamDependencyMetadataManagerMock = new Mock();
+ downstreamDependencyMetadataManagerMock
+ .Setup(m => m.GetRequestMetadata(It.IsAny()))
+ .Returns(It.IsAny());
+
+ using var host = FakeHost.CreateBuilder()
+ .ConfigureServices((_, services) => services
+ .AddHttpClient()
+ .AddHttpClientMeteringForAllHttpClients()
+ .AddDownstreamDependencyMetadata()
+ .AddSingleton(downstreamDependencyMetadataManagerMock.Object))
+ .Build();
+
+ host.Start();
+ var client = host.Services
+ .GetRequiredService()
+ .CreateClient(nameof(AddDefaultHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt));
+
+ var meter = host.Services.GetRequiredService>();
+ using var meterCollector = new MetricCollector(meter);
+
+ DateTimeOffset startTime = DateTimeOffset.UtcNow;
+ Assert.Throws(() => client.GetAsync("https://localhost:12345").Result);
+ HttpClientMeteringListener.UsingDiagnosticsSource = false;
+
+ var record = meterCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
+ Assert.NotNull(record);
+ Assert.True(record.Value > 0);
+ Assert.True(record.Value <= (DateTimeOffset.UtcNow - startTime).TotalMilliseconds);
+ Assert.Equal((int)HttpStatusCode.ServiceUnavailable, record.GetDimension(Metric.RspResultCode));
+ HttpClientMeteringListener.UsingDiagnosticsSource = false;
+ }
+
+ [Fact]
+ public static void AddHttpClientMeteringForAllHttpClients_EmitMetrics_OnTaskCancelledException()
+ {
+ var downstreamDependencyMetadataManagerMock = new Mock();
+ downstreamDependencyMetadataManagerMock
+ .Setup(m => m.GetRequestMetadata(It.IsAny()))
+ .Returns(It.IsAny());
+
+ using var host = FakeHost.CreateBuilder()
+ .ConfigureServices((_, services) => services
+ .AddHttpClient()
+ .AddHttpClientMeteringForAllHttpClients()
+ .AddDownstreamDependencyMetadata()
+ .AddSingleton(downstreamDependencyMetadataManagerMock.Object))
+ .Build();
+
+ host.Start();
+ var client = host.Services
+ .GetRequiredService()
+ .CreateClient(nameof(AddDefaultHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt));
+
+ var meter = host.Services.GetRequiredService>();
+ using var meterCollector = new MetricCollector(meter);
+
+ DateTimeOffset startTime = DateTimeOffset.UtcNow;
+
+ var cts = new CancellationTokenSource();
+ using var testObserver = new RequestCancellationTestObserver(cts);
+
+ Assert.Throws(() => client.GetAsync("https://www.bing.com", cts.Token).Result);
+
+ HttpClientMeteringListener.UsingDiagnosticsSource = false;
+
+ var record = meterCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
+ Assert.NotNull(record);
+ Assert.True(record.Value >= 0);
+ Assert.True(record.Value <= (DateTimeOffset.UtcNow - startTime).TotalMilliseconds);
+ Assert.Equal((int)HttpStatusCode.GatewayTimeout, record.GetDimension(Metric.RspResultCode));
+ HttpClientMeteringListener.UsingDiagnosticsSource = false;
+ }
+
+ [Fact]
+ public static void AddHttpClientMeteringForAllHttpClients_EmitMetrics_OnError()
+ {
+ var downstreamDependencyMetadataManagerMock = new Mock();
+ downstreamDependencyMetadataManagerMock
+ .Setup(m => m.GetRequestMetadata(It.IsAny()))
+ .Returns(It.IsAny());
+
+ using var host = FakeHost.CreateBuilder()
+ .ConfigureServices((_, services) => services
+ .AddHttpClient()
+ .AddHttpClientMeteringForAllHttpClients()
+ .AddDownstreamDependencyMetadata()
+ .AddSingleton(downstreamDependencyMetadataManagerMock.Object))
+ .Build();
+
+ host.Start();
+ var client = host.Services
+ .GetRequiredService()
+ .CreateClient(nameof(AddDefaultHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt));
+
+ var meter = host.Services.GetRequiredService>();
+ using var meterCollector = new MetricCollector(meter);
+
+ DateTimeOffset startTime = DateTimeOffset.UtcNow;
+ _ = client.GetAsync("https://www.bing.com/request").Result;
+ HttpClientMeteringListener.UsingDiagnosticsSource = false;
+
+ var record = meterCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
+ Assert.NotNull(record);
+ Assert.True(record.Value > 0);
+ Assert.True(record.Value <= (DateTimeOffset.UtcNow - startTime).TotalMilliseconds);
+ Assert.Equal((int)HttpStatusCode.NotFound, record.GetDimension(Metric.RspResultCode));
+ HttpClientMeteringListener.UsingDiagnosticsSource = false;
+ }
+
+ [Fact]
+ public static void AddHttpClientMeteringForAllHttpClients_EmitMetrics_OnSuccessResponse()
+ {
+ var downstreamDependencyMetadataManagerMock = new Mock();
+ downstreamDependencyMetadataManagerMock
+ .Setup(m => m.GetRequestMetadata(It.IsAny()))
+ .Returns(It.IsAny());
+
+ using var host = FakeHost.CreateBuilder()
+ .ConfigureServices((_, services) => services
+ .AddHttpClient()
+ .AddHttpClientMeteringForAllHttpClients()
+ .AddDownstreamDependencyMetadata()
+ .AddSingleton(downstreamDependencyMetadataManagerMock.Object))
+ .Build();
+
+ host.Start();
+ var client = host.Services
+ .GetRequiredService()
+ .CreateClient(nameof(AddDefaultHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt));
+
+ var meter = host.Services.GetRequiredService>();
+ using var meterCollector = new MetricCollector(meter);
+
+ DateTimeOffset startTime = DateTimeOffset.UtcNow;
+ using var _ = client.GetAsync("https://www.bing.com").Result;
+ HttpClientMeteringListener.UsingDiagnosticsSource = false;
+
+ var record = meterCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
+ Assert.NotNull(record);
+ Assert.True(record.Value > 0);
+ Assert.True(record.Value <= (DateTimeOffset.UtcNow - startTime).TotalMilliseconds);
+ Assert.Equal(200, record.GetDimension(Metric.RspResultCode));
+ HttpClientMeteringListener.UsingDiagnosticsSource = false;
+ }
+
+ [Fact]
+ public static void AddHttpClientMeteringForAllHttpClients_CaptureMetrics_ForNonHttpClientFactoryClients()
+ {
+ var downstreamDependencyMetadataManagerMock = new Mock();
+ downstreamDependencyMetadataManagerMock
+ .Setup(m => m.GetRequestMetadata(It.IsAny()))
+ .Returns(It.IsAny());
+
+ using var host = FakeHost.CreateBuilder()
+ .ConfigureServices((_, services) => services
+ .AddHttpClientMeteringForAllHttpClients())
+ .Build();
+
+ host.Start();
+ var meter = host.Services.GetRequiredService>();
+ using var meterCollector = new MetricCollector(meter);
+
+ using var client = new System.Net.Http.HttpClient();
+
+ DateTimeOffset startTime = DateTimeOffset.UtcNow;
+ using var _ = client.GetAsync("https://www.bing.com").Result;
+ HttpClientMeteringListener.UsingDiagnosticsSource = false;
+
+ var record = meterCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
+ Assert.NotNull(record);
+ Assert.True(record.Value > 0);
+ Assert.True(record.Value <= (DateTimeOffset.UtcNow - startTime).TotalMilliseconds);
+ Assert.Equal(200, record.GetDimension(Metric.RspResultCode));
+ HttpClientMeteringListener.UsingDiagnosticsSource = false;
+ }
+
+ [Fact]
+ public static void When_DiagSourceAndDelegatingHandler_BothConfigured_MetricsOnlyEmittedOnce()
+ {
+ using var host = FakeHost.CreateBuilder()
+ .ConfigureServices((_, services) => services
+ .AddHttpClient()
+ .AddDefaultHttpClientMetering()
+ .AddHttpClientMeteringForAllHttpClients())
+ .Build();
+
+ host.Start();
+ var client = host.Services
+ .GetRequiredService()
+ .CreateClient(nameof(AddDefaultHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt));
+
+ var meter = host.Services.GetRequiredService>();
+ using var meterCollector = new MetricCollector(meter);
+
+ DateTimeOffset startTime = DateTimeOffset.UtcNow;
+ using var _ = client.GetAsync("https://www.bing.com").Result;
+ HttpClientMeteringListener.UsingDiagnosticsSource = false;
+
+ var records = meterCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!;
+ Assert.NotNull(records);
+ Assert.Equal(1, records.AllValues.Count);
+
+ var record = records.LatestWritten!;
+ Assert.True(record.Value > 0);
+ Assert.True(record.Value <= (DateTimeOffset.UtcNow - startTime).TotalMilliseconds);
+ Assert.Equal(200, record.GetDimension(Metric.RspResultCode));
+ HttpClientMeteringListener.UsingDiagnosticsSource = false;
+ }
+
+ [Fact]
+ public void HttpClientDiagnosticsObserver_OnError_DoesNotThrow()
+ {
+ using var meter = new Meter();
+ using var httpMeteringHandler = new HttpMeteringHandler(meter, new List());
+ using var httpClientDiagnosticsObserver = new HttpClientDiagnosticObserver(new HttpClientRequestAdapter(httpMeteringHandler));
+
+ Assert.NotNull(httpClientDiagnosticsObserver);
+ httpClientDiagnosticsObserver.OnError(new NotSupportedException());
+ }
+
+ [Fact]
+ public void HttpClientDiagnosticsObserver_OnNext_MultipleCalls_DoesNotThrow()
+ {
+ using var meter = new Meter();
+ using var httpMeteringHandler = new HttpMeteringHandler(meter, new List());
+ using var httpClientDiagnosticsObserver = new HttpClientDiagnosticObserver(new HttpClientRequestAdapter(httpMeteringHandler));
+
+ Assert.NotNull(httpClientDiagnosticsObserver);
+
+ using var diagnosticsListener = new DiagnosticListener("HttpHandlerDiagnosticListener");
+ httpClientDiagnosticsObserver.OnNext(diagnosticsListener);
+ httpClientDiagnosticsObserver.OnNext(diagnosticsListener);
+ }
+
+ [Fact]
+ public void HttpClientRequestAdapter_OnRequestStop_TaskStatusNotFaultedOrCancelled_ResponseNull_EmitInternalServerError()
+ {
+ using var meter = new Meter();
+ using var httpMeteringHandler = new HttpMeteringHandler(meter, new List());
+ var httpClientRequestAdapter = new HttpClientRequestAdapter(httpMeteringHandler);
+
+ Assert.NotNull(httpClientRequestAdapter);
+
+ using var httpRequestMessage = new HttpRequestMessage();
+ httpClientRequestAdapter.HttpClientListenerSubscribed(httpRequestMessage);
+ httpClientRequestAdapter.OnRequestStart(httpRequestMessage);
+
+ using var meterCollector = new MetricCollector(meter);
+
+ httpClientRequestAdapter.OnRequestStop(null, httpRequestMessage, TaskStatus.RanToCompletion);
+
+ var records = meterCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!;
+ Assert.NotNull(records);
+ Assert.Equal(1, records.AllValues.Count);
+
+ var record = records.LatestWritten!;
+ Assert.Equal((int)HttpStatusCode.InternalServerError, record.GetDimension(Metric.RspResultCode));
+ HttpClientMeteringListener.UsingDiagnosticsSource = false;
+ }
+#endif
+
+ [Fact]
+ public void SendAsync_Failure_NoExceptionThrown()
+ {
+ using var meter = new Meter();
+ using var metricCollector = new MetricCollector(meter);
+ using var client = CreateClientWithHandler(meter);
+
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _internalServerErrorUri);
+
+ using var _ = client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token).Result;
+
+ var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!;
+ Assert.NotNull(latest);
+ Assert.Equal("www.example-failure.com", latest.GetDimension(Metric.ReqHost));
+ Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(Metric.DependencyName));
+ Assert.Equal($"GET {TelemetryConstants.Unknown}", latest.GetDimension(Metric.ReqName));
+ Assert.Equal((int)HttpStatusCode.InternalServerError, latest.GetDimension(Metric.RspResultCode));
+ Assert.Equal(HttpRequestResultType.Failure.ToInvariantString(), latest.GetDimension(Metric.RspResultCategory));
+ }
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
}
diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/OverriddenHttpMeteringHandler.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/OverriddenHttpMeteringHandler.cs
new file mode 100644
index 00000000000..a7e9d678d06
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/OverriddenHttpMeteringHandler.cs
@@ -0,0 +1,22 @@
+// 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 Microsoft.Extensions.Telemetry.Metering;
+
+namespace Microsoft.Extensions.Http.Telemetry.Metering.Test.Internal;
+
+internal sealed class OverriddenHttpMeteringHandler : HttpMeteringHandler
+{
+ public OverriddenHttpMeteringHandler(
+ Meter meter,
+ IEnumerable enrichers)
+ : base(meter, enrichers)
+ {
+ }
+
+ public void ExternalDispose(bool dispose)
+ {
+ Dispose(dispose);
+ }
+}
diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/RequestCancellationTestObserver.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/RequestCancellationTestObserver.cs
new file mode 100644
index 00000000000..74dea72949c
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/RequestCancellationTestObserver.cs
@@ -0,0 +1,62 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if !NETFRAMEWORK
+using System;
+using System.Diagnostics;
+using System.Net.Http;
+using System.Threading;
+using Microsoft.Extensions.DiagnosticAdapter;
+
+namespace Microsoft.Extensions.Http.Telemetry.Metering.Test.Internal;
+
+internal class RequestCancellationTestObserver : IObserver, IDisposable
+{
+ private const string ListenerName = "HttpHandlerDiagnosticListener";
+ private readonly CancellationTokenSource _cts;
+ private IDisposable? _observer;
+ private IDisposable? _subscription;
+
+ public RequestCancellationTestObserver(CancellationTokenSource cts)
+ {
+ _cts = cts;
+ Initialize();
+ }
+
+ public void OnNext(DiagnosticListener value)
+ {
+ if (value.Name == ListenerName)
+ {
+ _observer?.Dispose();
+ _observer = value.SubscribeWithAdapter(this);
+ }
+ }
+
+ public void OnCompleted()
+ {
+ // Method intentionally left empty.
+ }
+
+ public void OnError(Exception error)
+ {
+ // Method intentionally left empty.
+ }
+
+ public void Dispose()
+ {
+ _observer?.Dispose();
+ _subscription?.Dispose();
+ }
+
+ [DiagnosticName("System.Net.Http.HttpRequestOut.Start")]
+ public virtual void OnRequestStart(HttpRequestMessage request)
+ {
+ _cts.Cancel();
+ }
+
+ private void Initialize()
+ {
+ _subscription = DiagnosticListener.AllListeners.Subscribe(this);
+ }
+}
+#endif
diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Microsoft.Extensions.Http.Telemetry.Tests.csproj b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Microsoft.Extensions.Http.Telemetry.Tests.csproj
index 1905330b7f9..5d1202d1058 100644
--- a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Microsoft.Extensions.Http.Telemetry.Tests.csproj
+++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Microsoft.Extensions.Http.Telemetry.Tests.csproj
@@ -14,6 +14,7 @@
+