Skip to main content

OpenTelemetry Tracing Bridge (juneau-rest-server-tracing-otel)

The juneau-rest-server-tracing-otel module bridges every @RestOp call to an OpenTelemetry Tracer, opening a SERVER span per request, propagating incoming W3C trace context, and injecting the resulting traceparent header on the response.

This page covers the module-specific classes and wiring patterns. The shared opt-in contract, the TracerHook SPI, per-resource / per-operation annotation overrides, log correlation, and the custom (non-request) observation API are described in Observability — Micrometer + OpenTelemetry.

Maven dependency​

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-tracing-otel</artifactId>
<version>${juneau.version}</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.43.0</version> <!-- consumer-supplied; provided scope on the bridge module -->
</dependency>
<!-- typically also the SDK + an exporter of your choice -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>1.43.0</version>
</dependency>

opentelemetry-api is declared in provided scope on the bridge module. Consumers pick their own OTel version (and SDK / exporter); core juneau-rest-server stays OTel-free.

Classes in this module​

ClassRole
OtelTracerHookTracerHook impl that opens OTel SERVER spans per request.
RestRequestTextMapGetterTextMapGetter<RestRequest> used to extract W3C propagation headers.

OtelTracerHook​

OtelTracerHook is the TracerHook bridge. Register one as a @Bean on your resource (or in a Spring @Configuration) to activate distributed tracing:

Constructors​

// Option A — use GlobalOpenTelemetry (recommended with the OTel auto-instrumentation agent).
new OtelTracerHook()

// Option B — explicit OpenTelemetry instance (e.g. multi-tenant, or manual SDK assembly).
new OtelTracerHook(OpenTelemetry otel)

// Option C — explicit Tracer + TextMapPropagator (typically for unit tests).
new OtelTracerHook(Tracer tracer, TextMapPropagator propagator)

The three-arg constructor accepts any Tracer / TextMapPropagator combination, which is especially useful in tests backed by OTel's in-memory SDK (opentelemetry-sdk-testing).

The instrumentation scope name used when obtaining a Tracer from an OpenTelemetry instance is exposed as the constant OtelTracerHook.DEFAULT_INSTRUMENTATION_NAME ("org.apache.juneau.rest.server").

Wiring​

@Configuration
public class ObservabilityConfig {

// Option A: rely on the JVM-wide GlobalOpenTelemetry (configured by the otel-java-agent or
// by calling GlobalOpenTelemetry.set(...) at startup).
@Bean
public TracerHook tracerHook() {
return new OtelTracerHook();
}

// Option B: explicit SDK instance (e.g. for tests or multi-tenant setups).
@Bean
public TracerHook tracerHook(OpenTelemetry otel) {
return new OtelTracerHook(otel);
}
}

Span shape​

Each @RestOp invocation produces one span with the following attributes:

Attribute / propertyValue
Span kindSpanKind.SERVER
Span nameUppercased HTTP method (GET, POST, …) per the OTel HTTP semantic conventions.
http.request.methodUppercased HTTP method.
http.response.status_codeHTTP response status (set immediately before the span closes, so error paths receive the framework's resolved status).
http.routeThe @RestOp path template (e.g. /users/{id}), not the raw URI — keeps trace cardinality bounded.

On the exception path:

  • Span.recordException(throwable) adds an exception event carrying the stack trace.
  • The span status is set to StatusCode.ERROR.
  • An exception.type attribute carries the exception's simple-name (e.g. IllegalStateException).

A 2xx status leaves the span status at StatusCode.UNSET per the OTel HTTP semantic conventions (servers should not pre-color successful spans as OK).

The attribute key constants are exposed on OtelTracerHook for programmatic use:

OtelTracerHook.ATTR_HTTP_REQUEST_METHOD     // AttributeKey<String>  "http.request.method"
OtelTracerHook.ATTR_HTTP_RESPONSE_STATUS_CODE // AttributeKey<Long> "http.response.status_code"
OtelTracerHook.ATTR_HTTP_ROUTE // AttributeKey<String> "http.route"
OtelTracerHook.ATTR_EXCEPTION_TYPE // AttributeKey<String> "exception.type"

W3C trace-context propagation​

Incoming traceparent​

OtelTracerHook uses RestRequestTextMapGetter to extract traceparent / tracestate (and any other propagator-defined headers, e.g. baggage) from the incoming request before starting the span, so the server span is created as a child of the caller's distributed trace:

Incoming request:   traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01

Server span: traceId = 0af7651916cd43dd8448eb211c80319c (continued from caller)
parentSpan = b7ad6b7169203331
spanId = <freshly minted>

The configured TextMapPropagator is used for extraction (default: W3CTraceContextPropagator). The same propagator handles outgoing downstream headers when the outbound HTTP client instruments the OTel Context.current() (all standard OTel client instrumentations do).

Outgoing response traceparent​

OtelTracerHook stashes the server-started span's W3C trace-context strings as request attributes at span-start time. A TraceContextResponseProcessor in juneau-rest-server's response chain reads those attributes and writes them as traceparent (and tracestate when non-empty) on the HTTP response:

Response:   traceparent: 00-0af7651916cd43dd8448eb211c80319c-<server-span-id>-01

This lets a caller correlate its own logs with the server's span without a tracing agent.

The response-header injection is on by default whenever a non-no-op TracerHook is active. Opt out globally with:

-DRestContext.responseTraceparent=false

RestRequestTextMapGetter​

RestRequestTextMapGetter is the TextMapGetter<RestRequest> singleton that OtelTracerHook uses to read propagation headers from incoming requests. It is exposed as a public singleton (RestRequestTextMapGetter.INSTANCE) so custom TracerHook implementations built on top of OtelTracerHook (or custom code that calls the OTel propagator directly) can reuse it:

TextMapGetter<RestRequest> getter = RestRequestTextMapGetter.INSTANCE;
Context extracted = propagator.extract(Context.current(), request, getter);

Header lookups are case-insensitive at the underlying RequestHeaderList, matching the RFC 9110 contract.

Custom (non-request) spans​

OtelTracerHook also overrides the non-request startSpan(String spanName) default method, opening an INTERNAL-kind span parented to the current OTel context. This integrates transparently with Observations.observe(...) for tracing arbitrary code blocks:

try (Observation o = Observations.observe("order.load", "team=payments")) {
return loadOrder(id);
} catch (RuntimeException e) {
o.setError(e);
throw e;
}

When called from inside an active @RestOp handler, the resulting span is automatically nested under the active request span.

Full wiring example​

@Configuration
public class ObservabilityConfig {

// Wire the OTel SDK (here: using GlobalOpenTelemetry configured by the otel-java-agent).
@Bean
public TracerHook tracerHook() {
return new OtelTracerHook();
}
}

@Rest(
path="/api",
observability="true" // fail-fast if no backend is wired
)
public class ApiResource extends BasicRestServlet {

@RestGet("/users/{id}")
public User getUser(@Path String id) { ... }

@RestGet("/health")
@RestOp(observability="false") // exclude health-check from tracing
public String health() { return "ok"; }
}

With an OTel-agent (or a manually configured SDK) in place, each call to GET /api/users/{id} produces one SERVER span exported to the configured backend (Jaeger, Zipkin, OTLP collector, etc.).

One-dependency OTLP export​

To wire both metrics and traces over OTLP in one go, use the curated OTLP bundle:

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-observability-otlp-bundle</artifactId>
<version>${juneau.version}</version>
<type>pom</type>
</dependency>

It transitively brings juneau-rest-server-tracing-otel, juneau-rest-server-metrics-micrometer, opentelemetry-exporter-otlp (+ SDK + autoconfigure), and micrometer-registry-otlp. You still wire the @Bean TracerHook / @Bean MetricsRecorder explicitly. See Dependency Management (BOM & Bundles).

See also​