[Instrumentation.Http] Move to contrib repository (#5574)

This commit is contained in:
Piotr Kiełkowicz 2024-04-29 21:02:59 +02:00 committed by GitHub
parent e25d128baf
commit e2a867f192
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 24 additions and 6788 deletions

View File

@ -86,6 +86,7 @@
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="[17.8.0,18.0.0)" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="[1.1.1,2.0)" />
<PackageVersion Include="MinVer" Version="[5.0.0,6.0)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="[1.8.1,2.0)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="[1.5.1,2.0)" />
<PackageVersion Include="RabbitMQ.Client" Version="[6.6.0,7.0)" />
<PackageVersion Include="StyleCop.Analyzers" Version="[1.2.0-beta.556,2.0)" />

View File

@ -130,10 +130,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.AspNetCore", "exam
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "test\Benchmarks\Benchmarks.csproj", "{DE9130A4-F30A-49D7-8834-41DE3021218B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.Http.Tests", "test\OpenTelemetry.Instrumentation.Http.Tests\OpenTelemetry.Instrumentation.Http.Tests.csproj", "{DE9EB7D8-9CC5-4073-90B3-9FBF685B3305}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.Http", "src\OpenTelemetry.Instrumentation.Http\OpenTelemetry.Instrumentation.Http.csproj", "{412C64D1-43D6-4E4C-8AD8-E20E63B415BD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.GrpcNetClient", "src\OpenTelemetry.Instrumentation.GrpcNetClient\OpenTelemetry.Instrumentation.GrpcNetClient.csproj", "{0246BFC4-8AAF-45E1-A127-DB43D6E345BB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{7C87CAF9-79D7-4C26-9FFB-F3F1FB6911F1}"
@ -432,14 +428,6 @@ Global
{DE9130A4-F30A-49D7-8834-41DE3021218B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DE9130A4-F30A-49D7-8834-41DE3021218B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DE9130A4-F30A-49D7-8834-41DE3021218B}.Release|Any CPU.Build.0 = Release|Any CPU
{DE9EB7D8-9CC5-4073-90B3-9FBF685B3305}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DE9EB7D8-9CC5-4073-90B3-9FBF685B3305}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DE9EB7D8-9CC5-4073-90B3-9FBF685B3305}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DE9EB7D8-9CC5-4073-90B3-9FBF685B3305}.Release|Any CPU.Build.0 = Release|Any CPU
{412C64D1-43D6-4E4C-8AD8-E20E63B415BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{412C64D1-43D6-4E4C-8AD8-E20E63B415BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{412C64D1-43D6-4E4C-8AD8-E20E63B415BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{412C64D1-43D6-4E4C-8AD8-E20E63B415BD}.Release|Any CPU.Build.0 = Release|Any CPU
{0246BFC4-8AAF-45E1-A127-DB43D6E345BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0246BFC4-8AAF-45E1-A127-DB43D6E345BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0246BFC4-8AAF-45E1-A127-DB43D6E345BB}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@ -51,8 +51,6 @@ libraries](https://github.com/open-telemetry/opentelemetry-specification/blob/ma
* [ASP.NET Core](./src/OpenTelemetry.Instrumentation.AspNetCore/README.md)
* gRPC client:
[Grpc.Net.Client](./src/OpenTelemetry.Instrumentation.GrpcNetClient/README.md)
* HTTP clients: [System.Net.Http.HttpClient and
System.Net.HttpWebRequest](./src/OpenTelemetry.Instrumentation.Http/README.md)
Here are the [exporter
libraries](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#exporter-library):

View File

@ -45,7 +45,6 @@
* Stable:
* `OpenTelemetry.Instrumentation.AspNetCore` (`Instrumentation.AspNetCore-`)
* `OpenTelemetry.Instrumentation.Http` (`Instrumentation.Http-`)
* Unstable:
* `OpenTelemetry.Instrumentation.GrpcNetClient` (`Instrumentation.GrpcNetClient-`)

View File

@ -164,12 +164,12 @@ expensive, excessive activities can also make trace visualization harder.
Instead of manually creating `Activity`, check if you can leverage
instrumentation libraries, such as [ASP.NET
Core](../../src/OpenTelemetry.Instrumentation.AspNetCore/README.md),
[HttpClient](../../src/OpenTelemetry.Instrumentation.Http/README.md) which will
not only create and populate `Activity` with tags(attributes), but also take
care of propagating/restoring the context across process boundaries. If the
`Activity` produced by the instrumentation library is missing some information
you need, it is generally recommended to enrich the existing Activity with that
information, as opposed to creating a new one.
[HttpClient](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/main/src/OpenTelemetry.Instrumentation.Http/README.md)
which will not only create and populate `Activity` with tags(attributes),
but also take care of propagating/restoring the context across process
boundaries. If the `Activity` produced by the instrumentation library is missing
some information you need, it is generally recommended to enrich the existing
Activity with that information, as opposed to creating a new one.
### Modelling static tags as Resource

View File

@ -113,7 +113,6 @@ the library they instrument, and steps for enabling them.
Core](../../../src/OpenTelemetry.Instrumentation.AspNetCore/README.md)
* [gRPC
client](../../../src/OpenTelemetry.Instrumentation.GrpcNetClient/README.md)
* [HTTP clients](../../../src/OpenTelemetry.Instrumentation.Http/README.md)
More community contributed instrumentations are available in [OpenTelemetry .NET
Contrib](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/main/src).
@ -147,7 +146,7 @@ Writing an instrumentation library typically involves 3 steps.
library](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/main/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs)
listens to in order to trigger code as Sql commands are executed. The [.NET
Framework HttpWebRequest
instrumentation](../../../src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs)
instrumentation](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/main/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs)
patches the runtime code (using reflection) and swaps a static reference that
gets invoked as requests are processed for custom code. Every library will be
different.
@ -227,13 +226,13 @@ activities does not by default runs through the sampler, and will have their
with it.
Some common examples of such libraries include [ASP.NET
Core](../../../src/OpenTelemetry.Instrumentation.AspNetCore/README.md), [HTTP
client .NET Core](../../../src/OpenTelemetry.Instrumentation.Http/README.md) .
Core](../../../src/OpenTelemetry.Instrumentation.AspNetCore/README.md).
Instrumentation libraries for these are already provided in this repo. The
[OpenTelemetry .NET
Contrib](https://github.com/open-telemetry/opentelemetry-dotnet-contrib)
repository also has instrumentations for libraries like
[ElasticSearchClient](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/main/src/OpenTelemetry.Instrumentation.ElasticsearchClient)
and [HTTP client .NET Core](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/main/src/OpenTelemetry.Instrumentation.Http/README.md)
etc. which fall in this category.
If you are writing instrumentation for such library, it is recommended to refer

View File

@ -25,12 +25,12 @@ dotnet run
Add reference to [Console
Exporter](../../../src/OpenTelemetry.Exporter.Console/README.md), [OTLP
Exporter](../../../src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md) and
[HttpClient Instrumentation](../../../src/OpenTelemetry.Instrumentation.Http/README.md):
[HttpClient Instrumentation](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/main/src/OpenTelemetry.Instrumentation.Http/README.md):
```sh
dotnet add package OpenTelemetry.Exporter.Console
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Instrumentation.Http --prerelease
dotnet add package OpenTelemetry.Instrumentation.Http
```
Now copy the code from [Program.cs](./Program.cs).

View File

@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Console\OpenTelemetry.Exporter.Console.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.OpenTelemetryProtocol\OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.Http\OpenTelemetry.Instrumentation.Http.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Net.Http" Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
</ItemGroup>
</Project>

View File

@ -6,6 +6,7 @@
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
</ItemGroup>
@ -14,7 +15,6 @@
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Console\OpenTelemetry.Exporter.Console.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.OpenTelemetryProtocol\OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.AspNetCore\OpenTelemetry.Instrumentation.AspNetCore.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.Http\OpenTelemetry.Instrumentation.Http.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus.AspNetCore\OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Zipkin\OpenTelemetry.Exporter.Zipkin.csproj" />
</ItemGroup>

View File

@ -20,13 +20,13 @@
<PackageReference Include="Grpc.Tools" PrivateAssets="All">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
<Compile Include="$(RepoRoot)\src\Shared\SpanAttributeConstants.cs" Link="Includes\SpanAttributeConstants.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.GrpcNetClient\OpenTelemetry.Instrumentation.GrpcNetClient.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.Http\OpenTelemetry.Instrumentation.Http.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Console\OpenTelemetry.Exporter.Console.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.OpenTelemetryProtocol\OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Shims.OpenTracing\OpenTelemetry.Shims.OpenTracing.csproj" />

View File

@ -153,11 +153,11 @@ required only for the following scenarios:
[Propagators](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/api-propagators.md),
to inject and extract context data. Some of the most common libraries
requiring this include
[HttpClient](../OpenTelemetry.Instrumentation.Http/README.md), [ASP.NET
Core](../OpenTelemetry.Instrumentation.AspNetCore/README.md). This repo
already provides instrumentation for these common libraries. If your library
is not built on top of these, and want to leverage propagators, follow the
[Context propagation](#context-propagation) section.
[HttpClient](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/main/src/OpenTelemetry.Instrumentation.Http/README.md),
[ASP.NET Core](../OpenTelemetry.Instrumentation.AspNetCore/README.md).
This or contrib repository already provides instrumentation for these common
libraries. If your library is not built on top of these, and want to leverage
propagators, follow the [Context propagation](#context-propagation) section.
3. You want to leverage
[Baggage](#baggage-api)

View File

@ -18,7 +18,6 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.Http\HttpRequestMessageContextPropagation.cs" Link="Includes\HttpRequestMessageContextPropagation.cs" />
<Compile Include="$(RepoRoot)\src\Shared\Guard.cs" Link="Includes\Guard.cs" />
<Compile Include="$(RepoRoot)\src\Shared\AssemblyVersionExtensions.cs" Link="Includes\AssemblyVersionExtensions.cs" />
<Compile Include="$(RepoRoot)\src\Shared\Options\*.cs" Link="Includes\Options\%(Filename).cs" />

View File

@ -46,7 +46,7 @@ exporter and adds instrumentation for HttpClient, which requires adding the
packages
[`OpenTelemetry.Exporter.Console`](../OpenTelemetry.Exporter.Console/README.md)
and
[`OpenTelemetry.Instrumentation.Http`](../OpenTelemetry.Instrumentation.Http/README.md)
[`OpenTelemetry.Instrumentation.Http`](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/main/src/OpenTelemetry.Instrumentation.Http/README.md)
to the application. As Grpc.Net.Client uses HttpClient underneath, it is
recommended to enable HttpClient instrumentation as well to ensure proper
context propagation. This would cause an activity being produced for both a gRPC

View File

@ -1,24 +0,0 @@
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithException.get -> System.Action<System.Diagnostics.Activity, System.Exception>
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithException.set -> void
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithHttpRequestMessage.get -> System.Action<System.Diagnostics.Activity, System.Net.Http.HttpRequestMessage>
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithHttpRequestMessage.set -> void
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithHttpResponseMessage.get -> System.Action<System.Diagnostics.Activity, System.Net.Http.HttpResponseMessage>
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithHttpResponseMessage.set -> void
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithHttpWebRequest.get -> System.Action<System.Diagnostics.Activity, System.Net.HttpWebRequest>
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithHttpWebRequest.set -> void
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithHttpWebResponse.get -> System.Action<System.Diagnostics.Activity, System.Net.HttpWebResponse>
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithHttpWebResponse.set -> void
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.FilterHttpRequestMessage.get -> System.Func<System.Net.Http.HttpRequestMessage, bool>
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.FilterHttpRequestMessage.set -> void
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.FilterHttpWebRequest.get -> System.Func<System.Net.HttpWebRequest, bool>
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.FilterHttpWebRequest.set -> void
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.HttpClientTraceInstrumentationOptions() -> void
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.RecordException.get -> bool
OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.RecordException.set -> void
OpenTelemetry.Metrics.HttpClientInstrumentationMeterProviderBuilderExtensions
OpenTelemetry.Trace.HttpClientInstrumentationTracerProviderBuilderExtensions
static OpenTelemetry.Metrics.HttpClientInstrumentationMeterProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder
static OpenTelemetry.Trace.HttpClientInstrumentationTracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder
static OpenTelemetry.Trace.HttpClientInstrumentationTracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action<OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions> configureHttpClientTraceInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder
static OpenTelemetry.Trace.HttpClientInstrumentationTracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action<OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions> configureHttpClientTraceInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder

View File

@ -1,10 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Runtime.CompilerServices;
#if SIGNED
[assembly: InternalsVisibleTo("OpenTelemetry.Instrumentation.Http.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")]
#else
[assembly: InternalsVisibleTo("OpenTelemetry.Instrumentation.Http.Tests")]
#endif

View File

@ -1,544 +0,0 @@
# Changelog
## 1.8.1
Released 2024-Apr-12
* **Breaking Change**: Fixed tracing instrumentation so that by default any
values detected in the query string component of requests are replaced with
the text `Redacted` when building the `url.full` tag. For example,
`?key1=value1&key2=value2` becomes `?key1=Redacted&key2=Redacted`. You can
disable this redaction by setting the environment variable
`OTEL_DOTNET_EXPERIMENTAL_HTTPCLIENT_DISABLE_URL_QUERY_REDACTION` to `true`.
([#5532](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5532))
## 1.8.0
Released 2024-Apr-04
* Fixed an issue for spans when `server.port` attribute was not set with
`server.address` when it has default values (`80` for `HTTP` and
`443` for `HTTPS` protocol).
([#5419](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5419))
* Fixed an issue where the `http.request.method_original` attribute was not set
on activity. Now, when `http.request.method` is set and the original method
is converted to its canonical form (e.g., `Get` is converted to `GET`),
the original value `Get` will be stored in `http.request.method_original`.
The attribute is not set on .NET Framework for non canonical form of `CONNECT`,
`GET`, `HEAD`, `PUT`, and `POST`. HTTP Client is converting these values
to canonical form.
([#5471](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5471))
## 1.7.1
Released 2024-Feb-09
* .NET Framework - fix description for `http.client.request.duration` metric.
([#5234](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5234))
## 1.7.0
Released 2023-Dec-13
## 1.6.0 - First stable release of this library
Released 2023-Dec-13
## 1.6.0-rc.1
Released 2023-Dec-01
* Removed support for `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. The
library will now emit only the
[stable](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0/docs/http)
semantic conventions.
([#5068](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5068))
* Update activity DisplayName as per the specification.
([#5078](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5078))
* Removed reference to `OpenTelemetry` package. This is a **breaking change**
for users relying on
[SuppressDownstreamInstrumentation](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Instrumentation.GrpcNetClient#suppressdownstreaminstrumentation)
option in `OpenTelemetry.Instrumentation.GrpcNetClient`. For details, check
out this
[issue](https://github.com/open-telemetry/opentelemetry-dotnet/issues/5092).
([#5077](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5077))
* **Breaking Change**: Renamed `HttpClientInstrumentationOptions` to
`HttpClientTraceInstrumentationOptions`.
([#5109](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5109))
* **Breaking Change**: Removed `http.user_agent` tag from HttpClient activity.
([#5110](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5110))
* `HttpWebRequest` : Introduced additional values for `error.type` tag on
activity and `http.client.request.duration` metric.
([#5111](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5111))
## 1.6.0-beta.3
Released 2023-Nov-17
* Removed the Activity Status Description that was being set during
exceptions. Activity Status will continue to be reported as `Error`.
This is a **breaking change**. `EnrichWithException` can be leveraged
to restore this behavior.
([#5025](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5025))
* Updated `http.request.method` to match specification guidelines.
* For activity, if the method does not belong to one of the [known
values](https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#:~:text=http.request.method%20has%20the%20following%20list%20of%20well%2Dknown%20values)
then the request method will be set on an additional tag
`http.request.method.original` and `http.request.method` will be set to
`_OTHER`.
* For metrics, if the original method does not belong to one of the [known
values](https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#:~:text=http.request.method%20has%20the%20following%20list%20of%20well%2Dknown%20values)
then `http.request.method` on `http.client.request.duration` metric will be
set to `_OTHER`
`http.request.method` is set on `http.client.request.duration` metric or
activity when `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable is set to
`http` or `http/dup`.
([#5003](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5003))
* An additional attribute `error.type` will be added to activity and
`http.client.request.duration` metric in case of failed requests as per the
[specification](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#common-attributes).
Users moving to `net8.0` or newer frameworks from lower versions will see
difference in values in case of an exception. `net8.0` or newer frameworks add
the ability to further drill down the exceptions to a specific type through
[HttpRequestError](https://learn.microsoft.com/dotnet/api/system.net.http.httprequesterror?view=net-8.0)
enum. For lower versions, the individual types will be rolled in to a single
type. This could be a **breaking change** if alerts are set based on the values.
The attribute will only be added when `OTEL_SEMCONV_STABILITY_OPT_IN`
environment variable is set to `http` or `http/dup`.
([#5005](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5005))
([#5034](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5034))
* Fixed `network.protocol.version` attribute values to match the specification.
([#5006](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5006))
* Set `network.protocol.version` value using the protocol version on the
received response. If the request fails without response, then
`network.protocol.version` attribute will not be set on Activity and
`http.client.request.duration` metric.
([#5043](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5043))
## 1.6.0-beta.2
Released 2023-Oct-26
* Introduced a new metric for `HttpClient`, `http.client.request.duration`
measured in seconds. The OTel SDK (starting with version 1.6.0)
[applies custom histogram buckets](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4820)
for this metric to comply with the
[Semantic Convention for Http Metrics](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-metrics.md).
This new metric is only available for users who opt-in to the new
semantic convention by configuring the `OTEL_SEMCONV_STABILITY_OPT_IN`
environment variable to either `http` (to emit only the new metric) or
`http/dup` (to emit both the new and old metrics).
([#4870](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4870))
* New metric: `http.client.request.duration`
* Unit: `s` (seconds)
* Histogram Buckets: `0, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5,
0.75, 1, 2.5, 5, 7.5, 10`
* Old metric: `http.client.duration`
* Unit: `ms` (milliseconds)
* Histogram Buckets: `0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500,
5000, 7500, 10000`
Note: The older `http.client.duration` metric and
`OTEL_SEMCONV_STABILITY_OPT_IN` environment variable will eventually be
removed after the HTTP semantic conventions are marked stable. At which time
this instrumentation can publish a stable release. Refer to the specification
for more information regarding the new HTTP semantic conventions:
* [http-spans](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-spans.md)
* [http-metrics](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-metrics.md)
* Added support for publishing `http.client.duration` &
`http.client.request.duration` metrics on .NET Framework for `HttpWebRequest`.
([#4870](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4870))
* Following `HttpClient` metrics will now be enabled by default when targeting
`.NET8.0` framework or newer.
* **Meter** : `System.Net.Http`
* `http.client.request.duration`
* `http.client.active_requests`
* `http.client.open_connections`
* `http.client.connection.duration`
* `http.client.request.time_in_queue`
* **Meter** : `System.Net.NameResolution`
* `dns.lookups.duration`
For details about each individual metric check [System.Net metrics
docs
page](https://learn.microsoft.com/dotnet/core/diagnostics/built-in-metrics-system-net).
**NOTES**:
* When targeting `.NET8.0` framework or newer, `http.client.request.duration` metric
will only follow
[v1.22.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-metrics.md#metric-httpclientrequestduration)
semantic conventions specification. Ability to switch behavior to older
conventions using `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable is
not available.
* Users can opt-out of metrics that are not required using
[views](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/customizing-the-sdk#drop-an-instrument).
([#4931](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4931))
* Added `url.scheme` attribute to `http.client.request.duration` metric. The
metric will be emitted when `OTEL_SEMCONV_STABILITY_OPT_IN` environment
variable is set to `http` or `http/dup`.
([#4989](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4989))
* Updated description for `http.client.request.duration` metrics to match spec
definition.
([#4990](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4990))
* `dns.lookups.duration` metric is renamed to `dns.lookup.duration`. This change
impacts only users on `.NET8.0` or newer framework.
([#5049](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5049))
## 1.5.1-beta.1
Released 2023-Jul-20
* The new HTTP and network semantic conventions can be opted in to by setting
the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. This allows for a
transition period for users to experiment with the new semantic conventions
and adapt as necessary. The environment variable supports the following
values:
* `http` - emit the new, frozen (proposed for stable) HTTP and networking
attributes, and stop emitting the old experimental HTTP and networking
attributes that the instrumentation emitted previously.
* `http/dup` - emit both the old and the frozen (proposed for stable) HTTP
and networking attributes, allowing for a more seamless transition.
* The default behavior (in the absence of one of these values) is to continue
emitting the same HTTP and network semantic conventions that were emitted in
`1.5.0-beta.1`.
* Note: this option will eventually be removed after the new HTTP and
network semantic conventions are marked stable. At which time this
instrumentation can receive a stable release, and the old HTTP and
network semantic conventions will no longer be supported. Refer to the
specification for more information regarding the new HTTP and network
semantic conventions for both
[spans](https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md)
and
[metrics](https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-metrics.md).
([#4538](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4538),
[#4639](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4639))
## 1.5.0-beta.1
Released 2023-Jun-05
* Bumped the package version to `1.5.0-beta.1` to keep its major and minor
version in sync with that of the core packages. This would make it more
intuitive for users to figure out what version of core packages would work
with a given version of this package. The pre-release identifier has also been
changed from `rc` to `beta` as we believe this more accurately reflects the
status of this package. We believe the `rc` identifier will be more
appropriate as semantic conventions reach stability.
* Fixed an issue of missing `http.client.duration` metric data in case of
network failures (when response is not available).
([#4098](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4098))
* Improve perf by avoiding boxing of common status codes values.
([#4361](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4361),
[#4363](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4363))
## 1.0.0-rc9.14
Released 2023-Feb-24
* Updated OTel SDK dependency to 1.4.0
## 1.4.0-rc9.13
Released 2023-Feb-10
## 1.0.0-rc9.12
Released 2023-Feb-01
## 1.0.0-rc9.11
Released 2023-Jan-09
## 1.0.0-rc9.10
Released 2022-Dec-12
* Added `net.peer.name` and `net.peer.port` as dimensions on
`http.client.duration` metric.
([#3907](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3907))
* **Breaking change** `http.host` will no longer be populated on activity.
`net.peer.name` and `net.peer.port` attributes will be populated instead.
([#3832](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3832))
## 1.0.0-rc9.9
Released 2022-Nov-07
* Added back `netstandard2.0` target.
([#3787](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3787))
* **Breaking change**: The `Enrich` callback option has been removed. For better
usability, it has been replaced by three separate options: In case of
`HttpClient` the new options are `EnrichWithHttpRequestMessage`,
`EnrichWithHttpResponseMessage` and `EnrichWithException` and in case of
`HttpWebRequest` the new options are `EnrichWithHttpWebRequest`,
`EnrichWithHttpWebResponse` and `EnrichWithException`. Previously, the single
`Enrich` callback required the consumer to detect which event triggered the
callback to be invoked (e.g., request start, response end, or an exception)
and then cast the object received to the appropriate type:
`HttpRequestMessage`, `HttpResponsemessage`, or `Exception` in case of
`HttpClient` and `HttpWebRequest`,`HttpWebResponse` and `Exception` in case of
`HttpWebRequest`. The separate callbacks make it clear what event triggers
them and there is no longer the need to cast the argument to the expected
type.
([#3792](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3792))
* Fixed an issue which prevented custom propagators from being called on .NET 7+
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
* In case of .NET Core, additional spans created during retries will now be
exported.
([[#3729](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3729)])
## 1.0.0-rc9.7
Released 2022-Sep-29
* Dropped `netstandard2.0` target and added `net6.0`. .NET 5 reached EOL
in May 2022 and .NET Core 3.1 reaches EOL in December 2022. End of support
dates for .NET are published
[here](https://dotnet.microsoft.com/download/dotnet).
The instrumentation for HttpClient now requires .NET 6 or later.
This does not affect applications targeting .NET Framework.
([#3664](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3664))
* Added overloads which accept a name to the `TracerProviderBuilder`
`AddHttpClientInstrumentation` extension to allow for more fine-grained
options management
([#3664](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3664),
[#3667](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3667))
## 1.0.0-rc9.6
Released 2022-Aug-18
* Updated to use Activity native support from `System.Diagnostics.DiagnosticSource`
to set activity status.
([#3118](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3118))
([#3555](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3555))
* Changed activity source name from `OpenTelemetry.HttpWebRequest`
to `OpenTelemetry.Instrumentation.Http.HttpWebRequest` for `HttpWebRequest`s
and from `OpenTelemetry.Instrumentation.Http`
to `OpenTelemetry.Instrumentation.Http.HttpClient` for `HttpClient`.
([#3515](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3515))
## 1.0.0-rc9.5
Released 2022-Aug-02
* Added `http.scheme` tag to tracing instrumentation.
([#3464](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3464))
* [Breaking] Removes `SetHttpFlavor` option. "http.flavor" is
now always automatically populated.
To remove this tag, set "http.flavor" to null using `ActivityProcessor`.
([#3380](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3380))
* Fix `Enrich` not getting invoked when SocketException due to HostNotFound
occurs.
([#3407](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3407))
## 1.0.0-rc9.4
Released 2022-Jun-03
## 1.0.0-rc9.3
Released 2022-Apr-15
* Removes .NET Framework 4.6.1. The minimum .NET Framework
version supported is .NET 4.6.2. ([#3190](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3190))
## 1.0.0-rc9.2
Released 2022-Apr-12
## 1.0.0-rc9.1
Released 2022-Mar-30
* Updated `TracerProviderBuilderExtensions.AddHttpClientInstrumentation` to support
`IDeferredTracerProviderBuilder` and `IOptions<HttpClientInstrumentationOptions>`
([#3051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3051))
## 1.0.0-rc10 (broken. use 1.0.0-rc9.1 and newer)
Released 2022-Mar-04
## 1.0.0-rc9
Released 2022-Feb-02
* Fixed an issue with `Filter` and `Enrich` callbacks not firing under certain
conditions when gRPC is used
([#2698](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2698))
## 1.0.0-rc8
Released 2021-Oct-08
* Removes .NET Framework 4.5.2 support. The minimum .NET Framework
version supported is .NET 4.6.1. ([#2138](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2138))
* `HttpClient` instances created before `AddHttpClientInstrumentation` is called
on .NET Framework will now also be instrumented
([#2364](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2364))
## 1.0.0-rc7
Released 2021-Jul-12
## 1.0.0-rc6
Released 2021-Jun-25
## 1.0.0-rc5
Released 2021-Jun-09
## 1.0.0-rc4
Released 2021-Apr-23
* Sanitize `http.url` attribute.
([#1961](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1961))
* Added `RecordException` to HttpClientInstrumentationOptions and
HttpWebRequestInstrumentationOptions which allows Exception to be reported as
ActivityEvent.
* Update `AddHttpClientInstrumentation` extension method for .NET Framework to
use only use `HttpWebRequestInstrumentationOptions`
([#1982](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1982))
## 1.0.0-rc3
Released 2021-Mar-19
* Leverages added AddLegacySource API from OpenTelemetry SDK to trigger Samplers
and ActivityProcessors. Samplers, ActivityProcessor.OnStart will now get the
Activity before any enrichment done by the instrumentation.
([#1836](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1836))
* Performance optimization by leveraging sampling decision and short circuiting
activity enrichment.
([#1894](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1894))
## 1.0.0-rc2
Released 2021-Jan-29
* `otel.status_description` tag will no longer be set to the http status
description/reason phrase for outgoing http spans.
([#1579](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1579))
* Moved the DiagnosticListener filtering logic from HttpClientInstrumentation
ctor to OnStartActivity method of HttpHandlerDiagnosticListener.cs; Updated
the logic of OnStartActivity to inject propagation data into Headers for
filtered out events as well.
([#1707](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1707))
## 1.0.0-rc1.1
Released 2020-Nov-17
* HttpInstrumentation sets ActivitySource to activities created outside
ActivitySource.
([#1515](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1515/))
## 0.8.0-beta.1
Released 2020-Nov-5
* Instrumentation for `HttpWebRequest` no longer store raw objects like
`HttpWebRequest` in Activity.CustomProperty. To enrich activity, use the
Enrich action on the instrumentation.
([#1407](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1407))
* Renamed TextMapPropagator to TraceContextPropagator, CompositePropagator to
CompositeTextMapPropagator. IPropagator is renamed to TextMapPropagator and
changed from interface to abstract class.
([#1427](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1427))
* Propagators.DefaultTextMapPropagator will be used as the default Propagator
([#1427](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1428))
* Removed Propagator from Instrumentation Options. Instrumentation now always
respect the Propagator.DefaultTextMapPropagator.
([#1448](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1448))
## 0.7.0-beta.1
Released 2020-Oct-16
* Instrumentation no longer store raw objects like `HttpRequestMessage` in
Activity.CustomProperty. To enrich activity, use the Enrich action on the
instrumentation.
([#1261](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1261))
* Span Status is populated as per new spec
([#1313](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1313))
## 0.6.0-beta.1
Released 2020-Sep-15
## 0.5.0-beta.2
Released 2020-08-28
* Rename FilterFunc to Filter.
* HttpClient/HttpWebRequest instrumentation will now add the raw Request,
Response, and/or Exception objects to the Activity it creates
([#1099](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1099))
* Changed the default propagation to support W3C Baggage
([#1048](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1048))
* The default ITextFormat is now `CompositePropagator(TraceContextFormat,
BaggageFormat)`. Outgoing Http request will now send Baggage using the [W3C
Baggage](https://github.com/w3c/baggage/blob/master/baggage/HTTP_HEADER_FORMAT.md)
header. Previously Baggage was sent using the `Correlation-Context` header,
which is now outdated.
* Removed `AddHttpInstrumentation` and `AddHttpWebRequestInstrumentation` (.NET
Framework) `TracerProviderBuilderExtensions`. `AddHttpClientInstrumentation`
will now register `HttpClient` instrumentation on .NET Core and `HttpClient` +
`HttpWebRequest` instrumentation on .NET Framework.
* Renamed `ITextPropagator` to `IPropagator`
([#1190](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1190))
## 0.3.0-beta
Released 2020-07-23
* Initial release

View File

@ -1,63 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using OpenTelemetry.Instrumentation.Http.Implementation;
namespace OpenTelemetry.Instrumentation.Http;
/// <summary>
/// HttpClient instrumentation.
/// </summary>
internal sealed class HttpClientInstrumentation : IDisposable
{
private static readonly HashSet<string> ExcludedDiagnosticSourceEventsNet7OrGreater = new()
{
"System.Net.Http.Request",
"System.Net.Http.Response",
"System.Net.Http.HttpRequestOut",
};
private static readonly HashSet<string> ExcludedDiagnosticSourceEvents = new()
{
"System.Net.Http.Request",
"System.Net.Http.Response",
};
private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber;
private readonly Func<string, object, object, bool> isEnabled = (eventName, _, _)
=> !ExcludedDiagnosticSourceEvents.Contains(eventName);
private readonly Func<string, object, object, bool> isEnabledNet7OrGreater = (eventName, _, _)
=> !ExcludedDiagnosticSourceEventsNet7OrGreater.Contains(eventName);
/// <summary>
/// Initializes a new instance of the <see cref="HttpClientInstrumentation"/> class.
/// </summary>
/// <param name="options">Configuration options for HTTP client instrumentation.</param>
public HttpClientInstrumentation(HttpClientTraceInstrumentationOptions options)
{
// For .NET7.0 activity will be created using activitySource.
// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs
// However, in case when activity creation returns null (due to sampling)
// the framework will fall back to creating activity anyways due to active diagnostic source listener
// To prevent this, isEnabled is implemented which will return false always
// so that the sampler's decision is respected.
if (HttpHandlerDiagnosticListener.IsNet7OrGreater)
{
this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(new HttpHandlerDiagnosticListener(options), this.isEnabledNet7OrGreater, HttpInstrumentationEventSource.Log.UnknownErrorProcessingEvent);
}
else
{
this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(new HttpHandlerDiagnosticListener(options), this.isEnabled, HttpInstrumentationEventSource.Log.UnknownErrorProcessingEvent);
}
this.diagnosticSourceSubscriber.Subscribe();
}
/// <inheritdoc/>
public void Dispose()
{
this.diagnosticSourceSubscriber?.Dispose();
}
}

View File

@ -1,49 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
#if !NET8_0_OR_GREATER
#if !NETFRAMEWORK
using OpenTelemetry.Instrumentation.Http;
#endif
using OpenTelemetry.Instrumentation.Http.Implementation;
#endif
using OpenTelemetry.Internal;
namespace OpenTelemetry.Metrics;
/// <summary>
/// Extension methods to simplify registering of HttpClient instrumentation.
/// </summary>
public static class HttpClientInstrumentationMeterProviderBuilderExtensions
{
/// <summary>
/// Enables HttpClient instrumentation.
/// </summary>
/// <param name="builder"><see cref="MeterProviderBuilder"/> being configured.</param>
/// <returns>The instance of <see cref="MeterProviderBuilder"/> to chain the calls.</returns>
public static MeterProviderBuilder AddHttpClientInstrumentation(
this MeterProviderBuilder builder)
{
Guard.ThrowIfNull(builder);
#if NET8_0_OR_GREATER
return builder
.AddMeter("System.Net.Http")
.AddMeter("System.Net.NameResolution");
#else
// Note: Warm-up the status code and method mapping.
_ = TelemetryHelper.BoxedStatusCodes;
_ = RequestMethodHelper.KnownMethods;
#if NETFRAMEWORK
builder.AddMeter(HttpWebRequestActivitySource.MeterName);
#else
builder.AddMeter(HttpHandlerMetricsDiagnosticListener.MeterName);
builder.AddInstrumentation(new HttpClientMetrics());
#endif
return builder;
#endif
}
}

View File

@ -1,106 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenTelemetry.Instrumentation.Http;
using OpenTelemetry.Instrumentation.Http.Implementation;
using OpenTelemetry.Internal;
namespace OpenTelemetry.Trace;
/// <summary>
/// Extension methods to simplify registering of HttpClient instrumentation.
/// </summary>
public static class HttpClientInstrumentationTracerProviderBuilderExtensions
{
/// <summary>
/// Enables HttpClient instrumentation.
/// </summary>
/// <param name="builder"><see cref="TracerProviderBuilder"/> being configured.</param>
/// <returns>The instance of <see cref="TracerProviderBuilder"/> to chain the calls.</returns>
public static TracerProviderBuilder AddHttpClientInstrumentation(this TracerProviderBuilder builder)
=> AddHttpClientInstrumentation(builder, name: null, configureHttpClientTraceInstrumentationOptions: null);
/// <summary>
/// Enables HttpClient instrumentation.
/// </summary>
/// <param name="builder"><see cref="TracerProviderBuilder"/> being configured.</param>
/// <param name="configureHttpClientTraceInstrumentationOptions">Callback action for configuring <see cref="HttpClientTraceInstrumentationOptions"/>.</param>
/// <returns>The instance of <see cref="TracerProviderBuilder"/> to chain the calls.</returns>
public static TracerProviderBuilder AddHttpClientInstrumentation(
this TracerProviderBuilder builder,
Action<HttpClientTraceInstrumentationOptions> configureHttpClientTraceInstrumentationOptions)
=> AddHttpClientInstrumentation(builder, name: null, configureHttpClientTraceInstrumentationOptions);
/// <summary>
/// Enables HttpClient instrumentation.
/// </summary>
/// <param name="builder"><see cref="TracerProviderBuilder"/> being configured.</param>
/// <param name="name">Name which is used when retrieving options.</param>
/// <param name="configureHttpClientTraceInstrumentationOptions">Callback action for configuring <see cref="HttpClientTraceInstrumentationOptions"/>.</param>
/// <returns>The instance of <see cref="TracerProviderBuilder"/> to chain the calls.</returns>
public static TracerProviderBuilder AddHttpClientInstrumentation(
this TracerProviderBuilder builder,
string name,
Action<HttpClientTraceInstrumentationOptions> configureHttpClientTraceInstrumentationOptions)
{
Guard.ThrowIfNull(builder);
// Note: Warm-up the status code and method mapping.
_ = TelemetryHelper.BoxedStatusCodes;
_ = RequestMethodHelper.KnownMethods;
name ??= Options.DefaultName;
builder.ConfigureServices(services =>
{
if (configureHttpClientTraceInstrumentationOptions != null)
{
services.Configure(name, configureHttpClientTraceInstrumentationOptions);
}
services.RegisterOptionsFactory(configuration => new HttpClientTraceInstrumentationOptions(configuration));
});
#if NETFRAMEWORK
builder.AddSource(HttpWebRequestActivitySource.ActivitySourceName);
if (builder is IDeferredTracerProviderBuilder deferredTracerProviderBuilder)
{
deferredTracerProviderBuilder.Configure((sp, builder) =>
{
var options = sp.GetRequiredService<IOptionsMonitor<HttpClientTraceInstrumentationOptions>>().Get(name);
HttpWebRequestActivitySource.TracingOptions = options;
});
}
#else
AddHttpClientInstrumentationSource(builder);
builder.AddInstrumentation(sp =>
{
var options = sp.GetRequiredService<IOptionsMonitor<HttpClientTraceInstrumentationOptions>>().Get(name);
return new HttpClientInstrumentation(options);
});
#endif
return builder;
}
#if !NETFRAMEWORK
internal static void AddHttpClientInstrumentationSource(
this TracerProviderBuilder builder)
{
if (HttpHandlerDiagnosticListener.IsNet7OrGreater)
{
builder.AddSource(HttpHandlerDiagnosticListener.HttpClientActivitySourceName);
}
else
{
builder.AddSource(HttpHandlerDiagnosticListener.ActivitySourceName);
builder.AddLegacySource("System.Net.Http.HttpRequestOut");
}
}
#endif
}

View File

@ -1,41 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using OpenTelemetry.Instrumentation.Http.Implementation;
namespace OpenTelemetry.Instrumentation.Http;
/// <summary>
/// HttpClient instrumentation.
/// </summary>
internal sealed class HttpClientMetrics : IDisposable
{
private static readonly HashSet<string> ExcludedDiagnosticSourceEvents = new()
{
"System.Net.Http.Request",
"System.Net.Http.Response",
};
private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber;
private readonly Func<string, object, object, bool> isEnabled = (activityName, obj1, obj2)
=> !ExcludedDiagnosticSourceEvents.Contains(activityName);
/// <summary>
/// Initializes a new instance of the <see cref="HttpClientMetrics"/> class.
/// </summary>
public HttpClientMetrics()
{
this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(
new HttpHandlerMetricsDiagnosticListener("HttpHandlerDiagnosticListener"),
this.isEnabled,
HttpInstrumentationEventSource.Log.UnknownErrorProcessingEvent);
this.diagnosticSourceSubscriber.Subscribe();
}
/// <inheritdoc/>
public void Dispose()
{
this.diagnosticSourceSubscriber?.Dispose();
}
}

View File

@ -1,195 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics;
using System.Net;
using System.Runtime.CompilerServices;
#if NETFRAMEWORK
using System.Net.Http;
#endif
using Microsoft.Extensions.Configuration;
using OpenTelemetry.Instrumentation.Http.Implementation;
namespace OpenTelemetry.Instrumentation.Http;
/// <summary>
/// Options for HttpClient instrumentation.
/// </summary>
public class HttpClientTraceInstrumentationOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="HttpClientTraceInstrumentationOptions"/> class.
/// </summary>
public HttpClientTraceInstrumentationOptions()
: this(new ConfigurationBuilder().AddEnvironmentVariables().Build())
{
}
internal HttpClientTraceInstrumentationOptions(IConfiguration configuration)
{
Debug.Assert(configuration != null, "configuration was null");
if (configuration.TryGetBoolValue(
HttpInstrumentationEventSource.Log,
"OTEL_DOTNET_EXPERIMENTAL_HTTPCLIENT_DISABLE_URL_QUERY_REDACTION",
out var disableUrlQueryRedaction))
{
this.DisableUrlQueryRedaction = disableUrlQueryRedaction;
}
}
/// <summary>
/// Gets or sets a filter function that determines whether or not to
/// collect telemetry on a per request basis.
/// </summary>
/// <remarks>
/// <para><b>FilterHttpRequestMessage is only executed on .NET and .NET
/// Core runtimes. <see cref="HttpClient"/> and <see
/// cref="HttpWebRequest"/> on .NET and .NET Core are both implemented
/// using <see cref="HttpRequestMessage"/>.</b></para>
/// Notes:
/// <list type="bullet">
/// <item>The return value for the filter function is interpreted as:
/// <list type="bullet">
/// <item>If filter returns <see langword="true" />, the request is
/// collected.</item>
/// <item>If filter returns <see langword="false" /> or throws an
/// exception the request is NOT collected.</item>
/// </list></item>
/// </list>
/// </remarks>
public Func<HttpRequestMessage, bool> FilterHttpRequestMessage { get; set; }
/// <summary>
/// Gets or sets an action to enrich an <see cref="Activity"/> with <see cref="HttpRequestMessage"/>.
/// </summary>
/// <remarks>
/// <para><b>EnrichWithHttpRequestMessage is only executed on .NET and .NET
/// Core runtimes. <see cref="HttpClient"/> and <see
/// cref="HttpWebRequest"/> on .NET and .NET Core are both implemented
/// using <see cref="HttpRequestMessage"/>.</b></para>
/// </remarks>
public Action<Activity, HttpRequestMessage> EnrichWithHttpRequestMessage { get; set; }
/// <summary>
/// Gets or sets an action to enrich an <see cref="Activity"/> with <see cref="HttpResponseMessage"/>.
/// </summary>
/// <remarks>
/// <para><b>EnrichWithHttpResponseMessage is only executed on .NET and .NET
/// Core runtimes. <see cref="HttpClient"/> and <see
/// cref="HttpWebRequest"/> on .NET and .NET Core are both implemented
/// using <see cref="HttpRequestMessage"/>.</b></para>
/// </remarks>
public Action<Activity, HttpResponseMessage> EnrichWithHttpResponseMessage { get; set; }
/// <summary>
/// Gets or sets an action to enrich an <see cref="Activity"/> with <see cref="Exception"/>.
/// </summary>
/// <remarks>
/// <para><b>EnrichWithException is called for all runtimes.</b></para>
/// </remarks>
public Action<Activity, Exception> EnrichWithException { get; set; }
/// <summary>
/// Gets or sets a filter function that determines whether or not to
/// collect telemetry on a per request basis.
/// </summary>
/// <remarks>
/// <para><b>FilterHttpWebRequest is only executed on .NET Framework
/// runtimes. <see cref="HttpClient"/> and <see cref="HttpWebRequest"/>
/// on .NET Framework are both implemented using <see
/// cref="HttpWebRequest"/>.</b></para>
/// Notes:
/// <list type="bullet">
/// <item>The return value for the filter function is interpreted as:
/// <list type="bullet">
/// <item>If filter returns <see langword="true" />, the request is
/// collected.</item>
/// <item>If filter returns <see langword="false" /> or throws an
/// exception the request is NOT collected.</item>
/// </list></item>
/// </list>
/// </remarks>
public Func<HttpWebRequest, bool> FilterHttpWebRequest { get; set; }
/// <summary>
/// Gets or sets an action to enrich an <see cref="Activity"/> with <see cref="HttpWebRequest"/>.
/// </summary>
/// <remarks>
/// <para><b>EnrichWithHttpWebRequest is only executed on .NET Framework
/// runtimes. <see cref="HttpClient"/> and <see cref="HttpWebRequest"/>
/// on .NET Framework are both implemented using <see
/// cref="HttpWebRequest"/>.</b></para>
/// </remarks>
public Action<Activity, HttpWebRequest> EnrichWithHttpWebRequest { get; set; }
/// <summary>
/// Gets or sets an action to enrich an <see cref="Activity"/> with <see cref="HttpWebResponse"/>.
/// </summary>
/// <remarks>
/// <para><b>EnrichWithHttpWebResponse is only executed on .NET Framework
/// runtimes. <see cref="HttpClient"/> and <see cref="HttpWebRequest"/>
/// on .NET Framework are both implemented using <see
/// cref="HttpWebRequest"/>.</b></para>
/// </remarks>
public Action<Activity, HttpWebResponse> EnrichWithHttpWebResponse { get; set; }
/// <summary>
/// Gets or sets a value indicating whether exception will be recorded
/// as an <see cref="ActivityEvent"/> or not. Default value: <see
/// langword="false"/>.
/// </summary>
/// <remarks>
/// <para><b>RecordException is supported on all runtimes.</b></para>
/// <para>For specification details see: <see
/// href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/exceptions/exceptions-spans.md"
/// />.</para>
/// </remarks>
public bool RecordException { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the url query value should be redacted or not.
/// </summary>
/// <remarks>
/// The query parameter values are redacted with value set as Redacted.
/// e.g. `?key1=value1` is set as `?key1=Redacted`.
/// The redaction can be disabled by setting this property to <see langword="true" />.
/// </remarks>
internal bool DisableUrlQueryRedaction { get; set; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool EventFilterHttpRequestMessage(string activityName, object arg1)
{
try
{
return
this.FilterHttpRequestMessage == null ||
!TryParseHttpRequestMessage(activityName, arg1, out HttpRequestMessage 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)
{
HttpInstrumentationEventSource.Log.RequestFilterException(ex);
return false;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryParseHttpRequestMessage(string activityName, object arg1, out HttpRequestMessage requestMessage)
{
return (requestMessage = arg1 as HttpRequestMessage) != null && activityName == "System.Net.Http.HttpRequestOut";
}
}

View File

@ -1,337 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics;
#if NET6_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif
#if NETFRAMEWORK
using System.Net.Http;
#endif
using System.Reflection;
using OpenTelemetry.Context.Propagation;
using OpenTelemetry.Internal;
using OpenTelemetry.Trace;
namespace OpenTelemetry.Instrumentation.Http.Implementation;
internal sealed class HttpHandlerDiagnosticListener : ListenerHandler
{
internal static readonly AssemblyName AssemblyName = typeof(HttpHandlerDiagnosticListener).Assembly.GetName();
internal static readonly bool IsNet7OrGreater;
// https://github.com/dotnet/runtime/blob/7d034ddbbbe1f2f40c264b323b3ed3d6b3d45e9a/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L19
internal static readonly string HttpClientActivitySourceName = "System.Net.Http";
internal static readonly string ActivitySourceName = AssemblyName.Name + ".HttpClient";
internal static readonly Version Version = AssemblyName.Version;
internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version.ToString());
private const string OnStartEvent = "System.Net.Http.HttpRequestOut.Start";
private const string OnStopEvent = "System.Net.Http.HttpRequestOut.Stop";
private const string OnUnhandledExceptionEvent = "System.Net.Http.Exception";
private static readonly PropertyFetcher<HttpRequestMessage> StartRequestFetcher = new("Request");
private static readonly PropertyFetcher<HttpResponseMessage> StopResponseFetcher = new("Response");
private static readonly PropertyFetcher<Exception> StopExceptionFetcher = new("Exception");
private static readonly PropertyFetcher<TaskStatus> StopRequestStatusFetcher = new("RequestTaskStatus");
private readonly HttpClientTraceInstrumentationOptions options;
static HttpHandlerDiagnosticListener()
{
try
{
IsNet7OrGreater = typeof(HttpClient).Assembly.GetName().Version.Major >= 7;
}
catch (Exception)
{
IsNet7OrGreater = false;
}
}
public HttpHandlerDiagnosticListener(HttpClientTraceInstrumentationOptions options)
: base("HttpHandlerDiagnosticListener")
{
this.options = options;
}
public override void OnEventWritten(string name, object payload)
{
switch (name)
{
case OnStartEvent:
{
this.OnStartActivity(Activity.Current, payload);
}
break;
case OnStopEvent:
{
this.OnStopActivity(Activity.Current, payload);
}
break;
case OnUnhandledExceptionEvent:
{
this.OnException(Activity.Current, payload);
}
break;
}
}
public void OnStartActivity(Activity activity, object payload)
{
// The overall flow of what HttpClient library does is as below:
// Activity.Start()
// DiagnosticSource.WriteEvent("Start", payload)
// DiagnosticSource.WriteEvent("Stop", payload)
// Activity.Stop()
// This method is in the WriteEvent("Start", payload) path.
// By this time, samplers have already run and
// activity.IsAllDataRequested populated accordingly.
if (!TryFetchRequest(payload, out HttpRequestMessage request))
{
HttpInstrumentationEventSource.Log.NullPayload(nameof(HttpHandlerDiagnosticListener), nameof(this.OnStartActivity));
return;
}
// Propagate context irrespective of sampling decision
var textMapPropagator = Propagators.DefaultTextMapPropagator;
if (textMapPropagator is not TraceContextPropagator)
{
textMapPropagator.Inject(new PropagationContext(activity.Context, Baggage.Current), request, HttpRequestMessageContextPropagation.HeaderValueSetter);
}
// For .NET7.0 or higher versions, activity is created using activity source.
// However the framework will fallback to creating activity if the sampler's decision is to drop and there is a active diagnostic listener.
// To prevent processing such activities we first check the source name to confirm if it was created using
// activity source or not.
if (IsNet7OrGreater && string.IsNullOrEmpty(activity.Source.Name))
{
activity.IsAllDataRequested = false;
}
// enrich Activity from payload only if sampling decision
// is favorable.
if (activity.IsAllDataRequested)
{
try
{
if (this.options.EventFilterHttpRequestMessage(activity.OperationName, request) == false)
{
HttpInstrumentationEventSource.Log.RequestIsFilteredOut(activity.OperationName);
activity.IsAllDataRequested = false;
activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded;
return;
}
}
catch (Exception ex)
{
HttpInstrumentationEventSource.Log.RequestFilterException(ex);
activity.IsAllDataRequested = false;
activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded;
return;
}
RequestMethodHelper.SetActivityDisplayName(activity, request.Method.Method);
if (!IsNet7OrGreater)
{
ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource);
ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Client);
}
// see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md
RequestMethodHelper.SetHttpMethodTag(activity, request.Method.Method);
activity.SetTag(SemanticConventions.AttributeServerAddress, request.RequestUri.Host);
activity.SetTag(SemanticConventions.AttributeServerPort, request.RequestUri.Port);
activity.SetTag(SemanticConventions.AttributeUrlFull, HttpTagHelper.GetUriTagValueFromRequestUri(request.RequestUri, this.options.DisableUrlQueryRedaction));
try
{
this.options.EnrichWithHttpRequestMessage?.Invoke(activity, request);
}
catch (Exception ex)
{
HttpInstrumentationEventSource.Log.EnrichmentException(ex);
}
}
// The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved.
// see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325
#if NET6_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top-level properties are preserved")]
#endif
static bool TryFetchRequest(object payload, out HttpRequestMessage request)
{
if (!StartRequestFetcher.TryFetch(payload, out request) || request == null)
{
return false;
}
return true;
}
}
public void OnStopActivity(Activity activity, object payload)
{
if (activity.IsAllDataRequested)
{
var requestTaskStatus = GetRequestStatus(payload);
ActivityStatusCode currentStatusCode = activity.Status;
if (requestTaskStatus != TaskStatus.RanToCompletion)
{
if (requestTaskStatus == TaskStatus.Canceled)
{
if (currentStatusCode == ActivityStatusCode.Unset)
{
activity.SetStatus(ActivityStatusCode.Error);
}
}
else if (requestTaskStatus != TaskStatus.Faulted)
{
if (currentStatusCode == ActivityStatusCode.Unset)
{
// Faults are handled in OnException and should already have a span.Status of Error w/ Description.
activity.SetStatus(ActivityStatusCode.Error);
}
}
}
if (TryFetchResponse(payload, out HttpResponseMessage response))
{
if (currentStatusCode == ActivityStatusCode.Unset)
{
activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, (int)response.StatusCode));
}
activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetProtocolVersionString(response.Version));
activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode));
if (activity.Status == ActivityStatusCode.Error)
{
activity.SetTag(SemanticConventions.AttributeErrorType, TelemetryHelper.GetStatusCodeString(response.StatusCode));
}
try
{
this.options.EnrichWithHttpResponseMessage?.Invoke(activity, response);
}
catch (Exception ex)
{
HttpInstrumentationEventSource.Log.EnrichmentException(ex);
}
}
// The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved.
// see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325
#if NET6_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top-level properties are preserved")]
#endif
static TaskStatus GetRequestStatus(object payload)
{
// requestTaskStatus (type is TaskStatus) is a non-nullable enum so we don't need to have a null check here.
// See: https://github.com/dotnet/runtime/blob/79c021d65c280020246d1035b0e87ae36f2d36a9/src/libraries/System.Net.Http/src/HttpDiagnosticsGuide.md?plain=1#L69
_ = StopRequestStatusFetcher.TryFetch(payload, out var requestTaskStatus);
return requestTaskStatus;
}
}
// The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved.
// see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325
#if NET6_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top-level properties are preserved")]
#endif
static bool TryFetchResponse(object payload, out HttpResponseMessage response)
{
if (StopResponseFetcher.TryFetch(payload, out response) && response != null)
{
return true;
}
return false;
}
}
public void OnException(Activity activity, object payload)
{
if (activity.IsAllDataRequested)
{
if (!TryFetchException(payload, out Exception exc))
{
HttpInstrumentationEventSource.Log.NullPayload(nameof(HttpHandlerDiagnosticListener), nameof(this.OnException));
return;
}
activity.SetTag(SemanticConventions.AttributeErrorType, GetErrorType(exc));
if (this.options.RecordException)
{
activity.RecordException(exc);
}
if (exc is HttpRequestException)
{
activity.SetStatus(ActivityStatusCode.Error);
}
try
{
this.options.EnrichWithException?.Invoke(activity, exc);
}
catch (Exception ex)
{
HttpInstrumentationEventSource.Log.EnrichmentException(ex);
}
}
// The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved.
// see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325
#if NET6_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top-level properties are preserved")]
#endif
static bool TryFetchException(object payload, out Exception exc)
{
if (!StopExceptionFetcher.TryFetch(payload, out exc) || exc == null)
{
return false;
}
return true;
}
}
private static string GetErrorType(Exception exc)
{
#if NET8_0_OR_GREATER
// For net8.0 and above exception type can be found using HttpRequestError.
// https://learn.microsoft.com/dotnet/api/system.net.http.httprequesterror?view=net-8.0
if (exc is HttpRequestException httpRequestException)
{
return httpRequestException.HttpRequestError switch
{
HttpRequestError.NameResolutionError => "name_resolution_error",
HttpRequestError.ConnectionError => "connection_error",
HttpRequestError.SecureConnectionError => "secure_connection_error",
HttpRequestError.HttpProtocolError => "http_protocol_error",
HttpRequestError.ExtendedConnectNotSupported => "extended_connect_not_supported",
HttpRequestError.VersionNegotiationError => "version_negotiation_error",
HttpRequestError.UserAuthenticationError => "user_authentication_error",
HttpRequestError.ProxyTunnelError => "proxy_tunnel_error",
HttpRequestError.InvalidResponse => "invalid_response",
HttpRequestError.ResponseEnded => "response_ended",
HttpRequestError.ConfigurationLimitExceeded => "configuration_limit_exceeded",
// Fall back to the exception type name in case of HttpRequestError.Unknown
_ => exc.GetType().FullName,
};
}
#endif
return exc.GetType().FullName;
}
}

View File

@ -1,168 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics;
#if NET6_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif
using System.Diagnostics.Metrics;
#if NETFRAMEWORK
using System.Net.Http;
#endif
using System.Reflection;
using OpenTelemetry.Internal;
using OpenTelemetry.Trace;
namespace OpenTelemetry.Instrumentation.Http.Implementation;
internal sealed class HttpHandlerMetricsDiagnosticListener : ListenerHandler
{
internal const string OnStopEvent = "System.Net.Http.HttpRequestOut.Stop";
internal static readonly AssemblyName AssemblyName = typeof(HttpClientMetrics).Assembly.GetName();
internal static readonly string MeterName = AssemblyName.Name;
internal static readonly string MeterVersion = AssemblyName.Version.ToString();
internal static readonly Meter Meter = new(MeterName, MeterVersion);
private const string OnUnhandledExceptionEvent = "System.Net.Http.Exception";
private static readonly Histogram<double> HttpClientRequestDuration = Meter.CreateHistogram<double>("http.client.request.duration", "s", "Duration of HTTP client requests.");
private static readonly PropertyFetcher<HttpRequestMessage> StopRequestFetcher = new("Request");
private static readonly PropertyFetcher<HttpResponseMessage> StopResponseFetcher = new("Response");
private static readonly PropertyFetcher<Exception> StopExceptionFetcher = new("Exception");
private static readonly PropertyFetcher<HttpRequestMessage> RequestFetcher = new("Request");
#if NET6_0_OR_GREATER
private static readonly HttpRequestOptionsKey<string> HttpRequestOptionsErrorKey = new(SemanticConventions.AttributeErrorType);
#endif
public HttpHandlerMetricsDiagnosticListener(string name)
: base(name)
{
}
public static void OnStopEventWritten(Activity activity, object payload)
{
if (TryFetchRequest(payload, out HttpRequestMessage request))
{
// see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-metrics.md
TagList tags = default;
var httpMethod = RequestMethodHelper.GetNormalizedHttpMethod(request.Method.Method);
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpRequestMethod, httpMethod));
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeServerAddress, request.RequestUri.Host));
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeUrlScheme, request.RequestUri.Scheme));
if (!request.RequestUri.IsDefaultPort)
{
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeServerPort, request.RequestUri.Port));
}
if (TryFetchResponse(payload, out HttpResponseMessage response))
{
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetProtocolVersionString(response.Version)));
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)));
// Set error.type to status code for failed requests
// https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#common-attributes
if (SpanHelper.ResolveSpanStatusForHttpStatusCode(ActivityKind.Client, (int)response.StatusCode) == ActivityStatusCode.Error)
{
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeErrorType, TelemetryHelper.GetStatusCodeString(response.StatusCode)));
}
}
if (response == null)
{
#if !NET6_0_OR_GREATER
request.Properties.TryGetValue(SemanticConventions.AttributeErrorType, out var errorType);
#else
request.Options.TryGetValue(HttpRequestOptionsErrorKey, out var errorType);
#endif
// Set error.type to exception type if response was not received.
// https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#common-attributes
if (errorType != null)
{
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeErrorType, errorType));
}
}
// We are relying here on HttpClient library to set duration before writing the stop event.
// https://github.com/dotnet/runtime/blob/90603686d314147017c8bbe1fa8965776ce607d0/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L178
// TODO: Follow up with .NET team if we can continue to rely on this behavior.
HttpClientRequestDuration.Record(activity.Duration.TotalSeconds, tags);
}
// The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved.
// see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325
#if NET6_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The System.Net.Http library guarantees that top-level properties are preserved")]
#endif
static bool TryFetchRequest(object payload, out HttpRequestMessage request) =>
StopRequestFetcher.TryFetch(payload, out request) && request != null;
// The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved.
// see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325
#if NET6_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The System.Net.Http library guarantees that top-level properties are preserved")]
#endif
static bool TryFetchResponse(object payload, out HttpResponseMessage response) =>
StopResponseFetcher.TryFetch(payload, out response) && response != null;
}
public static void OnExceptionEventWritten(Activity activity, object payload)
{
if (!TryFetchException(payload, out Exception exc) || !TryFetchRequest(payload, out HttpRequestMessage request))
{
HttpInstrumentationEventSource.Log.NullPayload(nameof(HttpHandlerMetricsDiagnosticListener), nameof(OnExceptionEventWritten));
return;
}
#if !NET6_0_OR_GREATER
request.Properties.Add(SemanticConventions.AttributeErrorType, exc.GetType().FullName);
#else
request.Options.Set(HttpRequestOptionsErrorKey, exc.GetType().FullName);
#endif
// The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved.
// see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325
#if NET6_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The System.Net.Http library guarantees that top-level properties are preserved")]
#endif
static bool TryFetchException(object payload, out Exception exc)
{
if (!StopExceptionFetcher.TryFetch(payload, out exc) || exc == null)
{
return false;
}
return true;
}
// The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved.
// see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325
#if NET6_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The System.Net.Http library guarantees that top-level properties are preserved")]
#endif
static bool TryFetchRequest(object payload, out HttpRequestMessage request)
{
if (!RequestFetcher.TryFetch(payload, out request) || request == null)
{
return false;
}
return true;
}
}
public override void OnEventWritten(string name, object payload)
{
if (name == OnStopEvent)
{
OnStopEventWritten(Activity.Current, payload);
}
else if (name == OnUnhandledExceptionEvent)
{
OnExceptionEventWritten(Activity.Current, payload);
}
}
}

View File

@ -1,115 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics.Tracing;
using Microsoft.Extensions.Configuration;
using OpenTelemetry.Internal;
namespace OpenTelemetry.Instrumentation.Http.Implementation;
/// <summary>
/// EventSource events emitted from the project.
/// </summary>
[EventSource(Name = "OpenTelemetry-Instrumentation-Http")]
internal sealed class HttpInstrumentationEventSource : EventSource, IConfigurationExtensionsLogger
{
public static HttpInstrumentationEventSource Log = new();
[NonEvent]
public void FailedProcessResult(Exception ex)
{
if (this.IsEnabled(EventLevel.Error, EventKeywords.All))
{
this.FailedProcessResult(ex.ToInvariantString());
}
}
[NonEvent]
public void RequestFilterException(Exception ex)
{
if (this.IsEnabled(EventLevel.Error, EventKeywords.All))
{
this.RequestFilterException(ex.ToInvariantString());
}
}
[NonEvent]
public void ExceptionInitializingInstrumentation(string instrumentationType, Exception ex)
{
if (this.IsEnabled(EventLevel.Error, EventKeywords.All))
{
this.ExceptionInitializingInstrumentation(instrumentationType, ex.ToInvariantString());
}
}
[NonEvent]
public void UnknownErrorProcessingEvent(string handlerName, string eventName, Exception ex)
{
if (this.IsEnabled(EventLevel.Error, EventKeywords.All))
{
this.UnknownErrorProcessingEvent(handlerName, eventName, ex.ToInvariantString());
}
}
[Event(1, Message = "Failed to process result: '{0}'", Level = EventLevel.Error)]
public void FailedProcessResult(string ex)
{
this.WriteEvent(1, ex);
}
[Event(2, Message = "Error initializing instrumentation type {0}. Exception : {1}", Level = EventLevel.Error)]
public void ExceptionInitializingInstrumentation(string instrumentationType, string ex)
{
this.WriteEvent(2, instrumentationType, ex);
}
[Event(3, Message = "Payload is NULL in event '{1}' from handler '{0}', span will not be recorded.", Level = EventLevel.Warning)]
public void NullPayload(string handlerName, string eventName)
{
this.WriteEvent(3, handlerName, eventName);
}
[Event(4, Message = "Filter threw exception. Request will not be collected. Exception {0}.", Level = EventLevel.Error)]
public void RequestFilterException(string exception)
{
this.WriteEvent(4, exception);
}
[NonEvent]
public void EnrichmentException(Exception ex)
{
if (this.IsEnabled(EventLevel.Error, EventKeywords.All))
{
this.EnrichmentException(ex.ToInvariantString());
}
}
[Event(5, Message = "Enrich threw exception. Exception {0}.", Level = EventLevel.Error)]
public void EnrichmentException(string exception)
{
this.WriteEvent(5, exception);
}
[Event(6, Message = "Request is filtered out.", Level = EventLevel.Verbose)]
public void RequestIsFilteredOut(string eventName)
{
this.WriteEvent(6, eventName);
}
[Event(7, Message = "Unknown error processing event '{1}' from handler '{0}', Exception: {2}", Level = EventLevel.Error)]
public void UnknownErrorProcessingEvent(string handlerName, string eventName, string ex)
{
this.WriteEvent(7, handlerName, eventName, ex);
}
[Event(8, Message = "Configuration key '{0}' has an invalid value: '{1}'", Level = EventLevel.Warning)]
public void InvalidConfigurationValue(string key, string value)
{
this.WriteEvent(8, key, value);
}
void IConfigurationExtensionsLogger.LogInvalidConfigurationValue(string key, string value)
{
this.InvalidConfigurationValue(key, value);
}
}

View File

@ -1,39 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using OpenTelemetry.Internal;
namespace OpenTelemetry.Instrumentation.Http.Implementation;
/// <summary>
/// A collection of helper methods to be used when building Http activities.
/// </summary>
internal static class HttpTagHelper
{
/// <summary>
/// Gets the OpenTelemetry standard uri tag value for a span based on its request <see cref="Uri"/>.
/// </summary>
/// <param name="uri"><see cref="Uri"/>.</param>
/// <param name="disableQueryRedaction">Indicates whether query parameter should be redacted or not.</param>
/// <returns>Span uri value.</returns>
public static string GetUriTagValueFromRequestUri(Uri uri, bool disableQueryRedaction)
{
if (string.IsNullOrEmpty(uri.UserInfo) && disableQueryRedaction)
{
return uri.OriginalString;
}
var query = disableQueryRedaction ? uri.Query : RedactionHelper.GetRedactedQueryString(uri.Query);
return string.Concat(uri.Scheme, Uri.SchemeDelimiter, uri.Authority, uri.AbsolutePath, query, uri.Fragment);
}
public static string GetProtocolVersionString(Version httpVersion) => (httpVersion.Major, httpVersion.Minor) switch
{
(1, 0) => "1.0",
(1, 1) => "1.1",
(2, 0) => "2",
(3, 0) => "3",
_ => httpVersion.ToString(),
};
}

View File

@ -1,42 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Net;
namespace OpenTelemetry.Instrumentation.Http.Implementation;
internal static class TelemetryHelper
{
public static readonly (object, string)[] BoxedStatusCodes;
static TelemetryHelper()
{
BoxedStatusCodes = new (object, string)[500];
for (int i = 0, c = 100; i < BoxedStatusCodes.Length; i++, c++)
{
BoxedStatusCodes[i] = (c, c.ToString());
}
}
public static object GetBoxedStatusCode(HttpStatusCode statusCode)
{
int intStatusCode = (int)statusCode;
if (intStatusCode >= 100 && intStatusCode < 600)
{
return BoxedStatusCodes[intStatusCode - 100].Item1;
}
return statusCode;
}
public static string GetStatusCodeString(HttpStatusCode statusCode)
{
int intStatusCode = (int)statusCode;
if (intStatusCode >= 100 && intStatusCode < 600)
{
return BoxedStatusCodes[intStatusCode - 100].Item2;
}
return statusCode.ToString();
}
}

View File

@ -1,40 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(TargetFrameworksForLibraries)</TargetFrameworks>
<Description>Http instrumentation for OpenTelemetry .NET</Description>
<PackageTags>$(PackageTags);distributed-tracing</PackageTags>
<MinVerTagPrefix>Instrumentation.Http-</MinVerTagPrefix>
<IncludeDiagnosticSourceInstrumentationHelpers>true</IncludeDiagnosticSourceInstrumentationHelpers>
<PackageValidationBaselineVersion>1.8.1</PackageValidationBaselineVersion>
<!-- this is temporary. will remove in future PR. -->
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(RepoRoot)\src\Shared\Configuration\*.cs" Link="Includes\Configuration\%(Filename).cs" />
<Compile Include="$(RepoRoot)\src\Shared\EnvironmentVariables\*.cs" Link="Includes\EnvironmentVariables\%(Filename).cs" />
<Compile Include="$(RepoRoot)\src\Shared\Guard.cs" Link="Includes\Guard.cs" />
<Compile Include="$(RepoRoot)\src\Shared\Shims\NullableAttributes.cs" Link="Includes\Shims\NullableAttributes.cs" />
<Compile Include="$(RepoRoot)\src\Shared\Options\*.cs" Link="Includes\Options\%(Filename).cs" />
<Compile Include="$(RepoRoot)\src\Shared\RedactionHelper.cs" Link="Includes\RedactionHelper.cs" />
<Compile Include="$(RepoRoot)\src\Shared\RequestMethodHelper.cs" Link="Includes\RequestMethodHelper.cs" />
</ItemGroup>
<ItemGroup Condition="'$(RunningDotNetPack)' != 'true'">
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Api.ProviderBuilderExtensions\OpenTelemetry.Api.ProviderBuilderExtensions.csproj" />
</ItemGroup>
<!-- Instrumentation packages when published should take a dependency only on the latest stable version of the API/SDK. -->
<ItemGroup Condition="'$(RunningDotNetPack)' == 'true'">
<PackageReference Include="OpenTelemetry.Api.ProviderBuilderExtensions" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Net.Http" Condition="'$(TargetFramework)' == '$(NetFrameworkMinimumSupportedVersion)'" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.Extensions.Configuration" />
</ItemGroup>
</Project>

View File

@ -1,362 +0,0 @@
# HttpClient and HttpWebRequest instrumentation for OpenTelemetry
[![NuGet](https://img.shields.io/nuget/v/OpenTelemetry.Instrumentation.Http.svg)](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.Http)
[![NuGet](https://img.shields.io/nuget/dt/OpenTelemetry.Instrumentation.Http.svg)](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.Http)
This is an [Instrumentation
Library](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library),
which instruments
[System.Net.Http.HttpClient](https://docs.microsoft.com/dotnet/api/system.net.http.httpclient)
and
[System.Net.HttpWebRequest](https://docs.microsoft.com/dotnet/api/system.net.httpwebrequest)
and collects metrics and traces about outgoing HTTP requests.
This component is based on the
[v1.23](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0/docs/http)
of http semantic conventions. For details on the default set of attributes that
are added, checkout [Traces](#traces) and [Metrics](#metrics) sections below.
## Steps to enable OpenTelemetry.Instrumentation.Http
### Step 1: Install Package
Add a reference to the
[`OpenTelemetry.Instrumentation.Http`](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.Http)
package. Also, add any other instrumentations & exporters you will need.
```shell
dotnet add package OpenTelemetry.Instrumentation.Http
```
### Step 2: Enable HTTP Instrumentation at application startup
HTTP instrumentation must be enabled at application startup.
#### Traces
The following example demonstrates adding `HttpClient` instrumentation with the
extension method `.AddHttpClientInstrumentation()` on `TracerProviderBuilder` to
a console application. This example also sets up the OpenTelemetry Console
Exporter, which requires adding the package
[`OpenTelemetry.Exporter.Console`](../OpenTelemetry.Exporter.Console/README.md)
to the application.
```csharp
using OpenTelemetry;
using OpenTelemetry.Trace;
public class Program
{
public static void Main(string[] args)
{
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation()
.AddConsoleExporter()
.Build();
}
}
```
Following list of attributes are added by default on activity. See
[http-spans](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0/docs/http/http-spans.md)
for more details about each individual attribute:
* `error.type`
* `http.request.method`
* `http.request.method_original`
* `http.response.status_code`
* `network.protocol.version`
* `server.address`
* `server.port`
* `url.full` - By default, the values in the query component of the url are
replaced with the text `Redacted`. For example, `?key1=value1&key2=value2`
becomes `?key1=Redacted&key2=Redacted`. You can disable this redaction by
setting the environment variable
`OTEL_DOTNET_EXPERIMENTAL_HTTPCLIENT_DISABLE_URL_QUERY_REDACTION` to `true`.
[Enrich Api](#enrich-httpclient-api) can be used if any additional attributes are
required on activity.
#### Metrics
The following example demonstrates adding `HttpClient` instrumentation with the
extension method `.AddHttpClientInstrumentation()` on `MeterProviderBuilder` to
a console application. This example also sets up the OpenTelemetry Console
Exporter, which requires adding the package
[`OpenTelemetry.Exporter.Console`](../OpenTelemetry.Exporter.Console/README.md)
to the application.
```csharp
using OpenTelemetry;
using OpenTelemetry.Metrics;
public class Program
{
public static void Main(string[] args)
{
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddHttpClientInstrumentation()
.AddConsoleExporter()
.Build();
}
}
```
Refer to this [example](../../examples/AspNetCore/Program.cs) to see how to
enable this instrumentation in an ASP.NET core application.
Refer to this
[example](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/main/src/OpenTelemetry.Instrumentation.AspNet/README.md)
to see how to enable this instrumentation in an ASP.NET application.
Following list of attributes are added by default on
`http.client.request.duration` metric. See
[http-metrics](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0/docs/http/http-metrics.md)
for more details about each individual attribute. `.NET8.0` and above supports
additional metrics, see [list of metrics produced](#list-of-metrics-produced) for
more details.
* `error.type`
* `http.request.method`
* `http.response.status_code`
* `network.protocol.version`
* `server.address`
* `server.port`
* `url.scheme`
#### List of metrics produced
When the application targets `NETFRAMEWORK`, `.NET6.0` or `.NET7.0`, the
instrumentation emits the following metric:
| Name | Details |
|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|
| `http.client.request.duration` | [Specification](https://github.com/open-telemetry/semantic-conventions/blob/release/v1.23.x/docs/http/http-metrics.md#metric-httpclientrequestduration) |
Starting from `.NET8.0`, metrics instrumentation is natively implemented, and
the HttpClient library has incorporated support for [built-in
metrics](https://learn.microsoft.com/dotnet/core/diagnostics/built-in-metrics-system-net)
following the OpenTelemetry semantic conventions. The library includes additional
metrics beyond those defined in the
[specification](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-metrics.md),
covering additional scenarios for HttpClient users. When the application targets
`.NET8.0` and newer versions, the instrumentation library automatically enables
all `built-in` metrics by default.
Note that the `AddHttpClientInstrumentation()` extension simplifies the process
of enabling all built-in metrics via a single line of code. Alternatively, for
more granular control over emitted metrics, you can utilize the `AddMeter()`
extension on `MeterProviderBuilder` for meters listed in
[built-in-metrics-system-net](https://learn.microsoft.com/dotnet/core/diagnostics/built-in-metrics-system-net).
Using `AddMeter()` for metrics activation eliminates the need to take dependency
on the instrumentation library package and calling
`AddHttpClientInstrumentation()`.
If you utilize `AddHttpClientInstrumentation()` and wish to exclude unnecessary
metrics, you can utilize
[Views](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/customizing-the-sdk#drop-an-instrument)
to achieve this.
> [!NOTE]
> There is no difference in features or emitted metrics when enabling metrics
using `AddMeter()` or `AddHttpClientInstrumentation()` on `.NET8.0` and newer
versions.
<!-- This comment is to make sure the two notes above and below are not merged -->
> [!NOTE]
> The `http.client.request.duration` metric is emitted in `seconds` as per the
semantic convention. While the convention [recommends using custom histogram
buckets](https://github.com/open-telemetry/semantic-conventions/blob/release/v1.23.x/docs/http/http-metrics.md)
, this feature is not yet available via .NET Metrics API. A
[workaround](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4820)
has been included in OTel SDK starting version `1.6.0` which applies recommended
buckets by default for `http.client.request.duration`. This applies to all
targeted frameworks.
## Advanced configuration
### Tracing
This instrumentation can be configured to change the default behavior by using
`HttpClientTraceInstrumentationOptions`. 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.
#### .NET & .NET Core
##### Filter HttpClient API
This instrumentation by default collects all the outgoing HTTP requests. It
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 `FilterHttpRequestMessage` to only
allow GET requests.
```csharp
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation(
// Note: Only called on .NET & .NET Core runtimes.
(options) => options.FilterHttpRequestMessage =
(httpRequestMessage) =>
{
// Example: Only collect telemetry about HTTP GET requests.
return httpRequestMessage.Method.Equals(HttpMethod.Get);
})
.AddConsoleExporter()
.Build();
```
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 `FilterHttpRequestMessage` option does the filtering *after* the Sampler
is invoked.
##### 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.
`HttpClientTraceInstrumentationOptions` 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.
Example:
```csharp
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);
};
})
.Build();
```
#### .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.
`HttpClientTraceInstrumentationOptions` 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.
Example:
```csharp
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);
};
})
.Build();
```
[Processor](../../docs/trace/extending-the-sdk/README.md#processor), is the
general extensibility point to add additional properties to any activity. The
`Enrich` option is specific to this instrumentation, and is provided to get
access to raw request, response, and exception objects.
#### RecordException
This instrumentation automatically sets Activity Status to Error if the Http
StatusCode is >= 400. Additionally, `RecordException` feature may be turned on,
to store the exception to the Activity itself as ActivityEvent.
## Activity duration and http.client.request.duration metric calculation
`Activity.Duration` and `http.client.request.duration` values represents the
time the underlying client handler takes to complete the request. Completing the
request includes the time up to reading response headers from the network
stream. It doesn't include the time spent reading the response body.
## Troubleshooting
This component uses an
[EventSource](https://docs.microsoft.com/dotnet/api/system.diagnostics.tracing.eventsource)
with the name "OpenTelemetry-Instrumentation-Http" for its internal logging.
Please refer to [SDK
troubleshooting](../OpenTelemetry/README.md#troubleshooting) for instructions on
seeing these internal logs.
## References
* [OpenTelemetry Project](https://opentelemetry.io/)

View File

@ -28,7 +28,6 @@
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.InMemory\OpenTelemetry.Exporter.InMemory.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.OpenTelemetryProtocol\OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj" Aliases="OpenTelemetryProtocol" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Zipkin\OpenTelemetry.Exporter.Zipkin.csproj" Aliases="Zipkin" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.Http\OpenTelemetry.Instrumentation.Http.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus.HttpListener\OpenTelemetry.Exporter.Prometheus.HttpListener.csproj" />
</ItemGroup>

View File

@ -1,160 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
#if !NETFRAMEWORK
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
/*
BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1702/22H2/2022Update/SunValley2)
Intel Core i7-8850H CPU 2.60GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET SDK=7.0.302
[Host] : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2
DefaultJob : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2
| Method | EnableInstrumentation | Mean | Error | StdDev | Gen0 | Allocated |
|------------------ |---------------------- |---------:|--------:|--------:|-------:|----------:|
| HttpClientRequest | None | 161.0 us | 1.27 us | 0.99 us | 0.4883 | 2.45 KB |
| HttpClientRequest | Traces | 173.1 us | 1.65 us | 1.54 us | 0.4883 | 4.26 KB |
| HttpClientRequest | Metrics | 165.8 us | 1.33 us | 1.18 us | 0.7324 | 3.66 KB |
| HttpClientRequest | Traces, Metrics | 175.6 us | 1.96 us | 1.83 us | 0.4883 | 4.28 KB |
*/
namespace Benchmarks.Instrumentation;
public class HttpClientInstrumentationBenchmarks
{
private HttpClient httpClient;
private WebApplication app;
private TracerProvider tracerProvider;
private MeterProvider meterProvider;
[Flags]
public enum EnableInstrumentationOption
{
/// <summary>
/// Instrumentation is not enabled for any signal.
/// </summary>
None = 0,
/// <summary>
/// Instrumentation is enbled only for Traces.
/// </summary>
Traces = 1,
/// <summary>
/// Instrumentation is enbled only for Metrics.
/// </summary>
Metrics = 2,
}
[Params(0, 1, 2, 3)]
public EnableInstrumentationOption EnableInstrumentation { get; set; }
[GlobalSetup(Target = nameof(HttpClientRequest))]
public void HttpClientRequestGlobalSetup()
{
if (this.EnableInstrumentation == EnableInstrumentationOption.None)
{
this.StartWebApplication();
this.httpClient = new HttpClient();
}
else if (this.EnableInstrumentation == EnableInstrumentationOption.Traces)
{
this.StartWebApplication();
this.httpClient = new HttpClient();
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation()
.Build();
}
else if (this.EnableInstrumentation == EnableInstrumentationOption.Metrics)
{
this.StartWebApplication();
this.httpClient = new HttpClient();
var exportedItems = new List<Metric>();
this.meterProvider = Sdk.CreateMeterProviderBuilder()
.AddHttpClientInstrumentation()
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000;
})
.Build();
}
else if (this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Traces) &&
this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Metrics))
{
this.StartWebApplication();
this.httpClient = new HttpClient();
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation()
.Build();
var exportedItems = new List<Metric>();
this.meterProvider = Sdk.CreateMeterProviderBuilder()
.AddHttpClientInstrumentation()
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000;
})
.Build();
}
}
[GlobalCleanup(Target = nameof(HttpClientRequest))]
public void HttpClientRequestGlobalCleanup()
{
if (this.EnableInstrumentation == EnableInstrumentationOption.None)
{
this.httpClient.Dispose();
this.app.DisposeAsync().GetAwaiter().GetResult();
}
else if (this.EnableInstrumentation == EnableInstrumentationOption.Traces)
{
this.httpClient.Dispose();
this.app.DisposeAsync().GetAwaiter().GetResult();
this.tracerProvider.Dispose();
}
else if (this.EnableInstrumentation == EnableInstrumentationOption.Metrics)
{
this.httpClient.Dispose();
this.app.DisposeAsync().GetAwaiter().GetResult();
this.meterProvider.Dispose();
}
else if (this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Traces) &&
this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Metrics))
{
this.httpClient.Dispose();
this.app.DisposeAsync().GetAwaiter().GetResult();
this.tracerProvider.Dispose();
this.meterProvider.Dispose();
}
}
[Benchmark]
public async Task HttpClientRequest()
{
var httpResponse = await this.httpClient.GetAsync("http://localhost:5000").ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
}
private void StartWebApplication()
{
var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders();
var app = builder.Build();
app.MapGet("/", async context => await context.Response.WriteAsync($"Hello World!"));
app.RunAsync();
this.app = app;
}
}
#endif

View File

@ -25,7 +25,6 @@
<TrimmerRootAssembly Include="OpenTelemetry.Extensions.Propagators" />
<TrimmerRootAssembly Include="OpenTelemetry.Instrumentation.AspNetCore" />
<TrimmerRootAssembly Include="OpenTelemetry.Instrumentation.GrpcNetClient" />
<TrimmerRootAssembly Include="OpenTelemetry.Instrumentation.Http" />
<TrimmerRootAssembly Include="OpenTelemetry.Shims.OpenTracing" />
<TrimmerRootAssembly Include="OpenTelemetry" />

View File

@ -17,6 +17,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="All">
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
@ -26,7 +27,6 @@
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Zipkin\OpenTelemetry.Exporter.Zipkin.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Extensions.Hosting\OpenTelemetry.Extensions.Hosting.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.Http\OpenTelemetry.Instrumentation.Http.csproj" />
</ItemGroup>
</Project>

View File

@ -17,6 +17,7 @@
<PackageReference Include="Grpc.Net.Client" />
<PackageReference Include="Grpc.Tools" PrivateAssets="All" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="All">
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
@ -30,7 +31,6 @@
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.AspNetCore\OpenTelemetry.Instrumentation.AspNetCore.csproj" Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.GrpcNetClient\OpenTelemetry.Instrumentation.GrpcNetClient.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.Http\OpenTelemetry.Instrumentation.Http.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(RunningDotNetPack)' != 'true'">

View File

@ -1,17 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using OpenTelemetry.Instrumentation.Http.Implementation;
using OpenTelemetry.Tests;
using Xunit;
namespace OpenTelemetry.Instrumentation.Http.Tests;
public class EventSourceTest
{
[Fact]
public void EventSourceTest_HttpInstrumentationEventSource()
{
EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(HttpInstrumentationEventSource.Log);
}
}

View File

@ -1,820 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics;
using Microsoft.Extensions.Configuration;
#if NETFRAMEWORK
using System.Net.Http;
#endif
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Context.Propagation;
using OpenTelemetry.Instrumentation.Http.Implementation;
using OpenTelemetry.Metrics;
using OpenTelemetry.Tests;
using OpenTelemetry.Trace;
using Xunit;
using Xunit.Abstractions;
namespace OpenTelemetry.Instrumentation.Http.Tests;
public partial class HttpClientTests : IDisposable
{
private readonly ITestOutputHelper output;
private readonly IDisposable serverLifeTime;
private readonly string host;
private readonly int port;
private readonly string url;
public HttpClientTests(ITestOutputHelper output)
{
this.output = output;
this.serverLifeTime = TestHttpServer.RunServer(
(ctx) =>
{
string traceparent = ctx.Request.Headers["traceparent"];
string custom_traceparent = ctx.Request.Headers["custom_traceparent"];
if ((ctx.Request.Headers["contextRequired"] == null
|| bool.Parse(ctx.Request.Headers["contextRequired"]))
&&
(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;
}
else if (ctx.Request.Url.PathAndQuery.Contains("redirect"))
{
ctx.Response.RedirectLocation = "/";
ctx.Response.StatusCode = 302;
}
else if (ctx.Request.Headers["responseCode"] != null)
{
ctx.Response.StatusCode = int.Parse(ctx.Request.Headers["responseCode"]);
}
else
{
ctx.Response.StatusCode = 200;
}
ctx.Response.OutputStream.Close();
},
out var host,
out var port);
this.host = host;
this.port = port;
this.url = $"http://{host}:{port}/";
this.output.WriteLine($"HttpServer started: {this.url}");
}
[Fact]
public void AddHttpClientInstrumentation_NamedOptions()
{
int defaultExporterOptionsConfigureOptionsInvocations = 0;
int namedExporterOptionsConfigureOptionsInvocations = 0;
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.ConfigureServices(services =>
{
services.Configure<HttpClientTraceInstrumentationOptions>(o => defaultExporterOptionsConfigureOptionsInvocations++);
services.Configure<HttpClientTraceInstrumentationOptions>("Instrumentation2", o => namedExporterOptionsConfigureOptionsInvocations++);
})
.AddHttpClientInstrumentation()
.AddHttpClientInstrumentation("Instrumentation2", configureHttpClientTraceInstrumentationOptions: null)
.Build();
Assert.Equal(1, defaultExporterOptionsConfigureOptionsInvocations);
Assert.Equal(1, namedExporterOptionsConfigureOptionsInvocations);
}
[Fact]
public void AddHttpClientInstrumentation_BadArgs()
{
TracerProviderBuilder builder = null;
Assert.Throws<ArgumentNullException>(() => builder.AddHttpClientInstrumentation());
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task InjectsHeadersAsync(bool shouldEnrich)
{
var exportedItems = new List<Activity>();
using var request = new HttpRequestMessage
{
RequestUri = new Uri(this.url),
Method = new HttpMethod("GET"),
};
using var parent = new Activity("parent")
.SetIdFormat(ActivityIdFormat.W3C)
.Start();
parent.TraceStateString = "k1=v1,k2=v2";
parent.ActivityTraceFlags = ActivityTraceFlags.Recorded;
using (Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation(o =>
{
if (shouldEnrich)
{
o.EnrichWithHttpWebRequest = (activity, httpWebRequest) =>
{
activity.SetTag("enrichedWithHttpWebRequest", "yes");
};
o.EnrichWithHttpWebResponse = (activity, httpWebResponse) =>
{
activity.SetTag("enrichedWithHttpWebResponse", "yes");
};
o.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) =>
{
activity.SetTag("enrichedWithHttpRequestMessage", "yes");
};
o.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) =>
{
activity.SetTag("enrichedWithHttpResponseMessage", "yes");
};
}
})
.AddInMemoryExporter(exportedItems)
.Build())
{
using var c = new HttpClient();
await c.SendAsync(request);
}
Assert.Single(exportedItems);
var activity = exportedItems[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);
#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);
Assert.Single(tracestates);
Assert.Equal($"00-{activity.Context.TraceId}-{activity.Context.SpanId}-01", traceparents.Single());
Assert.Equal("k1=v1,k2=v2", tracestates.Single());
#endif
#if NETFRAMEWORK
if (shouldEnrich)
{
Assert.Equal("yes", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpWebRequest").FirstOrDefault().Value);
Assert.Equal("yes", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpWebResponse").FirstOrDefault().Value);
}
else
{
Assert.DoesNotContain(activity.Tags, tag => tag.Key == "enrichedWithHttpWebRequest");
Assert.DoesNotContain(activity.Tags, tag => tag.Key == "enrichedWithHttpWebResponse");
}
Assert.DoesNotContain(activity.Tags, tag => tag.Key == "enrichedWithHttpRequestMessage");
Assert.DoesNotContain(activity.Tags, tag => tag.Key == "enrichedWithHttpResponseMessage");
#else
Assert.DoesNotContain(activity.Tags, tag => tag.Key == "enrichedWithHttpWebRequest");
Assert.DoesNotContain(activity.Tags, tag => tag.Key == "enrichedWithHttpWebResponse");
if (shouldEnrich)
{
Assert.Equal("yes", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpRequestMessage").FirstOrDefault().Value);
Assert.Equal("yes", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpResponseMessage").FirstOrDefault().Value);
}
else
{
Assert.DoesNotContain(activity.Tags, tag => tag.Key == "enrichedWithHttpRequestMessage");
Assert.DoesNotContain(activity.Tags, tag => tag.Key == "enrichedWithHttpResponseMessage");
}
#endif
}
[Fact]
public async Task InjectsHeadersAsync_CustomFormat()
{
var propagator = new CustomTextMapPropagator();
propagator.InjectValues.Add("custom_traceParent", context => $"00/{context.ActivityContext.TraceId}/{context.ActivityContext.SpanId}/01");
propagator.InjectValues.Add("custom_traceState", context => Activity.Current.TraceStateString);
var exportedItems = new List<Activity>();
using var request = new HttpRequestMessage
{
RequestUri = new Uri(this.url),
Method = new HttpMethod("GET"),
};
using var parent = new Activity("parent")
.SetIdFormat(ActivityIdFormat.W3C)
.Start();
parent.TraceStateString = "k1=v1,k2=v2";
parent.ActivityTraceFlags = ActivityTraceFlags.Recorded;
Sdk.SetDefaultTextMapPropagator(propagator);
using (Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation()
.AddInMemoryExporter(exportedItems)
.Build())
{
using var c = new HttpClient();
await c.SendAsync(request);
}
Assert.Single(exportedItems);
var activity = exportedItems[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);
#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);
Assert.Single(traceStates);
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(),
}));
}
[Fact(Skip = "https://github.com/open-telemetry/opentelemetry-dotnet/issues/5092")]
public async Task RespectsSuppress()
{
try
{
var propagator = new CustomTextMapPropagator();
propagator.InjectValues.Add("custom_traceParent", context => $"00/{context.ActivityContext.TraceId}/{context.ActivityContext.SpanId}/01");
propagator.InjectValues.Add("custom_traceState", context => Activity.Current.TraceStateString);
var exportedItems = new List<Activity>();
using var request = new HttpRequestMessage
{
RequestUri = new Uri(this.url),
Method = new HttpMethod("GET"),
};
using var parent = new Activity("parent")
.SetIdFormat(ActivityIdFormat.W3C)
.Start();
parent.TraceStateString = "k1=v1,k2=v2";
parent.ActivityTraceFlags = ActivityTraceFlags.Recorded;
Sdk.SetDefaultTextMapPropagator(propagator);
using (Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation()
.AddInMemoryExporter(exportedItems)
.Build())
{
using var c = new HttpClient();
using (SuppressInstrumentationScope.Begin())
{
await c.SendAsync(request);
}
}
// If suppressed, activity is not emitted and
// propagation is also not performed.
Assert.Empty(exportedItems);
Assert.False(request.Headers.Contains("custom_traceParent"));
Assert.False(request.Headers.Contains("custom_traceState"));
}
finally
{
Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[]
{
new TraceContextPropagator(),
new BaggagePropagator(),
}));
}
}
[Fact]
public async Task ExportsSpansCreatedForRetries()
{
var exportedItems = new List<Activity>();
using var request = new HttpRequestMessage
{
RequestUri = new Uri(this.url),
Method = new HttpMethod("GET"),
};
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation()
.AddInMemoryExporter(exportedItems)
.Build();
int maxRetries = 3;
using var clientHandler = new HttpClientHandler();
using var retryHandler = new RetryHandler(clientHandler, maxRetries);
using var httpClient = new HttpClient(retryHandler);
await httpClient.SendAsync(request);
// number of exported spans should be 3(maxRetries)
Assert.Equal(maxRetries, exportedItems.Count);
var spanid1 = exportedItems[0].SpanId;
var spanid2 = exportedItems[1].SpanId;
var spanid3 = exportedItems[2].SpanId;
// Validate span ids are different
Assert.NotEqual(spanid1, spanid2);
Assert.NotEqual(spanid3, spanid1);
Assert.NotEqual(spanid2, spanid3);
}
[Theory]
[InlineData("CONNECT", "CONNECT", null)]
[InlineData("DELETE", "DELETE", null)]
[InlineData("GET", "GET", null)]
[InlineData("PUT", "PUT", null)]
[InlineData("HEAD", "HEAD", null)]
[InlineData("OPTIONS", "OPTIONS", null)]
[InlineData("PATCH", "PATCH", null)]
[InlineData("POST", "POST", null)]
[InlineData("TRACE", "TRACE", null)]
[InlineData("Delete", "DELETE", "Delete")]
#if NETFRAMEWORK
[InlineData("Connect", "CONNECT", null)]// HTTP Client converts Connect to its canonical form (Connect). Expected original method is null.
[InlineData("Get", "GET", null)] // HTTP Client converts Get to its canonical form (GET). Expected original method is null.
[InlineData("Put", "PUT", null)] // HTTP Client converts Put to its canonical form (PUT). Expected original method is null.
[InlineData("Head", "HEAD", null)] // HTTP Client converts Head to its canonical form (HEAD). Expected original method is null.
[InlineData("Post", "POST", null)] // HTTP Client converts Post to its canonical form (POST). Expected original method is null.
#else
[InlineData("Connect", "CONNECT", "Connect")]
[InlineData("Get", "GET", "Get")]
[InlineData("Put", "PUT", "Put")]
[InlineData("Head", "HEAD", "Head")]
[InlineData("Post", "POST", "Post")]
#endif
[InlineData("Options", "OPTIONS", "Options")]
[InlineData("Patch", "PATCH", "Patch")]
[InlineData("Trace", "TRACE", "Trace")]
[InlineData("CUSTOM", "_OTHER", "CUSTOM")]
public async Task HttpRequestMethodIsSetOnActivityAsPerSpec(string originalMethod, string expectedMethod, string expectedOriginalMethod)
{
var exportedItems = new List<Activity>();
using var request = new HttpRequestMessage
{
RequestUri = new Uri(this.url),
Method = new HttpMethod(originalMethod),
};
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation()
.AddInMemoryExporter(exportedItems)
.Build();
using var httpClient = new HttpClient();
try
{
await httpClient.SendAsync(request);
}
catch
{
// ignore error.
}
Assert.Single(exportedItems);
var activity = exportedItems[0];
if (originalMethod.Equals(expectedMethod, StringComparison.OrdinalIgnoreCase))
{
Assert.Equal(expectedMethod, activity.DisplayName);
}
else
{
Assert.Equal("HTTP", activity.DisplayName);
}
Assert.Equal(expectedMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod));
Assert.Equal(expectedOriginalMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethodOriginal));
}
[Theory]
[InlineData("CONNECT", "CONNECT")]
[InlineData("DELETE", "DELETE")]
[InlineData("GET", "GET")]
[InlineData("PUT", "PUT")]
[InlineData("HEAD", "HEAD")]
[InlineData("OPTIONS", "OPTIONS")]
[InlineData("PATCH", "PATCH")]
[InlineData("Get", "GET")]
[InlineData("POST", "POST")]
[InlineData("TRACE", "TRACE")]
[InlineData("Trace", "TRACE")]
[InlineData("CUSTOM", "_OTHER")]
public async Task HttpRequestMethodIsSetonRequestDurationMetricAsPerSpec(string originalMethod, string expectedMethod)
{
var metricItems = new List<Metric>();
using var request = new HttpRequestMessage
{
RequestUri = new Uri(this.url),
Method = new HttpMethod(originalMethod),
};
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddHttpClientInstrumentation()
.AddInMemoryExporter(metricItems)
.Build();
using var httpClient = new HttpClient();
try
{
await httpClient.SendAsync(request);
}
catch
{
// ignore error.
}
meterProvider.Dispose();
var metric = metricItems.FirstOrDefault(m => m.Name == "http.client.request.duration");
Assert.NotNull(metric);
var metricPoints = new List<MetricPoint>();
foreach (var p in metric.GetMetricPoints())
{
metricPoints.Add(p);
}
Assert.Single(metricPoints);
var mp = metricPoints[0];
// Inspect Metric Attributes
var attributes = new Dictionary<string, object>();
foreach (var tag in mp.Tags)
{
attributes[tag.Key] = tag.Value;
}
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpRequestMethod && kvp.Value.ToString() == expectedMethod);
Assert.DoesNotContain(attributes, t => t.Key == SemanticConventions.AttributeHttpRequestMethodOriginal);
}
[Fact]
public async Task RedirectTest()
{
var exportedItems = new List<Activity>();
using (Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation()
.AddInMemoryExporter(exportedItems)
.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.Single(exportedItems);
Assert.Contains(exportedItems[0].TagObjects, t => t.Key == "http.response.status_code" && (int)t.Value == 200);
#else
Assert.Equal(2, exportedItems.Count);
Assert.Contains(exportedItems[0].TagObjects, t => t.Key == "http.response.status_code" && (int)t.Value == 302);
Assert.Contains(exportedItems[1].TagObjects, t => t.Key == "http.response.status_code" && (int)t.Value == 200);
#endif
}
[Fact]
public async void RequestNotCollectedWhenInstrumentationFilterApplied()
{
var exportedItems = new List<Activity>();
bool httpWebRequestFilterApplied = false;
bool httpRequestMessageFilterApplied = false;
using (Sdk.CreateTracerProviderBuilder()
.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();
await c.GetAsync(this.url);
}
#if NETFRAMEWORK
Assert.True(httpWebRequestFilterApplied);
Assert.False(httpRequestMessageFilterApplied);
#else
Assert.False(httpWebRequestFilterApplied);
Assert.True(httpRequestMessageFilterApplied);
#endif
Assert.Empty(exportedItems);
}
[Fact]
public async void RequestNotCollectedWhenInstrumentationFilterThrowsException()
{
var exportedItems = new List<Activity>();
using (Sdk.CreateTracerProviderBuilder()
.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();
using var inMemoryEventListener = new InMemoryEventListener(HttpInstrumentationEventSource.Log);
await c.GetAsync(this.url);
Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 4));
}
Assert.Empty(exportedItems);
}
[Fact]
public async Task ReportsExceptionEventForNetworkFailuresWithGetAsync()
{
var exportedItems = new List<Activity>();
bool exceptionThrown = false;
using var tracerProvider = 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 DoesNotReportExceptionEventOnErrorResponseWithGetAsync()
{
var exportedItems = new List<Activity>();
bool exceptionThrown = false;
using var tracerProvider = 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 DoesNotReportExceptionEventOnErrorResponseWithGetStringAsync()
{
var exportedItems = new List<Activity>();
bool exceptionThrown = false;
using var request = new HttpRequestMessage
{
RequestUri = new Uri($"{this.url}500"),
Method = new HttpMethod("GET"),
};
using var tracerProvider = 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);
}
[Theory]
[InlineData("?a", "?a", false)]
[InlineData("?a=bdjdjh", "?a=Redacted", false)]
[InlineData("?a=b&", "?a=Redacted&", false)]
[InlineData("?c=b&", "?c=Redacted&", false)]
[InlineData("?c=a", "?c=Redacted", false)]
[InlineData("?a=b&c", "?a=Redacted&c", false)]
[InlineData("?a=b&c=1123456&", "?a=Redacted&c=Redacted&", false)]
[InlineData("?a=b&c=1&a1", "?a=Redacted&c=Redacted&a1", false)]
[InlineData("?a=ghgjgj&c=1deedd&a1=", "?a=Redacted&c=Redacted&a1=Redacted", false)]
[InlineData("?a=b&c=11&a1=&", "?a=Redacted&c=Redacted&a1=Redacted&", false)]
[InlineData("?c&c&c&", "?c&c&c&", false)]
[InlineData("?a&a&a&a", "?a&a&a&a", false)]
[InlineData("?&&&&&&&", "?&&&&&&&", false)]
[InlineData("?c", "?c", false)]
[InlineData("?a", "?a", true)]
[InlineData("?a=bdfdfdf", "?a=bdfdfdf", true)]
[InlineData("?a=b&", "?a=b&", true)]
[InlineData("?c=b&", "?c=b&", true)]
[InlineData("?c=a", "?c=a", true)]
[InlineData("?a=b&c", "?a=b&c", true)]
[InlineData("?a=b&c=111111&", "?a=b&c=111111&", true)]
[InlineData("?a=b&c=1&a1", "?a=b&c=1&a1", true)]
[InlineData("?a=b&c=1&a1=", "?a=b&c=1&a1=", true)]
[InlineData("?a=b123&c=11&a1=&", "?a=b123&c=11&a1=&", true)]
[InlineData("?c&c&c&", "?c&c&c&", true)]
[InlineData("?a&a&a&a", "?a&a&a&a", true)]
[InlineData("?&&&&&&&", "?&&&&&&&", true)]
[InlineData("?c", "?c", true)]
[InlineData("?c=%26&", "?c=Redacted&", false)]
public async Task ValidateUrlQueryRedaction(string urlQuery, string expectedUrlQuery, bool disableQueryRedaction)
{
var exportedItems = new List<Activity>();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { ["OTEL_DOTNET_EXPERIMENTAL_HTTPCLIENT_DISABLE_URL_QUERY_REDACTION"] = disableQueryRedaction.ToString() })
.Build();
// Arrange
using var traceprovider = Sdk.CreateTracerProviderBuilder()
.ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration))
.AddHttpClientInstrumentation()
.AddInMemoryExporter(exportedItems)
.Build();
using var c = new HttpClient();
try
{
await c.GetStringAsync($"{this.url}path{urlQuery}");
}
catch
{
}
Assert.Single(exportedItems);
var activity = exportedItems[0];
var expectedUrl = $"{this.url}path{expectedUrlQuery}";
Assert.Equal(expectedUrl, activity.GetTagValue(SemanticConventions.AttributeUrlFull));
}
[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 CustomTextMapPropagator
{
Injected = (context) => contextFromPropagator = context.ActivityContext,
};
propagator.InjectValues.Add("custom_traceParent", context => $"00/{context.ActivityContext.TraceId}/{context.ActivityContext.SpanId}/01");
propagator.InjectValues.Add("custom_traceState", context => Activity.Current.TraceStateString);
var exportedItems = new List<Activity>();
using (var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation()
.AddInMemoryExporter(exportedItems)
.SetSampler(sample ? new ParentBasedSampler(new AlwaysOnSampler()) : new AlwaysOffSampler())
.Build())
{
var previousDefaultTextMapPropagator = Propagators.DefaultTextMapPropagator;
Sdk.SetDefaultTextMapPropagator(propagator);
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;
}
using var request = new HttpRequestMessage
{
RequestUri = new Uri(this.url),
Method = new HttpMethod("GET"),
};
using var c = new HttpClient();
await c.SendAsync(request);
parent?.Stop();
Sdk.SetDefaultTextMapPropagator(previousDefaultTextMapPropagator);
}
if (!sample)
{
Assert.Empty(exportedItems);
}
else
{
Assert.Single(exportedItems);
}
// Make sure custom propagator was called.
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
}
public void Dispose()
{
this.serverLifeTime?.Dispose();
this.output.WriteLine($"HttpServer stopped: {this.url}");
Activity.Current = null;
GC.SuppressFinalize(this);
}
}

View File

@ -1,497 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics;
#if NETFRAMEWORK
using System.Net.Http;
#endif
#if !NET8_0_OR_GREATER
using System.Reflection;
using System.Text.Json;
#endif
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using Xunit;
namespace OpenTelemetry.Instrumentation.Http.Tests;
public partial class HttpClientTests
{
public static readonly IEnumerable<object[]> TestData = HttpTestData.ReadTestCases();
[Theory]
[MemberData(nameof(TestData))]
public async Task HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsSemanticConventionsAsync(HttpTestData.HttpOutTestCase tc)
{
await HttpOutCallsAreCollectedSuccessfullyBodyAsync(
this.host,
this.port,
tc,
enableTracing: true,
enableMetrics: true);
}
[Theory]
[MemberData(nameof(TestData))]
public async Task HttpOutCallsAreCollectedSuccessfullyMetricsOnlyAsync(HttpTestData.HttpOutTestCase tc)
{
await HttpOutCallsAreCollectedSuccessfullyBodyAsync(
this.host,
this.port,
tc,
enableTracing: false,
enableMetrics: true);
}
[Theory]
[MemberData(nameof(TestData))]
public async Task HttpOutCallsAreCollectedSuccessfullyTracesOnlyAsync(HttpTestData.HttpOutTestCase tc)
{
await HttpOutCallsAreCollectedSuccessfullyBodyAsync(
this.host,
this.port,
tc,
enableTracing: true,
enableMetrics: false);
}
[Theory]
[MemberData(nameof(TestData))]
public async Task HttpOutCallsAreCollectedSuccessfullyNoSignalsAsync(HttpTestData.HttpOutTestCase tc)
{
await HttpOutCallsAreCollectedSuccessfullyBodyAsync(
this.host,
this.port,
tc,
enableTracing: false,
enableMetrics: false);
}
#if !NET8_0_OR_GREATER
[Fact]
public async Task DebugIndividualTestAsync()
{
var input = JsonSerializer.Deserialize<HttpTestData.HttpOutTestCase[]>(
@"
[
{
""name"": ""Response code: 399"",
""method"": ""GET"",
""url"": ""http://{host}:{port}/"",
""responseCode"": 399,
""responseExpected"": true,
""spanName"": ""GET"",
""spanStatus"": ""Unset"",
""spanKind"": ""Client"",
""spanAttributes"": {
""url.scheme"": ""http"",
""http.request.method"": ""GET"",
""server.address"": ""{host}"",
""server.port"": ""{port}"",
""http.response.status_code"": ""399"",
""network.protocol.version"": ""{flavor}"",
""url.full"": ""http://{host}:{port}/""
}
}
]
",
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
var t = (Task)this.GetType().InvokeMember(nameof(this.HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsSemanticConventionsAsync), BindingFlags.InvokeMethod, null, this, HttpTestData.GetArgumentsFromTestCaseObject(input).First());
await t;
}
#endif
[Fact]
public async Task CheckEnrichmentWhenSampling()
{
await CheckEnrichment(new AlwaysOffSampler(), false, this.url);
await CheckEnrichment(new AlwaysOnSampler(), true, this.url);
}
#if NET8_0_OR_GREATER
[Theory]
[MemberData(nameof(TestData))]
public async Task ValidateNet8MetricsAsync(HttpTestData.HttpOutTestCase tc)
{
var metrics = new List<Metric>();
var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddHttpClientInstrumentation()
.AddInMemoryExporter(metrics)
.Build();
var testUrl = HttpTestData.NormalizeValues(tc.Url, this.host, this.port);
try
{
using var c = new HttpClient();
using var request = new HttpRequestMessage
{
RequestUri = new Uri(testUrl),
Method = new HttpMethod(tc.Method),
};
request.Headers.Add("contextRequired", "false");
request.Headers.Add("responseCode", (tc.ResponseCode == 0 ? 200 : tc.ResponseCode).ToString());
await c.SendAsync(request);
}
catch (Exception)
{
// test case can intentionally send request that will result in exception
}
finally
{
meterProvider.Dispose();
}
var requestMetrics = metrics
.Where(metric =>
metric.Name == "http.client.request.duration" ||
metric.Name == "http.client.active_requests" ||
metric.Name == "http.client.request.time_in_queue" ||
metric.Name == "http.client.connection.duration" ||
metric.Name == "http.client.open_connections" ||
metric.Name == "dns.lookup.duration")
.ToArray();
if (tc.ResponseExpected)
{
Assert.Equal(6, requestMetrics.Count());
}
else
{
// http.client.connection.duration and http.client.open_connections will not be emitted.
Assert.Equal(4, requestMetrics.Count());
}
}
#endif
private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync(
string host,
int port,
HttpTestData.HttpOutTestCase tc,
bool enableTracing,
bool enableMetrics)
{
bool enrichWithHttpWebRequestCalled = false;
bool enrichWithHttpWebResponseCalled = false;
bool enrichWithHttpRequestMessageCalled = false;
bool enrichWithHttpResponseMessageCalled = false;
bool enrichWithExceptionCalled = false;
var testUrl = HttpTestData.NormalizeValues(tc.Url, host, port);
var meterProviderBuilder = Sdk.CreateMeterProviderBuilder();
if (enableMetrics)
{
meterProviderBuilder
.AddHttpClientInstrumentation();
}
var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder();
if (enableTracing)
{
tracerProviderBuilder
.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;
});
}
var metrics = new List<Metric>();
var activities = new List<Activity>();
var meterProvider = meterProviderBuilder
.AddInMemoryExporter(metrics)
.Build();
var tracerProvider = tracerProviderBuilder
.AddInMemoryExporter(activities)
.Build();
try
{
using var c = new HttpClient();
using var request = new HttpRequestMessage
{
RequestUri = new Uri(testUrl),
Method = new HttpMethod(tc.Method),
};
if (tc.Headers != null)
{
foreach (var header in tc.Headers)
{
request.Headers.Add(header.Key, header.Value);
}
}
request.Headers.Add("contextRequired", "false");
request.Headers.Add("responseCode", (tc.ResponseCode == 0 ? 200 : tc.ResponseCode).ToString());
await c.SendAsync(request);
}
catch (Exception)
{
// test case can intentionally send request that will result in exception
}
finally
{
tracerProvider.Dispose();
meterProvider.Dispose();
}
var requestMetrics = metrics
.Where(metric => metric.Name == "http.client.request.duration")
.ToArray();
var normalizedAttributesTestCase = tc.SpanAttributes.ToDictionary(x => x.Key, x => HttpTestData.NormalizeValues(x.Value, host, port));
if (!enableTracing)
{
Assert.Empty(activities);
}
else
{
var activity = Assert.Single(activities);
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());
Assert.Null(activity.StatusDescription);
var normalizedAttributes = activity.TagObjects.Where(kv => !kv.Key.StartsWith("otel.")).ToDictionary(x => x.Key, x => x.Value.ToString());
int numberOfTags = activity.Status == ActivityStatusCode.Error ? 5 : 4;
var expectedAttributeCount = numberOfTags + (tc.ResponseExpected ? 2 : 0);
Assert.Equal(expectedAttributeCount, normalizedAttributes.Count);
Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpRequestMethod && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpRequestMethod]);
Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeServerAddress && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeServerAddress]);
Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeServerPort && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeServerPort]);
Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeUrlFull && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeUrlFull]);
if (tc.ResponseExpected)
{
Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeNetworkProtocolVersion && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeNetworkProtocolVersion]);
Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpResponseStatusCode]);
if (tc.ResponseCode >= 400)
{
Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpResponseStatusCode]);
}
}
else
{
Assert.DoesNotContain(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode);
Assert.DoesNotContain(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeNetworkProtocolVersion);
#if NET8_0_OR_GREATER
// we are using fake address so it will be "name_resolution_error"
// TODO: test other error types.
Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_error");
#elif NETFRAMEWORK
Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_failure");
#else
Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "System.Net.Http.HttpRequestException");
#endif
}
if (tc.RecordException.HasValue && tc.RecordException.Value)
{
Assert.Single(activity.Events.Where(evt => evt.Name.Equals("exception")));
Assert.True(enrichWithExceptionCalled);
}
}
if (!enableMetrics)
{
Assert.Empty(requestMetrics);
}
else
{
Assert.Single(requestMetrics);
var metric = requestMetrics.FirstOrDefault(m => m.Name == "http.client.request.duration");
Assert.NotNull(metric);
Assert.Equal("s", metric.Unit);
Assert.True(metric.MetricType == MetricType.Histogram);
var metricPoints = new List<MetricPoint>();
foreach (var p in metric.GetMetricPoints())
{
metricPoints.Add(p);
}
Assert.Single(metricPoints);
var metricPoint = metricPoints[0];
var count = metricPoint.GetHistogramCount();
var sum = metricPoint.GetHistogramSum();
Assert.Equal(1L, count);
if (enableTracing)
{
var activity = Assert.Single(activities);
#if !NET8_0_OR_GREATER
Assert.Equal(activity.Duration.TotalSeconds, sum);
#endif
}
else
{
Assert.True(sum > 0);
}
// Inspect Metric Attributes
var attributes = new Dictionary<string, object>();
foreach (var tag in metricPoint.Tags)
{
attributes[tag.Key] = tag.Value;
}
var numberOfTags = 4;
if (tc.ResponseExpected)
{
var expectedStatusCode = int.Parse(normalizedAttributesTestCase[SemanticConventions.AttributeHttpResponseStatusCode]);
numberOfTags = (expectedStatusCode >= 400) ? 5 : 4; // error.type extra tag
}
else
{
numberOfTags = 5; // error.type would be extra
}
var expectedAttributeCount = numberOfTags + (tc.ResponseExpected ? 2 : 0); // responsecode + protocolversion
Assert.Equal(expectedAttributeCount, attributes.Count);
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpRequestMethod && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpRequestMethod]);
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeServerAddress && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeServerAddress]);
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeServerPort && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeServerPort]);
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeUrlScheme && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeUrlScheme]);
if (tc.ResponseExpected)
{
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeNetworkProtocolVersion && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeNetworkProtocolVersion]);
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpResponseStatusCode]);
if (tc.ResponseCode >= 400)
{
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpResponseStatusCode]);
}
}
else
{
Assert.DoesNotContain(attributes, kvp => kvp.Key == SemanticConventions.AttributeNetworkProtocolVersion);
Assert.DoesNotContain(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode);
#if NET8_0_OR_GREATER
// we are using fake address so it will be "name_resolution_error"
// TODO: test other error types.
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_error");
#elif NETFRAMEWORK
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_failure");
#else
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "System.Net.Http.HttpRequestException");
#endif
}
// Inspect Histogram Bounds
var histogramBuckets = metricPoint.GetHistogramBuckets();
var histogramBounds = new List<double>();
foreach (var t in histogramBuckets)
{
histogramBounds.Add(t.ExplicitBound);
}
// TODO: Remove the check for the older bounds once 1.7.0 is released. This is a temporary fix for instrumentation libraries CI workflow.
var expectedHistogramBoundsOld = new List<double> { 0, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, double.PositiveInfinity };
var expectedHistogramBoundsNew = new List<double> { 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, double.PositiveInfinity };
var histogramBoundsMatchCorrectly = Enumerable.SequenceEqual(expectedHistogramBoundsOld, histogramBounds) ||
Enumerable.SequenceEqual(expectedHistogramBoundsNew, histogramBounds);
Assert.True(histogramBoundsMatchCorrectly);
}
}
private static async Task CheckEnrichment(Sampler sampler, bool enrichExpected, string url)
{
bool enrichWithHttpWebRequestCalled = false;
bool enrichWithHttpWebResponseCalled = false;
bool enrichWithHttpRequestMessageCalled = false;
bool enrichWithHttpResponseMessageCalled = false;
using (Sdk.CreateTracerProviderBuilder()
.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; };
})
.Build())
{
using var c = new HttpClient();
using var r = await c.GetAsync(url);
}
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);
}
}
}

View File

@ -1,64 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Reflection;
using System.Text.Json;
namespace OpenTelemetry.Instrumentation.Http.Tests;
public static class HttpTestData
{
public static IEnumerable<object[]> ReadTestCases()
{
var assembly = Assembly.GetExecutingAssembly();
var input = JsonSerializer.Deserialize<HttpOutTestCase[]>(
assembly.GetManifestResourceStream("OpenTelemetry.Instrumentation.Http.Tests.http-out-test-cases.json"), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
return GetArgumentsFromTestCaseObject(input);
}
public static IEnumerable<object[]> GetArgumentsFromTestCaseObject(IEnumerable<HttpOutTestCase> input)
{
var result = new List<object[]>();
foreach (var testCase in input)
{
result.Add(new object[]
{
testCase,
});
}
return result;
}
public static string NormalizeValues(string value, string host, int port)
{
return value
.Replace("{host}", host)
.Replace("{port}", port.ToString())
.Replace("{flavor}", "1.1");
}
public class HttpOutTestCase
{
public string Name { get; set; }
public string Method { get; set; }
public string Url { get; set; }
public Dictionary<string, string> Headers { get; set; }
public int ResponseCode { get; set; }
public string SpanName { get; set; }
public bool ResponseExpected { get; set; }
public bool? RecordException { get; set; }
public string SpanStatus { get; set; }
public Dictionary<string, string> SpanAttributes { get; set; }
}
}

View File

@ -1,878 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
#if NETFRAMEWORK
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using OpenTelemetry.Instrumentation.Http.Implementation;
using OpenTelemetry.Tests;
using OpenTelemetry.Trace;
using Xunit;
namespace OpenTelemetry.Instrumentation.Http.Tests;
public class HttpWebRequestActivitySourceTests : IDisposable
{
private static bool validateBaggage;
private readonly IDisposable testServer;
private readonly string testServerHost;
private readonly int testServerPort;
private readonly string netPeerName;
private readonly int netPeerPort;
static HttpWebRequestActivitySourceTests()
{
HttpClientTraceInstrumentationOptions options = new()
{
EnrichWithHttpWebRequest = (activity, httpWebRequest) =>
{
VerifyHeaders(httpWebRequest);
if (validateBaggage)
{
ValidateBaggage(httpWebRequest);
}
},
DisableUrlQueryRedaction = true,
};
HttpWebRequestActivitySource.TracingOptions = options;
_ = Sdk.SuppressInstrumentation;
}
public HttpWebRequestActivitySourceTests()
{
Assert.Null(Activity.Current);
Activity.DefaultIdFormat = ActivityIdFormat.W3C;
Activity.ForceDefaultIdFormat = false;
this.testServer = TestHttpServer.RunServer(
ctx => ProcessServerRequest(ctx),
out this.testServerHost,
out this.testServerPort);
this.netPeerName = this.testServerHost;
this.netPeerPort = this.testServerPort;
void ProcessServerRequest(HttpListenerContext context)
{
string redirects = context.Request.QueryString["redirects"];
if (!string.IsNullOrWhiteSpace(redirects) && int.TryParse(redirects, out int parsedRedirects) && parsedRedirects > 0)
{
context.Response.Redirect(this.BuildRequestUrl(queryString: $"redirects={--parsedRedirects}"));
context.Response.OutputStream.Close();
return;
}
string responseContent;
if (context.Request.QueryString["skipRequestContent"] == null)
{
using StreamReader readStream = new StreamReader(context.Request.InputStream);
responseContent = readStream.ReadToEnd();
}
else
{
responseContent = $"{{\"Id\":\"{Guid.NewGuid()}\"}}";
}
string responseCode = context.Request.QueryString["responseCode"];
if (!string.IsNullOrWhiteSpace(responseCode))
{
context.Response.StatusCode = int.Parse(responseCode);
}
else
{
context.Response.StatusCode = 200;
}
if (context.Response.StatusCode != 204)
{
using StreamWriter writeStream = new StreamWriter(context.Response.OutputStream);
writeStream.Write(responseContent);
}
else
{
context.Response.OutputStream.Close();
}
}
}
public void Dispose()
{
this.testServer.Dispose();
}
/// <summary>
/// A simple test to make sure the Http Diagnostic Source is added into the list of DiagnosticListeners.
/// </summary>
[Fact]
public void TestHttpDiagnosticListenerIsRegistered()
{
bool listenerFound = false;
using ActivityListener activityListener = new ActivityListener
{
ShouldListenTo = activitySource =>
{
if (activitySource.Name == HttpWebRequestActivitySource.ActivitySourceName)
{
listenerFound = true;
return true;
}
return false;
},
};
ActivitySource.AddActivityListener(activityListener);
Assert.True(listenerFound, "The Http Diagnostic Listener didn't get added to the AllListeners list.");
}
/// <summary>
/// A simple test to make sure the Http Diagnostic Source is initialized properly after we subscribed to it, using
/// the subscribe overload with just the observer argument.
/// </summary>
[Fact]
public async Task TestReflectInitializationViaSubscription()
{
using var eventRecords = new ActivitySourceRecorder();
// Send a random Http request to generate some events
using (var client = new HttpClient())
{
(await client.GetAsync(this.BuildRequestUrl())).Dispose();
}
// Just make sure some events are written, to confirm we successfully subscribed to it.
// We should have exactly one Start and one Stop event
Assert.Equal(2, eventRecords.Records.Count);
}
/// <summary>
/// Test to make sure we get both request and response events.
/// </summary>
[Theory]
[InlineData("GET")]
[InlineData("POST")]
[InlineData("POST", "skipRequestContent=1")]
public async Task TestBasicReceiveAndResponseEvents(string method, string queryString = null)
{
var url = this.BuildRequestUrl(queryString: queryString);
using var eventRecords = new ActivitySourceRecorder();
// Send a random Http request to generate some events
using (var client = new HttpClient())
{
(method == "GET"
? await client.GetAsync(url)
: await client.PostAsync(url, new StringContent("hello world"))).Dispose();
}
// We should have exactly one Start and one Stop event
Assert.Equal(2, eventRecords.Records.Count);
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
// Check to make sure: The first record must be a request, the next record must be a response.
Activity activity = AssertFirstEventWasStart(eventRecords);
VerifyActivityStartTags(this.netPeerName, this.netPeerPort, method, url, activity);
Assert.True(eventRecords.Records.TryDequeue(out var stopEvent));
Assert.Equal("Stop", stopEvent.Key);
VerifyActivityStopTags(200, activity);
}
[Theory]
[InlineData("GET")]
[InlineData("POST")]
public async Task TestBasicReceiveAndResponseEventsWithoutSampling(string method)
{
using var eventRecords = new ActivitySourceRecorder(activitySamplingResult: ActivitySamplingResult.None);
// Send a random Http request to generate some events
using (var client = new HttpClient())
{
(method == "GET"
? await client.GetAsync(this.BuildRequestUrl())
: await client.PostAsync(this.BuildRequestUrl(), new StringContent("hello world"))).Dispose();
}
// There should be no events because we turned off sampling.
Assert.Empty(eventRecords.Records);
}
[Theory]
[InlineData("GET", 0)]
[InlineData("GET", 1)]
[InlineData("GET", 2)]
[InlineData("GET", 3)]
[InlineData("POST", 0)]
[InlineData("POST", 1)]
[InlineData("POST", 2)]
[InlineData("POST", 3)]
public async Task TestBasicReceiveAndResponseWebRequestEvents(string method, int mode)
{
string url = this.BuildRequestUrl();
using var eventRecords = new ActivitySourceRecorder();
// Send a random Http request to generate some events
var webRequest = (HttpWebRequest)WebRequest.Create(url);
if (method == "POST")
{
webRequest.Method = method;
Stream stream = null;
switch (mode)
{
case 0:
stream = webRequest.GetRequestStream();
break;
case 1:
stream = await webRequest.GetRequestStreamAsync();
break;
case 2:
{
object state = new object();
using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.ManualReset);
IAsyncResult asyncResult = webRequest.BeginGetRequestStream(
ar =>
{
Assert.Equal(state, ar.AsyncState);
handle.Set();
},
state);
stream = webRequest.EndGetRequestStream(asyncResult);
if (!handle.WaitOne(TimeSpan.FromSeconds(30)))
{
throw new InvalidOperationException();
}
handle.Dispose();
}
break;
case 3:
{
using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.ManualReset);
object state = new object();
webRequest.BeginGetRequestStream(
ar =>
{
stream = webRequest.EndGetRequestStream(ar);
Assert.Equal(state, ar.AsyncState);
handle.Set();
},
state);
if (!handle.WaitOne(TimeSpan.FromSeconds(30)))
{
throw new InvalidOperationException();
}
handle.Dispose();
}
break;
default:
throw new NotSupportedException();
}
Assert.NotNull(stream);
using StreamWriter writer = new StreamWriter(stream);
writer.WriteLine("hello world");
}
WebResponse webResponse = null;
switch (mode)
{
case 0:
webResponse = webRequest.GetResponse();
break;
case 1:
webResponse = await webRequest.GetResponseAsync();
break;
case 2:
{
object state = new object();
using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.ManualReset);
IAsyncResult asyncResult = webRequest.BeginGetResponse(
ar =>
{
Assert.Equal(state, ar.AsyncState);
handle.Set();
},
state);
webResponse = webRequest.EndGetResponse(asyncResult);
if (!handle.WaitOne(TimeSpan.FromSeconds(30)))
{
throw new InvalidOperationException();
}
handle.Dispose();
}
break;
case 3:
{
using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.ManualReset);
object state = new object();
webRequest.BeginGetResponse(
ar =>
{
webResponse = webRequest.EndGetResponse(ar);
Assert.Equal(state, ar.AsyncState);
handle.Set();
},
state);
if (!handle.WaitOne(TimeSpan.FromSeconds(30)))
{
throw new InvalidOperationException();
}
handle.Dispose();
}
break;
default:
throw new NotSupportedException();
}
Assert.NotNull(webResponse);
using StreamReader reader = new StreamReader(webResponse.GetResponseStream());
reader.ReadToEnd(); // Make sure response is not disposed.
// We should have exactly one Start and one Stop event
Assert.Equal(2, eventRecords.Records.Count);
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
// Check to make sure: The first record must be a request, the next record must be a response.
Activity activity = AssertFirstEventWasStart(eventRecords);
VerifyActivityStartTags(this.netPeerName, this.netPeerPort, method, url, activity);
Assert.True(eventRecords.Records.TryDequeue(out var stopEvent));
Assert.Equal("Stop", stopEvent.Key);
VerifyActivityStopTags(200, activity);
}
[Fact]
public async Task TestTraceStateAndBaggage()
{
try
{
using var eventRecords = new ActivitySourceRecorder();
using var parent = new Activity("w3c activity");
parent.SetParentId(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom());
parent.TraceStateString = "some=state";
parent.Start();
Baggage.SetBaggage("k", "v");
// Send a random Http request to generate some events
using (var client = new HttpClient())
{
(await client.GetAsync(this.BuildRequestUrl())).Dispose();
}
parent.Stop();
Assert.Equal(2, eventRecords.Records.Count());
// Check to make sure: The first record must be a request, the next record must be a response.
_ = AssertFirstEventWasStart(eventRecords);
}
finally
{
this.CleanUpActivity();
}
}
[Theory]
[InlineData("GET")]
[InlineData("POST")]
public async Task DoNotInjectTraceParentWhenPresent(string method)
{
try
{
using var eventRecords = new ActivitySourceRecorder();
// Send a random Http request to generate some events
using (var client = new HttpClient())
using (var request = new HttpRequestMessage(HttpMethod.Get, this.BuildRequestUrl()))
{
request.Headers.Add("traceparent", "00-abcdef0123456789abcdef0123456789-abcdef0123456789-01");
if (method == "GET")
{
request.Method = HttpMethod.Get;
}
else
{
request.Method = HttpMethod.Post;
request.Content = new StringContent("hello world");
}
(await client.SendAsync(request)).Dispose();
}
// No events are sent.
Assert.Empty(eventRecords.Records);
}
finally
{
this.CleanUpActivity();
}
}
/// <summary>
/// Test to make sure we get both request and response events.
/// </summary>
[Theory]
[InlineData("GET")]
[InlineData("POST")]
public async Task TestResponseWithoutContentEvents(string method)
{
string url = this.BuildRequestUrl(queryString: "responseCode=204");
using var eventRecords = new ActivitySourceRecorder();
// Send a random Http request to generate some events
using (var client = new HttpClient())
{
using HttpResponseMessage response = method == "GET"
? await client.GetAsync(url)
: await client.PostAsync(url, new StringContent("hello world"));
}
// We should have exactly one Start and one Stop event
Assert.Equal(2, eventRecords.Records.Count);
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
// Check to make sure: The first record must be a request, the next record must be a response.
Activity activity = AssertFirstEventWasStart(eventRecords);
VerifyActivityStartTags(this.netPeerName, this.netPeerPort, method, url, activity);
Assert.True(eventRecords.Records.TryDequeue(out var stopEvent));
Assert.Equal("Stop", stopEvent.Key);
VerifyActivityStopTags(204, activity);
}
/// <summary>
/// Test that if request is redirected, it gets only one Start and one Stop event.
/// </summary>
[Theory]
[InlineData("GET")]
[InlineData("POST")]
public async Task TestRedirectedRequest(string method)
{
using var eventRecords = new ActivitySourceRecorder();
using (var client = new HttpClient())
{
using HttpResponseMessage response = method == "GET"
? await client.GetAsync(this.BuildRequestUrl(queryString: "redirects=10"))
: await client.PostAsync(this.BuildRequestUrl(queryString: "redirects=10"), new StringContent("hello world"));
}
// We should have exactly one Start and one Stop event
Assert.Equal(2, eventRecords.Records.Count());
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
}
/// <summary>
/// Test exception in request processing: exception should have expected type/status and now be swallowed by reflection hook.
/// </summary>
[Theory]
[InlineData("GET")]
[InlineData("POST")]
public async Task TestRequestWithException(string method)
{
string host = Guid.NewGuid().ToString() + ".com";
string url = method == "GET"
? $"http://{host}"
: $"http://{host}";
using var eventRecords = new ActivitySourceRecorder();
var ex = await Assert.ThrowsAsync<HttpRequestException>(() =>
{
return method == "GET"
? new HttpClient().GetAsync(url)
: new HttpClient().PostAsync(url, new StringContent("hello world"));
});
// check that request failed because of the wrong domain name and not because of reflection
var webException = (WebException)ex.InnerException;
Assert.NotNull(webException);
Assert.True(webException.Status == WebExceptionStatus.NameResolutionFailure);
// We should have one Start event and one Stop event with an exception.
Assert.Equal(2, eventRecords.Records.Count());
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
// Check to make sure: The first record must be a request, the next record must be an exception.
Activity activity = AssertFirstEventWasStart(eventRecords);
VerifyActivityStartTags(host, null, method, url, activity);
Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair<string, Activity> exceptionEvent));
Assert.Equal("Stop", exceptionEvent.Key);
Assert.True(activity.Status != ActivityStatusCode.Unset);
Assert.Null(activity.StatusDescription);
}
/// <summary>
/// Test request cancellation: reflection hook does not throw.
/// </summary>
[Theory]
[InlineData("GET")]
[InlineData("POST")]
public async Task TestCanceledRequest(string method)
{
string url = this.BuildRequestUrl();
CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var eventRecords = new ActivitySourceRecorder(_ => { cts.Cancel(); });
using (var client = new HttpClient())
{
var ex = await Assert.ThrowsAnyAsync<Exception>(() =>
{
return method == "GET"
? client.GetAsync(url, cts.Token)
: client.PostAsync(url, new StringContent("hello world"), cts.Token);
});
Assert.True(ex is TaskCanceledException || ex is WebException);
}
// We should have one Start event and one Stop event with an exception.
Assert.Equal(2, eventRecords.Records.Count());
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
Activity activity = AssertFirstEventWasStart(eventRecords);
VerifyActivityStartTags(this.netPeerName, this.netPeerPort, method, url, activity);
Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair<string, Activity> exceptionEvent));
Assert.Equal("Stop", exceptionEvent.Key);
Assert.True(exceptionEvent.Value.Status != ActivityStatusCode.Unset);
Assert.True(exceptionEvent.Value.StatusDescription == null);
}
/// <summary>
/// Test request connection exception: reflection hook does not throw.
/// </summary>
[Theory]
[InlineData("GET")]
[InlineData("POST")]
public async Task TestSecureTransportFailureRequest(string method)
{
string url = "https://expired.badssl.com/";
using var eventRecords = new ActivitySourceRecorder();
using (var client = new HttpClient())
{
var ex = await Assert.ThrowsAnyAsync<Exception>(() =>
{
// https://expired.badssl.com/ has an expired certificae.
return method == "GET"
? client.GetAsync(url)
: client.PostAsync(url, new StringContent("hello world"));
});
Assert.True(ex is HttpRequestException);
}
// We should have one Start event and one Stop event with an exception.
Assert.Equal(2, eventRecords.Records.Count());
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
Activity activity = AssertFirstEventWasStart(eventRecords);
VerifyActivityStartTags("expired.badssl.com", null, method, url, activity);
Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair<string, Activity> exceptionEvent));
Assert.Equal("Stop", exceptionEvent.Key);
Assert.True(exceptionEvent.Value.Status != ActivityStatusCode.Unset);
Assert.Null(exceptionEvent.Value.StatusDescription);
}
/// <summary>
/// Test request connection retry: reflection hook does not throw.
/// </summary>
[Theory]
[InlineData("GET")]
[InlineData("POST")]
public async Task TestSecureTransportRetryFailureRequest(string method)
{
// This test sends an https request to an endpoint only set up for http.
// It should retry. What we want to test for is 1 start, 1 exception event even
// though multiple are actually sent.
string url = this.BuildRequestUrl(useHttps: true);
using var eventRecords = new ActivitySourceRecorder();
using (var client = new HttpClient())
{
var ex = await Assert.ThrowsAnyAsync<Exception>(() =>
{
return method == "GET"
? client.GetAsync(url)
: client.PostAsync(url, new StringContent("hello world"));
});
Assert.True(ex is HttpRequestException);
}
// We should have one Start event and one Stop event with an exception.
Assert.Equal(2, eventRecords.Records.Count());
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
Activity activity = AssertFirstEventWasStart(eventRecords);
VerifyActivityStartTags(this.netPeerName, this.netPeerPort, method, url, activity);
Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair<string, Activity> exceptionEvent));
Assert.Equal("Stop", exceptionEvent.Key);
Assert.True(exceptionEvent.Value.Status != ActivityStatusCode.Unset);
Assert.Null(exceptionEvent.Value.StatusDescription);
}
[Fact]
public async Task TestInvalidBaggage()
{
validateBaggage = true;
Baggage
.SetBaggage("key", "value")
.SetBaggage("bad/key", "value")
.SetBaggage("goodkey", "bad/value");
using var eventRecords = new ActivitySourceRecorder();
using (var client = new HttpClient())
{
(await client.GetAsync(this.BuildRequestUrl())).Dispose();
}
Assert.Equal(2, eventRecords.Records.Count());
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
validateBaggage = false;
}
/// <summary>
/// Test to make sure every event record has the right dynamic properties.
/// </summary>
[Fact]
public async Task TestMultipleConcurrentRequests()
{
ServicePointManager.DefaultConnectionLimit = int.MaxValue;
using var parentActivity = new Activity("parent").Start();
using var eventRecords = new ActivitySourceRecorder();
Dictionary<Uri, Tuple<WebRequest, WebResponse>> requestData = new Dictionary<Uri, Tuple<WebRequest, WebResponse>>();
for (int i = 0; i < 10; i++)
{
Uri uriWithRedirect = new Uri(this.BuildRequestUrl(queryString: $"q={i}&redirects=3"));
requestData[uriWithRedirect] = null;
}
// Issue all requests simultaneously
using var httpClient = new HttpClient();
Dictionary<Uri, Task<HttpResponseMessage>> tasks = new Dictionary<Uri, Task<HttpResponseMessage>>();
CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
foreach (var url in requestData.Keys)
{
tasks.Add(url, httpClient.GetAsync(url, cts.Token));
}
// wait up to 10 sec for all requests and suppress exceptions
await Task.WhenAll(tasks.Select(t => t.Value).ToArray()).ContinueWith(async tt =>
{
foreach (var task in tasks)
{
(await task.Value)?.Dispose();
}
});
// Examine the result. Make sure we got all successful requests.
// Just make sure some events are written, to confirm we successfully subscribed to it. We should have
// exactly 1 Start event per request and exactly 1 Stop event per response (if request succeeded)
var successfulTasks = tasks.Where(t => t.Value.Status == TaskStatus.RanToCompletion);
Assert.Equal(tasks.Count, eventRecords.Records.Count(rec => rec.Key == "Start"));
Assert.Equal(successfulTasks.Count(), eventRecords.Records.Count(rec => rec.Key == "Stop"));
// Check to make sure: We have a WebRequest and a WebResponse for each successful request
foreach (var pair in eventRecords.Records)
{
Activity activity = pair.Value;
Assert.True(
pair.Key == "Start" ||
pair.Key == "Stop",
"An unexpected event of name " + pair.Key + "was received");
}
}
private static Activity AssertFirstEventWasStart(ActivitySourceRecorder eventRecords)
{
Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair<string, Activity> startEvent));
Assert.Equal("Start", startEvent.Key);
return startEvent.Value;
}
private static void VerifyHeaders(HttpWebRequest startRequest)
{
var tracestate = startRequest.Headers["tracestate"];
Assert.Equal("some=state", tracestate);
var baggage = startRequest.Headers["baggage"];
Assert.Equal("k=v", baggage);
var traceparent = startRequest.Headers["traceparent"];
Assert.NotNull(traceparent);
Assert.Matches("^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$", traceparent);
}
private static void VerifyActivityStartTags(string netPeerName, int? netPeerPort, string method, string url, Activity activity)
{
Assert.NotNull(activity.TagObjects);
Assert.Equal(method, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod));
if (netPeerPort != null)
{
Assert.Equal(netPeerPort, activity.GetTagValue(SemanticConventions.AttributeServerPort));
}
Assert.Equal(netPeerName, activity.GetTagValue(SemanticConventions.AttributeServerAddress));
Assert.Equal(url, activity.GetTagValue(SemanticConventions.AttributeUrlFull));
}
private static void VerifyActivityStopTags(int statusCode, Activity activity)
{
Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpResponseStatusCode));
}
private static void ActivityEnrichment(Activity activity, string method, object obj)
{
switch (method)
{
case "OnStartActivity":
Assert.True(obj is HttpWebRequest);
VerifyHeaders(obj as HttpWebRequest);
if (validateBaggage)
{
ValidateBaggage(obj as HttpWebRequest);
}
break;
case "OnStopActivity":
Assert.True(obj is HttpWebResponse);
break;
case "OnException":
Assert.True(obj is Exception);
break;
default:
break;
}
}
private static void ValidateBaggage(HttpWebRequest request)
{
string[] baggage = request.Headers["baggage"].Split(',');
Assert.Equal(3, baggage.Length);
Assert.Contains("key=value", baggage);
Assert.Contains("bad%2Fkey=value", baggage);
Assert.Contains("goodkey=bad%2Fvalue", baggage);
}
private string BuildRequestUrl(bool useHttps = false, string path = "echo", string queryString = null)
{
return $"{(useHttps ? "https" : "http")}://{this.testServerHost}:{this.testServerPort}/{path}{(string.IsNullOrWhiteSpace(queryString) ? string.Empty : $"?{queryString}")}";
}
private void CleanUpActivity()
{
while (Activity.Current != null)
{
Activity.Current.Stop();
}
}
/// <summary>
/// <see cref="ActivitySourceRecorder"/> is a helper class for recording <see cref="HttpWebRequestActivitySource.ActivitySourceName"/> events.
/// </summary>
private class ActivitySourceRecorder : IDisposable
{
private readonly Action<KeyValuePair<string, Activity>> onEvent;
private readonly ActivityListener activityListener;
public ActivitySourceRecorder(Action<KeyValuePair<string, Activity>> onEvent = null, ActivitySamplingResult activitySamplingResult = ActivitySamplingResult.AllDataAndRecorded)
{
this.activityListener = new ActivityListener
{
ShouldListenTo = (activitySource) => activitySource.Name == HttpWebRequestActivitySource.ActivitySourceName,
ActivityStarted = this.ActivityStarted,
ActivityStopped = this.ActivityStopped,
Sample = (ref ActivityCreationOptions<ActivityContext> options) => activitySamplingResult,
};
ActivitySource.AddActivityListener(this.activityListener);
this.onEvent = onEvent;
}
public ConcurrentQueue<KeyValuePair<string, Activity>> Records { get; } = new ConcurrentQueue<KeyValuePair<string, Activity>>();
public void Dispose()
{
this.activityListener.Dispose();
}
public void ActivityStarted(Activity activity) => this.Record("Start", activity);
public void ActivityStopped(Activity activity) => this.Record("Stop", activity);
private void Record(string eventName, Activity activity)
{
var record = new KeyValuePair<string, Activity>(eventName, activity);
this.Records.Enqueue(record);
this.onEvent?.Invoke(record);
}
}
}
#endif

View File

@ -1,363 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics;
using System.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
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 exportedItems = new List<Activity>();
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddInMemoryExporter(exportedItems)
.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
Assert.Empty(exportedItems);
#else
Assert.Single(exportedItems);
#endif
}
[Fact]
public async Task RequestNotCollectedWhenInstrumentationFilterApplied()
{
bool httpWebRequestFilterApplied = false;
bool httpRequestMessageFilterApplied = false;
var exportedItems = new List<Activity>();
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()
{
var exportedItems = new List<Activity>();
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 exportedItems = new List<Activity>();
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddInMemoryExporter(exportedItems)
.AddHttpClientInstrumentation()
.Build();
var request = (HttpWebRequest)WebRequest.Create(this.url);
request.Method = "GET";
using 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.Single(exportedItems);
var activity = exportedItems[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
}
[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 CustomTextMapPropagator
{
Injected = (PropagationContext context) => contextFromPropagator = context.ActivityContext,
};
propagator.InjectValues.Add("custom_traceParent", context => $"00/{context.ActivityContext.TraceId}/{context.ActivityContext.SpanId}/01");
propagator.InjectValues.Add("custom_traceState", context => Activity.Current.TraceStateString);
var exportedItems = new List<Activity>();
using (var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation()
.AddInMemoryExporter(exportedItems)
.SetSampler(sample ? new ParentBasedSampler(new AlwaysOnSampler()) : new AlwaysOffSampler())
.Build())
{
var previousDefaultTextMapPropagator = Propagators.DefaultTextMapPropagator;
Sdk.SetDefaultTextMapPropagator(propagator);
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;
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.ConfigureServices(services =>
{
services.Configure<HttpClientTraceInstrumentationOptions>(name, o => configurationDelegateInvocations++);
})
.AddHttpClientInstrumentation(name, options =>
{
Assert.IsType<HttpClientTraceInstrumentationOptions>(options);
})
.Build();
Assert.Equal(1, configurationDelegateInvocations);
}
[Fact]
public async Task ReportsExceptionEventForNetworkFailures()
{
var exportedItems = new List<Activity>();
bool exceptionThrown = false;
using var tracerProvider = 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<Activity>();
bool exceptionThrown = false;
using var tracerProvider = 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
}
}

View File

@ -1,186 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics;
using System.Net;
using System.Text.Json;
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
{
public static IEnumerable<object[]> TestData => HttpTestData.ReadTestCases();
[Theory]
[MemberData(nameof(TestData))]
public void HttpOutCallsAreCollectedSuccessfully(HttpTestData.HttpOutTestCase tc)
{
using var serverLifeTime = TestHttpServer.RunServer(
(ctx) =>
{
ctx.Response.StatusCode = tc.ResponseCode == 0 ? 200 : tc.ResponseCode;
ctx.Response.OutputStream.Close();
},
out var host,
out var port);
bool enrichWithHttpWebRequestCalled = false;
bool enrichWithHttpWebResponseCalled = false;
bool enrichWithHttpRequestMessageCalled = false;
bool enrichWithHttpResponseMessageCalled = false;
bool enrichWithExceptionCalled = false;
var exportedItems = new List<Activity>();
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddInMemoryExporter(exportedItems)
.AddHttpClientInstrumentation(options =>
{
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 ?? false;
})
.Build();
tc.Url = HttpTestData.NormalizeValues(tc.Url, host, port);
try
{
var request = (HttpWebRequest)WebRequest.Create(tc.Url);
request.Method = tc.Method;
if (tc.Headers != null)
{
foreach (var header in tc.Headers)
{
request.Headers.Add(header.Key, header.Value);
}
}
request.ContentLength = 0;
using var response = (HttpWebResponse)request.GetResponse();
using var streamReader = new StreamReader(response.GetResponseStream());
streamReader.ReadToEnd();
}
catch (Exception)
{
// test case can intentionally send request that will result in exception
tc.ResponseExpected = false;
}
Assert.Single(exportedItems);
var activity = exportedItems[0];
ValidateHttpWebRequestActivity(activity);
Assert.Equal(tc.SpanName, activity.DisplayName);
tc.SpanAttributes = tc.SpanAttributes.ToDictionary(
x => x.Key,
x =>
{
if (x.Key == "network.protocol.version")
{
return "1.1";
}
return HttpTestData.NormalizeValues(x.Value, host, port);
});
foreach (KeyValuePair<string, object> tag in activity.TagObjects)
{
var tagValue = tag.Value.ToString();
if (!tc.SpanAttributes.TryGetValue(tag.Key, out string value))
{
if (tag.Key == SpanAttributeConstants.StatusCodeKey)
{
Assert.Equal(tc.SpanStatus, tagValue);
continue;
}
if (tag.Key == SpanAttributeConstants.StatusDescriptionKey)
{
Assert.Null(tagValue);
continue;
}
if (tag.Key == SemanticConventions.AttributeErrorType)
{
// TODO: Add validation for error.type in test cases.
continue;
}
Assert.Fail($"Tag {tag.Key} was not found in test data.");
}
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)
{
Assert.Single(activity.Events.Where(evt => evt.Name.Equals("exception")));
Assert.True(enrichWithExceptionCalled);
}
}
[Fact]
public void DebugIndividualTest()
{
var input = JsonSerializer.Deserialize<HttpTestData.HttpOutTestCase>(
@"
{
""name"": ""Http version attribute populated"",
""method"": ""GET"",
""url"": ""http://{host}:{port}/"",
""responseCode"": 200,
""spanName"": ""GET"",
""spanStatus"": ""UNSET"",
""spanKind"": ""Client"",
""setHttpFlavor"": true,
""spanAttributes"": {
""url.scheme"": ""http"",
""http.request.method"": ""GET"",
""server.address"": ""{host}"",
""server.port"": ""{port}"",
""network.protocol.version"": ""1.1"",
""http.response.status_code"": ""200"",
""url.full"": ""http://{host}:{port}/""
}
}
",
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
this.HttpOutCallsAreCollectedSuccessfully(input);
}
private static void ValidateHttpWebRequestActivity(Activity activityToValidate)
{
Assert.Equal(ActivityKind.Client, activityToValidate.Kind);
}
}

View File

@ -1,49 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Unit test project for OpenTelemetry HTTP instrumentations</Description>
<TargetFrameworks>$(TargetFrameworksForTests)</TargetFrameworks>
<!-- this is temporary. will remove in future PR. -->
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\CustomTextMapPropagator.cs" Link="/Includes/CustomTextMapPropagator.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\EventSourceTestHelper.cs" Link="/Includes/EventSourceTestHelper.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\InMemoryEventListener.cs" Link="/Includes/InMemoryEventListener.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\SkipUnlessEnvVarFoundTheoryAttribute.cs" Link="/Includes/SkipUnlessEnvVarFoundTheoryAttribute.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\TestEventListener.cs" Link="/Includes/TestEventListener.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\TestHttpServer.cs" Link="/Includes/TestHttpServer.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="System.Text.Json" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="All">
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Reference Include="System.Net.Http" Condition="'$(TargetFramework)' == '$(NetFrameworkMinimumSupportedVersion)'" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="http-out-test-cases.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.Http\OpenTelemetry.Instrumentation.Http.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(RunningDotNetPack)' != 'true'">
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.InMemory\OpenTelemetry.Exporter.InMemory.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(RunningDotNetPack)' == 'true'">
<PackageReference Include="OpenTelemetry.Exporter.InMemory" />
</ItemGroup>
</Project>

View File

@ -1,40 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
#if NETFRAMEWORK
using System.Net.Http;
#endif
namespace OpenTelemetry.Tests;
public class RetryHandler : DelegatingHandler
{
private readonly int maxRetries;
public RetryHandler(HttpMessageHandler innerHandler, int maxRetries)
: base(innerHandler)
{
this.maxRetries = maxRetries;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
HttpResponseMessage response = null;
for (int i = 0; i < this.maxRetries; i++)
{
response?.Dispose();
try
{
response = await base.SendAsync(request, cancellationToken);
}
catch
{
}
}
return response;
}
}

View File

@ -1,340 +0,0 @@
[
{
"name": "Successful GET call to localhost",
"method": "GET",
"url": "http://{host}:{port}/",
"spanName": "GET",
"spanStatus": "Unset",
"responseExpected": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "{host}",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"http.response.status_code": "200",
"url.full": "http://{host}:{port}/"
}
},
{
"name": "Successfully POST call to localhost",
"method": "POST",
"url": "http://{host}:{port}/",
"spanName": "POST",
"spanStatus": "Unset",
"responseExpected": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "POST",
"server.address": "{host}",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"http.response.status_code": "200",
"url.full": "http://{host}:{port}/"
}
},
{
"name": "Name is populated as a path",
"method": "GET",
"url": "http://{host}:{port}/path/to/resource/",
"responseCode": 200,
"spanName": "GET",
"spanStatus": "Unset",
"responseExpected": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "{host}",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"http.response.status_code": "200",
"url.full": "http://{host}:{port}/path/to/resource/"
}
},
{
"name": "URL with fragment",
"method": "GET",
"url": "http://{host}:{port}/path/to/resource#fragment",
"responseCode": 200,
"spanName": "GET",
"spanStatus": "Unset",
"responseExpected": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "{host}",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"http.response.status_code": "200",
"url.full": "http://{host}:{port}/path/to/resource#fragment"
}
},
{
"name": "url.full must not contain username nor password",
"method": "GET",
"url": "http://username:password@{host}:{port}/path/to/resource#fragment",
"responseCode": 200,
"spanName": "GET",
"spanStatus": "Unset",
"responseExpected": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "{host}",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"http.response.status_code": "200",
"url.full": "http://{host}:{port}/path/to/resource#fragment"
}
},
{
"name": "Call that cannot resolve DNS will be reported as error span",
"method": "GET",
"url": "http://sdlfaldfjalkdfjlkajdflkajlsdjf:{port}/",
"spanName": "GET",
"spanStatus": "Error",
"responseExpected": false,
"recordException": false,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "sdlfaldfjalkdfjlkajdflkajlsdjf",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"url.full": "http://sdlfaldfjalkdfjlkajdflkajlsdjf:{port}/"
}
},
{
"name": "Call that cannot resolve DNS will be reported as error span. And Records exception",
"method": "GET",
"url": "http://sdlfaldfjalkdfjlkajdflkajlsdjf:{port}/",
"spanName": "GET",
"spanStatus": "Error",
"responseExpected": false,
"recordException": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "sdlfaldfjalkdfjlkajdflkajlsdjf",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"url.full": "http://sdlfaldfjalkdfjlkajdflkajlsdjf:{port}/"
}
},
{
"name": "Response code: 199. This test case is not possible to implement on some platforms as they don't allow to return this status code. Keeping this test case for visibility, but it actually simply a fallback into 200 test case",
"method": "GET",
"url": "http://{host}:{port}/",
"responseCode": 200,
"spanName": "GET",
"spanStatus": "Unset",
"responseExpected": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "{host}",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"http.response.status_code": "200",
"url.full": "http://{host}:{port}/"
}
},
{
"name": "Response code: 200",
"method": "GET",
"url": "http://{host}:{port}/",
"responseCode": 200,
"spanName": "GET",
"spanStatus": "Unset",
"responseExpected": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "{host}",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"http.response.status_code": "200",
"url.full": "http://{host}:{port}/"
}
},
{
"name": "Response code: 399",
"method": "GET",
"url": "http://{host}:{port}/",
"responseCode": 399,
"spanName": "GET",
"spanStatus": "Unset",
"responseExpected": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "{host}",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"http.response.status_code": "399",
"url.full": "http://{host}:{port}/"
}
},
{
"name": "Response code: 400",
"method": "GET",
"url": "http://{host}:{port}/",
"responseCode": 400,
"spanName": "GET",
"spanStatus": "Error",
"responseExpected": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "{host}",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"http.response.status_code": "400",
"url.full": "http://{host}:{port}/"
}
},
{
"name": "Response code: 401",
"method": "GET",
"url": "http://{host}:{port}/",
"responseCode": 401,
"spanName": "GET",
"spanStatus": "Error",
"responseExpected": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "{host}",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"http.response.status_code": "401",
"url.full": "http://{host}:{port}/"
}
},
{
"name": "Response code: 403",
"method": "GET",
"url": "http://{host}:{port}/",
"responseCode": 403,
"spanName": "GET",
"spanStatus": "Error",
"responseExpected": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "{host}",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"http.response.status_code": "403",
"url.full": "http://{host}:{port}/"
}
},
{
"name": "Response code: 404",
"method": "GET",
"url": "http://{host}:{port}/",
"responseCode": 404,
"spanName": "GET",
"spanStatus": "Error",
"responseExpected": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "{host}",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"http.response.status_code": "404",
"url.full": "http://{host}:{port}/"
}
},
{
"name": "Response code: 429",
"method": "GET",
"url": "http://{host}:{port}/",
"responseCode": 429,
"spanName": "GET",
"spanStatus": "Error",
"responseExpected": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "{host}",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"http.response.status_code": "429",
"url.full": "http://{host}:{port}/"
}
},
{
"name": "Response code: 501",
"method": "GET",
"url": "http://{host}:{port}/",
"responseCode": 501,
"spanName": "GET",
"spanStatus": "Error",
"responseExpected": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "{host}",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"http.response.status_code": "501",
"url.full": "http://{host}:{port}/"
}
},
{
"name": "Response code: 503",
"method": "GET",
"url": "http://{host}:{port}/",
"responseCode": 503,
"spanName": "GET",
"spanStatus": "Error",
"responseExpected": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "{host}",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"http.response.status_code": "503",
"url.full": "http://{host}:{port}/"
}
},
{
"name": "Response code: 504",
"method": "GET",
"url": "http://{host}:{port}/",
"responseCode": 504,
"spanName": "GET",
"spanStatus": "Error",
"responseExpected": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "{host}",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"http.response.status_code": "504",
"url.full": "http://{host}:{port}/"
}
},
{
"name": "Http version attribute populated",
"method": "GET",
"url": "http://{host}:{port}/",
"responseCode": 200,
"spanName": "GET",
"spanStatus": "Unset",
"responseExpected": true,
"spanAttributes": {
"url.scheme": "http",
"http.request.method": "GET",
"server.address": "{host}",
"server.port": "{port}",
"network.protocol.version": "{flavor}",
"http.response.status_code": "200",
"url.full": "http://{host}:{port}/"
}
}
]

View File

@ -17,7 +17,6 @@
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.AspNetCore\OpenTelemetry.Instrumentation.AspNetCore.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.Http\OpenTelemetry.Instrumentation.Http.csproj" />
<ProjectReference Include="$(RepoRoot)\test\TestApp.AspNetCore\TestApp.AspNetCore.csproj" />
</ItemGroup>