ZipkinExporter: HttpClientFactory option (#2654)

This commit is contained in:
Mikel Blanchard 2021-11-22 21:06:39 -08:00 committed by GitHub
parent ec67afe6db
commit bc0e8afd96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 283 additions and 40 deletions

View File

@ -56,38 +56,33 @@ namespace OpenTelemetry.Trace
{
configure?.Invoke(options);
if (options.Protocol == JaegerExportProtocol.HttpBinaryThrift && options.HttpClientFactory == null)
if (serviceProvider != null
&& options.Protocol == JaegerExportProtocol.HttpBinaryThrift
&& options.HttpClientFactory == JaegerExporterOptions.DefaultHttpClientFactory)
{
if (serviceProvider != null)
options.HttpClientFactory = () =>
{
options.HttpClientFactory = () =>
Type httpClientFactoryType = Type.GetType("System.Net.Http.IHttpClientFactory, Microsoft.Extensions.Http", throwOnError: false);
if (httpClientFactoryType != null)
{
Type httpClientFactoryType = Type.GetType("System.Net.Http.IHttpClientFactory, Microsoft.Extensions.Http", throwOnError: false);
if (httpClientFactoryType != null)
object httpClientFactory = serviceProvider.GetService(httpClientFactoryType);
if (httpClientFactory != null)
{
object httpClientFactory = serviceProvider.GetService(httpClientFactoryType);
if (httpClientFactory != null)
MethodInfo createClientMethod = httpClientFactoryType.GetMethod(
"CreateClient",
BindingFlags.Public | BindingFlags.Instance,
binder: null,
new Type[] { typeof(string) },
modifiers: null);
if (createClientMethod != null)
{
MethodInfo createClientMethod = httpClientFactoryType.GetMethod(
"CreateClient",
BindingFlags.Public | BindingFlags.Instance,
binder: null,
new Type[] { typeof(string) },
modifiers: null);
if (createClientMethod != null)
{
return (HttpClient)createClientMethod.Invoke(httpClientFactory, new object[] { "JaegerExporter" });
}
return (HttpClient)createClientMethod.Invoke(httpClientFactory, new object[] { "JaegerExporter" });
}
}
}
return new HttpClient();
};
}
else
{
options.HttpClientFactory = () => new HttpClient();
}
return new HttpClient();
};
}
var jaegerExporter = new JaegerExporter(options);

View File

@ -40,6 +40,8 @@ namespace OpenTelemetry.Exporter
internal const string OTelAgentPortEnvVarKey = "OTEL_EXPORTER_JAEGER_AGENT_PORT";
internal const string OTelEndpointEnvVarKey = "OTEL_EXPORTER_JAEGER_ENDPOINT";
internal static readonly Func<HttpClient> DefaultHttpClientFactory = () => new HttpClient();
public JaegerExporterOptions()
{
if (EnvironmentVariableHelper.LoadString(OtelProtocolEnvVarKey, out string protocolEnvVar)
@ -115,9 +117,9 @@ namespace OpenTelemetry.Exporter
/// instance can be resolved through the application <see
/// cref="IServiceProvider"/> then an <see cref="HttpClient"/> will be
/// created through the factory with the name "JaegerExporter" otherwise
/// an <see cref="HttpClient"/> will be instantiated directly."/></item>
/// an <see cref="HttpClient"/> will be instantiated directly.</item>
/// </list>
/// </remarks>
public Func<HttpClient> HttpClientFactory { get; set; }
public Func<HttpClient> HttpClientFactory { get; set; } = DefaultHttpClientFactory;
}
}

View File

@ -0,0 +1,2 @@
OpenTelemetry.Exporter.ZipkinExporterOptions.HttpClientFactory.get -> System.Func<System.Net.Http.HttpClient>
OpenTelemetry.Exporter.ZipkinExporterOptions.HttpClientFactory.set -> void

View File

@ -0,0 +1,2 @@
OpenTelemetry.Exporter.ZipkinExporterOptions.HttpClientFactory.get -> System.Func<System.Net.Http.HttpClient>
OpenTelemetry.Exporter.ZipkinExporterOptions.HttpClientFactory.set -> void

View File

@ -0,0 +1,2 @@
OpenTelemetry.Exporter.ZipkinExporterOptions.HttpClientFactory.get -> System.Func<System.Net.Http.HttpClient>
OpenTelemetry.Exporter.ZipkinExporterOptions.HttpClientFactory.set -> void

View File

@ -10,6 +10,9 @@ Released 2021-Nov-19
`FormatException` if it fails to parse any of the supported environment
variables.
* Added `HttpClientFactory` option
([#2654](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2654))
## 1.2.0-beta1
Released 2021-Oct-08

View File

@ -26,19 +26,29 @@ take precedence over the environment variables.
### Configuration using Properties
* `BatchExportProcessorOptions`: Configuration options for the batch exporter.
Only used if ExportProcessorType is set to Batch.
* `Endpoint`: URI address to receive telemetry (default
`http://localhost:9411/api/v2/spans`).
* `ExportProcessorType`: Whether the exporter should use [Batch or Simple
exporting
processor](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#built-in-span-processors).
* `HttpClientFactory`: A factory function called to create the `HttpClient`
instance that will be used at runtime to transmit spans over HTTP. See
[Configure HttpClient](#configure-httpclient) for more details.
* `MaxPayloadSizeInBytes`: Maximum payload size - for .NET versions **other**
than 4.5.2 (default 4096).
* `ServiceName`: Name of the service reporting telemetry. If the `Resource`
associated with the telemetry has "service.name" defined, then it'll be
preferred over this option.
* `Endpoint`: URI address to receive telemetry (default `http://localhost:9411/api/v2/spans`).
* `UseShortTraceIds`: Whether the trace's ID should be shortened before
sending to Zipkin (default false).
* `MaxPayloadSizeInBytes`: Maximum payload size - for .NET versions
**other** than 4.5.2 (default 4096).
* `ExportProcessorType`: Whether the exporter should use
[Batch or Simple exporting processor](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#built-in-span-processors)
.
* `BatchExportProcessorOptions`: Configuration options for the batch exporter.
Only used if ExportProcessorType is set to Batch.
* `UseShortTraceIds`: Whether the trace's ID should be shortened before sending
to Zipkin (default false).
See
[`TestZipkinExporter.cs`](../../examples/Console/TestZipkinExporter.cs)
@ -65,6 +75,38 @@ values of the `ZipkinExporterOptions`.
`FormatException` is thrown in case of an invalid value for any of the
supported environment variables.
## Configure HttpClient
The `HttpClientFactory` option is provided on `ZipkinExporterOptions` for users
who want to configure the `HttpClient` used by the `ZipkinExporter`. Simply
replace the function with your own implementation if you want to customize the
generated `HttpClient`:
```csharp
services.AddOpenTelemetryTracing((builder) => builder
.AddZipkinExporter(o => o.HttpClientFactory = () =>
{
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value");
return client;
}));
```
For users using
[IHttpClientFactory](https://docs.microsoft.com/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests)
you may also customize the named "ZipkinExporter" `HttpClient` using the
built-in `AddHttpClient` extension:
```csharp
services.AddHttpClient(
"ZipkinExporter",
configureClient: (client) =>
client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value"));
```
Note: The single instance returned by `HttpClientFactory` is reused by all
export requests.
## References
* [OpenTelemetry Project](https://opentelemetry.io/)

View File

@ -52,7 +52,7 @@ namespace OpenTelemetry.Exporter
this.options = options;
this.maxPayloadSizeInBytes = (!options.MaxPayloadSizeInBytes.HasValue || options.MaxPayloadSizeInBytes <= 0) ? ZipkinExporterOptions.DefaultMaxPayloadSizeInBytes : options.MaxPayloadSizeInBytes.Value;
this.httpClient = client ?? new HttpClient();
this.httpClient = client ?? options.HttpClientFactory?.Invoke() ?? throw new InvalidOperationException("ZipkinExporter was missing HttpClientFactory or it returned null.");
}
internal ZipkinEndpoint LocalEndpoint { get; private set; }

View File

@ -15,6 +15,8 @@
// </copyright>
using System;
using System.Net.Http;
using System.Reflection;
using OpenTelemetry.Exporter;
using OpenTelemetry.Internal;
@ -40,17 +42,48 @@ namespace OpenTelemetry.Trace
{
return deferredTracerProviderBuilder.Configure((sp, builder) =>
{
AddZipkinExporter(builder, sp.GetOptions<ZipkinExporterOptions>(), configure);
AddZipkinExporter(builder, sp.GetOptions<ZipkinExporterOptions>(), configure, sp);
});
}
return AddZipkinExporter(builder, new ZipkinExporterOptions(), configure);
return AddZipkinExporter(builder, new ZipkinExporterOptions(), configure, serviceProvider: null);
}
private static TracerProviderBuilder AddZipkinExporter(TracerProviderBuilder builder, ZipkinExporterOptions options, Action<ZipkinExporterOptions> configure = null)
private static TracerProviderBuilder AddZipkinExporter(
TracerProviderBuilder builder,
ZipkinExporterOptions options,
Action<ZipkinExporterOptions> configure,
IServiceProvider serviceProvider)
{
configure?.Invoke(options);
if (serviceProvider != null && options.HttpClientFactory == ZipkinExporterOptions.DefaultHttpClientFactory)
{
options.HttpClientFactory = () =>
{
Type httpClientFactoryType = Type.GetType("System.Net.Http.IHttpClientFactory, Microsoft.Extensions.Http", throwOnError: false);
if (httpClientFactoryType != null)
{
object httpClientFactory = serviceProvider.GetService(httpClientFactoryType);
if (httpClientFactory != null)
{
MethodInfo createClientMethod = httpClientFactoryType.GetMethod(
"CreateClient",
BindingFlags.Public | BindingFlags.Instance,
binder: null,
new Type[] { typeof(string) },
modifiers: null);
if (createClientMethod != null)
{
return (HttpClient)createClientMethod.Invoke(httpClientFactory, new object[] { "ZipkinExporter" });
}
}
}
return new HttpClient();
};
}
var zipkinExporter = new ZipkinExporter(options);
if (options.ExportProcessorType == ExportProcessorType.Simple)

View File

@ -16,6 +16,7 @@
using System;
using System.Diagnostics;
using System.Net.Http;
using OpenTelemetry.Internal;
using OpenTelemetry.Trace;
@ -36,6 +37,8 @@ namespace OpenTelemetry.Exporter
internal const string ZipkinEndpointEnvVar = "OTEL_EXPORTER_ZIPKIN_ENDPOINT";
internal const string DefaultZipkinEndpoint = "http://localhost:9411/api/v2/spans";
internal static readonly Func<HttpClient> DefaultHttpClientFactory = () => new HttpClient();
/// <summary>
/// Initializes a new instance of the <see cref="ZipkinExporterOptions"/> class.
/// Initializes zipkin endpoint.
@ -73,5 +76,23 @@ namespace OpenTelemetry.Exporter
/// Gets or sets the BatchExportProcessor options. Ignored unless ExportProcessorType is BatchExporter.
/// </summary>
public BatchExportProcessorOptions<Activity> BatchExportProcessorOptions { get; set; } = new BatchExportActivityProcessorOptions();
/// <summary>
/// Gets or sets the factory function called to create the <see
/// cref="HttpClient"/> instance that will be used at runtime to
/// transmit spans over HTTP. The returned instance will be reused for
/// all export invocations.
/// </summary>
/// <remarks>
/// Note: The default behavior when using the <see
/// cref="ZipkinExporterHelperExtensions.AddZipkinExporter(TracerProviderBuilder,
/// Action{ZipkinExporterOptions})"/> extension is if an <a
/// href="https://docs.microsoft.com/dotnet/api/system.net.http.ihttpclientfactory">IHttpClientFactory</a>
/// instance can be resolved through the application <see
/// cref="IServiceProvider"/> then an <see cref="HttpClient"/> will be
/// created through the factory with the name "ZipkinExporter" otherwise
/// an <see cref="HttpClient"/> will be instantiated directly.
/// </remarks>
public Func<HttpClient> HttpClientFactory { get; set; } = DefaultHttpClientFactory;
}
}

View File

@ -18,6 +18,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Exporter.Jaeger.Implementation;
using OpenTelemetry.Exporter.Jaeger.Implementation.Tests;
using OpenTelemetry.Resources;
@ -43,6 +44,71 @@ namespace OpenTelemetry.Exporter.Jaeger.Tests
Assert.NotNull(jaegerTraceExporter);
}
[Fact]
public void UserHttpFactoryCalled()
{
JaegerExporterOptions options = new JaegerExporterOptions();
var defaultFactory = options.HttpClientFactory;
int invocations = 0;
options.Protocol = JaegerExportProtocol.HttpBinaryThrift;
options.HttpClientFactory = () =>
{
invocations++;
return defaultFactory();
};
using (var exporter = new JaegerExporter(options))
{
Assert.Equal(1, invocations);
}
using (var provider = Sdk.CreateTracerProviderBuilder()
.AddJaegerExporter(o =>
{
o.Protocol = JaegerExportProtocol.HttpBinaryThrift;
o.HttpClientFactory = options.HttpClientFactory;
})
.Build())
{
Assert.Equal(2, invocations);
}
options.HttpClientFactory = null;
Assert.Throws<InvalidOperationException>(() =>
{
using var exporter = new JaegerExporter(options);
});
options.HttpClientFactory = () => null;
Assert.Throws<InvalidOperationException>(() =>
{
using var exporter = new JaegerExporter(options);
});
}
[Fact]
public void ServiceProviderHttpClientFactoryInvoked()
{
IServiceCollection services = new ServiceCollection();
services.AddHttpClient();
int invocations = 0;
services.AddHttpClient("JaegerExporter", configureClient: (client) => invocations++);
services.AddOpenTelemetryTracing(builder => builder.AddJaegerExporter(
o => o.Protocol = JaegerExportProtocol.HttpBinaryThrift));
using var serviceProvider = services.BuildServiceProvider();
var tracerProvider = serviceProvider.GetRequiredService<TracerProvider>();
Assert.Equal(1, invocations);
}
[Fact]
public void JaegerTraceExporter_SetResource_UpdatesServiceName()
{

View File

@ -16,10 +16,13 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<DotNetCliToolReference Include="dotnet-xunit" Version="$(DotNetXUnitCliVer)" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="$(MicrosoftExtensionsHostingPkgVer)" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.20" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Jaeger\OpenTelemetry.Exporter.Jaeger.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Extensions.Hosting\OpenTelemetry.Extensions.Hosting.csproj" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\EventSourceTestHelper.cs" Link="Includes\EventSourceTestHelper.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\TestActivityProcessor.cs" Link="Includes\TestActivityProcessor.cs" />

View File

@ -22,10 +22,13 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<DotNetCliToolReference Include="dotnet-xunit" Version="$(DotNetXUnitCliVer)" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="$(MicrosoftExtensionsHostingPkgVer)" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.20" />
</ItemGroup>
<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>

View File

@ -21,7 +21,9 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Exporter.Zipkin.Implementation;
using OpenTelemetry.Resources;
using OpenTelemetry.Tests;
@ -183,6 +185,73 @@ namespace OpenTelemetry.Exporter.Zipkin.Tests
}
}
[Fact]
public void UserHttpFactoryCalled()
{
ZipkinExporterOptions options = new ZipkinExporterOptions();
var defaultFactory = options.HttpClientFactory;
int invocations = 0;
options.HttpClientFactory = () =>
{
invocations++;
return defaultFactory();
};
using (var exporter = new ZipkinExporter(options))
{
Assert.Equal(1, invocations);
}
using (var provider = Sdk.CreateTracerProviderBuilder()
.AddZipkinExporter(o => o.HttpClientFactory = options.HttpClientFactory)
.Build())
{
Assert.Equal(2, invocations);
}
using var client = new HttpClient();
using (var exporter = new ZipkinExporter(options, client))
{
// Factory not called when client is passed as a param.
Assert.Equal(2, invocations);
}
options.HttpClientFactory = null;
Assert.Throws<InvalidOperationException>(() =>
{
using var exporter = new ZipkinExporter(options);
});
options.HttpClientFactory = () => null;
Assert.Throws<InvalidOperationException>(() =>
{
using var exporter = new ZipkinExporter(options);
});
}
[Fact]
public void ServiceProviderHttpClientFactoryInvoked()
{
IServiceCollection services = new ServiceCollection();
services.AddHttpClient();
int invocations = 0;
services.AddHttpClient("ZipkinExporter", configureClient: (client) => invocations++);
services.AddOpenTelemetryTracing(builder => builder.AddZipkinExporter());
using var serviceProvider = services.BuildServiceProvider();
var tracerProvider = serviceProvider.GetRequiredService<TracerProvider>();
Assert.Equal(1, invocations);
}
[Theory]
[InlineData(true, false, false)]
[InlineData(false, false, false)]