All Products
Search
Document Center

Application Real-Time Monitoring Service:Implement cross-process context propagation with the OpenTelemetry SDK for Java

Last Updated:Mar 11, 2026

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:

  1. Client side (inject): Before sending a request, the client serializes the current trace context and writes it into the outgoing message headers.

  2. 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:

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.