diff --git a/examples/Console/Program.cs b/examples/Console/Program.cs
index 81572e68317..bd1200338d7 100644
--- a/examples/Console/Program.cs
+++ b/examples/Console/Program.cs
@@ -33,6 +33,7 @@ public class Program
/// dotnet run -p Examples.Console.csproj jaeger -h localhost -p 6831
/// dotnet run -p Examples.Console.csproj prometheus -i 15 -p 9184 -d 2
/// dotnet run -p Examples.Console.csproj otlp -e "http://localhost:4317"
+ /// dotnet run -p Examples.Console.csproj otlphttp -e "http://localhost:4318"
/// dotnet run -p Examples.Console.csproj zpages
/// dotnet run -p Examples.Console.csproj metrics --help
///
@@ -42,7 +43,7 @@ public class Program
/// Arguments from command line.
public static void Main(string[] args)
{
- Parser.Default.ParseArguments(args)
+ Parser.Default.ParseArguments(args)
.MapResult(
(JaegerOptions options) => TestJaegerExporter.Run(options.Host, options.Port),
(ZipkinOptions options) => TestZipkinExporter.Run(options.Uri),
@@ -56,6 +57,7 @@ public static void Main(string[] args)
(OpenTelemetryShimOptions options) => TestOTelShimWithConsoleExporter.Run(options),
(OpenTracingShimOptions options) => TestOpenTracingShim.Run(options),
(OtlpOptions options) => TestOtlpExporter.Run(options.Endpoint),
+ (OtlpHttpOptions options) => TestOtlpHttpExporter.Run(options.Endpoint),
(InMemoryOptions options) => TestInMemoryExporter.Run(options),
errs => 1);
}
@@ -165,6 +167,13 @@ internal class OtlpOptions
public string Endpoint { get; set; }
}
+ [Verb("otlphttp", HelpText = "Specify the options required to test OpenTelemetry Protocol (OTLP) over HTTP")]
+ internal class OtlpHttpOptions
+ {
+ [Option('e', "endpoint", HelpText = "Target to which the exporter is going to send traces or metrics", Default = "http://localhost:4318")]
+ public string Endpoint { get; set; }
+ }
+
[Verb("inmemory", HelpText = "Specify the options required to test InMemory Exporter")]
internal class InMemoryOptions
{
diff --git a/examples/Console/TestOtlpHttpExporter.cs b/examples/Console/TestOtlpHttpExporter.cs
new file mode 100644
index 00000000000..b442b7cca61
--- /dev/null
+++ b/examples/Console/TestOtlpHttpExporter.cs
@@ -0,0 +1,56 @@
+//
+// Copyright The OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using OpenTelemetry;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
+
+namespace Examples.Console
+{
+ internal static class TestOtlpHttpExporter
+ {
+ internal static object Run(string endpoint)
+ {
+ // Adding the OtlpExporter creates a GrpcChannel.
+ // This switch must be set before creating a GrpcChannel/HttpClient when calling an insecure gRPC service.
+ // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client
+ AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
+
+ // Enable OpenTelemetry for the sources "Samples.SampleServer" and "Samples.SampleClient"
+ // and use OTLP over HTTP exporter.
+ using var openTelemetry = Sdk.CreateTracerProviderBuilder()
+ .AddSource("Samples.SampleClient", "Samples.SampleServer")
+ .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("otlphttp-test"))
+ .AddOtlpHttpExporter(opt => opt.Endpoint = new Uri(endpoint))
+ .Build();
+
+ // The above line is required only in Applications
+ // which decide to use OpenTelemetry.
+ using (var sample = new InstrumentationWithActivitySource())
+ {
+ sample.Start();
+
+ System.Console.WriteLine("Traces are being created and exported" +
+ "to the OpenTelemetry Collector in the background. " +
+ "Press ENTER to stop.");
+ System.Console.ReadLine();
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/BaseOtlpHttpExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/BaseOtlpHttpExporter.cs
new file mode 100644
index 00000000000..3b35ffc5045
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/BaseOtlpHttpExporter.cs
@@ -0,0 +1,90 @@
+//
+// Copyright The OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Collections.Generic;
+using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
+using OtlpResource = Opentelemetry.Proto.Resource.V1;
+
+namespace OpenTelemetry.Exporter.OpenTelemetryProtocol
+{
+ ///
+ /// Implements exporter that exports telemetry objects over OTLP/HTTP.
+ ///
+ /// The type of telemetry object to be exported.
+ public abstract class BaseOtlpHttpExporter : BaseExporter
+ where T : class
+ {
+ private OtlpResource.Resource processResource;
+ private bool disposedValue; // To avoid duplicate dispose calls
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The for configuring the exporter.
+ /// The used http requests.
+ protected BaseOtlpHttpExporter(OtlpExporterOptions options, IHttpHandler httpHandler = null)
+ {
+ this.Options = options ?? throw new ArgumentNullException(nameof(options));
+ this.Headers = options.GetHeaders>((d, k, v) => d.Add(k, v));
+ if (this.Options.TimeoutMilliseconds <= 0)
+ {
+ throw new ArgumentException("Timeout value provided is not a positive number.", nameof(this.Options.TimeoutMilliseconds));
+ }
+
+ this.HttpHandler = httpHandler ?? new HttpHandler(TimeSpan.FromMilliseconds(this.Options.TimeoutMilliseconds));
+ }
+
+ internal OtlpResource.Resource ProcessResource => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource();
+
+ internal OtlpExporterOptions Options { get; }
+
+ internal IReadOnlyDictionary Headers { get; }
+
+ internal IHttpHandler HttpHandler { get; }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (!this.disposedValue)
+ {
+ if (disposing)
+ {
+ this.HttpHandler?.Dispose();
+ }
+
+ this.disposedValue = true;
+ }
+
+ base.Dispose(disposing);
+ }
+
+ ///
+ protected override bool OnShutdown(int timeoutMilliseconds)
+ {
+ try
+ {
+ this.HttpHandler.CancelPendingRequests();
+ return true;
+ }
+ catch (Exception ex)
+ {
+ OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex);
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/HttpHandler.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/HttpHandler.cs
new file mode 100644
index 00000000000..db8f0071ffb
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/HttpHandler.cs
@@ -0,0 +1,54 @@
+//
+// Copyright The OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation
+{
+ ///
+ /// Class decorating .
+ ///
+ internal class HttpHandler : IHttpHandler
+ {
+ internal readonly HttpClient HttpClient;
+
+ public HttpHandler(TimeSpan timeout)
+ {
+ this.HttpClient = new HttpClient
+ {
+ Timeout = timeout,
+ };
+ }
+
+ public void CancelPendingRequests()
+ {
+ this.HttpClient.CancelPendingRequests();
+ }
+
+ public void Dispose()
+ {
+ this.HttpClient.Dispose();
+ }
+
+ public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return await this.HttpClient.SendAsync(request, cancellationToken);
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/IHttpHandler.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/IHttpHandler.cs
new file mode 100644
index 00000000000..c2e55f4a85a
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/IHttpHandler.cs
@@ -0,0 +1,42 @@
+//
+// Copyright The OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation
+{
+ ///
+ /// Interface partialy exposing methods.
+ ///
+ public interface IHttpHandler : IDisposable
+ {
+ ///
+ /// Cancel all pending requests on this instance.
+ ///
+ void CancelPendingRequests();
+
+ ///
+ /// Send an HTTP request as an asynchronous operation.
+ ///
+ /// The HTTP request message to send.
+ /// The cancellation token to cancel operation.
+ /// Result of the export operation.
+ Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsCommonExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsCommonExtensions.cs
new file mode 100644
index 00000000000..3e7cfe07b07
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsCommonExtensions.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace OpenTelemetry.Exporter.OpenTelemetryProtocol
+{
+ internal static class OtlpExporterOptionsCommonExtensions
+ {
+ internal static THeaders GetHeaders(this OtlpExporterOptions options, Action addHeader)
+ where THeaders : new()
+ {
+ var optionHeaders = options.Headers;
+ var headers = new THeaders();
+ if (!string.IsNullOrEmpty(optionHeaders))
+ {
+ Array.ForEach(
+ optionHeaders.Split(','),
+ (pair) =>
+ {
+ // Specify the maximum number of substrings to return to 2
+ // This treats everything that follows the first `=` in the string as the value to be added for the metadata key
+ var keyValueData = pair.Split(new char[] { '=' }, 2);
+ if (keyValueData.Length != 2)
+ {
+ throw new ArgumentException("Headers provided in an invalid format.");
+ }
+
+ var key = keyValueData[0].Trim();
+ var value = keyValueData[1].Trim();
+ addHeader(headers, key, value);
+ });
+ }
+
+ return headers;
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsGrpcExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsGrpcExtensions.cs
index f3750e654db..30de5313c9e 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsGrpcExtensions.cs
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsGrpcExtensions.cs
@@ -54,29 +54,7 @@ public static Channel CreateChannel(this OtlpExporterOptions options)
public static Metadata GetMetadataFromHeaders(this OtlpExporterOptions options)
{
- var headers = options.Headers;
- var metadata = new Metadata();
- if (!string.IsNullOrEmpty(headers))
- {
- Array.ForEach(
- headers.Split(','),
- (pair) =>
- {
- // Specify the maximum number of substrings to return to 2
- // This treats everything that follows the first `=` in the string as the value to be added for the metadata key
- var keyValueData = pair.Split(new char[] { '=' }, 2);
- if (keyValueData.Length != 2)
- {
- throw new ArgumentException("Headers provided in an invalid format.");
- }
-
- var key = keyValueData[0].Trim();
- var value = keyValueData[1].Trim();
- metadata.Add(key, value);
- });
- }
-
- return metadata;
+ return options.GetHeaders((m, k, v) => m.Add(k, v));
}
}
}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpHttpTraceExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpHttpTraceExporter.cs
new file mode 100644
index 00000000000..7700df9f2b3
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpHttpTraceExporter.cs
@@ -0,0 +1,101 @@
+//
+// Copyright The OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using Google.Protobuf;
+using OpenTelemetry.Exporter.OpenTelemetryProtocol;
+using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
+using OtlpCollector = Opentelemetry.Proto.Collector.Trace.V1;
+
+namespace OpenTelemetry.Exporter
+{
+ ///
+ /// Exporter consuming and exporting the data using
+ /// the OpenTelemetry protocol (OTLP) over HTTP.
+ ///
+ public class OtlpHttpTraceExporter : BaseOtlpHttpExporter
+ {
+ internal const string MediaContentType = "application/x-protobuf";
+
+ internal OtlpHttpTraceExporter(OtlpExporterOptions options, IHttpHandler httpHandler = null)
+ : base(options, httpHandler)
+ {
+ }
+
+ ///
+ public override ExportResult Export(in Batch activityBatch)
+ {
+ // Prevents the exporter's gRPC and HTTP operations from being instrumented.
+ using var scope = SuppressInstrumentationScope.Begin();
+
+ var request = new OtlpCollector.ExportTraceServiceRequest();
+ request.AddBatch(this.ProcessResource, activityBatch);
+
+ try
+ {
+ var httpRequest = this.CreateHttpRequest(request);
+
+ // TODO: replace by synchronous vesrion of Send method when it becomes availabe.
+ // See https://github.com/dotnet/runtime/pull/34948 (should be available starting form .NET 5.0).
+ var response = this.HttpHandler.SendAsync(httpRequest).Result;
+ }
+ catch (HttpRequestException ex)
+ {
+ OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex);
+
+ return ExportResult.Failure;
+ }
+ catch (Exception ex)
+ {
+ OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex);
+
+ return ExportResult.Failure;
+ }
+ finally
+ {
+ request.Return();
+ }
+
+ return ExportResult.Success;
+ }
+
+ private HttpRequestMessage CreateHttpRequest(OtlpCollector.ExportTraceServiceRequest request)
+ {
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, this.Options.Endpoint);
+ foreach (var header in this.Headers)
+ {
+ httpRequestMessage.Headers.Add(header.Key, header.Value);
+ }
+
+ var content = Array.Empty();
+ using (var stream = new MemoryStream())
+ {
+ request.WriteTo(stream);
+ content = stream.ToArray();
+ }
+
+ var binaryContent = new ByteArrayContent(content);
+ binaryContent.Headers.ContentType = new MediaTypeHeaderValue(MediaContentType);
+ httpRequestMessage.Content = binaryContent;
+
+ return httpRequestMessage;
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpHttpTraceExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpHttpTraceExporterHelperExtensions.cs
new file mode 100644
index 00000000000..09b03666714
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpHttpTraceExporterHelperExtensions.cs
@@ -0,0 +1,71 @@
+//
+// Copyright The OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using OpenTelemetry.Exporter;
+
+namespace OpenTelemetry.Trace
+{
+ ///
+ /// Extension methods to simplify registering of the OpenTelemetry Protocol (OTLP) over HTTP exporter.
+ ///
+ public static class OtlpHttpTraceExporterHelperExtensions
+ {
+ ///
+ /// Adds OpenTelemetry Protocol (OTLP) over HTTP exporter to the TracerProvider.
+ ///
+ /// builder to use.
+ /// Exporter configuration options.
+ /// The instance of to chain the calls.
+ public static TracerProviderBuilder AddOtlpHttpExporter(this TracerProviderBuilder builder, Action configure = null)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (builder is IDeferredTracerProviderBuilder deferredTracerProviderBuilder)
+ {
+ return deferredTracerProviderBuilder.Configure((sp, builder) =>
+ {
+ AddOtlpHttpExporter(builder, sp.GetOptions(), configure);
+ });
+ }
+
+ return AddOtlpHttpExporter(builder, new OtlpExporterOptions(), configure);
+ }
+
+ private static TracerProviderBuilder AddOtlpHttpExporter(TracerProviderBuilder builder, OtlpExporterOptions exporterOptions, Action configure = null)
+ {
+ configure?.Invoke(exporterOptions);
+ var otlpHttpExporter = new OtlpHttpTraceExporter(exporterOptions);
+
+ if (exporterOptions.ExportProcessorType == ExportProcessorType.Simple)
+ {
+ return builder.AddProcessor(new SimpleActivityExportProcessor(otlpHttpExporter));
+ }
+ else
+ {
+ return builder.AddProcessor(new BatchActivityExportProcessor(
+ otlpHttpExporter,
+ exporterOptions.BatchExportProcessorOptions.MaxQueueSize,
+ exporterOptions.BatchExportProcessorOptions.ScheduledDelayMilliseconds,
+ exporterOptions.BatchExportProcessorOptions.ExporterTimeoutMilliseconds,
+ exporterOptions.BatchExportProcessorOptions.MaxExportBatchSize));
+ }
+ }
+ }
+}
diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsGrpcExtensionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsGrpcExtensionsTests.cs
index c75384cb0aa..f4bdf00a925 100644
--- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsGrpcExtensionsTests.cs
+++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsGrpcExtensionsTests.cs
@@ -46,7 +46,7 @@ public void GetMetadataFromHeadersWorksCorrectFormat(string headers, string[] ke
[Theory]
[InlineData("headers")]
[InlineData("key,value")]
- public void GetMetadataFromHeadersThrowsExceptionOnOnvalidFormat(string headers)
+ public void GetMetadataFromHeadersThrowsExceptionOnInvalidFormat(string headers)
{
try
{
diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpHttpTraceExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpHttpTraceExporterTests.cs
new file mode 100644
index 00000000000..12ac99f5b9d
--- /dev/null
+++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpHttpTraceExporterTests.cs
@@ -0,0 +1,159 @@
+//
+// Copyright The OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Moq;
+using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
+using OpenTelemetry.Trace;
+using Xunit;
+using OtlpCollector = Opentelemetry.Proto.Collector.Trace.V1;
+
+namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests
+{
+ public class OtlpHttpTraceExporterTests
+ {
+ static OtlpHttpTraceExporterTests()
+ {
+ Activity.DefaultIdFormat = ActivityIdFormat.W3C;
+ Activity.ForceDefaultIdFormat = true;
+
+ var listener = new ActivityListener
+ {
+ ShouldListenTo = _ => true,
+ Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData,
+ };
+
+ ActivitySource.AddActivityListener(listener);
+ }
+
+ [Fact]
+ public void OtlpExporter_BadArgs()
+ {
+ TracerProviderBuilder builder = null;
+ Assert.Throws(() => builder.AddOtlpHttpExporter());
+ }
+
+ [Fact]
+ public void NewOtlpHttpTraceExporter_ExportHasDefaulProperties()
+ {
+ var exporter = new OtlpHttpTraceExporter(new OtlpExporterOptions());
+
+ Assert.NotNull(exporter.HttpHandler);
+ Assert.IsType(exporter.HttpHandler);
+ }
+
+ [Fact]
+ public void NewOtlpHttpTraceExporter_ExporterHasDefinedProperties()
+ {
+ var header1 = new { Name = "hdr1", Value = "val1" };
+ var header2 = new { Name = "hdr2", Value = "val2" };
+
+ var options = new OtlpExporterOptions
+ {
+ Headers = $"{header1.Name}={header1.Value}, {header2.Name} = {header2.Value}",
+ };
+
+ var exporter = new OtlpHttpTraceExporter(options, new NoopHttpHandler());
+
+ Assert.NotNull(exporter.HttpHandler);
+ Assert.IsType(exporter.HttpHandler);
+
+ Assert.Equal(2, exporter.Headers.Count);
+ Assert.Contains(exporter.Headers, kvp => kvp.Key == header1.Name && kvp.Value == header1.Value);
+ Assert.Contains(exporter.Headers, kvp => kvp.Key == header2.Name && kvp.Value == header2.Value);
+ }
+
+ [Fact]
+ public async Task Export_ActivityBatch_SendsCorrectHttpRequest()
+ {
+ using var activitySource = new ActivitySource(nameof(this.Export_ActivityBatch_SendsCorrectHttpRequest));
+
+ using var activity = activitySource.StartActivity($"activity-{nameof(this.Export_ActivityBatch_SendsCorrectHttpRequest)}", ActivityKind.Producer);
+
+ var header1 = new { Name = "hdr1", Value = "val1" };
+ var header2 = new { Name = "hdr2", Value = "val2" };
+
+ var options = new OtlpExporterOptions
+ {
+ Endpoint = new Uri("http://localhost:4318"),
+ Headers = $"{header1.Name}={header1.Value}, {header2.Name} = {header2.Value}",
+ };
+
+ var httpHandlerMock = new Mock();
+ HttpRequestMessage httpRequest = null;
+ httpHandlerMock.Setup(h => h.SendAsync(It.IsAny(), It.IsAny()))
+ .Callback((r, ct) => httpRequest = r);
+
+ var exporter = new OtlpHttpTraceExporter(options, httpHandlerMock.Object);
+
+ var result = exporter.Export(new Batch(activity));
+
+ httpHandlerMock.Verify(m => m.SendAsync(It.IsAny(), It.IsAny()), Times.Once());
+
+ Assert.Equal(ExportResult.Success, result);
+ Assert.NotNull(httpRequest);
+ Assert.Equal(HttpMethod.Post, httpRequest.Method);
+ Assert.Equal(options.Endpoint.AbsoluteUri, httpRequest.RequestUri.AbsoluteUri);
+ Assert.Equal(2, httpRequest.Headers.Count());
+ Assert.Contains(httpRequest.Headers, h => h.Key == header1.Name && h.Value.First() == header1.Value);
+ Assert.Contains(httpRequest.Headers, h => h.Key == header2.Name && h.Value.First() == header2.Value);
+
+ Assert.NotNull(httpRequest.Content);
+ Assert.IsType(httpRequest.Content);
+ Assert.Contains(httpRequest.Content.Headers, h => h.Key == "Content-Type" && h.Value.First() == OtlpHttpTraceExporter.MediaContentType);
+
+ var exportTraceRequest = OtlpCollector.ExportTraceServiceRequest.Parser.ParseFrom(await httpRequest.Content.ReadAsByteArrayAsync());
+ Assert.NotNull(exportTraceRequest);
+
+ Assert.Single(exportTraceRequest.ResourceSpans);
+ Assert.Single(exportTraceRequest.ResourceSpans.First().Resource.Attributes);
+ }
+
+ [Fact]
+ public void Shutdown_PendingHttpRequestsCancelled()
+ {
+ var httpHandlerMock = new Mock();
+
+ var exporter = new OtlpHttpTraceExporter(new OtlpExporterOptions(), httpHandlerMock.Object);
+
+ var result = exporter.Shutdown();
+
+ httpHandlerMock.Verify(m => m.CancelPendingRequests(), Times.Once());
+ }
+
+ private class NoopHttpHandler : IHttpHandler
+ {
+ public void CancelPendingRequests()
+ {
+ }
+
+ public void Dispose()
+ {
+ }
+
+ public Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
+ {
+ return null;
+ }
+ }
+ }
+}