diff --git a/src/OpenTelemetry.Instrumentation.Http/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Http/.publicApi/net462/PublicAPI.Unshipped.txt index 2dd55551b0..1efd333d9a 100644 --- a/src/OpenTelemetry.Instrumentation.Http/.publicApi/net462/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.Http/.publicApi/net462/PublicAPI.Unshipped.txt @@ -19,6 +19,7 @@ OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.RecordExcept OpenTelemetry.Metrics.MeterProviderBuilderExtensions OpenTelemetry.Trace.TracerProviderBuilderExtensions static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name) -> OpenTelemetry.Metrics.MeterProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configureHttpClientInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureHttpClientInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.Http/.publicApi/net6.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Http/.publicApi/net6.0/PublicAPI.Unshipped.txt index 2dd55551b0..1efd333d9a 100644 --- a/src/OpenTelemetry.Instrumentation.Http/.publicApi/net6.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.Http/.publicApi/net6.0/PublicAPI.Unshipped.txt @@ -19,6 +19,7 @@ OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.RecordExcept OpenTelemetry.Metrics.MeterProviderBuilderExtensions OpenTelemetry.Trace.TracerProviderBuilderExtensions static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name) -> OpenTelemetry.Metrics.MeterProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configureHttpClientInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureHttpClientInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.Http/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Http/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index 2dd55551b0..1efd333d9a 100644 --- a/src/OpenTelemetry.Instrumentation.Http/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.Http/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -19,6 +19,7 @@ OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.RecordExcept OpenTelemetry.Metrics.MeterProviderBuilderExtensions OpenTelemetry.Trace.TracerProviderBuilderExtensions static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name) -> OpenTelemetry.Metrics.MeterProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configureHttpClientInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureHttpClientInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md index a94fa9789e..0ae13ac49c 100644 --- a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +* Added support for exposing `http.client.duration` on .NET Framework for `HttpWebRequest`, including `HttpClient`. Metrics support requires tracing to be enabled via `AddHttpClientInstrumentation` (#4768)[https://github.com/open-telemetry/opentelemetry-dotnet/pull/4768] + ## 1.5.1-beta.1 Released 2023-Jul-20 diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs index 670ce8f361..18ec90f70c 100644 --- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs +++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs @@ -17,6 +17,7 @@ #if NETFRAMEWORK using System.Collections; using System.Diagnostics; +using System.Diagnostics.Metrics; using System.Net; using System.Reflection; using System.Reflection.Emit; @@ -39,6 +40,7 @@ internal static class HttpWebRequestActivitySource internal static readonly AssemblyName AssemblyName = typeof(HttpWebRequestActivitySource).Assembly.GetName(); internal static readonly string ActivitySourceName = AssemblyName.Name + ".HttpWebRequest"; internal static readonly string ActivityName = ActivitySourceName + ".HttpRequestOut"; + internal static readonly string InstrumentationName = AssemblyName.Name; internal static readonly Func> HttpWebRequestHeaderValuesGetter = (request, name) => request.Headers.GetValues(name); internal static readonly Action HttpWebRequestHeaderValuesSetter = (request, name, value) => request.Headers.Add(name, value); @@ -46,6 +48,9 @@ internal static class HttpWebRequestActivitySource private static readonly Version Version = AssemblyName.Version; private static readonly ActivitySource WebRequestActivitySource = new ActivitySource(ActivitySourceName, Version.ToString()); + private static readonly Meter WebRequestMeter = new Meter(InstrumentationName, Version.ToString()); + private static readonly Histogram HttpClientDuration; + private static HttpClientInstrumentationOptions options; private static bool emitOldAttributes; @@ -87,6 +92,7 @@ static HttpWebRequestActivitySource() PerformInjection(); Options = new HttpClientInstrumentationOptions(); + HttpClientDuration = WebRequestMeter.CreateHistogram("http.client.duration", "ms", "Measures the duration of outbound HTTP requests."); } catch (Exception ex) { @@ -396,6 +402,51 @@ private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncC } activity.Stop(); + + // Only calculate duration if the Meter is subscribed to + if (HttpClientDuration.Enabled) + { + TagList tags = default; + + // see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md + if (emitOldAttributes) + { + foreach (ref readonly var tag in activity.EnumerateTagObjects()) + { + switch (tag.Key) + { + case SemanticConventions.AttributeHttpMethod: + case SemanticConventions.AttributeHttpScheme: + case SemanticConventions.AttributeHttpFlavor: + case SemanticConventions.AttributeNetPeerName: + case SemanticConventions.AttributeNetPeerPort: + case SemanticConventions.AttributeHttpStatusCode: + tags.Add(new KeyValuePair(tag.Key, tag.Value)); + break; + } + } + } + + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md + if (emitNewAttributes) + { + foreach (ref readonly var tag in activity.EnumerateTagObjects()) + { + switch (tag.Key) + { + case SemanticConventions.AttributeHttpRequestMethod: + case SemanticConventions.AttributeNetworkProtocolVersion: + case SemanticConventions.AttributeServerAddress: + case SemanticConventions.AttributeServerPort: + case SemanticConventions.AttributeHttpResponseStatusCode: + tags.Add(new KeyValuePair(tag.Key, tag.Value)); + break; + } + } + } + + HttpClientDuration.Record(activity.Duration.TotalMilliseconds, tags); + } } private static void PrepareReflectionObjects() diff --git a/src/OpenTelemetry.Instrumentation.Http/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Http/MeterProviderBuilderExtensions.cs index 38d375436b..fe9ee65275 100644 --- a/src/OpenTelemetry.Instrumentation.Http/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Http/MeterProviderBuilderExtensions.cs @@ -34,12 +34,26 @@ public static class MeterProviderBuilderExtensions /// The instance of to chain the calls. public static MeterProviderBuilder AddHttpClientInstrumentation( this MeterProviderBuilder builder) + { + return AddHttpClientInstrumentation(builder, name: null); + } + + /// + /// Enables HttpClient instrumentation. + /// + /// being configured. + /// Name which is used when retrieving options. + /// The instance of to chain the calls. + public static MeterProviderBuilder AddHttpClientInstrumentation( + this MeterProviderBuilder builder, string name) { Guard.ThrowIfNull(builder); // Note: Warm-up the status code mapping. _ = TelemetryHelper.BoxedStatusCodes; + name ??= Options.DefaultName; + builder.ConfigureServices(services => { services.RegisterOptionsFactory(configuration => new HttpClientMetricInstrumentationOptions(configuration)); @@ -53,8 +67,23 @@ public static MeterProviderBuilder AddHttpClientInstrumentation( // 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.InstrumentationName); + builder.ConfigureServices(s => + { + s.ConfigureOpenTelemetryMeterProvider((sp, _) => + { + var options = sp.GetRequiredService>().Get(name); + + HttpWebRequestActivitySource.Options = options; + }); + }); +#else builder.AddMeter(HttpClientMetrics.InstrumentationName); - return builder.AddInstrumentation(sp => new HttpClientMetrics( + builder.AddInstrumentation(sp => new HttpClientMetrics( sp.GetRequiredService>().CurrentValue)); +#endif + + return builder; } } diff --git a/src/OpenTelemetry.Instrumentation.Http/README.md b/src/OpenTelemetry.Instrumentation.Http/README.md index 7ca9400d4b..ec33b7303c 100644 --- a/src/OpenTelemetry.Instrumentation.Http/README.md +++ b/src/OpenTelemetry.Instrumentation.Http/README.md @@ -68,7 +68,9 @@ public class Program #### Metrics > **Note** -> Metrics are not available for .NET Framework. +> Metrics are only available for .NET Framework when traces are recorded. This requires: +> 1. Tracing to be enabled by calling `.AddHttpClientInstrumentation()` on `TracerProviderBuilder` +> 2. [If a sampler is in place, return `SamplingDecision.RecordAndSampled` for `ShouldSample`](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/docs/trace/extending-the-sdk/README.md#filtering-processor) The following example demonstrates adding `HttpClient` instrumentation with the extension method `.AddHttpClientInstrumentation()` on `MeterProviderBuilder` to diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs index 0ceeaa1eb4..620222a49a 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs @@ -159,9 +159,6 @@ public async Task HttpOutCallsAreCollectedSuccessfullyAsync(HttpTestData.HttpOut Assert.True(enrichWithExceptionCalled); } -#if NETFRAMEWORK - Assert.Empty(requestMetrics); -#else Assert.Single(requestMetrics); var metric = requestMetrics[0]; @@ -193,7 +190,11 @@ public async Task HttpOutCallsAreCollectedSuccessfullyAsync(HttpTestData.HttpOut var method = new KeyValuePair(SemanticConventions.AttributeHttpMethod, tc.Method); var scheme = new KeyValuePair(SemanticConventions.AttributeHttpScheme, "http"); var statusCode = new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, tc.ResponseCode == 0 ? 200 : tc.ResponseCode); +#if NETFRAMEWORK + var flavor = new KeyValuePair(SemanticConventions.AttributeHttpFlavor, "1.1"); +#else var flavor = new KeyValuePair(SemanticConventions.AttributeHttpFlavor, "2.0"); +#endif var hostName = new KeyValuePair(SemanticConventions.AttributeNetPeerName, tc.ResponseExpected ? host : "sdlfaldfjalkdfjlkajdflkajlsdjf"); var portNumber = new KeyValuePair(SemanticConventions.AttributeNetPeerPort, port); Assert.Contains(hostName, attributes); @@ -211,7 +212,6 @@ public async Task HttpOutCallsAreCollectedSuccessfullyAsync(HttpTestData.HttpOut Assert.DoesNotContain(statusCode, attributes); Assert.Equal(5, attributes.Length); } -#endif } [Fact]