Skip to content

Add OTLP for Activity Exporter #679

New issue

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

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

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions samples/Exporters/Console/InstrumentationWithActivitySource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// <copyright file="InstrumentationWithActivitySource.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.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Samples
{
internal class InstrumentationWithActivitySource : IDisposable
{
private const string RequestPath = "/api/request";
private SampleServer server = new SampleServer();
private SampleClient client = new SampleClient();

public void Start(ushort port = 19999)
{
var url = $"http://localhost:{port.ToString(CultureInfo.InvariantCulture)}{RequestPath}/";
this.server.Start(url);
this.client.Start(url);
}

public void Dispose()
{
this.client.Dispose();
this.server.Dispose();
}

private class SampleServer : IDisposable
{
private HttpListener listener = new HttpListener();

public void Start(string url)
{
this.listener.Prefixes.Add(url);
this.listener.Start();

Task.Run(() =>
{
using var source = new ActivitySource("Samples.SampleServer");

while (this.listener.IsListening)
{
try
{
var context = this.listener.GetContext();

using var activity = source.StartActivity(
$"{context.Request.HttpMethod}:{context.Request.Url.AbsolutePath}",
ActivityKind.Server);

var headerKeys = context.Request.Headers.AllKeys;
foreach (var headerKey in headerKeys)
{
string headerValue = context.Request.Headers[headerKey];
activity?.AddTag($"http.header.{headerKey}", headerValue);
}

string requestContent;
using (var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding))
{
requestContent = reader.ReadToEnd();
}

activity?.AddTag("request.content", requestContent);
activity?.AddTag("request.length", requestContent.Length.ToString());

var echo = Encoding.UTF8.GetBytes("echo: " + requestContent);
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.ContentLength64 = echo.Length;
context.Response.OutputStream.Write(echo, 0, echo.Length);
context.Response.Close();
}
catch (Exception)
{
// expected when closing the listener.
}
}
});
}

public void Dispose()
{
((IDisposable)this.listener).Dispose();
}
}

private class SampleClient : IDisposable
{
private CancellationTokenSource cts;
private Task requestTask;

public void Start(string url)
{
this.cts = new CancellationTokenSource();
var cancellationToken = this.cts.Token;

this.requestTask = Task.Run(async () =>
{
using var source = new ActivitySource("Samples.SampleClient");
using var client = new HttpClient();

var count = 1;
while (!cancellationToken.IsCancellationRequested)
{
var content = new StringContent($"client message: {DateTime.Now}", Encoding.UTF8);

using (var activity = source.StartActivity("POST:" + RequestPath, ActivityKind.Client))
{
count++;

activity?.AddEvent(new ActivityEvent("PostAsync:Started"));
using var response = await client.PostAsync(url, content, cancellationToken).ConfigureAwait(false);
activity?.AddEvent(new ActivityEvent("PostAsync:Ended"));

activity?.AddTag("http.status_code", $"{response.StatusCode:D}");

var responseContent = await response.Content.ReadAsStringAsync();
activity?.AddTag("response.content", responseContent);
activity?.AddTag("response.length", responseContent.Length.ToString(CultureInfo.InvariantCulture));

foreach (var header in response.Headers)
{
if (header.Value is IEnumerable<object> enumerable)
{
activity?.AddTag($"http.header.{header.Key}", string.Join(",", enumerable));
}
else
{
activity?.AddTag($"http.header.{header.Key}", header.Value.ToString());
}
}
}

try
{
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
return;
}
}
},
cancellationToken);
}

public void Dispose()
{
if (this.cts != null)
{
this.cts.Cancel();
this.requestTask.Wait();
this.requestTask.Dispose();
this.cts.Dispose();
}
}
}
}
}
5 changes: 4 additions & 1 deletion samples/Exporters/Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public static void Main(string[] args)
(ZPagesOptions options) => TestZPages.Run(),
(ConsoleOptions options) => TestConsole.Run(options),
(ConsoleActivityOptions options) => TestConsoleActivity.Run(options),
(OtlpOptions options) => TestOtlp.Run(options.Endpoint),
(OtlpOptions options) => TestOtlp.Run(options.Endpoint, options.UseActivitySource),
errs => 1);

Console.ReadLine();
Expand Down Expand Up @@ -122,6 +122,9 @@ internal class OtlpOptions
{
[Option('e', "endpoint", HelpText = "Target to which the exporter is going to send traces or metrics", Default = "localhost:55680")]
public string Endpoint { get; set; }

[Option('a', "activity", HelpText = "Set it to true to export ActivitySource data", Default = false)]
public bool UseActivitySource { get; set; }
}

#pragma warning restore SA1402 // File may only contain a single type
Expand Down
35 changes: 34 additions & 1 deletion samples/Exporters/Console/TestOtlp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,17 @@ namespace Samples
{
internal static class TestOtlp
{
internal static object Run(string endpoint)
internal static object Run(string endpoint, bool useActivitySource)
{
if (useActivitySource)
{
return RunWithActivitySource(endpoint);
}

return RunWithSdk(endpoint);
}

private static object RunWithSdk(string endpoint)
{
using var tracerFactory = TracerFactory.Create(builder => builder
.SetResource(Resources.CreateServiceResource("otlp-test"))
Expand All @@ -47,5 +57,28 @@ internal static object Run(string endpoint)

return null;
}

private static object RunWithActivitySource(string endpoint)
{
// Enable OpenTelemetry for the sources "Samples.SampleServer" and "Samples.SampleClient"
// and use OTLP exporter.
OpenTelemetrySdk.EnableOpenTelemetry(
builder => builder
.AddActivitySource("Samples.SampleServer")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cijothomas I'm leaning into listening to all sources by default and filter out un-wanted ones. Depending on how some libraries are instrumented we may need some processors to filter out some activities.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. Lets address this separately.
Libraries like httpclient, which was instrumented using DiagnosticSource, will have their activities emitted with empty ActivitySource. Listening to them requires AddActivitySource("") now.
Anyway, lets address this after some exporters/adapters are ready with Activity API.

.AddActivitySource("Samples.SampleClient")
.UseOpenTelemetryProtocolActivityExporter(opt => opt.Endpoint = endpoint));

// The above line is required only in Applications
// which decide to use OT.
using (var sample = new InstrumentationWithActivitySource())
{
sample.Start();

Console.WriteLine("Sample is running on the background, press ENTER to stop");
Console.ReadLine();
}

return null;
}
}
}
14 changes: 14 additions & 0 deletions src/OpenTelemetry.Exporter.Console/ConsoleActivityExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public override Task<ExportResult> ExportAsync(IEnumerable<Activity> activityBat

System.Console.WriteLine("Activity OperationName - " + activity.OperationName);
System.Console.WriteLine("Activity DisplayName - " + activity.DisplayName);
System.Console.WriteLine("Activity Kind - " + activity.Kind);
System.Console.WriteLine("Activity StartTime - " + activity.StartTimeUtc);
System.Console.WriteLine("Activity Duration - " + activity.Duration);
if (activity.Tags.Count() > 0)
Expand All @@ -79,6 +80,19 @@ public override Task<ExportResult> ExportAsync(IEnumerable<Activity> activityBat
foreach (var activityEvent in activity.Events)
{
System.Console.WriteLine($"Event Name: {activityEvent.Name} TimeStamp: {activityEvent.Timestamp}");
foreach (var attribute in activityEvent.Attributes)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

{
System.Console.WriteLine($"\t {attribute.Key} : {attribute.Value}");
}
}
}

if (activity.Baggage.Any())
{
System.Console.WriteLine("Activity Baggage");
foreach (var baggage in activity.Baggage)
{
System.Console.WriteLine($"\t {baggage.Key} : {baggage.Value}");
}
}

Expand Down
Loading