Skip to content

Commit

Permalink
Add OTLP/HTTP binary protobuf trace exporter for .NET open-telemetry#…
Browse files Browse the repository at this point in the history
…2292 (open-telemetry#2292)

- HttpClient based Implementation
- unit tests for OtlpHttpTraceExporter
- added console test example using OtlpHttpTraceExporter
  • Loading branch information
rypdal committed Sep 3, 2021
1 parent 79d4c49 commit 7071ed5
Show file tree
Hide file tree
Showing 11 changed files with 622 additions and 25 deletions.
11 changes: 10 additions & 1 deletion examples/Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand All @@ -42,7 +43,7 @@ public class Program
/// <param name="args">Arguments from command line.</param>
public static void Main(string[] args)
{
Parser.Default.ParseArguments<JaegerOptions, ZipkinOptions, PrometheusOptions, MetricsOptions, GrpcNetClientOptions, HttpClientOptions, RedisOptions, ZPagesOptions, ConsoleOptions, OpenTelemetryShimOptions, OpenTracingShimOptions, OtlpOptions, InMemoryOptions>(args)
Parser.Default.ParseArguments<JaegerOptions, ZipkinOptions, PrometheusOptions, MetricsOptions, GrpcNetClientOptions, HttpClientOptions, RedisOptions, ZPagesOptions, ConsoleOptions, OpenTelemetryShimOptions, OpenTracingShimOptions, OtlpOptions, OtlpHttpOptions, InMemoryOptions>(args)
.MapResult(
(JaegerOptions options) => TestJaegerExporter.Run(options.Host, options.Port),
(ZipkinOptions options) => TestZipkinExporter.Run(options.Uri),
Expand All @@ -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);
}
Expand Down Expand Up @@ -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
{
Expand Down
56 changes: 56 additions & 0 deletions examples/Console/TestOtlpHttpExporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// <copyright file="TestOtlpExporter.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 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// <copyright file="BaseOtlpHttpExporter.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;
using OtlpResource = Opentelemetry.Proto.Resource.V1;

namespace OpenTelemetry.Exporter.OpenTelemetryProtocol
{
/// <summary>
/// Implements exporter that exports telemetry objects over OTLP/HTTP.
/// </summary>
/// <typeparam name="T">The type of telemetry object to be exported.</typeparam>
public abstract class BaseOtlpHttpExporter<T> : BaseExporter<T>
where T : class
{
private OtlpResource.Resource processResource;
private bool disposedValue; // To avoid duplicate dispose calls

/// <summary>
/// Initializes a new instance of the <see cref="BaseOtlpHttpExporter{T}"/> class.
/// </summary>
/// <param name="options">The <see cref="OtlpExporterOptions"/> for configuring the exporter.</param>
/// <param name="httpHandler">The <see cref="IHttpHandler"/> used http requests.</param>
protected BaseOtlpHttpExporter(OtlpExporterOptions options, IHttpHandler httpHandler = null)
{
this.Options = options ?? throw new ArgumentNullException(nameof(options));
this.Headers = options.GetHeaders<Dictionary<string, string>>((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<string, string> Headers { get; }

internal IHttpHandler HttpHandler { get; }

/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (!this.disposedValue)
{
if (disposing)
{
this.HttpHandler?.Dispose();
}

this.disposedValue = true;
}

base.Dispose(disposing);
}

/// <inheritdoc/>
protected override bool OnShutdown(int timeoutMilliseconds)
{
try
{
this.HttpHandler.CancelPendingRequests();
return true;
}
catch (Exception ex)
{
OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex);
return false;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// <copyright file="HttpHandler.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.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation
{
/// <summary>
/// Class decorating <see cref="System.Net.Http.HttpClient"/>.
/// </summary>
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<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return await this.HttpClient.SendAsync(request, cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// <copyright file="IHttpHandler.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.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation
{
/// <summary>
/// Interface partialy exposing <see cref="HttpClient"/> methods.
/// </summary>
public interface IHttpHandler : IDisposable
{
/// <summary>
/// Cancel all pending requests on this instance.
/// </summary>
void CancelPendingRequests();

/// <summary>
/// Send an HTTP request as an asynchronous operation.
/// </summary>
/// <param name="request">The HTTP request message to send.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
/// <returns>Result of the export operation.</returns>
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default);
}
}
Original file line number Diff line number Diff line change
@@ -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<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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Metadata>((m, k, v) => m.Add(k, v));
}
}
}
Loading

0 comments on commit 7071ed5

Please sign in to comment.