OpenTelemetry Spans in Java

OpenTelemetry comes with many instrumentation plugins for libraries and frameworks. This should be enough detail to get started with tracing in production.

As great as that is, you will still want to add additional spans to your application code, in order to break down larger operations and gain more detailed insights into where your application is spending its time.

When you create a new span to measure a subcomponent, that span is added to the current trace as the child of the current span, and then becomes the current span itself.

Annotations

An easy way to wrap a method in a span is to use the @WithSpan annotation.

import io.opentelemetry.contrib.auto.annotations.WithSpan;
public class Chef {
  @WithSpan
  public void bakeCake() {}
}

Each time the annotated method is invoked, it will create a child span within the current trace, and record any thrown exceptions.

Disabling Annotations

Individual annotations can be disabled at runtime via the exclude configuration or environment variables:

System propertyEnvironment variablePurpose
trace.classes.excludeTRACE_CLASSES_EXCLUDEExclude classes with the @WithSpan annotation
trace.methods.excludeTRACE_METHODS_EXCLUDEExclude methods with the @WithSpan annotation

Tracer API

Accessing the tracer

In order to interact with traces, you must first acquire a handle to a Tracer. By convention, Tracers are named after the component they are instrumenting; usually a library, a package, or a class.

// Access to the tracer requires a name to associate the spans with
Tracer tracer =
    OpenTelemetry.getTracer("instrumentation-library-name");

Tracer tracer =
    OpenTelemetry.getTracer("instrumentation-library-name", "semver:1.0.0");

Note that there is no need to "set" a tracer by name before getting it. The getTracer method always returns a handle to the same tracing client. The name you provide is to help identify which component generated which spans, and to potentially disable tracing for individual components.

We recommend calling getTracer once per component during initialization and retaining a handle to the tracer, rather than calling getTracer repeatedly.

Accessing the current span

Ideally, when tracing application code, spans are created and managed in the application framework.

Assuming that your application framework is supported, a trace will automatically be created for each request, and your application code will already be wrapped in a span, which can be used for adding application specific attributes and events.

To access the currently active span, call getCurrentSpan:

Span span = tracer.getCurrentSpan()

Setting a new current span

Let’s demonstrate creating a new span by example. Imagine you have an automated kitchen, and you want to time how long the robot chef takes to bake a cake. The naive way to do this would be to just start a span, call your method, then end the span:

// Note that all new spans will set the current span as their parent by default.
Span cakeSpan = tracer.spanBuilder("bake-cake")
                      .startSpan();
chef.bakeCake()
cakeSpan.end()

The above example will work just fine, but with one big problem: the bakeCake method itself has no access to this new bake-cake span. That means there would be no way to add attributes and events to this span from within the bakeCake method. Even worse, getCurrentSpan would return the parent of “bake-cake,” since that span is still set as current.

What should we do instead? Replace the current span with bake-cake. To do this, call withSpan to make a closure around the bakeCake method. Within this closure, the getCurrentSpan method will now return bake-cake.

// in this scope, getCurrentSpan returns the parentSpan.
Span parentSpan = tracer.getCurrentSpan()

// Create a new span. Note that it is possible to set the parent manually.
Span cakeSpan = tracer.spanBuilder("bake-cake")
                      .setParent(parentSpan)
                      .startSpan();

// Replace the current active span by creating a scope.
try (Scope scope = tracer.withSpan(cakeSpan)) {

  // In this scope, cakeSpan is returned by getCurrentSpan
  Span span = tracer.getCurrentSpan()

  // bake your cake!
  chef.bakeCake()

} catch (Throwable t) {
    span.setStatus(Status.UNKNOWN.withDescription("Someone threw the cake!"));
} finally {
    span.end();
}

This pattern of wrapping method calls is important, because we always want application code to be able to assume that the current span is correct. Though you might now see why there are many cases where using the withSpan annotation is cleaner than calling withSpan manually.

Next Steps

As you dive deeper into observing your application, you will naturally want to add additional, application-specific details to your spans. Check out how to decorate your spans with attributes and events.