[Instrumentation.Http] Move to contrib repository (#5574)
This commit is contained in:
parent
e25d128baf
commit
e2a867f192
|
|
@ -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)" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@
|
|||
|
||||
* Stable:
|
||||
* `OpenTelemetry.Instrumentation.AspNetCore` (`Instrumentation.AspNetCore-`)
|
||||
* `OpenTelemetry.Instrumentation.Http` (`Instrumentation.Http-`)
|
||||
|
||||
* Unstable:
|
||||
* `OpenTelemetry.Instrumentation.GrpcNetClient` (`Instrumentation.GrpcNetClient-`)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,362 +0,0 @@
|
|||
# HttpClient and HttpWebRequest instrumentation for OpenTelemetry
|
||||
|
||||
[](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.Http)
|
||||
[](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/)
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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}/"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue