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:
@WithSpanannotation — 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.
Prerequisites
Section titled “Prerequisites”- A Java service already reporting to Osuite through one of the automatic paths above. Custom spans attach to the same traces.
Annotate methods with @WithSpan
Section titled “Annotate methods with @WithSpan”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>implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:2.11.0")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); }}Create spans with the tracer API
Section titled “Create spans with the tracer API”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>implementation("io.opentelemetry:opentelemetry-api")Add attributes to the current span
Section titled “Add attributes to the current span”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");Verify in Osuite
Section titled “Verify in Osuite”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.