Overview
Distributed Context Propagation enables consistent tracking and propagation of contextual information across service boundaries in distributed systems. This is essential for observability, tracing, and maintaining request-specific state in microservices architectures.
Core Concepts
1. Context Elements
- Trace Context: Distributed tracing identifiers
- Baggage: Custom key-value pairs for business context
- Correlation IDs: Request correlation across services
- Security Context: Authentication and authorization tokens
- Tenant Context: Multi-tenancy information
2. Propagation Mechanisms
- HTTP Headers: Most common for synchronous communication
- Message Properties: For asynchronous messaging (Kafka, RabbitMQ)
- gRPC Metadata: For gRPC-based communication
- Thread-Local Storage: For in-process context propagation
Implementation Architectures
1. Manual Context Propagation
public class ManualContextPropagator {
private static final String TRACE_ID_HEADER = "X-Trace-Id";
private static final String SPAN_ID_HEADER = "X-Span-Id";
private static final String CORRELATION_ID_HEADER = "X-Correlation-Id";
private static final String TENANT_ID_HEADER = "X-Tenant-Id";
// Outgoing HTTP request context propagation
public HttpHeaders propagateContextForOutgoingRequest() {
DistributedContext context = DistributedContext.getCurrent();
HttpHeaders headers = new HttpHeaders();
headers.set(TRACE_ID_HEADER, context.getTraceId());
headers.set(SPAN_ID_HEADER, context.getSpanId());
headers.set(CORRELATION_ID_HEADER, context.getCorrelationId());
headers.set(TENANT_ID_HEADER, context.getTenantId());
// Propagate baggage
context.getBaggage().forEach((key, value) -> {
headers.set("X-Baggage-" + key, value);
});
return headers;
}
// Incoming HTTP request context extraction
public void extractContextFromIncomingRequest(HttpServletRequest request) {
String traceId = getHeaderOrDefault(request, TRACE_ID_HEADER, generateTraceId());
String spanId = getHeaderOrDefault(request, SPAN_ID_HEADER, generateSpanId());
String correlationId = getHeaderOrDefault(request, CORRELATION_ID_HEADER,
generateCorrelationId());
String tenantId = getHeaderOrDefault(request, TENANT_ID_HEADER, "default");
DistributedContext context = DistributedContext.create()
.withTraceId(traceId)
.withSpanId(spanId)
.withCorrelationId(correlationId)
.withTenantId(tenantId);
// Extract baggage
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
if (headerName.startsWith("X-Baggage-")) {
String key = headerName.substring("X-Baggage-".length());
String value = request.getHeader(headerName);
context.withBaggageItem(key, value);
}
}
DistributedContext.setCurrent(context);
}
private String getHeaderOrDefault(HttpServletRequest request,
String header, String defaultValue) {
String value = request.getHeader(header);
return value != null ? value : defaultValue;
}
}
2. Automatic Context Propagation Framework
public class DistributedContext implements AutoCloseable {
private static final ThreadLocal<DistributedContext> CURRENT_CONTEXT =
new InheritableThreadLocal<>();
private final String traceId;
private final String spanId;
private final String parentSpanId;
private final String correlationId;
private final String tenantId;
private final Map<String, String> baggage;
private final Map<String, Object> customAttributes;
private final Instant createdAt;
private DistributedContext(Builder builder) {
this.traceId = builder.traceId;
this.spanId = builder.spanId;
this.parentSpanId = builder.parentSpanId;
this.correlationId = builder.correlationId;
this.tenantId = builder.tenantId;
this.baggage = Collections.unmodifiableMap(new HashMap<>(builder.baggage));
this.customAttributes = Collections.unmodifiableMap(
new HashMap<>(builder.customAttributes));
this.createdAt = Instant.now();
}
public static DistributedContext getCurrent() {
DistributedContext context = CURRENT_CONTEXT.get();
if (context == null) {
context = createDefault();
CURRENT_CONTEXT.set(context);
}
return context;
}
public static void setCurrent(DistributedContext context) {
CURRENT_CONTEXT.set(context);
}
public static DistributedContext create() {
return new Builder().build();
}
public DistributedContext createChild() {
return new Builder()
.withTraceId(this.traceId)
.withParentSpanId(this.spanId)
.withCorrelationId(this.correlationId)
.withTenantId(this.tenantId)
.withBaggage(this.baggage)
.build();
}
@Override
public void close() {
CURRENT_CONTEXT.remove();
}
// Builder pattern
public static class Builder {
private String traceId = IdGenerator.generateTraceId();
private String spanId = IdGenerator.generateSpanId();
private String parentSpanId;
private String correlationId = IdGenerator.generateCorrelationId();
private String tenantId = "default";
private Map<String, String> baggage = new HashMap<>();
private Map<String, Object> customAttributes = new HashMap<>();
public Builder withTraceId(String traceId) {
this.traceId = traceId;
return this;
}
public Builder withSpanId(String spanId) {
this.spanId = spanId;
return this;
}
public Builder withParentSpanId(String parentSpanId) {
this.parentSpanId = parentSpanId;
return this;
}
public Builder withCorrelationId(String correlationId) {
this.correlationId = correlationId;
return this;
}
public Builder withTenantId(String tenantId) {
this.tenantId = tenantId;
return this;
}
public Builder withBaggage(Map<String, String> baggage) {
this.baggage.putAll(baggage);
return this;
}
public Builder withBaggageItem(String key, String value) {
this.baggage.put(key, value);
return this;
}
public Builder withCustomAttribute(String key, Object value) {
this.customAttributes.put(key, value);
return this;
}
public DistributedContext build() {
return new DistributedContext(this);
}
}
// Getters
public String getTraceId() { return traceId; }
public String getSpanId() { return spanId; }
public String getParentSpanId() { return parentSpanId; }
public String getCorrelationId() { return correlationId; }
public String getTenantId() { return tenantId; }
public Map<String, String> getBaggage() { return baggage; }
public Map<String, Object> getCustomAttributes() { return customAttributes; }
public Instant getCreatedAt() { return createdAt; }
}
Propagation Implementations
1. HTTP Client/Server Propagation
@Component
public class HttpContextPropagator {
private static final String TRACE_ID = "X-Trace-Id";
private static final String SPAN_ID = "X-Span-Id";
private static final String PARENT_SPAN_ID = "X-Parent-Span-Id";
private static final String CORRELATION_ID = "X-Correlation-Id";
private static final String TENANT_ID = "X-Tenant-Id";
private static final String BAGGAGE_PREFIX = "X-Baggage-";
// Server-side: Extract context from incoming HTTP request
@Component
public static class ServerContextFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
DistributedContext context = extractContextFromRequest(httpRequest);
try (DistributedContext ignored = context) {
chain.doFilter(request, response);
}
}
private DistributedContext extractContextFromRequest(HttpServletRequest request) {
DistributedContext.Builder builder = DistributedContext.create()
.withTraceId(getHeader(request, TRACE_ID, IdGenerator.generateTraceId()))
.withSpanId(getHeader(request, SPAN_ID, IdGenerator.generateSpanId()))
.withParentSpanId(getHeader(request, PARENT_SPAN_ID, null))
.withCorrelationId(getHeader(request, CORRELATION_ID,
IdGenerator.generateCorrelationId()))
.withTenantId(getHeader(request, TENANT_ID, "default"));
// Extract baggage
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames != null && headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
if (headerName.startsWith(BAGGAGE_PREFIX)) {
String key = headerName.substring(BAGGAGE_PREFIX.length());
String value = request.getHeader(headerName);
builder.withBaggageItem(key, value);
}
}
return builder.build();
}
}
// Client-side: Inject context into outgoing HTTP request
@Component
public static class ClientContextInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
injectContextIntoRequest(request);
return execution.execute(request, body);
}
private void injectContextIntoRequest(HttpRequest request) {
DistributedContext context = DistributedContext.getCurrent();
request.getHeaders().set(TRACE_ID, context.getTraceId());
request.getHeaders().set(SPAN_ID, context.getSpanId());
request.getHeaders().set(CORRELATION_ID, context.getCorrelationId());
request.getHeaders().set(TENANT_ID, context.getTenantId());
// Inject baggage
context.getBaggage().forEach((key, value) -> {
request.getHeaders().set(BAGGAGE_PREFIX + key, value);
});
}
}
private static String getHeader(HttpServletRequest request, String name,
String defaultValue) {
String value = request.getHeader(name);
return value != null ? value : defaultValue;
}
}
2. Async Messaging Propagation (Kafka)
@Component
public class KafkaContextPropagator {
private static final String TRACE_ID = "trace_id";
private static final String SPAN_ID = "span_id";
private static final String CORRELATION_ID = "correlation_id";
private static final String TENANT_ID = "tenant_id";
private static final String BAGGAGE_PREFIX = "baggage_";
// Producer: Inject context into Kafka message headers
@Component
public static class ContextualKafkaProducer {
private final KafkaTemplate<String, Object> kafkaTemplate;
public void sendMessage(String topic, Object message) {
kafkaTemplate.executeInTransaction(operations -> {
ProducerRecord<String, Object> record =
new ProducerRecord<>(topic, message);
injectContextIntoRecord(record);
operations.send(record);
return null;
});
}
private void injectContextIntoRecord(ProducerRecord<String, Object> record) {
DistributedContext context = DistributedContext.getCurrent();
record.headers()
.add(TRACE_ID, context.getTraceId().getBytes(StandardCharsets.UTF_8))
.add(SPAN_ID, context.getSpanId().getBytes(StandardCharsets.UTF_8))
.add(CORRELATION_ID, context.getCorrelationId().getBytes(StandardCharsets.UTF_8))
.add(TENANT_ID, context.getTenantId().getBytes(StandardCharsets.UTF_8));
// Inject baggage
context.getBaggage().forEach((key, value) -> {
record.headers().add(BAGGAGE_PREFIX + key,
value.getBytes(StandardCharsets.UTF_8));
});
}
}
// Consumer: Extract context from Kafka message headers
@Component
public static class ContextualKafkaConsumer {
@KafkaListener(topics = "${kafka.topic}")
public void consume(ConsumerRecord<String, Object> record) {
DistributedContext context = extractContextFromRecord(record);
try (DistributedContext ignored = context) {
processMessage(record.value());
}
}
private DistributedContext extractContextFromRecord(ConsumerRecord<String, Object> record) {
DistributedContext.Builder builder = DistributedContext.create();
// Extract standard context
extractHeader(record, TRACE_ID).ifPresent(builder::withTraceId);
extractHeader(record, SPAN_ID).ifPresent(builder::withSpanId);
extractHeader(record, CORRELATION_ID).ifPresent(builder::withCorrelationId);
extractHeader(record, TENANT_ID).ifPresent(builder::withTenantId);
// Extract baggage
record.headers().headers(BAGGAGE_PREFIX + "*").forEach(header -> {
String key = header.key().substring(BAGGAGE_PREFIX.length());
String value = new String(header.value(), StandardCharsets.UTF_8);
builder.withBaggageItem(key, value);
});
return builder.build();
}
private Optional<String> extractHeader(ConsumerRecord<String, Object> record,
String headerName) {
Header header = record.headers().lastHeader(headerName);
if (header != null && header.value() != null) {
return Optional.of(new String(header.value(), StandardCharsets.UTF_8));
}
return Optional.empty();
}
private void processMessage(Object message) {
// Process message with context available
System.out.println("Processing message with trace: " +
DistributedContext.getCurrent().getTraceId());
}
}
}
3. gRPC Context Propagation
public class GrpcContextPropagator {
private static final Metadata.Key<String> TRACE_ID =
Metadata.Key.of("x-trace-id", Metadata.ASCII_STRING_MARSHALLER);
private static final Metadata.Key<String> SPAN_ID =
Metadata.Key.of("x-span-id", Metadata.ASCII_STRING_MARSHALLER);
private static final Metadata.Key<String> CORRELATION_ID =
Metadata.Key.of("x-correlation-id", Metadata.ASCII_STRING_MARSHALLER);
// Server-side interceptor
public static class ServerContextInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call, Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
DistributedContext context = extractContextFromMetadata(headers);
Context grpcContext = Context.current().withValue(
ContextKeys.DISTRIBUTED_CONTEXT, context);
return Contexts.interceptCall(grpcContext, call, headers, next);
}
private DistributedContext extractContextFromMetadata(Metadata headers) {
DistributedContext.Builder builder = DistributedContext.create();
String traceId = headers.get(TRACE_ID);
if (traceId != null) {
builder.withTraceId(traceId);
}
String spanId = headers.get(SPAN_ID);
if (spanId != null) {
builder.withSpanId(spanId);
}
String correlationId = headers.get(CORRELATION_ID);
if (correlationId != null) {
builder.withCorrelationId(correlationId);
}
return builder.build();
}
}
// Client-side interceptor
public static class ClientContextInterceptor implements ClientInterceptor {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions,
Channel next) {
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
next.newCall(method, callOptions)) {
@Override
public void start(Listener<RespT> responseListener, Metadata headers) {
injectContextIntoMetadata(headers);
super.start(responseListener, headers);
}
};
}
private void injectContextIntoMetadata(Metadata headers) {
DistributedContext context = DistributedContext.getCurrent();
headers.put(TRACE_ID, context.getTraceId());
headers.put(SPAN_ID, context.getSpanId());
headers.put(CORRELATION_ID, context.getCorrelationId());
}
}
}
Advanced Context Management
1. Context Storage and Scoping
public class ContextManager {
private static final ThreadLocal<Deque<DistributedContext>> CONTEXT_STACK =
ThreadLocal.withInitial(ArrayDeque::new);
public static void pushContext(DistributedContext context) {
CONTEXT_STACK.get().push(context);
}
public static DistributedContext popContext() {
Deque<DistributedContext> stack = CONTEXT_STACK.get();
return stack.isEmpty() ? null : stack.pop();
}
public static DistributedContext getCurrentContext() {
Deque<DistributedContext> stack = CONTEXT_STACK.get();
return stack.isEmpty() ? null : stack.peek();
}
public static <T> T executeInContext(DistributedContext context, Supplier<T> task) {
pushContext(context);
try {
return task.get();
} finally {
popContext();
}
}
public static void executeInContext(DistributedContext context, Runnable task) {
pushContext(context);
try {
task.run();
} finally {
popContext();
}
}
}
// Scoped context for try-with-resources
public class ScopedContext implements AutoCloseable {
private final DistributedContext previousContext;
public ScopedContext(DistributedContext newContext) {
this.previousContext = ContextManager.getCurrentContext();
ContextManager.pushContext(newContext);
}
@Override
public void close() {
ContextManager.popContext();
if (previousContext != null) {
ContextManager.pushContext(previousContext);
}
}
}
2. Context Propagation for Async Operations
public class AsyncContextPropagator {
public static <T> CompletableFuture<T> propagateContext(Supplier<CompletableFuture<T>> task) {
DistributedContext currentContext = DistributedContext.getCurrent();
return CompletableFuture.supplyAsync(() -> {
try (DistributedContext context = currentContext) {
return task.get().join();
}
}).thenCompose(Function.identity());
}
public static ExecutorService contextualExecutorService(ExecutorService delegate) {
return new ContextAwareExecutorService(delegate);
}
private static class ContextAwareExecutorService extends AbstractExecutorService {
private final ExecutorService delegate;
public ContextAwareExecutorService(ExecutorService delegate) {
this.delegate = delegate;
}
@Override
public void execute(Runnable command) {
DistributedContext context = DistributedContext.getCurrent();
delegate.execute(() -> {
try (DistributedContext ctx = context) {
command.run();
}
});
}
// Delegate other methods
@Override
public void shutdown() { delegate.shutdown(); }
@Override
public List<Runnable> shutdownNow() { return delegate.shutdownNow(); }
@Override
public boolean isShutdown() { return delegate.isShutdown(); }
@Override
public boolean isTerminated() { return delegate.isTerminated(); }
@Override
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
return delegate.awaitTermination(timeout, unit);
}
}
// Reactor Context propagation
public static <T> Mono<T> propagateContext(Mono<T> mono) {
return Mono.deferContextual(contextView -> {
DistributedContext distributedContext = DistributedContext.getCurrent();
return mono.contextWrite(context ->
context.put("distributedContext", distributedContext));
});
}
public static <T> Flux<T> propagateContext(Flux<T> flux) {
return Flux.deferContextual(contextView -> {
DistributedContext distributedContext = DistributedContext.getCurrent();
return flux.contextWrite(context ->
context.put("distributedContext", distributedContext));
});
}
}
Integration with Observability Tools
1. OpenTelemetry Integration
@Component
public class OpenTelemetryContextPropagator {
private final ContextPropagators contextPropagators;
public OpenTelemetryContextPropagator() {
this.contextPropagators = ContextPropagators.create(
TextMapPropagator.composite(
W3CTraceContextPropagator.getInstance(),
W3CBaggagePropagator.getInstance(),
new CustomContextPropagator()
)
);
}
// Custom context propagator for business context
public static class CustomContextPropagator implements TextMapPropagator {
private static final String CORRELATION_ID = "x-correlation-id";
private static final String TENANT_ID = "x-tenant-id";
@Override
public Collection<String> fields() {
return Arrays.asList(CORRELATION_ID, TENANT_ID);
}
@Override
public <C> void inject(Context context, C carrier, TextMapSetter<C> setter) {
DistributedContext distContext = ContextKeys.DISTRIBUTED_CONTEXT.get(context);
if (distContext != null) {
setter.set(carrier, CORRELATION_ID, distContext.getCorrelationId());
setter.set(carrier, TENANT_ID, distContext.getTenantId());
}
}
@Override
public <C> Context extract(Context context, C carrier, TextMapGetter<C> getter) {
String correlationId = getter.get(carrier, CORRELATION_ID);
String tenantId = getter.get(carrier, TENANT_ID);
if (correlationId != null || tenantId != null) {
DistributedContext.Builder builder = DistributedContext.create();
if (correlationId != null) builder.withCorrelationId(correlationId);
if (tenantId != null) builder.withTenantId(tenantId);
return context.with(ContextKeys.DISTRIBUTED_CONTEXT, builder.build());
}
return context;
}
}
}
2. MDC (Mapped Diagnostic Context) Integration
@Component
public class MDCContextPropagator {
private static final String TRACE_ID = "traceId";
private static final String SPAN_ID = "spanId";
private static final String CORRELATION_ID = "correlationId";
private static final String TENANT_ID = "tenantId";
public static void updateMDCFromContext() {
DistributedContext context = DistributedContext.getCurrent();
if (context != null) {
MDC.put(TRACE_ID, context.getTraceId());
MDC.put(SPAN_ID, context.getSpanId());
MDC.put(CORRELATION_ID, context.getCorrelationId());
MDC.put(TENANT_ID, context.getTenantId());
// Add baggage to MDC
context.getBaggage().forEach(MDC::put);
}
}
public static void clearMDC() {
MDC.clear();
}
@Component
public static class MDCContextFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
try {
updateMDCFromContext();
chain.doFilter(request, response);
} finally {
clearMDC();
}
}
}
}
Security Context Propagation
@Component
public class SecurityContextPropagator {
public void propagateSecurityContext(HttpHeaders headers) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
String token = generateToken(authentication);
headers.set("Authorization", "Bearer " + token);
// Propagate user context
if (authentication.getPrincipal() instanceof UserDetails) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
headers.set("X-User-Id", userDetails.getUsername());
headers.set("X-User-Roles", String.join(",", userDetails.getAuthorities()
.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList())));
}
}
}
public void extractSecurityContext(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
Authentication authentication = validateToken(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
// Extract user context for distributed context
String userId = request.getHeader("X-User-Id");
if (userId != null) {
DistributedContext current = DistributedContext.getCurrent();
if (current != null) {
current.getCustomAttributes().put("userId", userId);
}
}
}
private String generateToken(Authentication authentication) {
// Implement token generation logic
return "generated-token";
}
private Authentication validateToken(String token) {
// Implement token validation logic
return null;
}
}
Testing Distributed Context Propagation
@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class ContextPropagationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void testHttpContextPropagation() {
// Create initial context
DistributedContext context = DistributedContext.create()
.withTraceId("test-trace-123")
.withCorrelationId("test-correlation-456")
.withTenantId("test-tenant");
// Execute request with context
String result = ContextManager.executeInContext(context, () -> {
ResponseEntity<String> response = restTemplate.getForEntity("/api/test", String.class);
return response.getBody();
});
// Verify context was propagated
assertThat(result).contains("test-trace-123");
}
@Test
public void testAsyncContextPropagation() throws Exception {
DistributedContext context = DistributedContext.create()
.withTraceId("async-trace-123");
CompletableFuture<String> future = AsyncContextPropagator.propagateContext(() ->
CompletableFuture.supplyAsync(() -> {
// This should have the context
return DistributedContext.getCurrent().getTraceId();
})
);
String result = future.get(5, TimeUnit.SECONDS);
assertThat(result).isEqualTo("async-trace-123");
}
@Test
public void testKafkaContextPropagation() {
DistributedContext context = DistributedContext.create()
.withTraceId("kafka-trace-123")
.withBaggageItem("businessKey", "test-value");
try (DistributedContext ctx = context) {
// Send Kafka message
kafkaTemplate.send("test-topic", "test-message");
// Verify context was propagated in consumer
// This would typically be verified in integration tests
}
}
}
Configuration and Best Practices
1. Spring Boot Auto-Configuration
@Configuration
@EnableConfigurationProperties(ContextPropagationProperties.class)
public class ContextPropagationAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public HttpContextPropagator.ServerContextFilter serverContextFilter() {
return new HttpContextPropagator.ServerContextFilter();
}
@Bean
@ConditionalOnMissingBean
public HttpContextPropagator.ClientContextInterceptor clientContextInterceptor() {
return new HttpContextPropagator.ClientContextInterceptor();
}
@Bean
@ConditionalOnMissingBean
public MDCContextPropagator.MDCContextFilter mdcContextFilter() {
return new MDCContextPropagator.MDCContextFilter();
}
@Bean
public AsyncContextPropagator asyncContextPropagator() {
return new AsyncContextPropagator();
}
}
@ConfigurationProperties(prefix = "context.propagation")
public class ContextPropagationProperties {
private boolean enabled = true;
private List<String> propagateHeaders = Arrays.asList(
"X-Trace-Id", "X-Span-Id", "X-Correlation-Id", "X-Tenant-Id"
);
private Baggage baggage = new Baggage();
// Getters and setters
public static class Baggage {
private boolean enabled = true;
private int maxEntries = 10;
private List<String> allowedKeys = new ArrayList<>();
// Getters and setters
}
}
2. application.yml Configuration
context:
propagation:
enabled: true
propagate-headers:
- "X-Trace-Id"
- "X-Span-Id"
- "X-Correlation-Id"
- "X-Tenant-Id"
- "X-User-Id"
baggage:
enabled: true
max-entries: 20
allowed-keys:
- "business-unit"
- "feature-flag"
- "user-segment"
logging:
pattern:
level: "%5p [%X{traceId},%X{correlationId},%X{tenantId}]"
Performance Considerations
@Component
public class ContextPropagationMetrics {
private final MeterRegistry meterRegistry;
private final Timer contextExtractionTimer;
private final Timer contextInjectionTimer;
private final Counter contextPropagationErrors;
public ContextPropagationMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.contextExtractionTimer = Timer.builder("context.propagation.extraction.time")
.description("Time taken to extract context from incoming requests")
.register(meterRegistry);
this.contextInjectionTimer = Timer.builder("context.propagation.injection.time")
.description("Time taken to inject context into outgoing requests")
.register(meterRegistry);
this.contextPropagationErrors = Counter.builder("context.propagation.errors")
.description("Number of context propagation errors")
.register(meterRegistry);
}
public void recordExtraction(Runnable extraction) {
contextExtractionTimer.record(extraction);
}
public void recordInjection(Runnable injection) {
contextInjectionTimer.record(injection);
}
public void recordError() {
contextPropagationErrors.increment();
}
}
Conclusion
Distributed Context Propagation in Java is essential for:
- End-to-end Tracing: Track requests across service boundaries
- Contextual Logging: Include relevant context in log messages
- Business Context: Propagate business-specific information
- Security: Maintain security context across services
- Multi-tenancy: Support tenant isolation in SaaS applications
Key implementation patterns include:
- Using thread-local storage with proper cleanup
- Supporting multiple communication protocols (HTTP, gRPC, messaging)
- Handling asynchronous operations correctly
- Integrating with observability frameworks
- Maintaining performance while ensuring context consistency
This comprehensive approach ensures that contextual information flows seamlessly through your distributed system, enabling better observability, debugging, and user experience.