Skip to content

Java custom OpenTelemetry instrumentation

Automatic instrumentation — the Java agent, the Spring Boot starter, or the Quarkus extension — already traces your web endpoints, clients, and database calls. Use custom instrumentation to add spans around your own business logic and to attach domain attributes to the spans you already produce.

There are two approaches:

  • @WithSpan annotation — the simplest way to trace a method. No span lifecycle to manage.
  • The tracer API — full control over span names, timing, attributes, events, and status when an annotation is not enough.
  • A Java service already reporting to Osuite through one of the automatic paths above. Custom spans attach to the same traces.

Add the annotations artifact to your build. Its version is managed by the OpenTelemetry instrumentation BOM:

<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-annotations</artifactId>
<version>2.11.0</version>
</dependency>

Annotate a method with @WithSpan. A span is created when the method is entered and ended when it returns. Use @SpanAttribute to capture parameters as span attributes:

import io.opentelemetry.instrumentation.annotations.SpanAttribute;
import io.opentelemetry.instrumentation.annotations.WithSpan;
public class OrderService {
@WithSpan
public Order process(@SpanAttribute("order.id") String orderId) {
return reserveInventory(orderId);
}
@WithSpan("inventory.reserve")
private Order reserveInventory(String orderId) {
return doReserve(orderId);
}
}

When you need explicit control, build spans directly. Obtain a Tracer, start a span, activate its scope, and always end it in a finally block:

import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
public class PaymentService {
private final Tracer tracer =
GlobalOpenTelemetry.get().getTracer("com.example.payments");
public void charge(String orderId, long amountCents) {
Span span = tracer.spanBuilder("payment.charge").startSpan();
try (Scope scope = span.makeCurrent()) {
span.setAttribute("order.id", orderId);
span.setAttribute("payment.amount_cents", amountCents);
callPaymentGateway(orderId, amountCents);
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR);
throw e;
} finally {
span.end();
}
}
}

GlobalOpenTelemetry.get() returns the instance configured by the agent, the starter, or the Quarkus extension. To use the API without the agent, add the core API dependency; its version is managed by the same instrumentation BOM:

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

To enrich a span that automatic instrumentation already created — for example, the span for the current HTTP request — annotate the active span without creating a new one:

import io.opentelemetry.api.trace.Span;
Span.current().setAttribute("customer.tier", "enterprise");
Span.current().addEvent("cache.miss");

What you should see

Exercise the annotated code, then open a matching trace in the trace explorer. Your custom spans appear as child spans under the request, with the attributes you set visible in the span detail.