Issue/2292 otlp http binary protobuf trace exporter (#2316)

This commit is contained in:
Oleksiy Dubinin 2021-10-05 20:35:54 +02:00 committed by GitHub
parent 2512aa1709
commit 1aa4da2098
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1027 additions and 252 deletions

View File

@ -15,6 +15,7 @@
// </copyright>
using CommandLine;
using OpenTelemetry.Exporter;
namespace Examples.Console
{
@ -32,7 +33,7 @@ namespace Examples.Console
/// dotnet run -p Examples.Console.csproj zipkin -u http://localhost:9411/api/v2/spans
/// dotnet run -p Examples.Console.csproj jaeger -h localhost -p 6831
/// dotnet run -p Examples.Console.csproj prometheus -p 9184 -d 2
/// dotnet run -p Examples.Console.csproj otlp -e "http://localhost:4317"
/// dotnet run -p Examples.Console.csproj otlp -e "http://localhost:4317" -p "grpc"
/// dotnet run -p Examples.Console.csproj zpages
/// dotnet run -p Examples.Console.csproj metrics --help
///
@ -55,7 +56,7 @@ namespace Examples.Console
(ConsoleOptions options) => TestConsoleExporter.Run(options),
(OpenTelemetryShimOptions options) => TestOTelShimWithConsoleExporter.Run(options),
(OpenTracingShimOptions options) => TestOpenTracingShim.Run(options),
(OtlpOptions options) => TestOtlpExporter.Run(options.Endpoint),
(OtlpOptions options) => TestOtlpExporter.Run(options.Endpoint, options.Protocol),
(InMemoryOptions options) => TestInMemoryExporter.Run(options),
errs => 1);
}
@ -163,6 +164,9 @@ namespace Examples.Console
{
[Option('e', "endpoint", HelpText = "Target to which the exporter is going to send traces or metrics", Default = "http://localhost:4317")]
public string Endpoint { get; set; }
[Option('p', "protocol", HelpText = "Transport protocol used by exporter. Supported values: grpc and http/protobuf.", Default = "grpc")]
public string Protocol { get; set; }
}
[Verb("inmemory", HelpText = "Specify the options required to test InMemory Exporter")]

View File

@ -16,6 +16,7 @@
using System;
using OpenTelemetry;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
@ -23,7 +24,7 @@ namespace Examples.Console
{
internal static class TestOtlpExporter
{
internal static object Run(string endpoint)
internal static object Run(string endpoint, string protocol)
{
/*
* Prerequisite to run this example:
@ -49,22 +50,33 @@ namespace Examples.Console
* For more information about the OpenTelemetry Collector go to https://github.com/open-telemetry/opentelemetry-collector
*
*/
return RunWithActivitySource(endpoint);
return RunWithActivitySource(endpoint, protocol);
}
private static object RunWithActivitySource(string endpoint)
private static object RunWithActivitySource(string endpoint, string protocol)
{
// 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);
var otlpExportProtocol = ToOtlpExportProtocol(protocol);
if (!otlpExportProtocol.HasValue)
{
System.Console.WriteLine($"Export protocol {protocol} is not supported. Default protocol 'grpc' will be used.");
otlpExportProtocol = OtlpExportProtocol.Grpc;
}
// Enable OpenTelemetry for the sources "Samples.SampleServer" and "Samples.SampleClient"
// and use OTLP exporter.
using var openTelemetry = Sdk.CreateTracerProviderBuilder()
.AddSource("Samples.SampleClient", "Samples.SampleServer")
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("otlp-test"))
.AddOtlpExporter(opt => opt.Endpoint = new Uri(endpoint))
.AddOtlpExporter(opt =>
{
opt.Endpoint = new Uri(endpoint);
opt.Protocol = otlpExportProtocol.Value;
})
.Build();
// The above line is required only in Applications
@ -81,5 +93,13 @@ namespace Examples.Console
return null;
}
private static OtlpExportProtocol? ToOtlpExportProtocol(string protocol) =>
protocol.Trim().ToLower() switch
{
"grpc" => OtlpExportProtocol.Grpc,
"http/protobuf" => OtlpExportProtocol.HttpProtobuf,
_ => null
};
}
}

View File

@ -10,9 +10,14 @@ OpenTelemetry.Exporter.OtlpExporterOptions.Headers.set -> void
OpenTelemetry.Exporter.OtlpExporterOptions.OtlpExporterOptions() -> void
OpenTelemetry.Exporter.OtlpExporterOptions.TimeoutMilliseconds.get -> int
OpenTelemetry.Exporter.OtlpExporterOptions.TimeoutMilliseconds.set -> void
OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.get -> OpenTelemetry.Exporter.ExportProtocol
OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.set -> void
OpenTelemetry.Exporter.OtlpTraceExporter
OpenTelemetry.Exporter.OtlpTraceExporter.OtlpTraceExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void
OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions
override OpenTelemetry.Exporter.OtlpTraceExporter.Export(in OpenTelemetry.Batch<System.Diagnostics.Activity> activityBatch) -> OpenTelemetry.ExportResult
override OpenTelemetry.Exporter.OtlpTraceExporter.OnShutdown(int timeoutMilliseconds) -> bool
static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action<OpenTelemetry.Exporter.OtlpExporterOptions> configure = null) -> OpenTelemetry.Trace.TracerProviderBuilder
OpenTelemetry.Exporter.OtlpExportProtocol
OpenTelemetry.Exporter.OtlpExportProtocol.Grpc = 0 -> OpenTelemetry.Exporter.OtlpExportProtocol
OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf = 1 -> OpenTelemetry.Exporter.OtlpExportProtocol

View File

@ -0,0 +1,5 @@
OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.get -> OpenTelemetry.Exporter.ExportProtocol
OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.set -> void
OpenTelemetry.Exporter.OtlpExportProtocol
OpenTelemetry.Exporter.OtlpExportProtocol.Grpc = 0 -> OpenTelemetry.Exporter.OtlpExportProtocol
OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf = 1 -> OpenTelemetry.Exporter.OtlpExportProtocol

View File

@ -0,0 +1,5 @@
OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.get -> OpenTelemetry.Exporter.ExportProtocol
OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.set -> void
OpenTelemetry.Exporter.OtlpExportProtocol
OpenTelemetry.Exporter.OtlpExportProtocol.Grpc = 0 -> OpenTelemetry.Exporter.OtlpExportProtocol
OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf = 1 -> OpenTelemetry.Exporter.OtlpExportProtocol

View File

@ -5,6 +5,10 @@
* `MeterProviderBuilder` extension methods now support `OtlpExporterOptions`
bound to `IConfiguration` when using OpenTelemetry.Extensions.Hosting
([#2413](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2413))
* Extended `OtlpExporterOptions` by `Protocol` property. The property can be
overridden by `OTEL_EXPORTER_OTLP_PROTOCOL` environmental variable (grpc or http/protobuf).
Implemented OTLP over HTTP binary protobuf trace exporter.
([#2292](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2292))
## 1.2.0-alpha4
@ -18,7 +22,7 @@ Released 2021-Sep-13
`BatchExportActivityProcessorOptions` which supports field value overriding
using `OTEL_BSP_SCHEDULE_DELAY`, `OTEL_BSP_EXPORT_TIMEOUT`,
`OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE`
envionmental variables as defined in the
environmental variables as defined in the
[specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.5.0/specification/sdk-environment-variables.md#batch-span-processor).
([#2219](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2219))
@ -28,7 +32,7 @@ Released 2021-Aug-24
* The `OtlpExporterOptions` defaults can be overridden using
`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_HEADERS` and `OTEL_EXPORTER_OTLP_TIMEOUT`
envionmental variables as defined in the
environmental variables as defined in the
[specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md).
([#2188](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2188))

View File

@ -1,4 +1,4 @@
// <copyright file="BaseOtlpExporter.cs" company="OpenTelemetry Authors">
// <copyright file="BaseOtlpGrpcExportClient.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
@ -15,54 +15,46 @@
// </copyright>
using System;
using System.Threading;
using System.Threading.Tasks;
using Grpc.Core;
using OpenTelemetry.Exporter.OpenTelemetryProtocol;
#if NETSTANDARD2_1
#if NETSTANDARD2_1 || NET5_0_OR_GREATER
using Grpc.Net.Client;
#endif
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
using OtlpResource = Opentelemetry.Proto.Resource.V1;
namespace OpenTelemetry.Exporter
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient
{
/// <summary>
/// Implements exporter that exports telemetry objects over OTLP/gRPC.
/// </summary>
/// <typeparam name="T">The type of telemetry object to be exported.</typeparam>
public abstract class BaseOtlpExporter<T> : BaseExporter<T>
where T : class
/// <summary>Base class for sending OTLP export request over gRPC.</summary>
/// <typeparam name="TRequest">Type of export request.</typeparam>
internal abstract class BaseOtlpGrpcExportClient<TRequest> : IExportClient<TRequest>
{
private OtlpResource.Resource processResource;
/// <summary>
/// Initializes a new instance of the <see cref="BaseOtlpExporter{T}"/> class.
/// </summary>
/// <param name="options">The <see cref="OtlpExporterOptions"/> for configuring the exporter.</param>
protected BaseOtlpExporter(OtlpExporterOptions options)
protected BaseOtlpGrpcExportClient(OtlpExporterOptions options)
{
this.Options = options ?? throw new ArgumentNullException(nameof(options));
this.Headers = options.GetMetadataFromHeaders();
if (this.Options.TimeoutMilliseconds <= 0)
if (options.TimeoutMilliseconds <= 0)
{
throw new ArgumentException("Timeout value provided is not a positive number.", nameof(this.Options.TimeoutMilliseconds));
throw new ArgumentException("Timeout value provided is not a positive number.", nameof(options.TimeoutMilliseconds));
}
this.Headers = options.GetMetadataFromHeaders();
}
internal OtlpResource.Resource ProcessResource => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource();
internal OtlpExporterOptions Options { get; }
#if NETSTANDARD2_1
#if NETSTANDARD2_1 || NET5_0_OR_GREATER
internal GrpcChannel Channel { get; set; }
#else
internal Channel Channel { get; set; }
#endif
internal OtlpExporterOptions Options { get; }
internal Metadata Headers { get; }
/// <inheritdoc/>
protected override bool OnShutdown(int timeoutMilliseconds)
public abstract bool SendExportRequest(TRequest request, CancellationToken cancellationToken = default);
/// <inheritdoc/>
public virtual bool Shutdown(int timeoutMilliseconds)
{
if (this.Channel == null)
{

View File

@ -0,0 +1,87 @@
// <copyright file="BaseOtlpHttpExportClient.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.Collections.Generic;
using System.Net.Http;
using System.Threading;
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient
{
/// <summary>Base class for sending OTLP export request over HTTP.</summary>
/// <typeparam name="TRequest">Type of export request.</typeparam>
internal abstract class BaseOtlpHttpExportClient<TRequest> : IExportClient<TRequest>
{
protected BaseOtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpClient = null)
{
this.Options = options ?? throw new ArgumentNullException(nameof(options));
if (this.Options.TimeoutMilliseconds <= 0)
{
throw new ArgumentException("Timeout value provided is not a positive number.", nameof(this.Options.TimeoutMilliseconds));
}
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) };
}
internal OtlpExporterOptions Options { get; }
internal HttpClient HttpClient { get; }
internal IReadOnlyDictionary<string, string> Headers { get; }
/// <inheritdoc/>
public bool SendExportRequest(TRequest request, CancellationToken cancellationToken = default)
{
try
{
using var httpRequest = this.CreateHttpRequest(request);
using var httpResponse = this.SendHttpRequest(httpRequest, cancellationToken);
httpResponse?.EnsureSuccessStatusCode();
}
catch (HttpRequestException ex)
{
OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(ex);
return false;
}
return true;
}
/// <inheritdoc/>
public bool Shutdown(int timeoutMilliseconds)
{
this.HttpClient.CancelPendingRequests();
return true;
}
protected abstract HttpRequestMessage CreateHttpRequest(TRequest request);
protected HttpResponseMessage SendHttpRequest(HttpRequestMessage request, CancellationToken cancellationToken)
{
#if NET5_0_OR_GREATER
return this.HttpClient.Send(request, cancellationToken);
#else
return this.HttpClient.SendAsync(request, cancellationToken).GetAwaiter().GetResult();
#endif
}
}
}

View File

@ -0,0 +1,45 @@
// <copyright file="IExportClient.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.Threading;
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient
{
/// <summary>Export client interface.</summary>
/// <typeparam name="TRequest">Type of export request.</typeparam>
internal interface IExportClient<in TRequest>
{
/// <summary>
/// Method for sending export request to the server.
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="cancellationToken">An optional token for canceling the call.</param>
/// <returns>True if the request has been sent successfully, otherwise false.</returns>
bool SendExportRequest(TRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Method for shutting down the export client.
/// </summary>
/// <param name="timeoutMilliseconds">
/// The number of milliseconds to wait, or <c>Timeout.Infinite</c> to
/// wait indefinitely.
/// </param>
/// <returns>
/// Returns <c>true</c> if shutdown succeeded; otherwise, <c>false</c>.
/// </returns>
bool Shutdown(int timeoutMilliseconds);
}
}

View File

@ -0,0 +1,62 @@
// <copyright file="OtlpGrpcMetricsExportClient.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.Threading;
using Grpc.Core;
using OtlpCollector = Opentelemetry.Proto.Collector.Metrics.V1;
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient
{
/// <summary>Class for sending OTLP metrics export request over gRPC.</summary>
internal sealed class OtlpGrpcMetricsExportClient : BaseOtlpGrpcExportClient<OtlpCollector.ExportMetricsServiceRequest>
{
private readonly OtlpCollector.MetricsService.IMetricsServiceClient metricsClient;
public OtlpGrpcMetricsExportClient(OtlpExporterOptions options, OtlpCollector.MetricsService.IMetricsServiceClient metricsServiceClient = null)
: base(options)
{
if (metricsServiceClient != null)
{
this.metricsClient = metricsServiceClient;
}
else
{
this.Channel = options.CreateChannel();
this.metricsClient = new OtlpCollector.MetricsService.MetricsServiceClient(this.Channel);
}
}
/// <inheritdoc/>
public override bool SendExportRequest(OtlpCollector.ExportMetricsServiceRequest request, CancellationToken cancellationToken = default)
{
var deadline = DateTime.UtcNow.AddMilliseconds(this.Options.TimeoutMilliseconds);
try
{
this.metricsClient.Export(request, headers: this.Headers, deadline: deadline);
}
catch (RpcException ex)
{
OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(ex);
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,62 @@
// <copyright file="OtlpGrpcTraceExportClient.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.Threading;
using Grpc.Core;
using OtlpCollector = Opentelemetry.Proto.Collector.Trace.V1;
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient
{
/// <summary>Class for sending OTLP trace export request over gRPC.</summary>
internal sealed class OtlpGrpcTraceExportClient : BaseOtlpGrpcExportClient<OtlpCollector.ExportTraceServiceRequest>
{
private readonly OtlpCollector.TraceService.ITraceServiceClient traceClient;
public OtlpGrpcTraceExportClient(OtlpExporterOptions options, OtlpCollector.TraceService.ITraceServiceClient traceServiceClient = null)
: base(options)
{
if (traceServiceClient != null)
{
this.traceClient = traceServiceClient;
}
else
{
this.Channel = options.CreateChannel();
this.traceClient = new OtlpCollector.TraceService.TraceServiceClient(this.Channel);
}
}
/// <inheritdoc/>
public override bool SendExportRequest(OtlpCollector.ExportTraceServiceRequest request, CancellationToken cancellationToken = default)
{
var deadline = DateTime.UtcNow.AddMilliseconds(this.Options.TimeoutMilliseconds);
try
{
this.traceClient.Export(request, headers: this.Headers, deadline: deadline);
}
catch (RpcException ex)
{
OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(ex);
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,60 @@
// <copyright file="OtlpHttpTraceExportClient.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.Http;
using System.Net.Http.Headers;
using Google.Protobuf;
using OtlpCollector = Opentelemetry.Proto.Collector.Trace.V1;
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient
{
/// <summary>Class for sending OTLP trace export request over HTTP.</summary>
internal sealed class OtlpHttpTraceExportClient : BaseOtlpHttpExportClient<OtlpCollector.ExportTraceServiceRequest>
{
internal const string MediaContentType = "application/x-protobuf";
private readonly Uri exportTracesUri;
public OtlpHttpTraceExportClient(OtlpExporterOptions options, HttpClient httpClient = null)
: base(options, httpClient)
{
this.exportTracesUri = this.Options.Endpoint.AppendPathIfNotPresent(OtlpExporterOptions.TracesExportPath);
}
protected override HttpRequestMessage CreateHttpRequest(OtlpCollector.ExportTraceServiceRequest exportRequest)
{
var request = new HttpRequestMessage(HttpMethod.Post, this.exportTracesUri);
foreach (var header in this.Headers)
{
request.Headers.Add(header.Key, header.Value);
}
var content = Array.Empty<byte>();
using (var stream = new MemoryStream())
{
exportRequest.WriteTo(stream);
content = stream.ToArray();
}
var binaryContent = new ByteArrayContent(content);
binaryContent.Headers.ContentType = new MediaTypeHeaderValue(MediaContentType);
request.Content = binaryContent;
return request;
}
}
}

View File

@ -103,5 +103,11 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation
{
this.WriteEvent(7, exception);
}
[Event(8, Message = "Unsupported value for protocol '{0}' is configured, default protocol 'grpc' will be used.", Level = EventLevel.Warning)]
public void UnsupportedProtocol(string protocol)
{
this.WriteEvent(8, protocol);
}
}
}

View File

@ -1,9 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net461</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net461;net5.0</TargetFrameworks>
<Description>OpenTelemetry protocol exporter for OpenTelemetry .NET</Description>
<PackageTags>$(PackageTags);OTLP</PackageTags>
<MinVerTagPrefix>core-</MinVerTagPrefix>
<!-- TODO: Remove this once a version targeting net5.0 is released -->
<RunApiCompat Condition="$(TargetFramework) == 'net5.0'">false</RunApiCompat>
</PropertyGroup>
<!--Do not run ApiCompat for net461 as this is newly added. There is no existing contract for net461 against which we could compare the implementation.
@ -16,7 +18,15 @@
<PackageReference Include="Grpc.Net.Client" Version="$(GrpcNetClientPkgVer)" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'netstandard2.1'">
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
<PackageReference Include="Grpc.Net.Client" Version="$(GrpcNetClientPkgVer)" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Grpc" Version="$(GrpcPkgVer)" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net461'">
<PackageReference Include="Grpc" Version="$(GrpcPkgVer)" />
</ItemGroup>

View File

@ -0,0 +1,34 @@
// <copyright file="OtlpExportProtocol.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>
namespace OpenTelemetry.Exporter
{
/// <summary>
/// Supported by OTLP exporter protocol types according to the specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md.
/// </summary>
public enum OtlpExportProtocol : byte
{
/// <summary>
/// OTLP over gRPC (corresponds to 'grpc' Protocol configuration option). Used as default.
/// </summary>
Grpc = 0,
/// <summary>
/// OTLP over HTTP with protobuf payloads (corresponds to 'http/protobuf' Protocol configuration option).
/// </summary>
HttpProtobuf = 1,
}
}

View File

@ -31,6 +31,10 @@ namespace OpenTelemetry.Exporter
internal const string EndpointEnvVarName = "OTEL_EXPORTER_OTLP_ENDPOINT";
internal const string HeadersEnvVarName = "OTEL_EXPORTER_OTLP_HEADERS";
internal const string TimeoutEnvVarName = "OTEL_EXPORTER_OTLP_TIMEOUT";
internal const string ProtocolEnvVarName = "OTEL_EXPORTER_OTLP_PROTOCOL";
internal const string TracesExportPath = "v1/traces";
internal const string MetricsExportPath = "v1/metrics";
/// <summary>
/// Initializes a new instance of the <see cref="OtlpExporterOptions"/> class.
@ -70,6 +74,20 @@ namespace OpenTelemetry.Exporter
OpenTelemetryProtocolExporterEventSource.Log.FailedToParseEnvironmentVariable(TimeoutEnvVarName, timeoutEnvVar);
}
}
string protocolEnvVar = Environment.GetEnvironmentVariable(ProtocolEnvVarName);
if (!string.IsNullOrEmpty(protocolEnvVar))
{
var protocol = protocolEnvVar.ToOtlpExportProtocol();
if (protocol.HasValue)
{
this.Protocol = protocol.Value;
}
else
{
OpenTelemetryProtocolExporterEventSource.Log.UnsupportedProtocol(protocolEnvVar);
}
}
}
catch (SecurityException ex)
{
@ -98,6 +116,11 @@ namespace OpenTelemetry.Exporter
/// </summary>
public int TimeoutMilliseconds { get; set; } = 10000;
/// <summary>
/// Gets or sets the the OTLP transport protocol. Supported values: Grpc and HttpProtobuf.
/// </summary>
public OtlpExportProtocol Protocol { get; set; } = OtlpExportProtocol.Grpc;
/// <summary>
/// Gets or sets the export processor type to be used with the OpenTelemetry Protocol Exporter. The default value is <see cref="ExportProcessorType.Batch"/>.
/// </summary>

View File

@ -0,0 +1,133 @@
// <copyright file="OtlpExporterOptionsExtensions.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 Grpc.Core;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
#if NETSTANDARD2_1 || NET5_0_OR_GREATER
using Grpc.Net.Client;
#endif
using OtlpCollector = Opentelemetry.Proto.Collector.Trace.V1;
namespace OpenTelemetry.Exporter
{
internal static class OtlpExporterOptionsExtensions
{
#if NETSTANDARD2_1 || NET5_0_OR_GREATER
public static GrpcChannel CreateChannel(this OtlpExporterOptions options)
#else
public static Channel CreateChannel(this OtlpExporterOptions options)
#endif
{
if (options.Endpoint.Scheme != Uri.UriSchemeHttp && options.Endpoint.Scheme != Uri.UriSchemeHttps)
{
throw new NotSupportedException($"Endpoint URI scheme ({options.Endpoint.Scheme}) is not supported. Currently only \"http\" and \"https\" are supported.");
}
#if NETSTANDARD2_1 || NET5_0_OR_GREATER
return GrpcChannel.ForAddress(options.Endpoint);
#else
ChannelCredentials channelCredentials;
if (options.Endpoint.Scheme == Uri.UriSchemeHttps)
{
channelCredentials = new SslCredentials();
}
else
{
channelCredentials = ChannelCredentials.Insecure;
}
return new Channel(options.Endpoint.Authority, channelCredentials);
#endif
}
public static Metadata GetMetadataFromHeaders(this OtlpExporterOptions options)
{
return options.GetHeaders<Metadata>((m, k, v) => m.Add(k, v));
}
public static THeaders GetHeaders<THeaders>(this OtlpExporterOptions options, Action<THeaders, string, string> 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;
}
public static IExportClient<OtlpCollector.ExportTraceServiceRequest> GetTraceExportClient(this OtlpExporterOptions options) =>
options.Protocol switch
{
OtlpExportProtocol.Grpc => new OtlpGrpcTraceExportClient(options),
OtlpExportProtocol.HttpProtobuf => new OtlpHttpTraceExportClient(options),
_ => throw new NotSupportedException($"Protocol {options.Protocol} is not supported.")
};
public static OtlpExportProtocol? ToOtlpExportProtocol(this string protocol) =>
protocol.Trim() switch
{
"grpc" => OtlpExportProtocol.Grpc,
"http/protobuf" => OtlpExportProtocol.HttpProtobuf,
_ => null
};
public static Uri AppendPathIfNotPresent(this Uri uri, string path)
{
var absoluteUri = uri.AbsoluteUri;
var separator = string.Empty;
if (absoluteUri.EndsWith("/"))
{
// Endpoint already ends with 'path/'
if (absoluteUri.EndsWith(string.Concat(path, "/"), StringComparison.OrdinalIgnoreCase))
{
return uri;
}
}
else
{
// Endpoint already ends with 'path'
if (absoluteUri.EndsWith(path, StringComparison.OrdinalIgnoreCase))
{
return uri;
}
separator = "/";
}
return new Uri(string.Concat(uri.AbsoluteUri, separator, path));
}
}
}

View File

@ -1,82 +0,0 @@
// <copyright file="OtlpExporterOptionsGrpcExtensions.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 Grpc.Core;
#if NETSTANDARD2_1
using Grpc.Net.Client;
#endif
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol
{
internal static class OtlpExporterOptionsGrpcExtensions
{
#if NETSTANDARD2_1
public static GrpcChannel CreateChannel(this OtlpExporterOptions options)
#else
public static Channel CreateChannel(this OtlpExporterOptions options)
#endif
{
if (options.Endpoint.Scheme != Uri.UriSchemeHttp && options.Endpoint.Scheme != Uri.UriSchemeHttps)
{
throw new NotSupportedException($"Endpoint URI scheme ({options.Endpoint.Scheme}) is not supported. Currently only \"http\" and \"https\" are supported.");
}
#if NETSTANDARD2_1
return GrpcChannel.ForAddress(options.Endpoint);
#else
ChannelCredentials channelCredentials;
if (options.Endpoint.Scheme == Uri.UriSchemeHttps)
{
channelCredentials = new SslCredentials();
}
else
{
channelCredentials = ChannelCredentials.Insecure;
}
return new Channel(options.Endpoint.Authority, channelCredentials);
#endif
}
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;
}
}
}

View File

@ -15,12 +15,11 @@
// </copyright>
using System;
using System.Threading.Tasks;
using Grpc.Core;
using OpenTelemetry.Exporter.OpenTelemetryProtocol;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
using OpenTelemetry.Metrics;
using OtlpCollector = Opentelemetry.Proto.Collector.Metrics.V1;
using OtlpResource = Opentelemetry.Proto.Resource.V1;
namespace OpenTelemetry.Exporter
{
@ -29,9 +28,11 @@ namespace OpenTelemetry.Exporter
/// the OpenTelemetry protocol (OTLP).
/// </summary>
[AggregationTemporality(AggregationTemporality.Cumulative | AggregationTemporality.Delta, AggregationTemporality.Cumulative)]
public class OtlpMetricExporter : BaseOtlpExporter<Metric>
public class OtlpMetricExporter : BaseExporter<Metric>
{
private readonly OtlpCollector.MetricsService.IMetricsServiceClient metricsClient;
private readonly IExportClient<OtlpCollector.ExportMetricsServiceRequest> exportClient;
private OtlpResource.Resource processResource;
/// <summary>
/// Initializes a new instance of the <see cref="OtlpMetricExporter"/> class.
@ -45,22 +46,23 @@ namespace OpenTelemetry.Exporter
/// <summary>
/// Initializes a new instance of the <see cref="OtlpMetricExporter"/> class.
/// </summary>
/// <param name="options">Configuration options for the exporter.</param>
/// <param name="metricsServiceClient"><see cref="OtlpCollector.MetricsService.IMetricsServiceClient"/>.</param>
internal OtlpMetricExporter(OtlpExporterOptions options, OtlpCollector.MetricsService.IMetricsServiceClient metricsServiceClient = null)
: base(options)
/// <param name="options">Configuration options for the export.</param>
/// <param name="exportClient">Client used for sending export request.</param>
internal OtlpMetricExporter(OtlpExporterOptions options, IExportClient<OtlpCollector.ExportMetricsServiceRequest> exportClient = null)
{
if (metricsServiceClient != null)
if (exportClient != null)
{
this.metricsClient = metricsServiceClient;
this.exportClient = exportClient;
}
else
{
this.Channel = options.CreateChannel();
this.metricsClient = new OtlpCollector.MetricsService.MetricsServiceClient(this.Channel);
// TODO: this instantiation should be aligned with the protocol option (grpc or http/protobuf) when OtlpHttpMetricsExportClient will be implemented.
this.exportClient = new OtlpGrpcMetricsExportClient(options);
}
}
internal OtlpResource.Resource ProcessResource => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource();
/// <inheritdoc />
public override ExportResult Export(in Batch<Metric> metrics)
{
@ -70,16 +72,13 @@ namespace OpenTelemetry.Exporter
var request = new OtlpCollector.ExportMetricsServiceRequest();
request.AddMetrics(this.ProcessResource, metrics);
var deadline = DateTime.UtcNow.AddMilliseconds(this.Options.TimeoutMilliseconds);
try
{
this.metricsClient.Export(request, headers: this.Headers, deadline: deadline);
}
catch (RpcException ex)
{
OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(ex);
return ExportResult.Failure;
if (!this.exportClient.SendExportRequest(request))
{
return ExportResult.Failure;
}
}
catch (Exception ex)
{
@ -97,20 +96,7 @@ namespace OpenTelemetry.Exporter
/// <inheritdoc />
protected override bool OnShutdown(int timeoutMilliseconds)
{
if (this.Channel == null)
{
return true;
}
if (timeoutMilliseconds == -1)
{
this.Channel.ShutdownAsync().Wait();
return true;
}
else
{
return Task.WaitAny(new Task[] { this.Channel.ShutdownAsync(), Task.Delay(timeoutMilliseconds) }) == 0;
}
return this.exportClient?.Shutdown(timeoutMilliseconds) ?? true;
}
}
}

View File

@ -16,10 +16,10 @@
using System;
using System.Diagnostics;
using Grpc.Core;
using OpenTelemetry.Exporter.OpenTelemetryProtocol;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
using OtlpCollector = Opentelemetry.Proto.Collector.Trace.V1;
using OtlpResource = Opentelemetry.Proto.Resource.V1;
namespace OpenTelemetry.Exporter
{
@ -27,14 +27,16 @@ namespace OpenTelemetry.Exporter
/// Exporter consuming <see cref="Activity"/> and exporting the data using
/// the OpenTelemetry protocol (OTLP).
/// </summary>
public class OtlpTraceExporter : BaseOtlpExporter<Activity>
public class OtlpTraceExporter : BaseExporter<Activity>
{
private readonly OtlpCollector.TraceService.ITraceServiceClient traceClient;
private readonly IExportClient<OtlpCollector.ExportTraceServiceRequest> exportClient;
private OtlpResource.Resource processResource;
/// <summary>
/// Initializes a new instance of the <see cref="OtlpTraceExporter"/> class.
/// </summary>
/// <param name="options">Configuration options for the exporter.</param>
/// <param name="options">Configuration options for the export.</param>
public OtlpTraceExporter(OtlpExporterOptions options)
: this(options, null)
{
@ -43,42 +45,38 @@ namespace OpenTelemetry.Exporter
/// <summary>
/// Initializes a new instance of the <see cref="OtlpTraceExporter"/> class.
/// </summary>
/// <param name="options">Configuration options for the exporter.</param>
/// <param name="traceServiceClient"><see cref="OtlpCollector.TraceService.TraceServiceClient"/>.</param>
internal OtlpTraceExporter(OtlpExporterOptions options, OtlpCollector.TraceService.ITraceServiceClient traceServiceClient = null)
: base(options)
/// <param name="options">Configuration options for the export.</param>
/// <param name="exportClient">Client used for sending export request.</param>
internal OtlpTraceExporter(OtlpExporterOptions options, IExportClient<OtlpCollector.ExportTraceServiceRequest> exportClient = null)
{
if (traceServiceClient != null)
if (exportClient != null)
{
this.traceClient = traceServiceClient;
this.exportClient = exportClient;
}
else
{
this.Channel = options.CreateChannel();
this.traceClient = new OtlpCollector.TraceService.TraceServiceClient(this.Channel);
this.exportClient = options.GetTraceExportClient();
}
}
internal OtlpResource.Resource ProcessResource => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource();
/// <inheritdoc/>
public override ExportResult Export(in Batch<Activity> activityBatch)
{
// Prevents the exporter's gRPC and HTTP operations from being instrumented.
using var scope = SuppressInstrumentationScope.Begin();
OtlpCollector.ExportTraceServiceRequest request = new OtlpCollector.ExportTraceServiceRequest();
var request = new OtlpCollector.ExportTraceServiceRequest();
request.AddBatch(this.ProcessResource, activityBatch);
var deadline = DateTime.UtcNow.AddMilliseconds(this.Options.TimeoutMilliseconds);
try
{
this.traceClient.Export(request, headers: this.Headers, deadline: deadline);
}
catch (RpcException ex)
{
OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(ex);
return ExportResult.Failure;
if (!this.exportClient.SendExportRequest(request))
{
return ExportResult.Failure;
}
}
catch (Exception ex)
{
@ -93,5 +91,11 @@ namespace OpenTelemetry.Exporter
return ExportResult.Success;
}
/// <inheritdoc />
protected override bool OnShutdown(int timeoutMilliseconds)
{
return this.exportClient?.Shutdown(timeoutMilliseconds) ?? true;
}
}
}

View File

@ -29,6 +29,7 @@ setters take precedence over the environment variables.
contain a port and path.
* `Headers`: Optional headers for the connection.
* `TimeoutMilliseconds` : Max waiting time for the backend to process a batch.
* `Protocol`: OTLP transport protocol. Supported values: Grpc and 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)
@ -45,11 +46,12 @@ The following environment variables can be used to override the default
values of the `OtlpExporterOptions`
(following the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md)).
| Environment variable | `OtlpExporterOptions` property |
| ------------------------------| -------------------------------|
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `Endpoint` |
| `OTEL_EXPORTER_OTLP_HEADERS` | `Headers` |
| `OTEL_EXPORTER_OTLP_TIMEOUT` | `TimeoutMilliseconds` |
| Environment variable | `OtlpExporterOptions` property |
| ------------------------------| ----------------------------------|
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `Endpoint` |
| `OTEL_EXPORTER_OTLP_HEADERS` | `Headers` |
| `OTEL_EXPORTER_OTLP_TIMEOUT` | `TimeoutMilliseconds` |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `Protocol` (grpc or http/protobuf)|
## Special case when using insecure channel

View File

@ -22,6 +22,7 @@ using Benchmarks.Helper;
using Grpc.Core;
using OpenTelemetry;
using OpenTelemetry.Exporter;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
using OpenTelemetry.Internal;
using OtlpCollector = Opentelemetry.Proto.Collector.Trace.V1;
@ -43,9 +44,11 @@ namespace Benchmarks.Exporter
[GlobalSetup]
public void GlobalSetup()
{
var options = new OtlpExporterOptions();
this.exporter = new OtlpTraceExporter(
new OtlpExporterOptions(),
new NoopTraceServiceClient());
options,
new OtlpGrpcTraceExportClient(options, new NoopTraceServiceClient()));
this.activity = ActivityHelper.CreateTestActivity();
this.activityBatch = new CircularBuffer<Activity>(this.NumberOfSpans);
}

View File

@ -0,0 +1,204 @@
// <copyright file="OtlpHttpTraceExportClientTests.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.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Moq.Protected;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
using OpenTelemetry.Resources;
using OpenTelemetry.Tests;
using OpenTelemetry.Trace;
using Xunit;
using OtlpCollector = Opentelemetry.Proto.Collector.Trace.V1;
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.Implementation.ExportClient
{
public class OtlpHttpTraceExportClientTests
{
static OtlpHttpTraceExportClientTests()
{
Activity.DefaultIdFormat = ActivityIdFormat.W3C;
Activity.ForceDefaultIdFormat = true;
var listener = new ActivityListener
{
ShouldListenTo = _ => true,
Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllData,
};
ActivitySource.AddActivityListener(listener);
}
[Fact]
public void NewOtlpHttpTraceExportClient_OtlpExporterOptions_ExporterHasCorrectProperties()
{
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 client = new OtlpHttpTraceExportClient(options);
Assert.NotNull(client.HttpClient);
Assert.Equal(2, client.Headers.Count);
Assert.Contains(client.Headers, kvp => kvp.Key == header1.Name && kvp.Value == header1.Value);
Assert.Contains(client.Headers, kvp => kvp.Key == header2.Name && kvp.Value == header2.Value);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void SendExportRequest_ExportTraceServiceRequest_SendsCorrectHttpRequest(bool includeServiceNameInResource)
{
// Arrange
var evenTags = new[] { new KeyValuePair<string, object>("k0", "v0") };
var oddTags = new[] { new KeyValuePair<string, object>("k1", "v1") };
var sources = new[]
{
new ActivitySource("even", "2.4.6"),
new ActivitySource("odd", "1.3.5"),
};
var header1 = new { Name = "hdr1", Value = "val1" };
var header2 = new { Name = "hdr2", Value = "val2" };
var options = new OtlpExporterOptions
{
Endpoint = new Uri("http://localhost:4317"),
Headers = $"{header1.Name}={header1.Value}, {header2.Name} = {header2.Value}",
};
var httpHandlerMock = new Mock<HttpMessageHandler>();
HttpRequestMessage httpRequest = null;
var httpRequestContent = Array.Empty<byte>();
httpHandlerMock.Protected()
#if NET5_0_OR_GREATER
.Setup<HttpResponseMessage>("Send", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.Returns((HttpRequestMessage request, CancellationToken token) =>
{
return new HttpResponseMessage();
})
.Callback<HttpRequestMessage, CancellationToken>((r, ct) =>
{
httpRequest = r;
// We have to capture content as it can't be accessed after request is disposed inside of SendExportRequest method
httpRequestContent = r.Content.ReadAsByteArrayAsync()?.Result;
})
#else
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync((HttpRequestMessage request, CancellationToken token) =>
{
return new HttpResponseMessage();
})
.Callback<HttpRequestMessage, CancellationToken>(async (r, ct) =>
{
httpRequest = r;
// We have to capture content as it can't be accessed after request is disposed inside of SendExportRequest method
httpRequestContent = await r.Content.ReadAsByteArrayAsync();
})
#endif
.Verifiable();
var exportClient = new OtlpHttpTraceExportClient(options, new HttpClient(httpHandlerMock.Object));
var resourceBuilder = ResourceBuilder.CreateEmpty();
if (includeServiceNameInResource)
{
resourceBuilder.AddAttributes(
new List<KeyValuePair<string, object>>
{
new KeyValuePair<string, object>(ResourceSemanticConventions.AttributeServiceName, "service_name"),
new KeyValuePair<string, object>(ResourceSemanticConventions.AttributeServiceNamespace, "ns_1"),
});
}
var builder = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(resourceBuilder)
.AddSource(sources[0].Name)
.AddSource(sources[1].Name);
using var openTelemetrySdk = builder.Build();
var processor = new BatchActivityExportProcessor(new TestExporter<Activity>(RunTest));
const int numOfSpans = 10;
bool isEven;
for (var i = 0; i < numOfSpans; i++)
{
isEven = i % 2 == 0;
var source = sources[i % 2];
var activityKind = isEven ? ActivityKind.Client : ActivityKind.Server;
var activityTags = isEven ? evenTags : oddTags;
using Activity activity = source.StartActivity($"span-{i}", activityKind, parentContext: default, activityTags);
processor.OnEnd(activity);
}
processor.Shutdown();
void RunTest(Batch<Activity> batch)
{
var request = new OtlpCollector.ExportTraceServiceRequest();
request.AddBatch(resourceBuilder.Build().ToOtlpResource(), batch);
// Act
var result = exportClient.SendExportRequest(request);
// Assert
Assert.True(result);
Assert.NotNull(httpRequest);
Assert.Equal(HttpMethod.Post, httpRequest.Method);
Assert.Equal("http://localhost:4317/v1/traces", 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<ByteArrayContent>(httpRequest.Content);
Assert.Contains(httpRequest.Content.Headers, h => h.Key == "Content-Type" && h.Value.First() == OtlpHttpTraceExportClient.MediaContentType);
var exportTraceRequest = OtlpCollector.ExportTraceServiceRequest.Parser.ParseFrom(httpRequestContent);
Assert.NotNull(exportTraceRequest);
Assert.Single(exportTraceRequest.ResourceSpans);
var resourceSpan = exportTraceRequest.ResourceSpans.First();
if (includeServiceNameInResource)
{
Assert.Contains(resourceSpan.Resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service_name");
Assert.Contains(resourceSpan.Resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceNamespace && kvp.Value.StringValue == "ns_1");
}
else
{
Assert.Contains(resourceSpan.Resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:"));
}
}
}
}
}

View File

@ -0,0 +1,135 @@
// <copyright file="OtlpExporterOptionsExtensionsTests.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.Collections.Generic;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
using Xunit;
using Xunit.Sdk;
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests
{
public class OtlpExporterOptionsExtensionsTests
{
[Theory]
[InlineData("key=value", new string[] { "key" }, new string[] { "value" })]
[InlineData("key1=value1,key2=value2", new string[] { "key1", "key2" }, new string[] { "value1", "value2" })]
[InlineData("key1 = value1, key2=value2 ", new string[] { "key1", "key2" }, new string[] { "value1", "value2" })]
[InlineData("key==value", new string[] { "key" }, new string[] { "=value" })]
[InlineData("access-token=abc=/123,timeout=1234", new string[] { "access-token", "timeout" }, new string[] { "abc=/123", "1234" })]
[InlineData("key1=value1;key2=value2", new string[] { "key1" }, new string[] { "value1;key2=value2" })] // semicolon is not treated as a delimeter (https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables)
public void GetMetadataFromHeadersWorksCorrectFormat(string headers, string[] keys, string[] values)
{
var options = new OtlpExporterOptions();
options.Headers = headers;
var metadata = options.GetMetadataFromHeaders();
Assert.Equal(keys.Length, metadata.Count);
for (int i = 0; i < keys.Length; i++)
{
Assert.Contains(metadata, entry => entry.Key == keys[i] && entry.Value == values[i]);
}
}
[Theory]
[InlineData("headers")]
[InlineData("key,value")]
public void GetMetadataFromHeadersThrowsExceptionOnInvalidFormat(string headers)
{
try
{
var options = new OtlpExporterOptions();
options.Headers = headers;
var metadata = options.GetMetadataFromHeaders();
}
catch (Exception ex)
{
Assert.IsType<ArgumentException>(ex);
Assert.Equal("Headers provided in an invalid format.", ex.Message);
return;
}
throw new XunitException("GetMetadataFromHeaders did not throw an exception for invalid input headers");
}
[Theory]
[InlineData("")]
[InlineData(null)]
public void GetHeaders_NoOptionHeaders_ReturnsEmptyHeadres(string optionHeaders)
{
var options = new OtlpExporterOptions
{
Headers = optionHeaders,
};
var headers = options.GetHeaders<Dictionary<string, string>>((d, k, v) => d.Add(k, v));
Assert.Empty(headers);
}
[Theory]
[InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcTraceExportClient))]
[InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpTraceExportClient))]
public void GetTraceExportClient_SupportedProtocol_ReturnsCorrectExportClient(OtlpExportProtocol protocol, Type expectedExportClientType)
{
var options = new OtlpExporterOptions
{
Protocol = protocol,
};
var exportClient = options.GetTraceExportClient();
Assert.Equal(expectedExportClientType, exportClient.GetType());
}
[Fact]
public void GetTraceExportClient_UnsupportedProtocol_Throws()
{
var options = new OtlpExporterOptions
{
Protocol = (OtlpExportProtocol)123,
};
Assert.Throws<NotSupportedException>(() => options.GetTraceExportClient());
}
[Theory]
[InlineData("grpc", OtlpExportProtocol.Grpc)]
[InlineData("http/protobuf", OtlpExportProtocol.HttpProtobuf)]
[InlineData("unsupported", null)]
public void ToOtlpExportProtocol_Protocol_MapsToCorrectValue(string protocol, OtlpExportProtocol? expectedExportProtocol)
{
var exportProtocol = protocol.ToOtlpExportProtocol();
Assert.Equal(expectedExportProtocol, exportProtocol);
}
[Theory]
[InlineData("http://test:8888", "http://test:8888/v1/traces")]
[InlineData("http://test:8888/", "http://test:8888/v1/traces")]
[InlineData("http://test:8888/v1/traces", "http://test:8888/v1/traces")]
[InlineData("http://test:8888/v1/traces/", "http://test:8888/v1/traces/")]
public void AppendPathIfNotPresent_TracesPath_AppendsCorrectly(string inputUri, string expectedUri)
{
var uri = new Uri(inputUri, UriKind.Absolute);
var resultUri = uri.AppendPathIfNotPresent(OtlpExporterOptions.TracesExportPath);
Assert.Equal(expectedUri, resultUri.AbsoluteUri);
}
}
}

View File

@ -1,67 +0,0 @@
// <copyright file="OtlpExporterOptionsGrpcExtensionsTests.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 Xunit;
using Xunit.Sdk;
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests
{
public class OtlpExporterOptionsGrpcExtensionsTests
{
[Theory]
[InlineData("key=value", new string[] { "key" }, new string[] { "value" })]
[InlineData("key1=value1,key2=value2", new string[] { "key1", "key2" }, new string[] { "value1", "value2" })]
[InlineData("key1 = value1, key2=value2 ", new string[] { "key1", "key2" }, new string[] { "value1", "value2" })]
[InlineData("key==value", new string[] { "key" }, new string[] { "=value" })]
[InlineData("access-token=abc=/123,timeout=1234", new string[] { "access-token", "timeout" }, new string[] { "abc=/123", "1234" })]
[InlineData("key1=value1;key2=value2", new string[] { "key1" }, new string[] { "value1;key2=value2" })] // semicolon is not treated as a delimeter (https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables)
public void GetMetadataFromHeadersWorksCorrectFormat(string headers, string[] keys, string[] values)
{
var options = new OtlpExporterOptions();
options.Headers = headers;
var metadata = options.GetMetadataFromHeaders();
Assert.Equal(keys.Length, metadata.Count);
for (int i = 0; i < keys.Length; i++)
{
Assert.Contains(metadata, entry => entry.Key == keys[i] && entry.Value == values[i]);
}
}
[Theory]
[InlineData("headers")]
[InlineData("key,value")]
public void GetMetadataFromHeadersThrowsExceptionOnOnvalidFormat(string headers)
{
try
{
var options = new OtlpExporterOptions();
options.Headers = headers;
var metadata = options.GetMetadataFromHeaders();
}
catch (Exception ex)
{
Assert.IsType<ArgumentException>(ex);
Assert.Equal("Headers provided in an invalid format.", ex.Message);
return;
}
throw new XunitException("GetMetadataFromHeaders did not throw an exception for invalid input headers");
}
}
}

View File

@ -39,6 +39,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests
Assert.Equal(new Uri("http://localhost:4317"), options.Endpoint);
Assert.Null(options.Headers);
Assert.Equal(10000, options.TimeoutMilliseconds);
Assert.Equal(OtlpExportProtocol.Grpc, options.Protocol);
}
[Fact]
@ -47,12 +48,14 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests
Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "http://test:8888");
Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, "A=2,B=3");
Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, "2000");
Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "http/protobuf");
var options = new OtlpExporterOptions();
Assert.Equal(new Uri("http://test:8888"), options.Endpoint);
Assert.Equal("A=2,B=3", options.Headers);
Assert.Equal(2000, options.TimeoutMilliseconds);
Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol);
}
[Fact]
@ -75,23 +78,36 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests
Assert.Equal(10000, options.TimeoutMilliseconds); // use default
}
[Fact]
public void OtlpExporterOptions_InvalidProtocolVariableOverride()
{
Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "invalid");
var options = new OtlpExporterOptions();
Assert.Equal(OtlpExportProtocol.Grpc, options.Protocol); // use default
}
[Fact]
public void OtlpExporterOptions_SetterOverridesEnvironmentVariable()
{
Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "http://test:8888");
Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, "A=2,B=3");
Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, "2000");
Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "grpc");
var options = new OtlpExporterOptions
{
Endpoint = new Uri("http://localhost:200"),
Headers = "C=3",
TimeoutMilliseconds = 40000,
Protocol = OtlpExportProtocol.HttpProtobuf,
};
Assert.Equal(new Uri("http://localhost:200"), options.Endpoint);
Assert.Equal("C=3", options.Headers);
Assert.Equal(40000, options.TimeoutMilliseconds);
Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol);
}
[Fact]
@ -100,6 +116,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests
Assert.Equal("OTEL_EXPORTER_OTLP_ENDPOINT", OtlpExporterOptions.EndpointEnvVarName);
Assert.Equal("OTEL_EXPORTER_OTLP_HEADERS", OtlpExporterOptions.HeadersEnvVarName);
Assert.Equal("OTEL_EXPORTER_OTLP_TIMEOUT", OtlpExporterOptions.TimeoutEnvVarName);
Assert.Equal("OTEL_EXPORTER_OTLP_PROTOCOL", OtlpExporterOptions.ProtocolEnvVarName);
}
private static void ClearEnvVars()
@ -107,6 +124,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests
Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, null);
Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, null);
Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, null);
Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, null);
}
}
}

View File

@ -20,6 +20,7 @@ using System.Diagnostics.Metrics;
using System.Linq;
using System.Threading;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Tests;

View File

@ -19,7 +19,9 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Moq;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
using OpenTelemetry.Resources;
using OpenTelemetry.Tests;
using OpenTelemetry.Trace;
@ -112,12 +114,12 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests
var oltpResource = request.ResourceSpans.First().Resource;
if (includeServiceNameInResource)
{
Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == Resources.ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service-name");
Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == Resources.ResourceSemanticConventions.AttributeServiceNamespace && kvp.Value.StringValue == "ns1");
Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service-name");
Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceNamespace && kvp.Value.StringValue == "ns1");
}
else
{
Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == Resources.ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:"));
Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:"));
}
foreach (var instrumentationLibrarySpans in request.ResourceSpans.First().InstrumentationLibrarySpans)
@ -332,6 +334,18 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests
Assert.True(endCalled);
}
[Fact]
public void Shutdown_ClientShutdownIsCalled()
{
var exportClientMock = new Mock<IExportClient<OtlpCollector.ExportTraceServiceRequest>>();
var exporter = new OtlpTraceExporter(new OtlpExporterOptions(), exportClientMock.Object);
var result = exporter.Shutdown();
exportClientMock.Verify(m => m.Shutdown(It.IsAny<int>()), Times.Once());
}
private class NoopTraceServiceClient : OtlpCollector.TraceService.ITraceServiceClient
{
public OtlpCollector.ExportTraceServiceResponse Export(OtlpCollector.ExportTraceServiceRequest request, GrpcCore.Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default)