When threads or processes communicate through intermediaries such as message queues or databases, the ARMS agent cannot automatically propagate trace context. The consumer starts a new, disconnected trace instead of continuing the producer's trace. Use the OpenTelemetry SDK for Java to manually capture and restore trace context across these async boundaries, keeping all spans correlated in a single trace.
When you need manual propagation
The ARMS agent automatically propagates trace context for the following async patterns without code changes:
Thread pools created with JDK, Spring, or Netty
Reactive programming with Reactor or RxJava
Spring
@AsyncannotationsReactive frameworks such as Spring Webflux and Spring Gateway
For the full list of supported frameworks, see Java components and frameworks supported by ARMS.
Manual propagation is required when threads or processes communicate through intermediaries that the ARMS agent does not instrument. Two common examples:
| Scenario | Description |
|---|---|
| Producer-consumer queue | Thread T1 places a message in a queue, and thread T2 retrieves it for processing. Without manual propagation, T2 starts a new trace instead of continuing T1's trace. |
| Database-mediated handoff | Process P1 writes a batch of records to a database, and process P2 reads and processes them. Without manual propagation, the processing trace is disconnected from the write trace. |
Prerequisites
Before you begin, make sure that you have:
Application Monitoring of ARMS integrated. For more information, see Application Monitoring overview
ARMS agent V4.x or later installed. To check or update your version, see Update the ARMS agent
Add dependencies
Add the following Maven dependencies to introduce OpenTelemetry SDK for Java. For more information, see Instrumentation.
<dependencies>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-trace</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-bom</artifactId>
<version>1.23.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>Pass context across async boundaries
The pattern has two parts:
Capture: On the sender side, call
Context.current()to capture the active trace context and store it alongside the data being passed.Restore: On the receiver side, call
context.makeCurrent()to restore the captured context before processing.
Example: Producer-consumer with a blocking queue
The following code passes trace context through a LinkedBlockingQueue. The producer captures the current context when enqueueing a message, and the consumer restores it before processing.
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import java.util.concurrent.LinkedBlockingQueue;
// Wrap both the message and its trace context
class Event {
private final Context context;
private final String msg;
public Event(Context context, String msg) {
this.context = context;
this.msg = msg;
}
public Context getContext() {
return context;
}
public String getMsg() {
return msg;
}
}
// Shared queue between producer and consumer threads
private final LinkedBlockingQueue<Event> queue = new LinkedBlockingQueue<>();
// Producer: capture the current trace context
public void produce(String msg) {
queue.add(new Event(Context.current(), msg));
}
// Consumer: restore the trace context before processing
public void consume() throws InterruptedException {
Event event = queue.take();
// makeCurrent() sets the captured context as the active context.
// The try-with-resources block ensures the context is cleaned up after processing.
try (Scope scope = event.getContext().makeCurrent()) {
processEvent(event);
}
}
public void processEvent(Event event) {
// Any spans created here automatically become children
// of the producer's trace.
}Key points
| Concept | Description |
|---|---|
Context.current() | Captures the active trace context at the time of the call. Store it alongside the data being passed between threads. |
context.makeCurrent() | Restores the captured context on the consumer thread. Spans, logs, and metrics created within the try (Scope scope = ...) block are correlated with the original trace. |
Scope | Implements AutoCloseable. Always use a try-with-resources block to make sure the context is properly cleaned up. |
What to do next
Associate trace IDs with application logs to streamline error analysis. See Associate trace IDs with logs for a Java application.