Add support for .NET8.0 HttpClient metrics (#4931)

This commit is contained in:
Vishwesh Bankwar 2023-10-20 13:47:28 -07:00 committed by GitHub
parent 38e21a99bb
commit d07d03086a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 117 additions and 28 deletions

View File

@ -34,6 +34,35 @@
`http.client.request.duration` metrics on .NET Framework for `HttpWebRequest`.
([#4870](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4870))
* Following `HttpClient` metrics will now be enabled by default when targeting
`.NET8.0` framework or newer.
* **Meter** : `System.Net.Http`
* `http.client.request.duration`
* `http.client.active_requests`
* `http.client.open_connections`
* `http.client.connection.duration`
* `http.client.request.time_in_queue`
* **Meter** : `System.Net.NameResolution`
* `dns.lookups.duration`
For details about each individual metric check [System.Net metrics
docs
page](https://learn.microsoft.com/dotnet/core/diagnostics/built-in-metrics-system-net).
**NOTES**:
* When targeting `.NET8.0` framework or newer, `http.client.request.duration` metric
will only follow
[v1.22.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-metrics.md#metric-httpclientrequestduration)
semantic conventions specification. Ability to switch behavior to older
conventions using `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable is
not available.
* Users can opt-out of metrics that are not required using
[views](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/customizing-the-sdk#drop-an-instrument).
([#4931](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4931))
## 1.5.1-beta.1
Released 2023-Jul-20

View File

@ -14,10 +14,13 @@
// limitations under the License.
// </copyright>
#if !NET8_0_OR_GREATER
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenTelemetry.Instrumentation.Http;
using OpenTelemetry.Instrumentation.Http.Implementation;
#endif
using OpenTelemetry.Internal;
namespace OpenTelemetry.Metrics;
@ -37,6 +40,11 @@ public static class MeterProviderBuilderExtensions
{
Guard.ThrowIfNull(builder);
#if NET8_0_OR_GREATER
return builder
.AddMeter("System.Net.Http")
.AddMeter("System.Net.NameResolution");
#else
// Note: Warm-up the status code mapping.
_ = TelemetryHelper.BoxedStatusCodes;
@ -45,12 +53,6 @@ public static class MeterProviderBuilderExtensions
services.RegisterOptionsFactory(configuration => new HttpClientMetricInstrumentationOptions(configuration));
});
// TODO: Handle HttpClientMetricInstrumentationOptions
// SetHttpFlavor - seems like this would be handled by views
// Filter - makes sense for metric instrumentation
// Enrich - do we want a similar kind of functionality for metrics?
// RecordException - probably doesn't make sense for metric instrumentation
#if NETFRAMEWORK
builder.AddMeter(HttpWebRequestActivitySource.MeterName);
@ -70,5 +72,6 @@ public static class MeterProviderBuilderExtensions
sp.GetRequiredService<IOptionsMonitor<HttpClientMetricInstrumentationOptions>>().Get(Options.DefaultName)));
#endif
return builder;
#endif
}
}

View File

@ -18,8 +18,10 @@ using System.Diagnostics;
#if NETFRAMEWORK
using System.Net.Http;
#endif
#if !NET8_0_OR_GREATER
using System.Reflection;
using System.Text.Json;
#endif
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Metrics;
@ -33,6 +35,7 @@ public partial class HttpClientTests
{
public static readonly IEnumerable<object[]> TestData = HttpTestData.ReadTestCases();
#if !NET8_0_OR_GREATER
[Theory]
[MemberData(nameof(TestData))]
public async Task HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsOldSemanticConventionsAsync(HttpTestData.HttpOutTestCase tc)
@ -71,18 +74,7 @@ public partial class HttpClientTests
enableMetrics: true,
semanticConvention: HttpSemanticConvention.Dupe).ConfigureAwait(false);
}
[Theory]
[MemberData(nameof(TestData))]
public async Task HttpOutCallsAreCollectedSuccessfullyTracesOnlyAsync(HttpTestData.HttpOutTestCase tc)
{
await HttpOutCallsAreCollectedSuccessfullyBodyAsync(
this.host,
this.port,
tc,
enableTracing: true,
enableMetrics: false).ConfigureAwait(false);
}
#endif
[Theory]
[MemberData(nameof(TestData))]
@ -96,6 +88,18 @@ public partial class HttpClientTests
enableMetrics: true).ConfigureAwait(false);
}
[Theory]
[MemberData(nameof(TestData))]
public async Task HttpOutCallsAreCollectedSuccessfullyTracesOnlyAsync(HttpTestData.HttpOutTestCase tc)
{
await HttpOutCallsAreCollectedSuccessfullyBodyAsync(
this.host,
this.port,
tc,
enableTracing: true,
enableMetrics: false).ConfigureAwait(false);
}
[Theory]
[MemberData(nameof(TestData))]
public async Task HttpOutCallsAreCollectedSuccessfullyNoSignalsAsync(HttpTestData.HttpOutTestCase tc)
@ -108,6 +112,7 @@ public partial class HttpClientTests
enableMetrics: false).ConfigureAwait(false);
}
#if !NET8_0_OR_GREATER
[Fact]
public async Task DebugIndividualTestAsync()
{
@ -140,6 +145,7 @@ public partial class HttpClientTests
var t = (Task)this.GetType().InvokeMember(nameof(this.HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsOldSemanticConventionsAsync), BindingFlags.InvokeMethod, null, this, HttpTestData.GetArgumentsFromTestCaseObject(input).First());
await t.ConfigureAwait(false);
}
#endif
[Fact]
public async Task CheckEnrichmentWhenSampling()
@ -148,6 +154,65 @@ public partial class HttpClientTests
await CheckEnrichment(new AlwaysOnSampler(), true, this.url).ConfigureAwait(false);
}
#if NET8_0_OR_GREATER
[Theory]
[MemberData(nameof(TestData))]
public async Task ValidateNet8MetricsAsync(HttpTestData.HttpOutTestCase tc)
{
var metrics = new List<Metric>();
var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddHttpClientInstrumentation()
.AddInMemoryExporter(metrics)
.Build();
var testUrl = HttpTestData.NormalizeValues(tc.Url, this.host, this.port);
try
{
using var c = new HttpClient();
using var request = new HttpRequestMessage
{
RequestUri = new Uri(testUrl),
Method = new HttpMethod(tc.Method),
};
request.Headers.Add("contextRequired", "false");
request.Headers.Add("responseCode", (tc.ResponseCode == 0 ? 200 : tc.ResponseCode).ToString());
await c.SendAsync(request).ConfigureAwait(false);
}
catch (Exception)
{
// test case can intentionally send request that will result in exception
}
finally
{
meterProvider.Dispose();
}
// dns.lookups.duration is a typo
// https://github.com/dotnet/runtime/issues/92917
var requestMetrics = metrics
.Where(metric =>
metric.Name == "http.client.request.duration" ||
metric.Name == "http.client.active_requests" ||
metric.Name == "http.client.request.time_in_queue" ||
metric.Name == "http.client.connection.duration" ||
metric.Name == "http.client.open_connections" ||
metric.Name == "dns.lookups.duration")
.ToArray();
if (tc.ResponseExpected)
{
Assert.Equal(6, requestMetrics.Count());
}
else
{
// http.client.connection.duration and http.client.open_connections will not be emitted.
Assert.Equal(4, requestMetrics.Count());
}
}
#endif
private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync(
string host,
int port,
@ -210,11 +275,6 @@ public partial class HttpClientTests
{
RequestUri = new Uri(testUrl),
Method = new HttpMethod(tc.Method),
#if NETFRAMEWORK
Version = new Version(1, 1),
#else
Version = new Version(2, 0),
#endif
};
if (tc.Headers != null)
@ -351,6 +411,7 @@ public partial class HttpClientTests
Assert.Single(requestMetrics);
}
#if !NET8_0_OR_GREATER
if (semanticConvention == null || semanticConvention.Value.HasFlag(HttpSemanticConvention.Old))
{
var metric = requestMetrics.FirstOrDefault(m => m.Name == "http.client.duration");
@ -419,7 +480,7 @@ public partial class HttpClientTests
expected: new List<double> { 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000, double.PositiveInfinity },
actual: histogramBounds);
}
#endif
if (semanticConvention != null && semanticConvention.Value.HasFlag(HttpSemanticConvention.New))
{
var metric = requestMetrics.FirstOrDefault(m => m.Name == "http.client.request.duration");

View File

@ -49,11 +49,7 @@ public static class HttpTestData
return value
.Replace("{host}", host)
.Replace("{port}", port.ToString())
#if NETFRAMEWORK
.Replace("{flavor}", "1.1");
#else
.Replace("{flavor}", "2.0");
#endif
}
public class HttpOutTestCase