Migrating from Jaeger Java client to OpenTelemetry SDK
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.
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 OpenTracingTracer
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:
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.