Migrating from Jaeger Java client to OpenTelemetry SDK

Juraci Paixão Kröhling
JaegerTracing
Published in
5 min readOct 26, 2021

A couple of years ago, the OpenTelemetry project was founded by the merger of two similarly aimed projects: OpenTracing and OpenCensus. One of the goals of this new project was to create an initial version that would “just work” with existing applications instrumented using OpenTracing and OpenCensus.

On the Jaeger community, we decided some time ago that we would start recommending users to migrate to OpenTelemetry SDK once there was feature-parity with our existing clients, and we believe this time has come.

In the next few months, we’ll be deprecating our own clients in favor of the OpenTelemetry SDK. With this, we believe we’ll remain focused on the backend side of our tracing solution, leaving a bigger community to provide and support clients in a myriad of languages.

This guide will help you with your first steps in migrating to OpenTelemetry SDK from Jaeger for your application instrumented using the OpenTracing API. For the longer term, we recommend that you get familiar with the OpenTelemetry API and start using it to instrument your applications. We also recommend that you get familiar with the OpenTelemetry SDK and understand its features and limitations.

Our instrumented application

For this guide, we’ll be using Yuri Shkuro’s OpenTracing tutorial as the starting point. More specifically, the solution for lesson 4. Before we start the migration, let’s do a sanity check. Fork and clone that repository, and run each one of the following commands on its own console:

$ ./run.sh lesson04.solution.Formatter server
$ ./run.sh lesson04.solution.Publisher server
$ podman run --rm --name jaeger -p 6831:6831/udp -p 14250:14250 -p 16686:16686 -p 14268:14268 jaegertracing/all-in-one:1.27

If you don’t have podman, replace it with docker in the last command. Even though we are using a recent version of the Jaeger backend, any version bigger than v1.8.2 (2018-11-28) should work.

Once all the servers are running, execute the client:

$ ./run.sh lesson04.solution.Hello Bryan Bonjour

You should now see a trace in your local Jaeger instance similar to the image below. If this is the case, you’re good to go. Stop the Formatter and the Publisher servers, but leave Jaeger running.

A trace generated by the Jaeger Client for Java

Adding the OpenTelemetry shim

Before we can start using the shim, we need to add two BOMs (Bill of Materials) to our application. The first one contains the stable components for the OpenTelemetry Java SDK like the actual SDK, the API, and a few exporters, where the second has components that might not have the same stability or criticality, like the shim or the semantic conventions library.

<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-bom</artifactId>
<version>1.7.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-bom-alpha</artifactId>
<version>1.7.0-alpha</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Next, we add the dependencies related to the OpenTelemetry SDK to our project:

<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-opentracing-shim</artifactId>
</dependency>

<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-semconv</artifactId>
</dependency>

<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-jaeger</artifactId>
</dependency>

<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-extension-trace-propagators</artifactId>
</dependency>
  • io.opentelemetry:opentelemetry-opentracing-shim is our shim, an OpenTracing Tracer implementation that will serve as the drop-in replacement for our Jaeger client.
  • io.opentelemetry:opentelemetry-semconv is a convenience library for adding attributes based on the semantic conventions.
  • io.opentelemetry:opentelemetry-exporter-jaeger is an exporter that sends data to our Jaeger instance. Note that there are no exporters sending Thrift data over UDP, which was the default encoding and transport for most Jaeger clients. Even though there is a Thrift HTTP exporter, we recommend using the gRPC exporter. Take a moment also to review the available exporters.
  • io.opentelemetry:opentelemetry-extension-trace-propagators contains the Jaeger propagator.

We also need a couple of gRPC dependencies. We are using netty-shaded as the underlying transport, but make sure to learn about the alternatives and pick the appropriate one for your scenario.

<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.41.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.41.0</version>
</dependency>

At this point, we can remove the io.jaegertracing:jaeger-client dependency from our project.

Replacing the tracer

With the dependencies in place, we can now replace the Tracer in our application with the OpenTelemetry Tracer. Most applications should have used the Tracer interface from the OpenTracing API in the method signatures instead of referencing the JaegerTracer directly. This makes our job easier now, as our changes are localized to the lib.Tracing class, init method.

The first step in our refactoring will be to remove the entire implementation and just return null. This is located in the java/src/main/java/lib/Tracing.java file.

public static Tracer init(String service) {
return null;
}

Take the opportunity to also remove the imports starting with io.jaegertracing.

We’ll now create the Resource attribute holding the service name for our application. In the init method, add the following:

Resource serviceNameResource = Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, service));

We now initialize a gRPC channel with our Jaeger collector. In the init method, add the following:

ManagedChannel jaegerChannel = ManagedChannelBuilder
.forAddress("localhost", 14250)
.usePlaintext()
.build();

We can now create the Jaeger exporter:

JaegerGrpcSpanExporter jaegerExporter = JaegerGrpcSpanExporter.builder()
.setChannel(jaegerChannel)
.setTimeout(1, TimeUnit.SECONDS)
.build();

Creating the Tracer provider is the next step:

SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(SimpleSpanProcessor.create(jaegerExporter))
.setResource(Resource.getDefault().merge(serviceNameResource))
.build();

We create an OpenTelemetry SDK instance with our tracer provider and the context propagators. To keep backward compatibility with existing services, we added the JaegerPropagator. For the long run though, we might want to start using the W3CTraceContextPropagator instead. Given that not all services are going to be updated at once, it's a good idea to run with both propagators during the transition time. The side-effect is that we'll end up having a bigger HTTP request between our microservices, but hopefully that's not big enough to have a considerable impact.

OpenTelemetrySdk openTelemetry = OpenTelemetrySdk.builder()
.setPropagators(ContextPropagators.create(
TextMapPropagator.composite(
W3CTraceContextPropagator.getInstance(),
JaegerPropagator.getInstance()
)
))
.setTracerProvider(tracerProvider)
.build();

As a good practice, we try to close our tracer provider when the JVM is shutting down:

Runtime.getRuntime().addShutdownHook(new Thread(tracerProvider::close));

And finally, we wrap our OpenTelemetry SDK instance in our shim, returning it to callers:

return OpenTracingShim.createTracerShim(openTelemetry);

Trying it out

Our migration should be ready by now, so, let’s try it out by running the same commands as before:

./run.sh lesson04.solution.Formatter server
./run.sh lesson04.solution.Publisher server
./run.sh lesson04.solution.Hello Bryan Bonjour

At this point, we should have a new trace in our Jaeger instance, created by the OpenTelemetry SDK: confirm this is the case by checking the otel.library.name tag and telemetry.sdk.name process attribute, like in the following image:

A trace generated by the OpenTelemetry SDK for Java

Remote sampling

If you are using the remotely controlled sampling configuration in the Jaeger client, you should double-check if the language you are using supports it already. Even though the jaeger_remote sampler is a valid value for the env var OTEL_TRACES_SAMPLER as per the OpenTelemetry SDK specification, it is not supported by most languages yet. At the moment of this writing, only the OpenTelemetry Java SDK supports it.

Wrapping up

This migration was simple and without a lot of work: a localized refactor of the init method was all it took. A bigger migration is certainly switching from the OpenTracing API to the OpenTelemetry API, but with the shim in place, you can take an opportunistic approach and migrate the APIs only for the services that are undergoing some maintenance or refactoring already.

--

--

Juraci Paixão Kröhling
JaegerTracing

Juraci Paixão Kröhling is a software engineer at Grafana Labs, a maintainer on the Jaeger project, and a contributor to the OpenTelemetry project.