I'm having trouble getting OTEL tracing to work in a .NET console app. I'm trying to setup tracing using
the services.AddOpenTelemetry().WithTracing(options => ...)
method. I've created a class to store an
instance of an ActivitySource
so that I can inject that throughout the app.
The following CoPilot sample works and I can see the trace in my local instance of Grafana. However, I don't want to build the app this way since I'd like to use DI.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTelemetry" Version="1.11.1"/>
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.1"/>
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.11.1"/>
</ItemGroup>
</Project>
using System.Diagnostics;
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
namespace OtelTest
{
class Program
{
static void Main(string[] args)
{
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource("OtelTest")
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("OtelTestService"))
.AddOtlpExporter(options => options.Endpoint = new Uri("http://localhost:4317"))
.Build();
var activitySource = new ActivitySource("OtelTest");
using (var activity = activitySource.StartActivity("TestActivity"))
{
Console.WriteLine("Hello, World!");
activity?.SetTag("foo", 1);
activity?.SetTag("bar", "baz");
}
}
}
}
Here's an expanded sample that demonstrates what I'm trying to (using the same packages as above). When I run this sample I don't get any traces in the local Grafana.
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
namespace OtelTest
{
public class OtelTestTracing
{
public ActivitySource ActivitySource { get; }
public OtelTestTracing()
{
ActivitySource = new ActivitySource("OtelTest");
}
}
public class ActivityService
{
private readonly OtelTestTracing _otelTestTracing;
public ActivityService(OtelTestTracing otelTestTracing)
{
_otelTestTracing = otelTestTracing;
}
public void DoWork()
{
using (var activity = _otelTestTracing.ActivitySource.StartActivity("TestActivity"))
{
Console.WriteLine("Hello, World!");
activity?.SetTag("foo", 1);
activity?.SetTag("bar", "baz");
}
}
}
class Program
{
static void Main(string[] args)
{
var serviceCollection = new ServiceCollection();
ConfigureServices(serviceCollection);
var serviceProvider = serviceCollection.BuildServiceProvider();
var activityService = serviceProvider.GetRequiredService<ActivityService>();
activityService.DoWork();
}
private static void ConfigureServices(IServiceCollection services)
{
services
.AddOpenTelemetry()
.WithTracing(builder => builder
.AddSource("OtelTest")
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("OtelTestService"))
.AddOtlpExporter(options => options.Endpoint = new Uri("http://localhost:4317")));
services.AddSingleton<OtelTestTracing>();
services.AddTransient<ActivityService>();
}
}
}
If I observe the OTEL collector logs in the Docker container instance, it doesn't show any incoming traces.
If I set a breakpoint and observe the _otelTestTracing.ActivitySource
instance being injected into
the ActivityService
class I notice that the private _listeners
properties is null. Whereas in the
working sample _listeners
is set of an instance of SynchronizedListeners
. That seems to be where
it's breaking but I don't understand why.
What changes can I make to get the tracing to work in the DI sample?
Using suggestions from Martin's answer, the following changes got the tracing to work. I couldn't get it working with services.AddOpenTelemetry().WithTracing(...)
. I had to use the Sdk
object to create the TracerProvider
and add an explicit call to the tracerProvider.Dispose()
method. The use of the host builder in this sample isn't related to the tracing.
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
namespace OtelTest
{
public class OtelTestTracing
{
public ActivitySource ActivitySource { get; }
public OtelTestTracing()
{
ActivitySource = new ActivitySource("OtelTest");
}
}
public class ActivityService
{
private readonly OtelTestTracing _otelTestTracing;
public ActivityService(OtelTestTracing otelTestTracing)
{
_otelTestTracing = otelTestTracing;
}
public void DoWork()
{
using (var activity = _otelTestTracing.ActivitySource.StartActivity("TestActivity"))
{
Console.WriteLine("Hello, World!");
activity?.SetTag("foo", 1);
activity?.SetTag("bar", "baz");
}
}
}
class Program
{
static void Main(string[] args)
{
var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource("OtelTest")
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("OtelTestService"))
.AddOtlpExporter(options => options.Endpoint = new Uri("http://localhost:4317"))
.Build();
var builder = Host.CreateApplicationBuilder();
builder.Services.AddSingleton<OtelTestTracing>();
builder.Services.AddSingleton<ActivityService>();
var host = builder.Build();
var activityService = host.Services.GetRequiredService<ActivityService>();
activityService.DoWork();
tracerProvider.Dispose();
}
}
}
I wouldn't say there's a "correct" way for this, but there are patterns that are more common that help avoid some of the inefficient patterns.
First, ActivitySource
should be static, there's no benefit to using instance methods, and they should generally be scoped to the DLL/project, rather than the class. These map to "instrumentation scope" in the OpenTelemetry world, and are generally reserved for libraries.
You should ensure that your application calls TracerProvider.Dispose()
and waits before it closes, this will ensure that any in-memory spans have been pushed to the exporter (same for MetricProvider
and LogRecordProvider
).
You also need to force the TracerProvider to "build". This is done through the use of a BackgroundService. If you're using something like a HostBuilder, these background services are already started, but if you're using the ServiceProvider manually, this won't happen. I believe you could resolve the BackgroundService, or resolve TracerProvider
, and this would work.
My recommendation though, would be to switch to using HostApplicationBuilder
instead of the IServiceCollection
directly, this is generally accepted as a better way to write console applications now.