The ARMS agent automatically instruments common Java frameworks to collect traces without code changes. However, if your application uses a proprietary protocol for cross-process communication, the client and server may appear as disconnected spans in the trace.
To link them, use the OpenTelemetry SDK for Java to manually propagate the trace context -- including the trace ID, span ID, sample flag, and baggage -- from the client to the server.
How it works
Context propagation follows an inject-extract pattern:
Client side (inject): Before sending a request, the client serializes the current trace context and writes it into the outgoing message headers.
Server side (extract): When the server receives the request, it reads the trace context from the message headers and sets it as the current context. Any spans created afterward become children of the original trace.
This document uses the W3C Trace Context and W3C Baggage propagation formats through W3CTraceContextPropagator and W3CBaggagePropagator.
Prerequisites
Before you begin, make sure that you have:
Application Monitoring of ARMS integrated
The ARMS agent V4.x or later. For upgrade instructions, see Update the ARMS agent
Add Maven dependencies
Add the following dependencies to your pom.xml. For details, see OpenTelemetry Java 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>Define the message carrier
The CustomRpcCallMessage class represents a message entity in a proprietary protocol. It uses a headerMap to carry trace context between processes. If your protocol's message class does not have a header map, add one.
class CustomRpcCallMessage {
private Map<String, String> headerMap;
// Other fields omitted.
public String getHeader(String key) {
return headerMap.get(key);
}
public void setHeader(String key, String value) {
this.headerMap.put(key, value);
}
public Map<String, String> getHeaderMap() {
return headerMap;
}
}Inject context on the client side
Before sending a request, inject the current trace context and baggage into the outgoing message. Define a TextMapSetter that writes key-value pairs into the message headers, then call inject() for both the trace context and baggage propagators.
import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapSetter;
public static void clientSide(String[] args) {
CustomRpcCallMessage rpcCall = new CustomRpcCallMessage();
// Define how to write context entries into the carrier
TextMapSetter<CustomRpcCallMessage> setter =
(carrier, key, value) -> carrier.setHeader(key, value);
// Inject W3C Trace Context and Baggage into the outgoing message
W3CTraceContextPropagator.getInstance().inject(Context.current(), rpcCall, setter);
W3CBaggagePropagator.getInstance().inject(Context.current(), rpcCall, setter);
// Send rpcCall to the server through your proprietary protocol
}Extract context on the server side
When the server receives a request, extract the trace context and baggage from the incoming message. Define a TextMapGetter that reads key-value pairs from the message headers, then call extract() to reconstruct the context. Use context.makeCurrent() to activate the extracted context so that subsequent spans join the original trace.
import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.context.propagation.TextMapGetter;
private static void serverSide() {
// Receive the incoming request
CustomRpcCallMessage rpcCall = /* received message */;
// Define how to read context entries from the carrier
TextMapGetter<CustomRpcCallMessage> getter = new TextMapGetter<>() {
@Override
public Iterable<String> keys(CustomRpcCallMessage carrier) {
return carrier.getHeaderMap().keySet();
}
@Override
public String get(CustomRpcCallMessage carrier, String key) {
return carrier.getHeader(key);
}
};
// Extract W3C Trace Context and Baggage from the incoming message
Context context = W3CTraceContextPropagator.getInstance().extract(Context.current(), rpcCall, getter);
context = W3CBaggagePropagator.getInstance().extract(context, rpcCall, getter);
// Activate the extracted context for this scope
try (Scope scope = context.makeCurrent()) {
// Process the request. Spans created here join the caller's trace.
}
}Adapt the code to your protocol
In most cases, only the TextMapSetter and TextMapGetter implementations need to change. Map their set and get methods to match how your protocol's message class stores and retrieves header values. The inject and extract calls remain the same regardless of the carrier type.
What's next
Associate trace IDs with application logs to locate and resolve errors faster. See Associate trace IDs with logs for a Java application.