Skip to main content
  1. Blog/

Instrumenting a Java service with OpenTelemetry

·540 words·3 mins·
Mohammed Abdessetar Elyagoubi
Author
Mohammed Abdessetar Elyagoubi
I write about Java backends, OpenTelemetry, and making production systems easier to see.
Table of Contents

Most Java services already have some telemetry — access logs, a /health endpoint, maybe a JVM metrics scrape. That is rarely enough when a request crosses three processes and fails in the third. OpenTelemetry gives you a vendor-neutral way to emit traces, metrics, and logs from the same mental model.

This post is a small, self-contained example using the OpenTelemetry Java API. It is the kind of snippet I reach for when the auto-instrumentation agent already covers HTTP, but a domain operation still looks like a blank gap in the trace.

What you want from a span
#

A useful span answers three questions:

  1. What work happened? The span name should be stable and readable (OrderService.placeOrder, not POST /api/v2/o).
  2. Which instance of that work? Attributes such as order.id or customer.tier let you filter in the backend.
  3. Did it succeed, and if not, why? Status and recorded exceptions turn a red trace into a diagnosis.

Keep high-cardinality identifiers as attributes, not as span names. Span names become time series; attributes stay queryable.

A minimal custom span
#

The API is intentionally small. You ask the global tracer provider for a tracer, start a span, put work inside a scope, and end the span in finally.

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 final class OrderService {
  private static final Tracer TRACER =
      GlobalOpenTelemetry.getTracer("com.example.orders", "1.0.0");

  private final PaymentClient payments;

  public OrderService(PaymentClient payments) {
    this.payments = payments;
  }

  public Order placeOrder(OrderRequest request) {
    Span span = TRACER.spanBuilder("OrderService.placeOrder")
        .setAttribute("order.id", request.orderId())
        .setAttribute("order.items", request.itemCount())
        .startSpan();

    try (Scope ignored = span.makeCurrent()) {
      Order order = persist(request);
      payments.charge(order);
      span.setStatus(StatusCode.OK);
      return order;
    } catch (RuntimeException exception) {
      span.recordException(exception);
      span.setStatus(StatusCode.ERROR, exception.getMessage());
      throw exception;
    } finally {
      span.end();
    }
  }

  private Order persist(OrderRequest request) {
    return new Order(request.orderId());
  }
}

makeCurrent() matters. Anything you call while the Scope is open — including the HTTP client the Java agent already instruments — should become a child of OrderService.placeOrder. Without the scope, you get a floating sibling span and the waterfall stops making sense.

Agent, SDK, or both
#

For most production Java services I start with the OpenTelemetry Java agent. It covers incoming HTTP, JDBC, commonly used clients, and a lot of framework glue. Custom spans like the one above sit on top of that: you add domain language the agent cannot invent.

Use the SDK directly when you are writing a library, a non-HTTP worker, or a test that needs an in-memory exporter. The API you call (spanBuilder, setAttribute, recordException) stays the same. The difference is who installs the TracerProvider.

What I leave out on purpose
#

  • Do not put secrets, tokens, or raw payloads on spans. Treat attributes as something an on-call engineer will read in a shared backend.
  • Do not create a span per loop iteration on a tight path. Batch work, or use events on a parent span.
  • Do name tracers after the instrumentation scope (com.example.orders), not after the process hostname.

If you want a next step, export to a local collector with OTLP and open the same request in your trace UI. If the custom span is the parent of the auto-instrumented client call, the setup is doing its job.

More notes on talks and related projects will land on the Projects/Talks page.