Skip to main content
DamienG
Code editor beside a diagram of connected services and databases

Background

OpenTracing is an interesting project that allows for the collection of various trace sources to be correlated together to provide a timeline of activity that can span services for reporting in a central system. They were competing with OpenCensus but have now merged to form OpenTelemetry.

Updated October 2026: OpenTelemetry .NET has grown up since I wrote this. Traces, metrics and logs are all stable, AddOpenTelemetryTracing was removed in 1.4.0 in favour of services.AddOpenTelemetry().WithTracing(...), and the Jaeger exporter was deprecated and removed because Jaeger now accepts OTLP natively, so the samples below export via OTLP instead. I've also fixed the ActivitySource sample (it's StartActivity, not BuildActivity), updated the OpenTracing shim registration (the shim itself is now deprecated), moved log-to-span correlation to the OpenTelemetry.Extensions package and refreshed the package table.

I was recently brought in as a consultant to help migrate an existing system that used OpenTracing in .NET that recorded trace data into Jaeger so that they might migrate to the latest OpenTelemetry libraries. I thought it would be useful to document what I learnt as the migration process is not particularly clear.

The first thing to note is that OpenTracing and OpenTelemetry are both multi-platform systems and so support many languages and services. While this is great from a standardisation process it does mean that a lot of information you find doesn't necessarily relate to the library you are using.

In this particular case we are using the .NET/C# library so moving from OpenTracing API for .NET to OpenTelemetry .NET. OpenTracing is deprecated and all the effort is now in OpenTelemetry. When I first wrote this many of the OpenTelemetry .NET libraries were still in beta or release-candidate status, but today the core packages (the API and SDK, OpenTelemetry.Extensions.Hosting, the OTLP exporter and the ASP.NET Core, HttpClient and SqlClient instrumentation) are all stable. A few of the contrib packages, such as the Entity Framework Core instrumentation, are still in beta.

How OpenTracing is used

The OpenTracing standard basically worked through the ITracer interface. You can register it as a singleton via DI and let it flow through where it is needed or access it via the GlobalTracer static.

This ITracer has an IScopeManager with an IScope being basically an active thread and the ISpan being a unit of work which can move between threads.

Typically usage looks a little something like this:

class Runner {
  private ITracer tracer;

  public Runner(ITracer tracer) {
    this.tracer = tracer;
  }

  public void Run(string command) {
      using (var scope = tracer.BuildSpan("Run").StartActive()) {
        // ...
      }
  }
}

With scopes capable of having sub-scopes, tagging and events within them to provide further detail. The moment the StartActive is called the clock starts and the moment the using goes out of scope the clock ends providing you the detail levels you need.

How OpenTelemetry changes things

Now OpenTelemetry changes things a little breaking Tracing, Metrics and Logging into separate things.

While OpenTelemetry uses much of the same terminology as OpenTracing when it comes to the .NET client they decided to take a different approach and rather than implement Span again they use .NET's built in ActivitySource and Activity as the replacement so there's less to learn. In .NET terms ActivitySource is the Tracer and Activity is the Span.

So we create an ActivitySource in the class and then use it much as we would have used ITracer.

class Runner {
  static readonly AssemblyName assemblyName = typeof(Runner).Assembly.GetName();
  static readonly ActivitySource activitySource = new ActivitySource(assemblyName.Name!, assemblyName.Version?.ToString());

  public void Run(string command) {
      using (var activity = activitySource.StartActivity("Run")) {
        activity?.SetTag("command", command);
        // ...
      }
  }
}

This has the advantage of not needing to pass around ITracer and StartActivity is optimised to immediately return null if nothing is listening (which the using keyword handles just fine—just remember the ?. when you touch the activity). Note that OpenTelemetry only listens to sources you tell it about, so you'll need to add your source name with AddSource when you configure tracing (see below) or you'll see nothing at all.

If, however, you are using multiple systems and want to stick to the OpenTelemetry terminology of Tracer and Span instead of ActivitySource and Activity then check out the OpenTelemetry.API shim (not to be confused with the OpenTracing shim covered below).

There is a full comparison available of how OpenTelemetry maps to the .NET Activity API available too.

WARNING using and C# 8+

Some code analysis/refactoring tools may recommend changing the using clause from the traditional using (var x...) to the brace-less using var x... of C# 8.

Do not do this with BuildSpan or StartActivity unless you have only one of them and you're happy for the timing to consider the end when the method exits.

This is because removing the braces removes the defined end of the span or activity. This means that they will continue on beyond their original intended scope and carry on their timings until the end of the method.

How to migrate

It is possible to migrate in steps by switching your application over from OpenTracing to OpenTelemetry while still temporarily supporting ITracer until you have moved although the code to set this up correctly is easy to get wrong (and if you do you will see duplicate spans in your viewer).

In ASP.NET Core you want to register OpenTelemetry with AddOpenTelemetry().WithTracing(...) from the OpenTelemetry.Extensions.Hosting package along with all your necessary instrumentation and an exporter. For example:

services.AddOpenTelemetry()
    .ConfigureResource(resource => resource.AddService("MyServiceName"))
    .WithTracing(tracing => tracing
        .AddSource("MyCompany.MyApp") // your own ActivitySource name(s)
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddSqlClientInstrumentation(o => o.RecordException = true)
        .AddOtlpExporter(o => o.Endpoint = new Uri("http://localhost:4317")));

The OTLP exporter comes from the OpenTelemetry.Exporter.OpenTelemetryProtocol package and sends over gRPC to http://localhost:4317 by default so you can drop the Endpoint line if that's where your collector or Jaeger lives.

The original sample also set SetDbStatementForText = true on the SqlClient instrumentation. That option has since been removed—the instrumentation now always captures the query text (sanitised, with literal values replaced by ?).

Backwards compatible support for ITracer & GlobalTracer

If you still need to support ITracer/GlobalTracer in the mean time you can use OpenTelemetry.Shims.OpenTracing to forward old requests on. Register the shim alongside the AddOpenTelemetry call covered above:

services.AddSingleton<ITracer>(serviceProvider => {
    var tracerProvider = serviceProvider.GetRequiredService<TracerProvider>();
    var tracer = new TracerShim(tracerProvider, Propagators.DefaultTextMapPropagator);
    GlobalTracer.RegisterIfAbsent(tracer);
    return tracer;
});

The shim creates its spans on an ActivitySource named opentracing-shim so you also need .AddSource("opentracing-shim") in your WithTracing block or those spans will be dropped.

Be aware that the shim is now deprecated - TracerShim is marked [Obsolete] (so expect a compiler warning), the package is still only published as a pre-release, and it stops receiving updates in March 2027. Treat it as a stepping stone and get your code onto ActivitySource directly.

Note that other combinations of trying to get this working for us resulted in duplicate spans. It is quite possible that duplicate trace providers can cause this.

Jaeger

With OpenTracing Jaeger required the use of its own C# Client for Jaeger. When I originally wrote this the replacement was the OpenTelemetry.Exporter.Jaeger package, which was tracing-only and used the UDP agent protocol so there was no authentication and message sizes were limited.

Thankfully that's all history now. Jaeger accepts the OpenTelemetry Protocol (OTLP) natively—on port 4317 for gRPC and port 4318 for HTTP—so the Jaeger exporter was deprecated and then removed from OpenTelemetry .NET in 2023. Just use the OTLP exporter shown above and point it at Jaeger (or at an OpenTelemetry Collector in front of it).

The "Jaeger 2" I mentioned back then has also shipped. Jaeger v2 was released in November 2024 and is built on the OpenTelemetry Collector itself.

Logs on a Span

OpenTracing helpfully correlated logs from ILogger to the appropriate span so they could appear alongside what they related to in, for example, Jaeger.

OpenTelemetry wants to keep logs separate and instead relies on .NET's ActivityEvent for this purpose but if you already have lots of ILogger usage then the OpenTelemetry.Extensions package (still in beta) has your back and will re-dispatch ILogger entries as ActivityEvent ensuring they end up in the right place as they did with OpenTracing.

services.AddLogging(logging => {
  logging.AddOpenTelemetry(options => {
    options.AttachLogsToActivityEvent();
  });
});

Dealing with pre-release NuGet packages

When I first wrote this a number of the OpenTelemetry libraries were pre-release. The core ones are stable now but a few you might still want, such as OpenTelemetry.Instrumentation.EntityFrameworkCore, OpenTelemetry.Extensions and OpenTelemetry.Shims.OpenTracing, are still pre-release.

If you are packing a stable NuGet package of your own that depends on one of these you'll get warning NU5104 (which fails the build if you treat warnings as errors). If you are absolutely sure you want to proceed then add NU5104 to the <NoWarn> section of your .csproj, e.g.

<NoWarn>NU5104</NoWarn>

Additional tracing

OpenTracing has a popular Contrib package that provides support for a variety of sources. Much of this is replaced by additional OpenTelemetry .NET packages, specifically the following packages which will need to be added to your project along with a call to the extension method in your WithTracing block.

Technology OpenTelemetry Package Extension method
ASP.NET Core OpenTelemetry.Instrumentation.AspNetCore AddAspNetCoreInstrumentation
Entity Framework Core OpenTelemetry.Instrumentation.EntityFrameworkCore (beta) AddEntityFrameworkCoreInstrumentation
HttpHandler OpenTelemetry.Instrumentation.Http AddHttpClientInstrumentation
Microsoft.Data.SqlClient OpenTelemetry.Instrumentation.SqlClient AddSqlClientInstrumentation
System.Data.SqlClient OpenTelemetry.Instrumentation.SqlClient AddSqlClientInstrumentation

There are many additional services that were not natively or contrib-supported as well including Elasticsearch, AWS, MassTransit, MySqlData, Wcf, Grpc, StackExchangeRedis etc.

Hope that helps!

[)amien

0 responses

  1. Information is only used to show your comment. See my Privacy Policy.