Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OtlpExporter: HttpClientFactory option #2696

Merged
merged 14 commits into from
Nov 29, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/OpenTelemetry.Exporter.Jaeger/README.md
Original file line number Diff line number Diff line change
@@ -96,11 +96,15 @@ implementation if you want to customize the generated `HttpClient`:

```csharp
services.AddOpenTelemetryTracing((builder) => builder
.AddJaegerExporter(o => o.HttpClientFactory = () =>
.AddJaegerExporter(o =>
{
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value");
return client;
o.Protocol = JaegerExportProtocol.HttpBinaryThrift;
o.HttpClientFactory = () =>
{
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value");
return client;
};
}));
```

Original file line number Diff line number Diff line change
@@ -13,6 +13,8 @@ OpenTelemetry.Exporter.OtlpExporterOptions.MetricReaderType.get -> OpenTelemetry
OpenTelemetry.Exporter.OtlpExporterOptions.MetricReaderType.set -> void
OpenTelemetry.Exporter.OtlpExporterOptions.PeriodicExportingMetricReaderOptions.get -> OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions
OpenTelemetry.Exporter.OtlpExporterOptions.PeriodicExportingMetricReaderOptions.set -> void
OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.get -> System.Func<System.Net.Http.HttpClient>
OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.set -> void
OpenTelemetry.Exporter.OtlpExporterOptions.OtlpExporterOptions() -> void
OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.get -> OpenTelemetry.Exporter.OtlpExportProtocol
OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.set -> void
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@ OpenTelemetry.Exporter.OtlpExporterOptions.MetricReaderType.get -> OpenTelemetry
OpenTelemetry.Exporter.OtlpExporterOptions.MetricReaderType.set -> void
OpenTelemetry.Exporter.OtlpExporterOptions.PeriodicExportingMetricReaderOptions.get -> OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions
OpenTelemetry.Exporter.OtlpExporterOptions.PeriodicExportingMetricReaderOptions.set -> void
OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.get -> System.Func<System.Net.Http.HttpClient>
OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.set -> void
OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.get -> OpenTelemetry.Exporter.OtlpExportProtocol
OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.set -> void
OpenTelemetry.Exporter.OtlpExportProtocol
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@ OpenTelemetry.Exporter.OtlpExporterOptions.MetricReaderType.get -> OpenTelemetry
OpenTelemetry.Exporter.OtlpExporterOptions.MetricReaderType.set -> void
OpenTelemetry.Exporter.OtlpExporterOptions.PeriodicExportingMetricReaderOptions.get -> OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions
OpenTelemetry.Exporter.OtlpExporterOptions.PeriodicExportingMetricReaderOptions.set -> void
OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.get -> System.Func<System.Net.Http.HttpClient>
OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.set -> void
OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.get -> OpenTelemetry.Exporter.OtlpExportProtocol
OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.set -> void
OpenTelemetry.Exporter.OtlpExportProtocol
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@ OpenTelemetry.Exporter.OtlpExporterOptions.MetricReaderType.get -> OpenTelemetry
OpenTelemetry.Exporter.OtlpExporterOptions.MetricReaderType.set -> void
OpenTelemetry.Exporter.OtlpExporterOptions.PeriodicExportingMetricReaderOptions.get -> OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions
OpenTelemetry.Exporter.OtlpExporterOptions.PeriodicExportingMetricReaderOptions.set -> void
OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.get -> System.Func<System.Net.Http.HttpClient>
OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.set -> void
OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.get -> OpenTelemetry.Exporter.OtlpExportProtocol
OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.set -> void
OpenTelemetry.Exporter.OtlpExportProtocol
Original file line number Diff line number Diff line change
@@ -9,6 +9,10 @@
will now include the endpoint uri as well.
([#2686](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2686))

* Support `HttpProtobuf` protocol with metrics & added `HttpClientFactory`
option
([#2696](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2696))

## 1.2.0-beta2

Released 2021-Nov-19
Original file line number Diff line number Diff line change
@@ -14,7 +14,6 @@
// limitations under the License.
// </copyright>

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
@@ -26,14 +25,15 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClie
/// <typeparam name="TRequest">Type of export request.</typeparam>
internal abstract class BaseOtlpHttpExportClient<TRequest> : IExportClient<TRequest>
{
protected BaseOtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpClient = null)
protected BaseOtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpClient)
{
Guard.Null(options, nameof(options));
Guard.Null(httpClient, nameof(httpClient));
Guard.InvalidTimeout(options.TimeoutMilliseconds, $"{nameof(options)}.{nameof(options.TimeoutMilliseconds)}");

this.Options = options;
this.Headers = options.GetHeaders<Dictionary<string, string>>((d, k, v) => d.Add(k, v));
this.HttpClient = httpClient ?? new HttpClient { Timeout = TimeSpan.FromMilliseconds(this.Options.TimeoutMilliseconds) };
this.HttpClient = httpClient;
}

internal OtlpExporterOptions Options { get; }
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// <copyright file="OtlpHttpMetricsExportClient.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>

using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
#if NET5_0_OR_GREATER
using System.Threading;
#endif
using System.Threading.Tasks;
using Google.Protobuf;
using OtlpCollector = Opentelemetry.Proto.Collector.Metrics.V1;

namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient
{
/// <summary>Class for sending OTLP metrics export request over HTTP.</summary>
internal sealed class OtlpHttpMetricsExportClient : BaseOtlpHttpExportClient<OtlpCollector.ExportMetricsServiceRequest>
CodeBlanch marked this conversation as resolved.
Show resolved Hide resolved
{
internal const string MediaContentType = "application/x-protobuf";
private readonly Uri exportMetricsUri;

public OtlpHttpMetricsExportClient(OtlpExporterOptions options, HttpClient httpClient)
: base(options, httpClient)
{
this.exportMetricsUri = this.Options.Endpoint;
}

protected override HttpRequestMessage CreateHttpRequest(OtlpCollector.ExportMetricsServiceRequest exportRequest)
{
var request = new HttpRequestMessage(HttpMethod.Post, this.exportMetricsUri);
foreach (var header in this.Headers)
{
request.Headers.Add(header.Key, header.Value);
}

request.Content = new ExportRequestContent(exportRequest);

return request;
}

internal sealed class ExportRequestContent : HttpContent
{
private static readonly MediaTypeHeaderValue ProtobufMediaTypeHeader = new MediaTypeHeaderValue(MediaContentType);

private readonly OtlpCollector.ExportMetricsServiceRequest exportRequest;

public ExportRequestContent(OtlpCollector.ExportMetricsServiceRequest exportRequest)
{
this.exportRequest = exportRequest;
this.Headers.ContentType = ProtobufMediaTypeHeader;
}

#if NET5_0_OR_GREATER
protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken)
{
this.SerializeToStreamInternal(stream);
}
#endif

protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
{
this.SerializeToStreamInternal(stream);
return Task.CompletedTask;
}

protected override bool TryComputeLength(out long length)
{
// We can't know the length of the content being pushed to the output stream.
length = -1;
return false;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SerializeToStreamInternal(Stream stream)
{
this.exportRequest.WriteTo(stream);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -35,7 +35,7 @@ internal sealed class OtlpHttpTraceExportClient : BaseOtlpHttpExportClient<OtlpC
internal const string MediaContentType = "application/x-protobuf";
private readonly Uri exportTracesUri;

public OtlpHttpTraceExportClient(OtlpExporterOptions options, HttpClient httpClient = null)
public OtlpHttpTraceExportClient(OtlpExporterOptions options, HttpClient httpClient)
: base(options, httpClient)
{
this.exportTracesUri = this.Options.Endpoint;
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@

using System;
using System.Diagnostics;
using System.Net.Http;
using OpenTelemetry.Internal;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
@@ -41,6 +42,8 @@ public class OtlpExporterOptions
internal const string TracesExportPath = "v1/traces";
internal const string MetricsExportPath = "v1/metrics";

internal readonly Func<HttpClient> DefaultHttpClientFactory;

/// <summary>
/// Initializes a new instance of the <see cref="OtlpExporterOptions"/> class.
/// </summary>
@@ -73,6 +76,14 @@ public OtlpExporterOptions()
throw new FormatException($"{ProtocolEnvVarName} environment variable has an invalid value: '${protocolEnvVar}'");
}
}

this.HttpClientFactory = this.DefaultHttpClientFactory = () =>
{
return new HttpClient()
{
Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds),
};
};
}

/// <summary>
@@ -123,5 +134,38 @@ public OtlpExporterOptions()
/// and Sum metrics.
/// </summary>
public AggregationTemporality AggregationTemporality { get; set; } = AggregationTemporality.Cumulative;

/// <summary>
/// Gets or sets the factory function called to create the <see
/// cref="HttpClient"/> instance that will be used at runtime to
/// transmit telemetry over HTTP. The returned instance will be reused
/// for all export invocations.
/// </summary>
/// <remarks>
/// Notes:
/// <list type="bullet">
/// <item>This is only invoked for the <see
/// cref="OtlpExportProtocol.HttpProtobuf"/> protocol.</item>
/// <item>The default behavior when using the <see
/// cref="OtlpTraceExporterHelperExtensions.AddOtlpExporter(TracerProviderBuilder,
/// Action{OtlpExporterOptions})"/> 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 "OtlpTraceExporter"
/// otherwise an <see cref="HttpClient"/> will be instantiated
/// directly.</item>
/// <item>The default behavior when using the <see
/// cref="OtlpMetricExporterExtensions.AddOtlpExporter(MeterProviderBuilder,
/// Action{OtlpExporterOptions})"/> 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 "OtlpMetricExporter"
/// otherwise an <see cref="HttpClient"/> will be instantiated
/// directly.</item>
/// </list>
/// </remarks>
public Func<HttpClient> HttpClientFactory { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -15,13 +15,16 @@
// </copyright>

using System;
using System.Net.Http;
using System.Reflection;
using Grpc.Core;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
using OpenTelemetry.Internal;
#if NETSTANDARD2_1 || NET5_0_OR_GREATER
using Grpc.Net.Client;
#endif
using OtlpCollector = Opentelemetry.Proto.Collector.Trace.V1;
using MetricsOtlpCollector = Opentelemetry.Proto.Collector.Metrics.V1;
using TraceOtlpCollector = Opentelemetry.Proto.Collector.Trace.V1;

namespace OpenTelemetry.Exporter
{
@@ -88,11 +91,23 @@ public static THeaders GetHeaders<THeaders>(this OtlpExporterOptions options, Ac
return headers;
}

public static IExportClient<OtlpCollector.ExportTraceServiceRequest> GetTraceExportClient(this OtlpExporterOptions options) =>
public static IExportClient<TraceOtlpCollector.ExportTraceServiceRequest> GetTraceExportClient(this OtlpExporterOptions options) =>
options.Protocol switch
{
OtlpExportProtocol.Grpc => new OtlpGrpcTraceExportClient(options),
OtlpExportProtocol.HttpProtobuf => new OtlpHttpTraceExportClient(options),
OtlpExportProtocol.HttpProtobuf => new OtlpHttpTraceExportClient(
options,
options.HttpClientFactory?.Invoke() ?? throw new InvalidOperationException("OtlpExporterOptions was missing HttpClientFactory or it returned null.")),
_ => throw new NotSupportedException($"Protocol {options.Protocol} is not supported."),
};

public static IExportClient<MetricsOtlpCollector.ExportMetricsServiceRequest> GetMetricsExportClient(this OtlpExporterOptions options) =>
options.Protocol switch
{
OtlpExportProtocol.Grpc => new OtlpGrpcMetricsExportClient(options),
OtlpExportProtocol.HttpProtobuf => new OtlpHttpMetricsExportClient(
options,
options.HttpClientFactory?.Invoke() ?? throw new InvalidOperationException("OtlpExporterOptions was missing HttpClientFactory or it returned null.")),
_ => throw new NotSupportedException($"Protocol {options.Protocol} is not supported."),
};

@@ -104,6 +119,42 @@ public static THeaders GetHeaders<THeaders>(this OtlpExporterOptions options, Ac
_ => null,
};

public static void TryEnableIHttpClientFactoryIntegration(this OtlpExporterOptions options, IServiceProvider serviceProvider, string httpClientName)
{
if (serviceProvider != null
&& options.Protocol == OtlpExportProtocol.HttpProtobuf
&& options.HttpClientFactory == options.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)
{
HttpClient client = (HttpClient)createClientMethod.Invoke(httpClientFactory, new object[] { httpClientName });

client.Timeout = TimeSpan.FromMilliseconds(options.TimeoutMilliseconds);

return client;
}
}
}

return options.DefaultHttpClientFactory();
};
}
}

internal static void AppendExportPath(this OtlpExporterOptions options, Uri initialEndpoint, string exportRelativePath)
{
// The exportRelativePath is only appended when the options.Endpoint property wasn't set by the user,
Original file line number Diff line number Diff line change
@@ -55,8 +55,7 @@ internal OtlpMetricExporter(OtlpExporterOptions options, IExportClient<OtlpColle
}
else
{
// TODO: this instantiation should be aligned with the protocol option (grpc or http/protobuf) when OtlpHttpMetricsExportClient will be implemented.
this.exportClient = new OtlpGrpcMetricsExportClient(options);
this.exportClient = options.GetMetricsExportClient();
}
}

Original file line number Diff line number Diff line change
@@ -39,19 +39,25 @@ public static MeterProviderBuilder AddOtlpExporter(this MeterProviderBuilder bui
{
return deferredMeterProviderBuilder.Configure((sp, builder) =>
{
AddOtlpExporter(builder, sp.GetOptions<OtlpExporterOptions>(), configure);
AddOtlpExporter(builder, sp.GetOptions<OtlpExporterOptions>(), configure, sp);
});
}

return AddOtlpExporter(builder, new OtlpExporterOptions(), configure);
return AddOtlpExporter(builder, new OtlpExporterOptions(), configure, serviceProvider: null);
}

private static MeterProviderBuilder AddOtlpExporter(MeterProviderBuilder builder, OtlpExporterOptions options, Action<OtlpExporterOptions> configure = null)
private static MeterProviderBuilder AddOtlpExporter(
MeterProviderBuilder builder,
OtlpExporterOptions options,
Action<OtlpExporterOptions> configure,
IServiceProvider serviceProvider)
{
var initialEndpoint = options.Endpoint;

configure?.Invoke(options);

options.TryEnableIHttpClientFactoryIntegration(serviceProvider, "OtlpMetricExporter");

options.AppendExportPath(initialEndpoint, OtlpExporterOptions.MetricsExportPath);

var metricExporter = new OtlpMetricExporter(options);
Original file line number Diff line number Diff line change
@@ -39,19 +39,25 @@ public static TracerProviderBuilder AddOtlpExporter(this TracerProviderBuilder b
{
return deferredTracerProviderBuilder.Configure((sp, builder) =>
{
AddOtlpExporter(builder, sp.GetOptions<OtlpExporterOptions>(), configure);
AddOtlpExporter(builder, sp.GetOptions<OtlpExporterOptions>(), configure, sp);
});
}

return AddOtlpExporter(builder, new OtlpExporterOptions(), configure);
return AddOtlpExporter(builder, new OtlpExporterOptions(), configure, serviceProvider: null);
}

private static TracerProviderBuilder AddOtlpExporter(TracerProviderBuilder builder, OtlpExporterOptions exporterOptions, Action<OtlpExporterOptions> configure = null)
private static TracerProviderBuilder AddOtlpExporter(
TracerProviderBuilder builder,
OtlpExporterOptions exporterOptions,
Action<OtlpExporterOptions> configure,
IServiceProvider serviceProvider)
{
var originalEndpoint = exporterOptions.Endpoint;

configure?.Invoke(exporterOptions);

exporterOptions.TryEnableIHttpClientFactoryIntegration(serviceProvider, "OtlpTraceExporter");

exporterOptions.AppendExportPath(originalEndpoint, OtlpExporterOptions.TracesExportPath);

var otlpExporter = new OtlpTraceExporter(exporterOptions);
58 changes: 52 additions & 6 deletions src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md
Original file line number Diff line number Diff line change
@@ -24,19 +24,28 @@ setters take precedence over the environment variables.

## Options Properties

* `BatchExportProcessorOptions`: Configuration options for the batch exporter.
Only used if ExportProcessorType is set to Batch.

* `Endpoint`: Target to which the exporter is going to send traces or metrics.
The endpoint must be a valid Uri with scheme (http or https) and host, and MAY
contain a port and path.

* `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).

* `Headers`: Optional headers for the connection.

* `HttpClientFactory`: A factory function called to create the `HttpClient`
instance that will be used at runtime to transmit telemetry over HTTP when the
`HttpProtobuf` protocol is configured. See [Configure
HttpClient](#configure-httpclient) for more details.

* `TimeoutMilliseconds` : Max waiting time for the backend to process a batch.

* `Protocol`: OTLP transport protocol. Supported values:
`OtlpExportProtocol.Grpc` and `OtlpExportProtocol.HttpProtobuf`.
* `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.

See the [`TestOtlpExporter.cs`](../../examples/Console/TestOtlpExporter.cs) for
an example of how to use the exporter.
@@ -81,6 +90,43 @@ See
[this](https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client)
for more information.

## Configure HttpClient

The `HttpClientFactory` option is provided on `OtlpExporterOptions` for users
who want to configure the `HttpClient` used by the `OtlpTraceExporter` and/or
`OtlpMetricExporter` when `HttpProtobuf` protocol is used. Simply replace the
function with your own implementation if you want to customize the generated
`HttpClient`:

```csharp
services.AddOpenTelemetryTracing((builder) => builder
.AddOtlpExporter(o =>
{
o.Protocol = OtlpExportProtocol.HttpProtobuf;
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 "OtlpTraceExporter" or "OtlpMetricExporter"
`HttpClient` using the built-in `AddHttpClient` extension:

```csharp
services.AddHttpClient(
"OtlpTraceExporter",
configureClient: (client) =>
client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value"));
```

Note: The single instance returned by `HttpClientFactory` is reused by all
export requests.

## References

* [OpenTelemetry
2 changes: 1 addition & 1 deletion test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -75,7 +75,7 @@ public void GlobalSetup()
};
this.exporter = new OtlpTraceExporter(
options,
new OtlpHttpTraceExportClient(options));
new OtlpHttpTraceExportClient(options, options.HttpClientFactory()));

this.activity = ActivityHelper.CreateTestActivity();
this.activityBatch = new CircularBuffer<Activity>(this.NumberOfSpans);
Original file line number Diff line number Diff line change
@@ -59,7 +59,7 @@ public void NewOtlpHttpTraceExportClient_OtlpExporterOptions_ExporterHasCorrectP
Headers = $"{header1.Name}={header1.Value}, {header2.Name} = {header2.Value}",
};

var client = new OtlpHttpTraceExportClient(options);
var client = new OtlpHttpTraceExportClient(options, options.HttpClientFactory());

Assert.NotNull(client.HttpClient);

Original file line number Diff line number Diff line change
@@ -15,10 +15,13 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="$(MicrosoftExtensionsHostingPkgVer)" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.20" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.OpenTelemetryProtocol\OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Extensions.Hosting\OpenTelemetry.Extensions.Hosting.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.InMemory\OpenTelemetry.Exporter.InMemory.csproj" />
</ItemGroup>

Original file line number Diff line number Diff line change
@@ -14,9 +14,11 @@
// limitations under the License.
// </copyright>

using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
@@ -30,6 +32,71 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests
{
public class OtlpMetricsExporterTests
{
[Fact]
public void UserHttpFactoryCalled()
{
OtlpExporterOptions options = new OtlpExporterOptions();

var defaultFactory = options.HttpClientFactory;

int invocations = 0;
options.Protocol = OtlpExportProtocol.HttpProtobuf;
options.HttpClientFactory = () =>
{
invocations++;
return defaultFactory();
};

using (var exporter = new OtlpMetricExporter(options))
{
Assert.Equal(1, invocations);
}

using (var provider = Sdk.CreateMeterProviderBuilder()
.AddOtlpExporter(o =>
{
o.Protocol = OtlpExportProtocol.HttpProtobuf;
o.HttpClientFactory = options.HttpClientFactory;
})
.Build())
{
Assert.Equal(2, invocations);
}

options.HttpClientFactory = null;
Assert.Throws<InvalidOperationException>(() =>
{
using var exporter = new OtlpMetricExporter(options);
});

options.HttpClientFactory = () => null;
Assert.Throws<InvalidOperationException>(() =>
{
using var exporter = new OtlpMetricExporter(options);
});
}

[Fact]
public void ServiceProviderHttpClientFactoryInvoked()
{
IServiceCollection services = new ServiceCollection();

services.AddHttpClient();

int invocations = 0;

services.AddHttpClient("OtlpMetricExporter", configureClient: (client) => invocations++);

services.AddOpenTelemetryMetrics(builder => builder.AddOtlpExporter(
o => o.Protocol = OtlpExportProtocol.HttpProtobuf));

using var serviceProvider = services.BuildServiceProvider();

var meterProvider = serviceProvider.GetRequiredService<MeterProvider>();

Assert.Equal(1, invocations);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
@@ -57,6 +58,71 @@ public void OtlpExporter_BadArgs()
Assert.Throws<ArgumentNullException>(() => builder.AddOtlpExporter());
}

[Fact]
public void UserHttpFactoryCalled()
{
OtlpExporterOptions options = new OtlpExporterOptions();

var defaultFactory = options.HttpClientFactory;

int invocations = 0;
options.Protocol = OtlpExportProtocol.HttpProtobuf;
options.HttpClientFactory = () =>
{
invocations++;
return defaultFactory();
};

using (var exporter = new OtlpTraceExporter(options))
{
Assert.Equal(1, invocations);
}

using (var provider = Sdk.CreateTracerProviderBuilder()
.AddOtlpExporter(o =>
{
o.Protocol = OtlpExportProtocol.HttpProtobuf;
o.HttpClientFactory = options.HttpClientFactory;
})
.Build())
{
Assert.Equal(2, invocations);
}

options.HttpClientFactory = null;
Assert.Throws<InvalidOperationException>(() =>
{
using var exporter = new OtlpTraceExporter(options);
});

options.HttpClientFactory = () => null;
Assert.Throws<InvalidOperationException>(() =>
{
using var exporter = new OtlpTraceExporter(options);
});
}

[Fact]
public void ServiceProviderHttpClientFactoryInvoked()
{
IServiceCollection services = new ServiceCollection();

services.AddHttpClient();

int invocations = 0;

services.AddHttpClient("OtlpTraceExporter", configureClient: (client) => invocations++);

services.AddOpenTelemetryTracing(builder => builder.AddOtlpExporter(
o => o.Protocol = OtlpExportProtocol.HttpProtobuf));

using var serviceProvider = services.BuildServiceProvider();

var tracerProvider = serviceProvider.GetRequiredService<TracerProvider>();

Assert.Equal(1, invocations);
}

[Theory]
[InlineData(true)]
[InlineData(false)]