diff --git a/src/OpenTelemetry.Instrumentation.Http/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Http/.publicApi/net462/PublicAPI.Unshipped.txt index dd82537749e..2dd55551b0c 100644 --- a/src/OpenTelemetry.Instrumentation.Http/.publicApi/net462/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.Http/.publicApi/net462/PublicAPI.Unshipped.txt @@ -5,26 +5,20 @@ OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHt OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpRequestMessage.set -> void OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpResponseMessage.get -> System.Action OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpResponseMessage.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.Filter.get -> System.Func -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.Filter.set -> void +OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpWebRequest.get -> System.Action +OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpWebRequest.set -> void +OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpWebResponse.get -> System.Action +OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpWebResponse.set -> void +OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.FilterHttpRequestMessage.get -> System.Func +OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.FilterHttpRequestMessage.set -> void +OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.FilterHttpWebRequest.get -> System.Func +OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.FilterHttpWebRequest.set -> void OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.HttpClientInstrumentationOptions() -> void OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.RecordException.get -> bool OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.RecordException.set -> void -OpenTelemetry.Instrumentation.Http.HttpWebRequestInstrumentationOptions -OpenTelemetry.Instrumentation.Http.HttpWebRequestInstrumentationOptions.EnrichWithException.get -> System.Action -OpenTelemetry.Instrumentation.Http.HttpWebRequestInstrumentationOptions.EnrichWithException.set -> void -OpenTelemetry.Instrumentation.Http.HttpWebRequestInstrumentationOptions.EnrichWithHttpWebRequest.get -> System.Action -OpenTelemetry.Instrumentation.Http.HttpWebRequestInstrumentationOptions.EnrichWithHttpWebRequest.set -> void -OpenTelemetry.Instrumentation.Http.HttpWebRequestInstrumentationOptions.EnrichWithHttpWebResponse.get -> System.Action -OpenTelemetry.Instrumentation.Http.HttpWebRequestInstrumentationOptions.EnrichWithHttpWebResponse.set -> void -OpenTelemetry.Instrumentation.Http.HttpWebRequestInstrumentationOptions.Filter.get -> System.Func -OpenTelemetry.Instrumentation.Http.HttpWebRequestInstrumentationOptions.Filter.set -> void -OpenTelemetry.Instrumentation.Http.HttpWebRequestInstrumentationOptions.HttpWebRequestInstrumentationOptions() -> void -OpenTelemetry.Instrumentation.Http.HttpWebRequestInstrumentationOptions.RecordException.get -> bool -OpenTelemetry.Instrumentation.Http.HttpWebRequestInstrumentationOptions.RecordException.set -> void OpenTelemetry.Metrics.MeterProviderBuilderExtensions OpenTelemetry.Trace.TracerProviderBuilderExtensions static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> 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 configureHttpWebRequestInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureHttpWebRequestInstrumentationOptions) -> 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.Shipped.txt b/src/OpenTelemetry.Instrumentation.Http/.publicApi/net6.0/PublicAPI.Shipped.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/OpenTelemetry.Instrumentation.Http/.publicApi/net6.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Http/.publicApi/net6.0/PublicAPI.Unshipped.txt deleted file mode 100644 index 1590f0c3d90..00000000000 --- a/src/OpenTelemetry.Instrumentation.Http/.publicApi/net6.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,18 +0,0 @@ -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithException.get -> System.Action -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithException.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpRequestMessage.get -> System.Action -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpRequestMessage.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpResponseMessage.get -> System.Action -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpResponseMessage.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.Filter.get -> System.Func -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.Filter.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.HttpClientInstrumentationOptions() -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.RecordException.get -> bool -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.RecordException.set -> void -OpenTelemetry.Metrics.MeterProviderBuilderExtensions -OpenTelemetry.Trace.TracerProviderBuilderExtensions -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> 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 1590f0c3d90..2dd55551b0c 100644 --- a/src/OpenTelemetry.Instrumentation.Http/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.Http/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -5,8 +5,14 @@ OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHt OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpRequestMessage.set -> void OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpResponseMessage.get -> System.Action OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpResponseMessage.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.Filter.get -> System.Func -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.Filter.set -> void +OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpWebRequest.get -> System.Action +OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpWebRequest.set -> void +OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpWebResponse.get -> System.Action +OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpWebResponse.set -> void +OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.FilterHttpRequestMessage.get -> System.Func +OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.FilterHttpRequestMessage.set -> void +OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.FilterHttpWebRequest.get -> System.Func +OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.FilterHttpWebRequest.set -> void OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.HttpClientInstrumentationOptions() -> void OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.RecordException.get -> bool OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.RecordException.set -> void diff --git a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md index 5ece3f84ac8..bf596e774fc 100644 --- a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md @@ -25,6 +25,12 @@ runtimes for non-sampled outgoing `HttpClient` spans. ([#3828](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3828)) +* **Breaking change**: The same API is now exposed for `net462` and + `netstandard2.0` targets. The `Filter` property on options is now exposed as + `FilterHttpRequestMessage` (called for .NET & .NET Core) and + `FilterHttpWebRequest` (called for .NET Framework). + ([#3793](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3793)) + ## 1.0.0-rc9.8 Released 2022-Oct-17 diff --git a/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationOptions.cs index 8bf9a96be78..2c5b478c8aa 100644 --- a/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationOptions.cs @@ -16,6 +16,7 @@ using System; using System.Diagnostics; +using System.Net; using System.Net.Http; using System.Runtime.CompilerServices; using OpenTelemetry.Instrumentation.Http.Implementation; @@ -28,17 +29,35 @@ namespace OpenTelemetry.Instrumentation.Http public class HttpClientInstrumentationOptions { /// - /// Gets or sets a Filter function that determines whether or not to collect telemetry about requests on a per request basis. - /// The Filter gets the HttpRequestMessage, and should return a boolean. - /// If Filter returns true, the request is collected. - /// If Filter returns false or throw exception, the request is filtered out. + /// Gets or sets a filter function that determines whether or not to + /// collect telemetry about requests on a per request basis. /// - public Func Filter { get; set; } + /// + /// Notes: + /// + /// FilterHttpRequestMessage is only executed on .NET and .NET + /// Core runtimes. and on .NET and .NET Core are both implemented + /// using . + /// The return value for the filter function is interpreted as: + /// + /// If filter returns , the request is + /// collected. + /// If filter returns or throws an + /// exception the request is NOT collected. + /// + /// + /// + public Func FilterHttpRequestMessage { get; set; } /// /// Gets or sets an action to enrich an Activity with . /// /// + /// EnrichWithHttpRequestMessage is only executed on .NET and .NET + /// Core runtimes. and on .NET and .NET Core are both implemented + /// using . /// : the activity being enriched. /// object from which additional information can be extracted to enrich the activity. /// @@ -48,6 +67,10 @@ public class HttpClientInstrumentationOptions /// Gets or sets an action to enrich an Activity with . /// /// + /// EnrichWithHttpResponseMessage is only executed on .NET and .NET + /// Core runtimes. and on .NET and .NET Core are both implemented + /// using . /// : the activity being enriched. /// object from which additional information can be extracted to enrich the activity. /// @@ -57,28 +80,90 @@ public class HttpClientInstrumentationOptions /// Gets or sets an action to enrich an Activity with . /// /// + /// EnrichWithException is called for all runtimes. /// : the activity being enriched. /// object from which additional information can be extracted to enrich the activity. /// public Action EnrichWithException { get; set; } /// - /// Gets or sets a value indicating whether exception will be recorded as ActivityEvent or not. + /// Gets or sets a filter function that determines whether or not to + /// collect telemetry about requests on a per request basis. + /// + /// + /// Notes: + /// + /// FilterHttpWebRequest is only executed on .NET Framework + /// runtimes. and + /// on .NET Framework are both implemented using . + /// The return value for the filter function is interpreted as: + /// + /// If filter returns , the request is + /// collected. + /// If filter returns or throws an + /// exception the request is NOT collected. + /// + /// + /// + public Func FilterHttpWebRequest { get; set; } + + /// + /// Gets or sets an action to enrich an Activity with . + /// + /// + /// EnrichWithHttpWebRequest is only executed on .NET Framework + /// runtimes. and + /// on .NET Framework are both implemented using . + /// : the activity being enriched. + /// object from which additional information can be extracted to enrich the activity. + /// + public Action EnrichWithHttpWebRequest { get; set; } + + /// + /// Gets or sets an action to enrich an Activity with . + /// + /// + /// EnrichWithHttpWebResponse is only executed on .NET Framework + /// runtimes. and + /// on .NET Framework are both implemented using . + /// : the activity being enriched. + /// object from which additional information can be extracted to enrich the activity. + /// + public Action EnrichWithHttpWebResponse { get; set; } + + /// + /// Gets or sets a value indicating whether exception will be recorded as an or not. /// /// - /// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/exceptions.md. + /// See: . /// public bool RecordException { get; set; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal bool EventFilter(string activityName, object arg1) + internal bool EventFilterHttpRequestMessage(string activityName, object arg1) { try { return - this.Filter == null || + this.FilterHttpRequestMessage == null || !TryParseHttpRequestMessage(activityName, arg1, out HttpRequestMessage requestMessage) || - this.Filter(requestMessage); + this.FilterHttpRequestMessage(requestMessage); + } + catch (Exception ex) + { + HttpInstrumentationEventSource.Log.RequestFilterException(ex); + return false; + } + } + + internal bool EventFilterHttpWebRequest(HttpWebRequest request) + { + try + { + return this.FilterHttpWebRequest?.Invoke(request) ?? true; } catch (Exception ex) { diff --git a/src/OpenTelemetry.Instrumentation.Http/HttpWebRequestInstrumentationOptions.netfx.cs b/src/OpenTelemetry.Instrumentation.Http/HttpWebRequestInstrumentationOptions.netfx.cs deleted file mode 100644 index 11243d151a9..00000000000 --- a/src/OpenTelemetry.Instrumentation.Http/HttpWebRequestInstrumentationOptions.netfx.cs +++ /dev/null @@ -1,87 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#if NETFRAMEWORK -using System; -using System.Diagnostics; -using System.Net; -using OpenTelemetry.Instrumentation.Http.Implementation; - -namespace OpenTelemetry.Instrumentation.Http -{ - /// - /// Options for HttpWebRequest instrumentation. - /// - public class HttpWebRequestInstrumentationOptions - { - /// - /// Gets or sets a Filter function that determines whether or not to collect telemetry about requests on a per request basis. - /// The Filter gets the HttpWebRequest, and should return a boolean. - /// If Filter returns true, the request is collected. - /// If Filter returns false or throw exception, the request is filtered out. - /// - public Func Filter { get; set; } - - /// - /// Gets or sets an action to enrich an Activity with . - /// - /// - /// : the activity being enriched. - /// object from which additional information can be extracted to enrich the activity. - /// - public Action EnrichWithHttpWebRequest { get; set; } - - /// - /// Gets or sets an action to enrich an Activity with . - /// - /// - /// : the activity being enriched. - /// object from which additional information can be extracted to enrich the activity. - /// - public Action EnrichWithHttpWebResponse { get; set; } - - /// - /// Gets or sets an action to enrich an Activity with . - /// - /// - /// : the activity being enriched. - /// object from which additional information can be extracted to enrich the activity. - /// - public Action EnrichWithException { get; set; } - - /// - /// Gets or sets a value indicating whether exception will be recorded as ActivityEvent or not. - /// - /// - /// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/exceptions.md. - /// - public bool RecordException { get; set; } - - internal bool EventFilter(HttpWebRequest request) - { - try - { - return this.Filter?.Invoke(request) ?? true; - } - catch (Exception ex) - { - HttpInstrumentationEventSource.Log.RequestFilterException(ex); - return false; - } - } - } -} -#endif diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs index 6183b62a76a..5e0b2fae944 100644 --- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs @@ -133,7 +133,7 @@ public void OnStartActivity(Activity activity, object payload) { try { - if (this.options.EventFilter(activity.OperationName, request) == false) + if (this.options.EventFilterHttpRequestMessage(activity.OperationName, request) == false) { HttpInstrumentationEventSource.Log.RequestIsFilteredOut(activity.OperationName); activity.IsAllDataRequested = false; diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs index 26c34cbd789..078997b1b10 100644 --- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs +++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs @@ -13,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // + #if NETFRAMEWORK using System; using System.Collections; @@ -43,7 +44,7 @@ internal static class HttpWebRequestActivitySource internal static readonly Func> HttpWebRequestHeaderValuesGetter = (request, name) => request.Headers.GetValues(name); internal static readonly Action HttpWebRequestHeaderValuesSetter = (request, name, value) => request.Headers.Add(name, value); - internal static HttpWebRequestInstrumentationOptions Options = new HttpWebRequestInstrumentationOptions(); + internal static HttpClientInstrumentationOptions Options = new HttpClientInstrumentationOptions(); private static readonly Version Version = AssemblyName.Version; private static readonly ActivitySource WebRequestActivitySource = new ActivitySource(ActivitySourceName, Version.ToString()); @@ -98,6 +99,7 @@ private static void AddRequestTagsAndInstrumentRequest(HttpWebRequest request, A if (activity.IsAllDataRequested) { activity.SetTag(SemanticConventions.AttributeHttpMethod, request.Method); + activity.SetTag(SemanticConventions.AttributeHttpScheme, request.RequestUri.Scheme); activity.SetTag(SemanticConventions.AttributeHttpHost, HttpTagHelper.GetHostTagValueFromRequestUri(request.RequestUri)); activity.SetTag(SemanticConventions.AttributeHttpUrl, HttpTagHelper.GetUriTagValueFromRequestUri(request.RequestUri)); activity.SetTag(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.ProtocolVersion)); @@ -208,7 +210,7 @@ private static bool IsRequestInstrumented(HttpWebRequest request) private static void ProcessRequest(HttpWebRequest request) { - if (!WebRequestActivitySource.HasListeners() || !Options.EventFilter(request)) + if (!WebRequestActivitySource.HasListeners() || !Options.EventFilterHttpWebRequest(request)) { // No subscribers to the ActivitySource or User provider Filter is // filtering this request. diff --git a/src/OpenTelemetry.Instrumentation.Http/OpenTelemetry.Instrumentation.Http.csproj b/src/OpenTelemetry.Instrumentation.Http/OpenTelemetry.Instrumentation.Http.csproj index 297a6ff0bcc..3665f2c85e0 100644 --- a/src/OpenTelemetry.Instrumentation.Http/OpenTelemetry.Instrumentation.Http.csproj +++ b/src/OpenTelemetry.Instrumentation.Http/OpenTelemetry.Instrumentation.Http.csproj @@ -2,17 +2,12 @@ - net6.0;netstandard2.0;net462 + netstandard2.0;net462 Http instrumentation for OpenTelemetry .NET $(PackageTags);distributed-tracing true - - - false - - diff --git a/src/OpenTelemetry.Instrumentation.Http/README.md b/src/OpenTelemetry.Instrumentation.Http/README.md index 6dcd670597b..d7375f467a4 100644 --- a/src/OpenTelemetry.Instrumentation.Http/README.md +++ b/src/OpenTelemetry.Instrumentation.Http/README.md @@ -20,7 +20,8 @@ These conventions are and hence, this package is a [pre-release](../../VERSIONING.md#pre-releases). Until a [stable version](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/telemetry-stability.md) -is released, there can be breaking changes. You can track the progress from +is released, there can be [breaking changes](./CHANGELOG.md). You can track the +progress from [milestones](https://github.com/open-telemetry/opentelemetry-dotnet/milestone/23).** ## Steps to enable OpenTelemetry.Instrumentation.Http @@ -72,56 +73,55 @@ For an ASP.NET application, adding instrumentation is typically done in the ## Advanced configuration This instrumentation can be configured to change the default behavior by using -`HttpClientInstrumentationOptions` (.NET/.NET Core applications) or -`HttpWebRequestInstrumentationOptions` (.NET Framework applications). It is -important to note that even if `HttpClient` is used in .NET Framework -applications, it underneath uses `HttpWebRequest`. Because of this, -`HttpWebRequestInstrumentationOptions` is the configuration option for .NET -Framework applications, irrespective of whether `HttpWebRequest` or `HttpClient` -is used. +`HttpClientInstrumentationOptions`. It is important to note that there are +differences between .NET Framework and newer .NET/.NET Core runtimes which +govern what options are used. On .NET Framework, `HttpClient` uses the +`HttpWebRequest` API. On .NET & .NET Core, `HttpWebRequest` uses the +`HttpClient` API. As such, depending on the runtime, only one half of the +"filter" & "enrich" options are used. -### Filter +### .NET & .NET Core + +#### Filter HttpClient API This instrumentation by default collects all the outgoing HTTP requests. It -allows filtering of requests by using the `Filter` function option. This defines -the condition for allowable requests. The Filter receives the request object - -`HttpRequestMessage` (when using `HttpClientInstrumentationOptions`) and -`HttpWebRequest` (when using `HttpWebRequestInstrumentationOptions`) - -representing the outgoing request and does not collect telemetry about the -request if the Filter returns false or throws exception. +allows filtering of requests by using the `FilterHttpRequestMessage` function +option. This defines the condition for allowable requests. The filter function +receives the request object (`HttpRequestMessage`) representing the outgoing +request and does not collect telemetry about the request if the filter function +returns `false` or throws an exception. -The following code snippet shows how to use `Filter` to only allow GET requests. +The following code snippet shows how to use `FilterHttpRequestMessage` to only +allow GET requests. ```csharp using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddHttpClientInstrumentation( - (options) => options.Filter = + // Note: Only called on .NET & .NET Core runtimes. + (options) => options.FilterHttpRequestMessage = (httpRequestMessage) => { - // only collect telemetry about HTTP GET requests + // Example: Only collect telemetry about HTTP GET requests. return httpRequestMessage.Method.Equals(HttpMethod.Get); }) .AddConsoleExporter() .Build(); ``` -It is important to note that this `Filter` option is specific to this -instrumentation. OpenTelemetry has a concept of a +It is important to note that this `FilterHttpRequestMessage` option is specific +to this instrumentation. OpenTelemetry has a concept of a [Sampler](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampling), -and the `Filter` option does the filtering *after* the Sampler is invoked. +and the `FilterHttpRequestMessage` option does the filtering *after* the Sampler +is invoked. -### Enrich +#### Enrich HttpClient API This instrumentation library provides options that can be used to enrich the activity with additional information. These actions are called only when `activity.IsAllDataRequested` is `true`. It contains the activity -itself (which can be enriched) and the actual raw object. The options -are different for `HttpClientInstrumentationOptions` vs -`HttpWebRequestInstrumentationOptions` and is detailed below. - -#### HttpClientInstrumentationOptions +itself (which can be enriched) and the actual raw object. -HttpClientInstrumentationOptions provides 3 enrich options, +`HttpClientInstrumentationOptions` provides 3 enrich options: `EnrichWithHttpRequestMessage`, `EnrichWithHttpResponseMessage` and `EnrichWithException`. These are based on the raw object that is passed in to the action to enrich the activity. @@ -134,14 +134,17 @@ using System.Net.Http; var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddHttpClientInstrumentation((options) => { + // Note: Only called on .NET & .NET Core runtimes. options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => { activity.SetTag("requestVersion", httpRequestMessage.Version); }; + // Note: Only called on .NET & .NET Core runtimes. options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { activity.SetTag("responseVersion", httpResponseMessage.Version); }; + // Note: Called for all runtimes. options.EnrichWithException = (activity, exception) => { activity.SetTag("stackTrace", exception.StackTrace); @@ -150,9 +153,48 @@ var tracerProvider = Sdk.CreateTracerProviderBuilder() .Build(); ``` -#### HttpWebRequestInstrumentationOptions +### .NET Framework + +#### Filter HttpWebRequest API + +This instrumentation by default collects all the outgoing HTTP requests. It +allows filtering of requests by using the `FilterHttpWebRequest` function +option. This defines the condition for allowable requests. The filter function +receives the request object (`HttpWebRequest`) representing the outgoing request +and does not collect telemetry about the request if the filter function returns +`false` or throws an exception. + +The following code snippet shows how to use `FilterHttpWebRequest` to only allow +GET requests. + +```csharp +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation( + // Note: Only called on .NET Framework. + (options) => options.FilterHttpWebRequest = + (httpWebRequest) => + { + // Example: Only collect telemetry about HTTP GET requests. + return httpWebRequest.Method.Equals(HttpMethod.Get.Method); + }) + .AddConsoleExporter() + .Build(); +``` + +It is important to note that this `FilterHttpWebRequest` option is specific to +this instrumentation. OpenTelemetry has a concept of a +[Sampler](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampling), +and the `FilterHttpWebRequest` option does the filtering *after* the Sampler is +invoked. + +#### Enrich HttpWebRequest API + +This instrumentation library provides options that can be used to +enrich the activity with additional information. These actions are called +only when `activity.IsAllDataRequested` is `true`. It contains the activity +itself (which can be enriched) and the actual raw object. -HttpClientInstrumentationOptions provides 3 enrich options, +`HttpClientInstrumentationOptions` provides 3 enrich options: `EnrichWithHttpWebRequest`, `EnrichWithHttpWebResponse` and `EnrichWithException`. These are based on the raw object that is passed in to the action to enrich the activity. @@ -165,14 +207,17 @@ using System.Net; var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddHttpClientInstrumentation((options) => { + // Note: Only called on .NET Framework. options.EnrichWithHttpWebRequest = (activity, httpWebRequest) => { activity.SetTag("requestVersion", httpWebRequest.Version); }; + // Note: Only called on .NET Framework. options.EnrichWithHttpWebResponse = (activity, httpWebResponse) => { activity.SetTag("responseVersion", httpWebResponse.Version); }; + // Note: Called for all runtimes. options.EnrichWithException = (activity, exception) => { activity.SetTag("stackTrace", exception.StackTrace); diff --git a/src/OpenTelemetry.Instrumentation.Http/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Http/TracerProviderBuilderExtensions.cs index 17dc4c29ebf..c900da51f61 100644 --- a/src/OpenTelemetry.Instrumentation.Http/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Http/TracerProviderBuilderExtensions.cs @@ -19,67 +19,15 @@ using Microsoft.Extensions.Options; using OpenTelemetry.Instrumentation.Http; using OpenTelemetry.Instrumentation.Http.Implementation; -#if !NETFRAMEWORK using OpenTelemetry.Internal; -#endif namespace OpenTelemetry.Trace { /// - /// Extension methods to simplify registering of httpclient instrumentation. + /// Extension methods to simplify registering of HttpClient instrumentation. /// public static class TracerProviderBuilderExtensions { -#if NETFRAMEWORK - /// - /// Enables HttpClient and HttpWebRequest instrumentation. - /// - /// being configured. - /// The instance of to chain the calls. - public static TracerProviderBuilder AddHttpClientInstrumentation(this TracerProviderBuilder builder) - => AddHttpClientInstrumentation(builder, name: null, configureHttpWebRequestInstrumentationOptions: null); - - /// - /// Enables HttpClient and HttpWebRequest instrumentation. - /// - /// being configured. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddHttpClientInstrumentation( - this TracerProviderBuilder builder, - Action configureHttpWebRequestInstrumentationOptions) - => AddHttpClientInstrumentation(builder, name: null, configureHttpWebRequestInstrumentationOptions); - - /// - /// Enables HttpClient and HttpWebRequest instrumentation. - /// - /// being configured. - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddHttpClientInstrumentation( - this TracerProviderBuilder builder, - string name, - Action configureHttpWebRequestInstrumentationOptions) - { - name ??= Options.DefaultName; - - if (configureHttpWebRequestInstrumentationOptions != null) - { - builder.ConfigureServices(services => services.Configure(name, configureHttpWebRequestInstrumentationOptions)); - } - - return builder.ConfigureBuilder((sp, builder) => - { - var options = sp.GetRequiredService>().Get(name); - - HttpWebRequestActivitySource.Options = options; - - builder.AddSource(HttpWebRequestActivitySource.ActivitySourceName); - }); - } - -#else /// /// Enables HttpClient instrumentation. /// @@ -124,10 +72,17 @@ public static TracerProviderBuilder AddHttpClientInstrumentation( { var options = sp.GetRequiredService>().Get(name); +#if NETFRAMEWORK + HttpWebRequestActivitySource.Options = options; + + builder.AddSource(HttpWebRequestActivitySource.ActivitySourceName); +#else AddHttpClientInstrumentation(builder, new HttpClientInstrumentation(options)); +#endif }); } +#if !NETFRAMEWORK internal static TracerProviderBuilder AddHttpClientInstrumentation( this TracerProviderBuilder builder, HttpClientInstrumentation instrumentation) diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.Basic.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.Basic.cs index 3b2877dc021..2a5e4a03e4f 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.Basic.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.Basic.cs @@ -13,15 +13,17 @@ // See the License for the specific language governing permissions and // limitations under the License. // -#if !NETFRAMEWORK + using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +#if NETFRAMEWORK +using System.Net; +#endif using System.Net.Http; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using Moq; using OpenTelemetry.Context.Propagation; using OpenTelemetry.Instrumentation.Http.Implementation; @@ -41,7 +43,15 @@ public HttpClientTests() this.serverLifeTime = TestHttpServer.RunServer( (ctx) => { - if (ctx.Request.Url.PathAndQuery.Contains("500")) + string traceparent = ctx.Request.Headers["traceparent"]; + string custom_traceparent = ctx.Request.Headers["custom_traceparent"]; + if (string.IsNullOrWhiteSpace(traceparent) + && string.IsNullOrWhiteSpace(custom_traceparent)) + { + ctx.Response.StatusCode = 500; + ctx.Response.StatusDescription = "Missing trace context"; + } + else if (ctx.Request.Url.PathAndQuery.Contains("500")) { ctx.Response.StatusCode = 500; } @@ -94,11 +104,13 @@ public void AddHttpClientInstrumentation_BadArgs() [Theory] [InlineData(true)] [InlineData(false)] - public async Task HttpClientInstrumentationInjectsHeadersAsync(bool shouldEnrich) + public async Task InjectsHeadersAsync(bool shouldEnrich) { var processor = new Mock>(); processor.Setup(x => x.OnStart(It.IsAny())).Callback(c => { + c.SetTag("enrichedWithHttpWebRequest", "no"); + c.SetTag("enrichedWithHttpWebResponse", "no"); c.SetTag("enrichedWithHttpRequestMessage", "no"); c.SetTag("enrichedWithHttpResponseMessage", "no"); }); @@ -115,41 +127,34 @@ public async Task HttpClientInstrumentationInjectsHeadersAsync(bool shouldEnrich parent.TraceStateString = "k1=v1,k2=v2"; parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; - // var isInjectedHeaderValueGetterThrows = false; - // mockTextFormat - // .Setup(x => x.IsInjected(It.IsAny(), It.IsAny>>())) - // .Callback>>( - // (carrier, getter) => - // { - // try - // { - // // traceparent doesn't exist - // getter(carrier, "traceparent"); - // } - // catch - // { - // isInjectedHeaderValueGetterThrows = true; - // } - // }); - using (Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation(o => + .AddHttpClientInstrumentation(o => + { + if (shouldEnrich) + { + o.EnrichWithHttpWebRequest = (activity, httpWebRequest) => + { + activity.SetTag("enrichedWithHttpWebRequest", "yes"); + }; + + o.EnrichWithHttpWebResponse = (activity, httpWebResponse) => { - if (shouldEnrich) - { - o.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => - { - activity.SetTag("enrichedWithHttpRequestMessage", "yes"); - }; - - o.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => - { - activity.SetTag("enrichedWithHttpResponseMessage", "yes"); - }; - } - }) - .AddProcessor(processor.Object) - .Build()) + activity.SetTag("enrichedWithHttpWebResponse", "yes"); + }; + + o.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => + { + activity.SetTag("enrichedWithHttpRequestMessage", "yes"); + }; + + o.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => + { + activity.SetTag("enrichedWithHttpResponseMessage", "yes"); + }; + } + }) + .AddProcessor(processor.Object) + .Build()) { using var c = new HttpClient(); await c.SendAsync(request); @@ -164,6 +169,11 @@ public async Task HttpClientInstrumentationInjectsHeadersAsync(bool shouldEnrich Assert.NotEqual(parent.SpanId, activity.Context.SpanId); Assert.NotEqual(default, activity.Context.SpanId); +#if NETFRAMEWORK + // Note: On .NET Framework a HttpWebRequest is created and enriched + // not the HttpRequestMessage passed to HttpClient. + Assert.Empty(request.Headers); +#else Assert.True(request.Headers.TryGetValues("traceparent", out var traceparents)); Assert.True(request.Headers.TryGetValues("tracestate", out var tracestates)); Assert.Single(traceparents); @@ -171,21 +181,26 @@ public async Task HttpClientInstrumentationInjectsHeadersAsync(bool shouldEnrich Assert.Equal($"00-{activity.Context.TraceId}-{activity.Context.SpanId}-01", traceparents.Single()); Assert.Equal("k1=v1,k2=v2", tracestates.Single()); +#endif + +#if NETFRAMEWORK + Assert.Equal(shouldEnrich ? "yes" : "no", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpWebRequest").FirstOrDefault().Value); + Assert.Equal(shouldEnrich ? "yes" : "no", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpWebResponse").FirstOrDefault().Value); + + Assert.Equal("no", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpRequestMessage").FirstOrDefault().Value); + Assert.Equal("no", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpResponseMessage").FirstOrDefault().Value); +#else + Assert.Equal("no", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpWebRequest").FirstOrDefault().Value); + Assert.Equal("no", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpWebResponse").FirstOrDefault().Value); - Assert.NotEmpty(activity.Tags.Where(tag => tag.Key == "enrichedWithHttpRequestMessage")); - Assert.NotEmpty(activity.Tags.Where(tag => tag.Key == "enrichedWithHttpResponseMessage")); Assert.Equal(shouldEnrich ? "yes" : "no", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpRequestMessage").FirstOrDefault().Value); Assert.Equal(shouldEnrich ? "yes" : "no", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpResponseMessage").FirstOrDefault().Value); +#endif } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task HttpClientInstrumentationInjectsHeadersAsync_CustomFormat(bool shouldEnrich) + [Fact] + public async Task InjectsHeadersAsync_CustomFormat() { - bool enrichWithHttpRequestMessageCalled = false; - bool enrichWithHttpResponseMessageCalled = false; - var propagator = new Mock(); propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) .Callback>((context, message, action) => @@ -211,16 +226,9 @@ public async Task HttpClientInstrumentationInjectsHeadersAsync_CustomFormat(bool Sdk.SetDefaultTextMapPropagator(propagator.Object); using (Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation((opt) => - { - if (shouldEnrich) - { - opt.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => { enrichWithHttpRequestMessageCalled = true; }; - opt.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; - } - }) - .AddProcessor(processor.Object) - .Build()) + .AddHttpClientInstrumentation() + .AddProcessor(processor.Object) + .Build()) { using var c = new HttpClient(); await c.SendAsync(request); @@ -235,6 +243,11 @@ public async Task HttpClientInstrumentationInjectsHeadersAsync_CustomFormat(bool Assert.NotEqual(parent.SpanId, activity.Context.SpanId); Assert.NotEqual(default, activity.Context.SpanId); +#if NETFRAMEWORK + // Note: On .NET Framework a HttpWebRequest is created and enriched + // not the HttpRequestMessage passed to HttpClient. + Assert.Empty(request.Headers); +#else Assert.True(request.Headers.TryGetValues("custom_traceparent", out var traceparents)); Assert.True(request.Headers.TryGetValues("custom_tracestate", out var tracestates)); Assert.Single(traceparents); @@ -242,21 +255,17 @@ public async Task HttpClientInstrumentationInjectsHeadersAsync_CustomFormat(bool Assert.Equal($"00/{activity.Context.TraceId}/{activity.Context.SpanId}/01", traceparents.Single()); Assert.Equal("k1=v1,k2=v2", tracestates.Single()); +#endif + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] { new TraceContextPropagator(), new BaggagePropagator(), })); - - if (shouldEnrich) - { - Assert.True(enrichWithHttpRequestMessageCalled); - Assert.True(enrichWithHttpResponseMessageCalled); - } } [Fact] - public async Task HttpClientInstrumentationRespectsSuppress() + public async Task RespectsSuppress() { try { @@ -285,9 +294,9 @@ public async Task HttpClientInstrumentationRespectsSuppress() Sdk.SetDefaultTextMapPropagator(propagator.Object); using (Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation() - .AddProcessor(processor.Object) - .Build()) + .AddHttpClientInstrumentation() + .AddProcessor(processor.Object) + .Build()) { using var c = new HttpClient(); using (SuppressInstrumentationScope.Begin()) @@ -313,26 +322,7 @@ public async Task HttpClientInstrumentationRespectsSuppress() } [Fact] - public async Task HttpClientInstrumentation_AddViaFactory_HttpInstrumentation_CollectsSpans() - { - var processor = new Mock>(); - - using (Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation() - .AddProcessor(processor.Object) - .Build()) - { - using var c = new HttpClient(); - await c.GetAsync(this.url); - } - - Assert.Single(processor.Invocations.Where(i => i.Method.Name == "OnStart")); - Assert.Single(processor.Invocations.Where(i => i.Method.Name == "OnEnd")); - Assert.IsType(processor.Invocations[1].Arguments[0]); - } - - [Fact] - public async Task HttpClientInstrumentationExportsSpansCreatedForRetries() + public async Task ExportsSpansCreatedForRetries() { var exportedItems = new List(); var request = new HttpRequestMessage @@ -342,9 +332,9 @@ public async Task HttpClientInstrumentationExportsSpansCreatedForRetries() }; using var traceprovider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation() - .AddInMemoryExporter(exportedItems) - .Build(); + .AddHttpClientInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); int maxRetries = 3; using var c = new HttpClient(new RetryHandler(new HttpClientHandler(), maxRetries)); @@ -364,18 +354,29 @@ public async Task HttpClientInstrumentationExportsSpansCreatedForRetries() } [Fact] - public async Task HttpClientRedirectTest() + public async Task RedirectTest() { var processor = new Mock>(); using (Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation() - .AddProcessor(processor.Object) - .Build()) + .AddHttpClientInstrumentation() + .AddProcessor(processor.Object) + .Build()) { using var c = new HttpClient(); await c.GetAsync($"{this.url}redirect"); } +#if NETFRAMEWORK + // Note: HttpWebRequest automatically handles redirects and reuses + // the same instance which is patched reflectively. There isn't a + // good way to produce two spans when redirecting that we have + // found. For now, this is not supported. + + Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. + + var firstActivity = (Activity)processor.Invocations[2].Arguments[0]; // First OnEnd + Assert.Contains(firstActivity.TagObjects, t => t.Key == "http.status_code" && (int)t.Value == 200); +#else Assert.Equal(7, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnStart/OnEnd/OnShutdown/Dispose called. var firstActivity = (Activity)processor.Invocations[2].Arguments[0]; // First OnEnd @@ -383,134 +384,84 @@ public async Task HttpClientRedirectTest() var secondActivity = (Activity)processor.Invocations[4].Arguments[0]; // Second OnEnd Assert.Contains(secondActivity.TagObjects, t => t.Key == "http.status_code" && (int)t.Value == 200); +#endif } [Fact] public async void RequestNotCollectedWhenInstrumentationFilterApplied() { - var processor = new Mock>(); - using (Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation( - (opt) => opt.Filter = (req) => !req.RequestUri.OriginalString.Contains(this.url)) - .AddProcessor(processor.Object) - .Build()) - { - using var c = new HttpClient(); - await c.GetAsync(this.url); - } + var exportedItems = new List(); - Assert.Equal(4, processor.Invocations.Count); // SetParentProvider/OnShutdown/Dispose/OnStart called. - } + bool httpWebRequestFilterApplied = false; + bool httpRequestMessageFilterApplied = false; - [Fact] - public async void RequestNotCollectedWhenInstrumentationFilterThrowsException() - { - var processor = new Mock>(); using (Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation( - (opt) => opt.Filter = (req) => throw new Exception("From InstrumentationFilter")) - .AddProcessor(processor.Object) - .Build()) + .AddHttpClientInstrumentation( + opt => + { + opt.FilterHttpWebRequest = (req) => + { + httpWebRequestFilterApplied = true; + return !req.RequestUri.OriginalString.Contains(this.url); + }; + opt.FilterHttpRequestMessage = (req) => + { + httpRequestMessageFilterApplied = true; + return !req.RequestUri.OriginalString.Contains(this.url); + }; + }) + .AddInMemoryExporter(exportedItems) + .Build()) { using var c = new HttpClient(); - using var inMemoryEventListener = new InMemoryEventListener(HttpInstrumentationEventSource.Log); await c.GetAsync(this.url); - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 4)); } - Assert.Equal(4, processor.Invocations.Count); // SetParentProvider/OnShutdown/Dispose/OnStart called. - } - - [Fact] - public async Task HttpClientInstrumentationCorrelationAndBaggage() - { - var activityProcessor = new Mock>(); - - bool enrichWithHttpRequestMessageCalled = false; - bool enrichWithHttpResponseMessageCalled = false; - - using var parent = new Activity("w3c activity"); - parent.SetIdFormat(ActivityIdFormat.W3C); - parent.AddBaggage("k1", "v1"); - parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; - parent.Start(); - - Baggage.SetBaggage("k2", "v2"); - - using (Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation(options => - { - options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => { enrichWithHttpRequestMessageCalled = true; }; - options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; - }) - .AddProcessor(activityProcessor.Object) - .Build()) - { - using var c = new HttpClient(); - using var r = await c.GetAsync(this.url).ConfigureAwait(false); - } +#if NETFRAMEWORK + Assert.True(httpWebRequestFilterApplied); + Assert.False(httpRequestMessageFilterApplied); +#else + Assert.False(httpWebRequestFilterApplied); + Assert.True(httpRequestMessageFilterApplied); +#endif - Assert.Equal(5, activityProcessor.Invocations.Count); - Assert.True(enrichWithHttpRequestMessageCalled); - Assert.True(enrichWithHttpResponseMessageCalled); + Assert.Empty(exportedItems); } [Fact] - public async Task HttpClientInstrumentationContextPropagation() + public async void RequestNotCollectedWhenInstrumentationFilterThrowsException() { - var processor = new Mock>(); - var request = new HttpRequestMessage - { - RequestUri = new Uri(this.url), - Method = new HttpMethod("GET"), - }; + var exportedItems = new List(); - var parent = new Activity("parent") - .SetIdFormat(ActivityIdFormat.W3C) - .Start(); - parent.TraceStateString = "k1=v1,k2=v2"; - parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; - Baggage.SetBaggage("b1", "v1"); using (Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation() - .AddProcessor(processor.Object) + .AddHttpClientInstrumentation( + (opt) => + { + opt.FilterHttpWebRequest = (req) => throw new Exception("From InstrumentationFilter"); + opt.FilterHttpRequestMessage = (req) => throw new Exception("From InstrumentationFilter"); + }) + .AddInMemoryExporter(exportedItems) .Build()) { using var c = new HttpClient(); - await c.SendAsync(request); + using var inMemoryEventListener = new InMemoryEventListener(HttpInstrumentationEventSource.Log); + await c.GetAsync(this.url); + Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 4)); } - Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. - var activity = (Activity)processor.Invocations[1].Arguments[0]; - - Assert.Equal(ActivityKind.Client, activity.Kind); - Assert.Equal(parent.TraceId, activity.Context.TraceId); - Assert.Equal(parent.SpanId, activity.ParentSpanId); - Assert.NotEqual(parent.SpanId, activity.Context.SpanId); - Assert.NotEqual(default, activity.Context.SpanId); - - Assert.True(request.Headers.TryGetValues("traceparent", out var traceparents)); - Assert.True(request.Headers.TryGetValues("tracestate", out var tracestates)); - Assert.True(request.Headers.TryGetValues("baggage", out var baggages)); - Assert.Single(traceparents); - Assert.Single(tracestates); - Assert.Single(baggages); - - Assert.Equal($"00-{activity.Context.TraceId}-{activity.Context.SpanId}-01", traceparents.Single()); - Assert.Equal("k1=v1,k2=v2", tracestates.Single()); - Assert.Equal("b1=v1", baggages.Single()); + Assert.Empty(exportedItems); } [Fact] - public async Task HttpClientInstrumentationReportsExceptionEventForNetworkFailuresWithGetAsync() + public async Task ReportsExceptionEventForNetworkFailuresWithGetAsync() { var exportedItems = new List(); bool exceptionThrown = false; using var traceprovider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation(o => o.RecordException = true) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddHttpClientInstrumentation(o => o.RecordException = true) + .AddInMemoryExporter(exportedItems) + .Build(); using var c = new HttpClient(); try @@ -528,15 +479,15 @@ public async Task HttpClientInstrumentationReportsExceptionEventForNetworkFailur } [Fact] - public async Task HttpClientInstrumentationDoesNotReportExceptionEventOnErrorResponseWithGetAsync() + public async Task DoesNotReportExceptionEventOnErrorResponseWithGetAsync() { var exportedItems = new List(); bool exceptionThrown = false; using var traceprovider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation(o => o.RecordException = true) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddHttpClientInstrumentation(o => o.RecordException = true) + .AddInMemoryExporter(exportedItems) + .Build(); using var c = new HttpClient(); try @@ -554,7 +505,7 @@ public async Task HttpClientInstrumentationDoesNotReportExceptionEventOnErrorRes } [Fact] - public async Task HttpClientInstrumentationDoesNotReportExceptionEventOnErrorResponseWithGetStringAsync() + public async Task DoesNotReportExceptionEventOnErrorResponseWithGetStringAsync() { var exportedItems = new List(); bool exceptionThrown = false; @@ -565,9 +516,9 @@ public async Task HttpClientInstrumentationDoesNotReportExceptionEventOnErrorRes }; using var traceprovider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation(o => o.RecordException = true) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddHttpClientInstrumentation(o => o.RecordException = true) + .AddInMemoryExporter(exportedItems) + .Build(); using var c = new HttpClient(); try @@ -595,6 +546,17 @@ public async Task CustomPropagatorCalled(bool sample, bool createParentActivity) ActivityContext contextFromPropagator = default; var propagator = new Mock(); + +#if NETFRAMEWORK + propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) + .Callback>((context, carrier, setter) => + { + contextFromPropagator = context.ActivityContext; + + setter(carrier, "custom_traceparent", $"00/{contextFromPropagator.TraceId}/{contextFromPropagator.SpanId}/01"); + setter(carrier, "custom_tracestate", contextFromPropagator.TraceState); + }); +#else propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) .Callback>((context, carrier, setter) => { @@ -603,9 +565,7 @@ public async Task CustomPropagatorCalled(bool sample, bool createParentActivity) setter(carrier, "custom_traceparent", $"00/{contextFromPropagator.TraceId}/{contextFromPropagator.SpanId}/01"); setter(carrier, "custom_tracestate", contextFromPropagator.TraceState); }); - - var previousDefaultTextMapPropagator = Propagators.DefaultTextMapPropagator; - Sdk.SetDefaultTextMapPropagator(propagator.Object); +#endif var exportedItems = new List(); @@ -615,6 +575,9 @@ public async Task CustomPropagatorCalled(bool sample, bool createParentActivity) .SetSampler(sample ? new ParentBasedSampler(new AlwaysOnSampler()) : new AlwaysOffSampler()) .Build()) { + var previousDefaultTextMapPropagator = Propagators.DefaultTextMapPropagator; + Sdk.SetDefaultTextMapPropagator(propagator.Object); + Activity parent = null; if (createParentActivity) { @@ -638,6 +601,8 @@ public async Task CustomPropagatorCalled(bool sample, bool createParentActivity) await c.SendAsync(request); parent?.Stop(); + + Sdk.SetDefaultTextMapPropagator(previousDefaultTextMapPropagator); } if (!sample) @@ -656,7 +621,13 @@ public async Task CustomPropagatorCalled(bool sample, bool createParentActivity) Assert.Equal(contextFromPropagator, exportedItems[0].Context); } - Sdk.SetDefaultTextMapPropagator(previousDefaultTextMapPropagator); +#if NETFRAMEWORK + if (!sample && createParentActivity) + { + Assert.Equal(parentContext.TraceId, contextFromPropagator.TraceId); + Assert.Equal(parentContext.SpanId, contextFromPropagator.SpanId); + } +#endif } public void Dispose() @@ -667,4 +638,3 @@ public void Dispose() } } } -#endif diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs index 047deb4d496..3798b48328f 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs @@ -13,10 +13,9 @@ // See the License for the specific language governing permissions and // limitations under the License. // -#if !NETFRAMEWORK + using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Net.Http; @@ -39,6 +38,8 @@ public partial class HttpClientTests [MemberData(nameof(TestData))] public async Task HttpOutCallsAreCollectedSuccessfullyAsync(HttpTestData.HttpOutTestCase tc) { + bool enrichWithHttpWebRequestCalled = false; + bool enrichWithHttpWebResponseCalled = false; bool enrichWithHttpRequestMessageCalled = false; bool enrichWithHttpResponseMessageCalled = false; bool enrichWithExceptionCalled = false; @@ -65,15 +66,17 @@ public async Task HttpOutCallsAreCollectedSuccessfullyAsync(HttpTestData.HttpOut using (serverLifeTime) using (Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation((opt) => - { - opt.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => { enrichWithHttpRequestMessageCalled = true; }; - opt.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; - opt.EnrichWithException = (activity, exception) => { enrichWithExceptionCalled = true; }; - opt.RecordException = tc.RecordException ?? false; - }) - .AddProcessor(processor.Object) - .Build()) + .AddHttpClientInstrumentation((opt) => + { + opt.EnrichWithHttpWebRequest = (activity, httpRequestMessage) => { enrichWithHttpWebRequestCalled = true; }; + opt.EnrichWithHttpWebResponse = (activity, httpResponseMessage) => { enrichWithHttpWebResponseCalled = true; }; + opt.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => { enrichWithHttpRequestMessageCalled = true; }; + opt.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; + opt.EnrichWithException = (activity, exception) => { enrichWithExceptionCalled = true; }; + opt.RecordException = tc.RecordException ?? false; + }) + .AddProcessor(processor.Object) + .Build()) { try { @@ -82,7 +85,11 @@ public async Task HttpOutCallsAreCollectedSuccessfullyAsync(HttpTestData.HttpOut { RequestUri = new Uri(tc.Url), Method = new HttpMethod(tc.Method), +#if NETFRAMEWORK + Version = new Version(1, 1), +#else Version = new Version(2, 0), +#endif }; if (tc.Headers != null) @@ -113,11 +120,23 @@ public async Task HttpOutCallsAreCollectedSuccessfullyAsync(HttpTestData.HttpOut Assert.Equal(ActivityKind.Client, activity.Kind); Assert.Equal(tc.SpanName, activity.DisplayName); +#if NETFRAMEWORK + Assert.True(enrichWithHttpWebRequestCalled); + Assert.False(enrichWithHttpRequestMessageCalled); + if (tc.ResponseExpected) + { + Assert.True(enrichWithHttpWebResponseCalled); + Assert.False(enrichWithHttpResponseMessageCalled); + } +#else + Assert.False(enrichWithHttpWebRequestCalled); Assert.True(enrichWithHttpRequestMessageCalled); if (tc.ResponseExpected) { + Assert.False(enrichWithHttpWebResponseCalled); Assert.True(enrichWithHttpResponseMessageCalled); } +#endif // Assert.Equal(tc.SpanStatus, d[span.Status.CanonicalCode]); Assert.Equal(tc.SpanStatus, activity.Status.ToString()); @@ -128,7 +147,7 @@ public async Task HttpOutCallsAreCollectedSuccessfullyAsync(HttpTestData.HttpOut Assert.Equal(tc.SpanStatusHasDescription.Value, !string.IsNullOrEmpty(desc)); } - var normalizedAttributes = activity.TagObjects.Where(kv => !kv.Key.StartsWith("otel.")).ToImmutableSortedDictionary(x => x.Key, x => x.Value.ToString()); + var normalizedAttributes = activity.TagObjects.Where(kv => !kv.Key.StartsWith("otel.")).ToDictionary(x => x.Key, x => x.Value.ToString()); var normalizedAttributesTestCase = tc.SpanAttributes.ToDictionary(x => x.Key, x => HttpTestData.NormalizeValues(x.Value, host, port)); Assert.Equal(normalizedAttributesTestCase.Count, normalizedAttributes.Count); @@ -146,6 +165,9 @@ public async Task HttpOutCallsAreCollectedSuccessfullyAsync(HttpTestData.HttpOut if (tc.ResponseExpected) { +#if NETFRAMEWORK + Assert.Empty(requestMetrics); +#else Assert.Single(requestMetrics); var metric = requestMetrics[0]; @@ -183,6 +205,7 @@ public async Task HttpOutCallsAreCollectedSuccessfullyAsync(HttpTestData.HttpOut Assert.Contains(statusCode, attributes); Assert.Contains(flavor, attributes); Assert.Equal(4, attributes.Length); +#endif } else { @@ -210,7 +233,7 @@ public async Task DebugIndividualTestAsync() ""http.method"": ""GET"", ""http.host"": ""{host}:{port}"", ""http.status_code"": ""399"", - ""http.flavor"": ""2.0"", + ""http.flavor"": ""{flavor}"", ""http.url"": ""http://{host}:{port}/"" } } @@ -231,6 +254,9 @@ public async Task CheckEnrichmentWhenSampling() private static async Task CheckEnrichment(Sampler sampler, bool enrichExpected, string url) { + bool enrichWithHttpWebRequestCalled = false; + bool enrichWithHttpWebResponseCalled = false; + bool enrichWithHttpRequestMessageCalled = false; bool enrichWithHttpResponseMessageCalled = false; @@ -239,6 +265,9 @@ private static async Task CheckEnrichment(Sampler sampler, bool enrichExpected, .SetSampler(sampler) .AddHttpClientInstrumentation(options => { + options.EnrichWithHttpWebRequest = (activity, httpRequestMessage) => { enrichWithHttpWebRequestCalled = true; }; + options.EnrichWithHttpWebResponse = (activity, httpResponseMessage) => { enrichWithHttpWebResponseCalled = true; }; + options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => { enrichWithHttpRequestMessageCalled = true; }; options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; }) @@ -251,15 +280,28 @@ private static async Task CheckEnrichment(Sampler sampler, bool enrichExpected, if (enrichExpected) { +#if NETFRAMEWORK + Assert.True(enrichWithHttpWebRequestCalled); + Assert.True(enrichWithHttpWebResponseCalled); + + Assert.False(enrichWithHttpRequestMessageCalled); + Assert.False(enrichWithHttpResponseMessageCalled); +#else + Assert.False(enrichWithHttpWebRequestCalled); + Assert.False(enrichWithHttpWebResponseCalled); + Assert.True(enrichWithHttpRequestMessageCalled); Assert.True(enrichWithHttpResponseMessageCalled); +#endif } else { + Assert.False(enrichWithHttpWebRequestCalled); + Assert.False(enrichWithHttpWebResponseCalled); + Assert.False(enrichWithHttpRequestMessageCalled); Assert.False(enrichWithHttpResponseMessageCalled); } } } } -#endif diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpTestData.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpTestData.cs index aa8efe91fbe..cdee4f50d38 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpTestData.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpTestData.cs @@ -13,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // + using System.Collections.Generic; using System.Reflection; using System.Text.Json; @@ -46,7 +47,14 @@ public static IEnumerable GetArgumentsFromTestCaseObject(IEnumerable + #if NETFRAMEWORK using System; using System.Collections.Concurrent; @@ -41,7 +42,7 @@ public class HttpWebRequestActivitySourceTests : IDisposable static HttpWebRequestActivitySourceTests() { - HttpWebRequestInstrumentationOptions options = new HttpWebRequestInstrumentationOptions + HttpClientInstrumentationOptions options = new() { EnrichWithHttpWebRequest = (activity, httpWebRequest) => { diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.cs new file mode 100644 index 00000000000..8811f395daf --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.cs @@ -0,0 +1,406 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +#if !NETFRAMEWORK +using System.Net.Http; +#endif +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Instrumentation.Http.Implementation; +using OpenTelemetry.Tests; +using OpenTelemetry.Trace; +using Xunit; + +#pragma warning disable SYSLIB0014 // Type or member is obsolete + +namespace OpenTelemetry.Instrumentation.Http.Tests +{ + public partial class HttpWebRequestTests : IDisposable + { + private readonly IDisposable serverLifeTime; + private readonly string url; + + public HttpWebRequestTests() + { + Assert.Null(Activity.Current); + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = false; + + this.serverLifeTime = TestHttpServer.RunServer( + (ctx) => + { + if (string.IsNullOrWhiteSpace(ctx.Request.Headers["traceparent"]) + && string.IsNullOrWhiteSpace(ctx.Request.Headers["custom_traceparent"]) + && ctx.Request.QueryString["bypassHeaderCheck"] != "true") + { + ctx.Response.StatusCode = 500; + ctx.Response.StatusDescription = "Missing trace context"; + } + else if (ctx.Request.Url.PathAndQuery.Contains("500")) + { + ctx.Response.StatusCode = 500; + } + else + { + ctx.Response.StatusCode = 200; + } + + ctx.Response.OutputStream.Close(); + }, + out var host, + out var port); + + this.url = $"http://{host}:{port}/"; + } + + public void Dispose() + { + this.serverLifeTime?.Dispose(); + } + + [Fact] + public async Task BacksOffIfAlreadyInstrumented() + { + var activityProcessor = new Mock>(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddProcessor(activityProcessor.Object) + .AddHttpClientInstrumentation() + .Build(); + + var request = (HttpWebRequest)WebRequest.Create(this.url); + + request.Method = "GET"; + + request.Headers.Add("traceparent", "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01"); + + using var response = await request.GetResponseAsync(); + +#if NETFRAMEWORK + // Note: Back-off is part of the .NET Framework reflection only and + // is needed to prevent issues when the same request is re-used for + // things like redirects or SSL negotiation. + Assert.Equal(1, activityProcessor.Invocations.Count); // SetParentProvider called +#else + Assert.Equal(3, activityProcessor.Invocations.Count); // SetParentProvider/Begin/End called +#endif + } + + [Fact] + public async Task RequestNotCollectedWhenInstrumentationFilterApplied() + { + bool httpWebRequestFilterApplied = false; + bool httpRequestMessageFilterApplied = false; + + List exportedItems = new(); + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedItems) + .AddHttpClientInstrumentation( + options => + { + options.FilterHttpWebRequest = (req) => + { + httpWebRequestFilterApplied = true; + return !req.RequestUri.OriginalString.Contains(this.url); + }; + options.FilterHttpRequestMessage = (req) => + { + httpRequestMessageFilterApplied = true; + return !req.RequestUri.OriginalString.Contains(this.url); + }; + }) + .Build(); + + var request = (HttpWebRequest)WebRequest.Create($"{this.url}?bypassHeaderCheck=true"); + + request.Method = "GET"; + + using var response = await request.GetResponseAsync(); + +#if NETFRAMEWORK + Assert.True(httpWebRequestFilterApplied); + Assert.False(httpRequestMessageFilterApplied); +#else + Assert.False(httpWebRequestFilterApplied); + Assert.True(httpRequestMessageFilterApplied); +#endif + + Assert.Empty(exportedItems); + } + + [Fact] + public async Task RequestNotCollectedWhenInstrumentationFilterThrowsException() + { + List exportedItems = new(); + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedItems) + .AddHttpClientInstrumentation( + c => + { + c.FilterHttpWebRequest = (req) => throw new Exception("From Instrumentation filter"); + c.FilterHttpRequestMessage = (req) => throw new Exception("From Instrumentation filter"); + }) + .Build(); + + using (var inMemoryEventListener = new InMemoryEventListener(HttpInstrumentationEventSource.Log)) + { + var request = (HttpWebRequest)WebRequest.Create($"{this.url}?bypassHeaderCheck=true"); + + request.Method = "GET"; + + using var response = await request.GetResponseAsync(); + + Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 4)); + } + + Assert.Empty(exportedItems); + } + + [Fact] + public async Task InjectsHeadersAsync() + { + var activityProcessor = new Mock>(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddProcessor(activityProcessor.Object) + .AddHttpClientInstrumentation() + .Build(); + + var request = (HttpWebRequest)WebRequest.Create(this.url); + + request.Method = "GET"; + + var parent = new Activity("parent") + .SetIdFormat(ActivityIdFormat.W3C) + .Start(); + parent.TraceStateString = "k1=v1,k2=v2"; + parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; + + using var response = await request.GetResponseAsync(); + + Assert.Equal(3, activityProcessor.Invocations.Count); // SetParentProvider/Begin/End called + var activity = (Activity)activityProcessor.Invocations[2].Arguments[0]; + + Assert.Equal(parent.TraceId, activity.Context.TraceId); + Assert.Equal(parent.SpanId, activity.ParentSpanId); + Assert.NotEqual(parent.SpanId, activity.Context.SpanId); + Assert.NotEqual(default, activity.Context.SpanId); + +#if NETFRAMEWORK + string traceparent = request.Headers.Get("traceparent"); + string tracestate = request.Headers.Get("tracestate"); + + Assert.Equal($"00-{activity.Context.TraceId}-{activity.Context.SpanId}-01", traceparent); + Assert.Equal("k1=v1,k2=v2", tracestate); +#else + // Note: On .NET HttpRequestMessage is created and enriched + // not the HttpWebRequest that was executed. + Assert.Empty(request.Headers); +#endif + + parent.Stop(); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public async Task CustomPropagatorCalled(bool sample, bool createParentActivity) + { + ActivityContext parentContext = default; + ActivityContext contextFromPropagator = default; + + var propagator = new Mock(); +#if NETFRAMEWORK + propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) + .Callback>((context, carrier, setter) => + { + contextFromPropagator = context.ActivityContext; + + setter(carrier, "traceparent", $"00/{contextFromPropagator.TraceId}/{contextFromPropagator.SpanId}/01"); + setter(carrier, "tracestate", contextFromPropagator.TraceState); + }); +#else + propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) + .Callback>((context, carrier, setter) => + { + contextFromPropagator = context.ActivityContext; + + setter(carrier, "traceparent", $"00/{contextFromPropagator.TraceId}/{contextFromPropagator.SpanId}/01"); + setter(carrier, "tracestate", contextFromPropagator.TraceState); + }); +#endif + + var exportedItems = new List(); + + using (var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation() + .AddInMemoryExporter(exportedItems) + .SetSampler(sample ? new ParentBasedSampler(new AlwaysOnSampler()) : new AlwaysOffSampler()) + .Build()) + { + var previousDefaultTextMapPropagator = Propagators.DefaultTextMapPropagator; + Sdk.SetDefaultTextMapPropagator(propagator.Object); + + Activity parent = null; + if (createParentActivity) + { + parent = new Activity("parent") + .SetIdFormat(ActivityIdFormat.W3C) + .Start(); + + parent.TraceStateString = "k1=v1,k2=v2"; + parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; + + parentContext = parent.Context; + } + + var request = (HttpWebRequest)WebRequest.Create(this.url); + + request.Method = "GET"; + + using var response = await request.GetResponseAsync(); + + parent?.Stop(); + + Sdk.SetDefaultTextMapPropagator(previousDefaultTextMapPropagator); + } + + if (!sample) + { + Assert.Empty(exportedItems); + } + else + { + Assert.Single(exportedItems); + } + + Assert.True(contextFromPropagator != default); + if (sample) + { + Assert.Equal(contextFromPropagator, exportedItems[0].Context); + } + +#if NETFRAMEWORK + if (!sample && createParentActivity) + { + Assert.Equal(parentContext.TraceId, contextFromPropagator.TraceId); + Assert.Equal(parentContext.SpanId, contextFromPropagator.SpanId); + } +#endif + } + + [Theory] + [InlineData(null)] + [InlineData("CustomName")] + public void AddHttpClientInstrumentationUsesOptionsApi(string name) + { + name ??= Options.DefaultName; + + int configurationDelegateInvocations = 0; + + var activityProcessor = new Mock>(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => + { + services.Configure(name, o => configurationDelegateInvocations++); + }) + .AddProcessor(activityProcessor.Object) + .AddHttpClientInstrumentation(name, options => + { + Assert.IsType(options); + }) + .Build(); + + Assert.Equal(1, configurationDelegateInvocations); + } + + [Fact] + public async Task ReportsExceptionEventForNetworkFailures() + { + var exportedItems = new List(); + bool exceptionThrown = false; + + using var traceprovider = Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation(o => o.RecordException = true) + .AddInMemoryExporter(exportedItems) + .Build(); + + try + { + var request = (HttpWebRequest)WebRequest.Create("https://sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com/"); + + request.Method = "GET"; + + using var response = await request.GetResponseAsync(); + } + catch + { + exceptionThrown = true; + } + + // Exception is thrown and collected as event + Assert.True(exceptionThrown); + Assert.Single(exportedItems[0].Events.Where(evt => evt.Name.Equals("exception"))); + } + + [Fact] + public async Task ReportsExceptionEventOnErrorResponse() + { + var exportedItems = new List(); + bool exceptionThrown = false; + + using var traceprovider = Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation(o => o.RecordException = true) + .AddInMemoryExporter(exportedItems) + .Build(); + + try + { + var request = (HttpWebRequest)WebRequest.Create($"{this.url}500"); + + request.Method = "GET"; + + using var response = await request.GetResponseAsync(); + } + catch + { + exceptionThrown = true; + } + +#if NETFRAMEWORK + // Exception is thrown and collected as event + Assert.True(exceptionThrown); + Assert.Single(exportedItems[0].Events.Where(evt => evt.Name.Equals("exception"))); +#else + // Note: On .NET Core exceptions through HttpWebRequest do not + // trigger exception events they just throw: + // https://github.com/dotnet/runtime/blob/cc5ba0994d6e8a6f5e4a63d1c921a68eda4350e8/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs#L1371 + Assert.True(exceptionThrown); + Assert.DoesNotContain(exportedItems[0].Events, evt => evt.Name.Equals("exception")); +#endif + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.netfx.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.netfx.cs deleted file mode 100644 index 36b838087ff..00000000000 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.netfx.cs +++ /dev/null @@ -1,451 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -#if NETFRAMEWORK -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Moq; -using OpenTelemetry.Context.Propagation; -using OpenTelemetry.Instrumentation.Http.Implementation; -using OpenTelemetry.Tests; -using OpenTelemetry.Trace; -using Xunit; - -namespace OpenTelemetry.Instrumentation.Http.Tests -{ - public partial class HttpWebRequestTests : IDisposable - { - private readonly IDisposable serverLifeTime; - private readonly string url; - - public HttpWebRequestTests() - { - Assert.Null(Activity.Current); - Activity.DefaultIdFormat = ActivityIdFormat.W3C; - Activity.ForceDefaultIdFormat = false; - - this.serverLifeTime = TestHttpServer.RunServer( - (ctx) => - { - if (ctx.Request.Url.PathAndQuery.Contains("500")) - { - ctx.Response.StatusCode = 500; - } - else - { - ctx.Response.StatusCode = 200; - } - - ctx.Response.OutputStream.Close(); - }, - out var host, - out var port); - - this.url = $"http://{host}:{port}/"; - } - - public void Dispose() - { - this.serverLifeTime?.Dispose(); - } - - [Fact] - public async Task HttpWebRequestInstrumentationInjectsHeadersAsync() - { - var activityProcessor = new Mock>(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(activityProcessor.Object) - .AddHttpClientInstrumentation() - .Build(); - - var request = (HttpWebRequest)WebRequest.Create(this.url); - - request.Method = "GET"; - - var parent = new Activity("parent") - .SetIdFormat(ActivityIdFormat.W3C) - .Start(); - parent.TraceStateString = "k1=v1,k2=v2"; - parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; - - using var response = await request.GetResponseAsync(); - - Assert.Equal(3, activityProcessor.Invocations.Count); // SetParentProvider/Begin/End called - var activity = (Activity)activityProcessor.Invocations[2].Arguments[0]; - - Assert.Equal(parent.TraceId, activity.Context.TraceId); - Assert.Equal(parent.SpanId, activity.ParentSpanId); - Assert.NotEqual(parent.SpanId, activity.Context.SpanId); - Assert.NotEqual(default, activity.Context.SpanId); - - string traceparent = request.Headers.Get("traceparent"); - string tracestate = request.Headers.Get("tracestate"); - - Assert.Equal($"00-{activity.Context.TraceId}-{activity.Context.SpanId}-01", traceparent); - Assert.Equal("k1=v1,k2=v2", tracestate); - - parent.Stop(); - } - - [Fact] - public async Task HttpWebRequestInstrumentationInjectsHeadersAsyncWhenActivityIsNotRecorded() - { - ActivityContext contentFromPropagator = default; - var activityProcessor = new Mock>(); - var propagator = new Mock(); - propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((context, message, action) => - { - contentFromPropagator = context.ActivityContext; - }); - - Sdk.SetDefaultTextMapPropagator(propagator.Object); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(activityProcessor.Object) - .AddHttpClientInstrumentation() - .Build(); - - var request = (HttpWebRequest)WebRequest.Create(this.url); - - request.Method = "GET"; - - var parent = new Activity("parent") - .SetIdFormat(ActivityIdFormat.W3C) - .Start(); - parent.TraceStateString = "k1=v1,k2=v2"; - parent.ActivityTraceFlags = ActivityTraceFlags.None; - - using var response = await request.GetResponseAsync(); - - // By default parentbasedsampler is used. - // In this case, the parent is the manually created parentactivity, which will have TraceFlags as None. - // This causes child to be not created. - Assert.Equal(1, activityProcessor.Invocations.Count); - - Assert.Equal(parent.TraceId, contentFromPropagator.TraceId); - Assert.Equal(parent.SpanId, contentFromPropagator.SpanId); - Assert.NotEqual(default, contentFromPropagator.SpanId); - - parent.Stop(); - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] - { - new TraceContextPropagator(), - new BaggagePropagator(), - })); - } - - [Fact] - public async Task HttpWebRequestInstrumentationInjectsHeadersAsync_CustomFormat() - { - var propagator = new Mock(); - propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((context, message, action) => - { - action(message, "custom_traceparent", $"00/{context.ActivityContext.TraceId}/{context.ActivityContext.SpanId}/01"); - action(message, "custom_tracestate", Activity.Current.TraceStateString); - }); - - var activityProcessor = new Mock>(); - Sdk.SetDefaultTextMapPropagator(propagator.Object); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(activityProcessor.Object) - .AddHttpClientInstrumentation() - .Build(); - - var request = (HttpWebRequest)WebRequest.Create(this.url); - - request.Method = "GET"; - - var parent = new Activity("parent") - .SetIdFormat(ActivityIdFormat.W3C) - .Start(); - parent.TraceStateString = "k1=v1,k2=v2"; - parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; - - using var response = await request.GetResponseAsync(); - - Assert.Equal(3, activityProcessor.Invocations.Count); // SetParentProvider/Begin/End called - - var activity = (Activity)activityProcessor.Invocations[2].Arguments[0]; - - Assert.Equal(parent.TraceId, activity.Context.TraceId); - Assert.Equal(parent.SpanId, activity.ParentSpanId); - Assert.NotEqual(parent.SpanId, activity.Context.SpanId); - Assert.NotEqual(default, activity.Context.SpanId); - - string traceparent = request.Headers.Get("custom_traceparent"); - string tracestate = request.Headers.Get("custom_tracestate"); - - Assert.Equal($"00/{activity.Context.TraceId}/{activity.Context.SpanId}/01", traceparent); - Assert.Equal("k1=v1,k2=v2", tracestate); - - parent.Stop(); - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] - { - new TraceContextPropagator(), - new BaggagePropagator(), - })); - } - - [Fact] - public async Task HttpWebRequestInstrumentationBacksOffIfAlreadyInstrumented() - { - var activityProcessor = new Mock>(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(activityProcessor.Object) - .AddHttpClientInstrumentation() - .Build(); - - var request = new HttpRequestMessage - { - RequestUri = new Uri(this.url), - Method = new HttpMethod("GET"), - }; - - request.Headers.Add("traceparent", "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01"); - - using var c = new HttpClient(); - await c.SendAsync(request); - - Assert.Equal(1, activityProcessor.Invocations.Count); - } - - [Fact] - public async Task RequestNotCollectedWhenInstrumentationFilterApplied() - { - var activityProcessor = new Mock>(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(activityProcessor.Object) - .AddHttpClientInstrumentation( - c => c.Filter = (req) => !req.RequestUri.OriginalString.Contains(this.url)) - .Build(); - - using var c = new HttpClient(); - await c.GetAsync(this.url); - - Assert.Equal(1, activityProcessor.Invocations.Count); - } - - [Fact] - public async Task RequestNotCollectedWhenInstrumentationFilterThrowsException() - { - var activityProcessor = new Mock>(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(activityProcessor.Object) - .AddHttpClientInstrumentation( - c => c.Filter = (req) => throw new Exception("From Instrumentation filter")) - .Build(); - - using var c = new HttpClient(); - using (var inMemoryEventListener = new InMemoryEventListener(HttpInstrumentationEventSource.Log)) - { - await c.GetAsync(this.url); - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 4)); - } - } - - [Theory] - [InlineData(null)] - [InlineData("CustomName")] - public void AddHttpClientInstrumentationUsesHttpWebRequestInstrumentationOptions(string name) - { - name ??= Options.DefaultName; - - int configurationDelegateInvocations = 0; - - var activityProcessor = new Mock>(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => - { - services.Configure(name, o => configurationDelegateInvocations++); - }) - .AddProcessor(activityProcessor.Object) - .AddHttpClientInstrumentation(name, options => - { - Assert.IsType(options); - }) - .Build(); - - Assert.Equal(1, configurationDelegateInvocations); - } - - [Fact] - public async Task HttpWebRequestInstrumentationOnExistingInstance() - { - using HttpClient client = new HttpClient(); - - await client.GetAsync(this.url).ConfigureAwait(false); - - var activityProcessor = new Mock>(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(activityProcessor.Object) - .AddHttpClientInstrumentation() - .Build(); - - await client.GetAsync(this.url).ConfigureAwait(false); - - Assert.Equal(3, activityProcessor.Invocations.Count); // SetParentProvider/Begin/End called - } - - [Fact] - public async Task HttpClientInstrumentationReportsExceptionEventForNetworkFailuresWithGetAsync() - { - var exportedItems = new List(); - bool exceptionThrown = false; - - using var traceprovider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation(o => o.RecordException = true) - .AddInMemoryExporter(exportedItems) - .Build(); - - using var c = new HttpClient(); - try - { - await c.GetAsync("https://sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com/"); - } - catch - { - exceptionThrown = true; - } - - // Exception is thrown and collected as event - Assert.True(exceptionThrown); - Assert.Single(exportedItems[0].Events.Where(evt => evt.Name.Equals("exception"))); - } - - [Fact] - public async Task HttpClientInstrumentationDoesNotReportExceptionEventOnErrorResponseWithGetAsync() - { - var exportedItems = new List(); - bool exceptionThrown = false; - - using var traceprovider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation(o => o.RecordException = true) - .AddInMemoryExporter(exportedItems) - .Build(); - - using var c = new HttpClient(); - try - { - await c.GetAsync($"{this.url}500"); - } - catch - { - exceptionThrown = true; - } - - // Exception is not thrown and not collected as event - Assert.False(exceptionThrown); - Assert.Empty(exportedItems[0].Events); - } - - [Fact] - public async Task HttpClientInstrumentationDoesNotReportExceptionEventOnErrorResponseWithGetStringAsync() - { - var exportedItems = new List(); - bool exceptionThrown = false; - var request = new HttpRequestMessage - { - RequestUri = new Uri($"{this.url}500"), - Method = new HttpMethod("GET"), - }; - - using var traceprovider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation(o => o.RecordException = true) - .AddInMemoryExporter(exportedItems) - .Build(); - - using var c = new HttpClient(); - try - { - await c.GetStringAsync($"{this.url}500"); - } - catch - { - exceptionThrown = true; - } - - // Exception is thrown and not collected as event - Assert.True(exceptionThrown); - Assert.Empty(exportedItems[0].Events); - } - - [Fact] - public async Task HttpWebRequestInstrumentationReportsExceptionEventForNetworkFailures() - { - var exportedItems = new List(); - bool exceptionThrown = false; - - using var traceprovider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation(o => o.RecordException = true) - .AddInMemoryExporter(exportedItems) - .Build(); - - try - { - var request = (HttpWebRequest)WebRequest.Create("https://sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com/"); - - request.Method = "GET"; - - using var response = await request.GetResponseAsync(); - } - catch - { - exceptionThrown = true; - } - - // Exception is thrown and collected as event - Assert.True(exceptionThrown); - Assert.Single(exportedItems[0].Events.Where(evt => evt.Name.Equals("exception"))); - } - - [Fact] - public async Task HttpWebRequestInstrumentationReportsExceptionEventOnErrorResponse() - { - var exportedItems = new List(); - bool exceptionThrown = false; - - using var traceprovider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation(o => o.RecordException = true) - .AddInMemoryExporter(exportedItems) - .Build(); - - try - { - var request = (HttpWebRequest)WebRequest.Create($"{this.url}500"); - - request.Method = "GET"; - - using var response = await request.GetResponseAsync(); - } - catch - { - exceptionThrown = true; - } - - // Exception is thrown and collected as event - Assert.True(exceptionThrown); - Assert.Single(exportedItems[0].Events.Where(evt => evt.Name.Equals("exception"))); - } - } -} -#endif diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.netfx.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.cs similarity index 85% rename from test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.netfx.cs rename to test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.cs index 9768999ab3b..61f11102d7b 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.netfx.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -#if NETFRAMEWORK + using System; using System.Collections.Generic; using System.Diagnostics; @@ -26,6 +26,8 @@ using OpenTelemetry.Trace; using Xunit; +#pragma warning disable SYSLIB0014 // Type or member is obsolete + namespace OpenTelemetry.Instrumentation.Http.Tests { public partial class HttpWebRequestTests @@ -47,6 +49,8 @@ public void HttpOutCallsAreCollectedSuccessfully(HttpTestData.HttpOutTestCase tc bool enrichWithHttpWebRequestCalled = false; bool enrichWithHttpWebResponseCalled = false; + bool enrichWithHttpRequestMessageCalled = false; + bool enrichWithHttpResponseMessageCalled = false; bool enrichWithExceptionCalled = false; var activityProcessor = new Mock>(); @@ -56,8 +60,10 @@ public void HttpOutCallsAreCollectedSuccessfully(HttpTestData.HttpOutTestCase tc { options.EnrichWithHttpWebRequest = (activity, httpWebRequest) => { enrichWithHttpWebRequestCalled = true; }; options.EnrichWithHttpWebResponse = (activity, httpWebResponse) => { enrichWithHttpWebResponseCalled = true; }; + options.EnrichWithHttpRequestMessage = (activity, request) => { enrichWithHttpRequestMessageCalled = true; }; + options.EnrichWithHttpResponseMessage = (activity, response) => { enrichWithHttpResponseMessageCalled = true; }; options.EnrichWithException = (activity, exception) => { enrichWithExceptionCalled = true; }; - options.RecordException = tc.RecordException.HasValue ? tc.RecordException.Value : false; + options.RecordException = tc.RecordException ?? false; }) .Build(); @@ -98,7 +104,7 @@ public void HttpOutCallsAreCollectedSuccessfully(HttpTestData.HttpOutTestCase tc x => x.Key, x => { - if (x.Key == "http.flavor" && x.Value == "2.0") + if (x.Key == "http.flavor") { return "1.1"; } @@ -134,11 +140,23 @@ public void HttpOutCallsAreCollectedSuccessfully(HttpTestData.HttpOutTestCase tc Assert.Equal(value, tagValue); } +#if NETFRAMEWORK Assert.True(enrichWithHttpWebRequestCalled); + Assert.False(enrichWithHttpRequestMessageCalled); if (tc.ResponseExpected) { Assert.True(enrichWithHttpWebResponseCalled); + Assert.False(enrichWithHttpResponseMessageCalled); } +#else + Assert.False(enrichWithHttpWebRequestCalled); + Assert.True(enrichWithHttpRequestMessageCalled); + if (tc.ResponseExpected) + { + Assert.False(enrichWithHttpWebResponseCalled); + Assert.True(enrichWithHttpResponseMessageCalled); + } +#endif if (tc.RecordException.HasValue && tc.RecordException.Value) { @@ -162,9 +180,10 @@ public void DebugIndividualTest() ""spanKind"": ""Client"", ""setHttpFlavor"": true, ""spanAttributes"": { + ""http.scheme"": ""http"", ""http.method"": ""GET"", ""http.host"": ""{host}:{port}"", - ""http.flavor"": ""2.0"", + ""http.flavor"": ""1.1"", ""http.status_code"": ""200"", ""http.url"": ""http://{host}:{port}/"" } @@ -180,4 +199,3 @@ private static void ValidateHttpWebRequestActivity(Activity activityToValidate) } } } -#endif diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/OpenTelemetry.Instrumentation.Http.Tests.csproj b/test/OpenTelemetry.Instrumentation.Http.Tests/OpenTelemetry.Instrumentation.Http.Tests.csproj index c6636eae611..485a3bf6db0 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/OpenTelemetry.Instrumentation.Http.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/OpenTelemetry.Instrumentation.Http.Tests.csproj @@ -7,14 +7,15 @@ - - - - - + + + + + + diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/RetryHandler.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/RetryHandler.cs index 2a2101bad5b..f543f58ee7b 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/RetryHandler.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/RetryHandler.cs @@ -22,7 +22,7 @@ namespace OpenTelemetry.Tests { public class RetryHandler : DelegatingHandler { - private int maxRetries; + private readonly int maxRetries; public RetryHandler(HttpMessageHandler innerHandler, int maxRetries) : base(innerHandler) @@ -37,6 +37,8 @@ protected override async Task SendAsync( HttpResponseMessage response = null; for (int i = 0; i < this.maxRetries; i++) { + response?.Dispose(); + try { response = await base.SendAsync(request, cancellationToken); diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json b/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json index a0b9eb51984..f389d393422 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json @@ -10,7 +10,7 @@ "http.scheme": "http", "http.method": "GET", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "200", "http.url": "http://{host}:{port}/" } @@ -26,7 +26,7 @@ "http.scheme": "http", "http.method": "POST", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "200", "http.url": "http://{host}:{port}/" } @@ -43,7 +43,7 @@ "http.scheme": "http", "http.method": "GET", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "200", "http.url": "http://{host}:{port}/path/to/resource/" } @@ -60,7 +60,7 @@ "http.scheme": "http", "http.method": "GET", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "200", "http.url": "http://{host}:{port}/path/to/resource#fragment" } @@ -77,7 +77,7 @@ "http.scheme": "http", "http.method": "GET", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "200", "http.url": "http://{host}:{port}/path/to/resource#fragment" } @@ -95,7 +95,7 @@ "http.scheme": "https", "http.method": "GET", "http.host": "sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.url": "https://sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com/" } }, @@ -112,7 +112,7 @@ "http.scheme": "https", "http.method": "GET", "http.host": "sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.url": "https://sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com/" } }, @@ -128,7 +128,7 @@ "http.scheme": "http", "http.method": "GET", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "200", "http.url": "http://{host}:{port}/" } @@ -145,7 +145,7 @@ "http.scheme": "http", "http.method": "GET", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "200", "http.url": "http://{host}:{port}/" } @@ -162,7 +162,7 @@ "http.scheme": "http", "http.method": "GET", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "399", "http.url": "http://{host}:{port}/" } @@ -179,7 +179,7 @@ "http.scheme": "http", "http.method": "GET", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "400", "http.url": "http://{host}:{port}/" } @@ -196,7 +196,7 @@ "http.scheme": "http", "http.method": "GET", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "401", "http.url": "http://{host}:{port}/" } @@ -213,7 +213,7 @@ "http.scheme": "http", "http.method": "GET", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "403", "http.url": "http://{host}:{port}/" } @@ -230,7 +230,7 @@ "http.scheme": "http", "http.method": "GET", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "404", "http.url": "http://{host}:{port}/" } @@ -247,7 +247,7 @@ "http.scheme": "http", "http.method": "GET", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "429", "http.url": "http://{host}:{port}/" } @@ -264,7 +264,7 @@ "http.scheme": "http", "http.method": "GET", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "501", "http.url": "http://{host}:{port}/" } @@ -281,7 +281,7 @@ "http.scheme": "http", "http.method": "GET", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "503", "http.url": "http://{host}:{port}/" } @@ -298,7 +298,7 @@ "http.scheme": "http", "http.method": "GET", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "504", "http.url": "http://{host}:{port}/" } @@ -315,7 +315,7 @@ "http.scheme": "http", "http.method": "GET", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "600", "http.url": "http://{host}:{port}/" } @@ -332,7 +332,7 @@ "http.scheme": "http", "http.method": "GET", "http.host": "{host}:{port}", - "http.flavor": "2.0", + "http.flavor": "{flavor}", "http.status_code": "200", "http.url": "http://{host}:{port}/" }