OpenTelemetry Context propagation mechanism for carrying scoped values across API boundaries and between threads in Java applications
—
Context storage customization allows you to plug in different context management strategies, add hooks for context lifecycle events, and implement debugging features. The storage system is designed to be extensible while maintaining thread safety and performance.
Returns the currently configured context storage implementation.
static ContextStorage get();Returns: The active ContextStorage instance
Usage Example:
// Get current storage (usually for advanced use cases)
ContextStorage currentStorage = ContextStorage.get();
Context current = currentStorage.current();
// Attach context directly through storage
try (Scope scope = currentStorage.attach(someContext)) {
// Context is active
}Returns the default ThreadLocal-based context storage.
static ContextStorage defaultStorage();Usage Example:
// Get default storage for wrapping or comparison
ContextStorage defaultStorage = ContextStorage.defaultStorage();
// Use as base for custom implementation
public class CustomStorage implements ContextStorage {
private final ContextStorage delegate = ContextStorage.defaultStorage();
// ... custom implementation
}Adds a wrapper function that will be applied when storage is first used.
static void addWrapper(Function<? super ContextStorage, ? extends ContextStorage> wrapper);Parameters:
wrapper - Function that wraps the storage implementationUsage Example:
// Must be called early in application lifecycle
static {
ContextStorage.addWrapper(storage -> new LoggingContextStorage(storage));
ContextStorage.addWrapper(storage -> new MetricsContextStorage(storage));
}
// Wrappers are applied in order: MetricsContextStorage(LoggingContextStorage(original))Sets the specified context as current and returns a scope for cleanup.
Scope attach(Context toAttach);Parameters:
toAttach - The context to make currentReturns: A Scope that must be closed to restore previous context
Returns the current context, or null if none is attached.
Context current();Returns: The current Context, or null
Returns the root context for this storage implementation.
Context root();Returns: The root Context instance
A wrapper that logs all context operations for debugging.
public class LoggingContextStorage implements ContextStorage {
private static final Logger logger = LoggerFactory.getLogger(LoggingContextStorage.class);
private final ContextStorage delegate;
public LoggingContextStorage(ContextStorage delegate) {
this.delegate = delegate;
}
@Override
public Scope attach(Context toAttach) {
logger.debug("Attaching context: {}", toAttach);
Context previous = delegate.current();
Scope scope = delegate.attach(toAttach);
return new LoggingScope(scope, previous, toAttach);
}
@Override
public Context current() {
Context current = delegate.current();
logger.trace("Current context: {}", current);
return current;
}
@Override
public Context root() {
return delegate.root();
}
private static class LoggingScope implements Scope {
private final Scope delegate;
private final Context previous;
private final Context attached;
LoggingScope(Scope delegate, Context previous, Context attached) {
this.delegate = delegate;
this.previous = previous;
this.attached = attached;
}
@Override
public void close() {
logger.debug("Closing scope, restoring context from {} to {}", attached, previous);
delegate.close();
}
}
}
// Register early in application
static {
ContextStorage.addWrapper(LoggingContextStorage::new);
}A wrapper that syncs context values with SLF4J MDC (Mapped Diagnostic Context).
public class MdcContextStorage implements ContextStorage {
private final ContextStorage delegate;
private static final ContextKey<Map<String, String>> MDC_KEY =
ContextKey.named("mdc-values");
public MdcContextStorage(ContextStorage delegate) {
this.delegate = delegate;
}
@Override
public Scope attach(Context toAttach) {
// Extract MDC values from context
Map<String, String> mdcValues = toAttach.get(MDC_KEY);
Map<String, String> previousMdc = null;
if (mdcValues != null) {
// Backup current MDC
previousMdc = MDC.getCopyOfContextMap();
// Clear and set new values
MDC.clear();
for (Map.Entry<String, String> entry : mdcValues.entrySet()) {
MDC.put(entry.getKey(), entry.getValue());
}
}
Scope delegate = this.delegate.attach(toAttach);
return new MdcScope(delegate, previousMdc);
}
@Override
public Context current() {
return delegate.current();
}
@Override
public Context root() {
return delegate.root();
}
private static class MdcScope implements Scope {
private final Scope delegate;
private final Map<String, String> previousMdc;
MdcScope(Scope delegate, Map<String, String> previousMdc) {
this.delegate = delegate;
this.previousMdc = previousMdc;
}
@Override
public void close() {
// Restore previous MDC state
MDC.clear();
if (previousMdc != null) {
MDC.setContextMap(previousMdc);
}
delegate.close();
}
}
// Helper method to add MDC values to context
public static Context withMdc(Context context, String key, String value) {
Map<String, String> mdcValues = context.get(MDC_KEY);
if (mdcValues == null) {
mdcValues = new HashMap<>();
} else {
mdcValues = new HashMap<>(mdcValues); // Copy for immutability
}
mdcValues.put(key, value);
return context.with(MDC_KEY, mdcValues);
}
}A wrapper that collects metrics on context operations.
public class MetricsContextStorage implements ContextStorage {
private final ContextStorage delegate;
private final Counter attachCount;
private final Timer attachTimer;
private final Gauge currentDepth;
private final AtomicLong depthCounter = new AtomicLong(0);
public MetricsContextStorage(ContextStorage delegate) {
this.delegate = delegate;
// Initialize metrics (using Micrometer as example)
MeterRegistry registry = Metrics.globalRegistry;
this.attachCount = Counter.builder("context.attach.count")
.description("Number of context attachments")
.register(registry);
this.attachTimer = Timer.builder("context.attach.duration")
.description("Time spent attaching contexts")
.register(registry);
this.currentDepth = Gauge.builder("context.depth.current")
.description("Current context depth")
.register(registry, depthCounter, AtomicLong::get);
}
@Override
public Scope attach(Context toAttach) {
attachCount.increment();
Timer.Sample sample = Timer.start();
depthCounter.incrementAndGet();
Scope scope = delegate.attach(toAttach);
sample.stop(attachTimer);
return new MetricsScope(scope, depthCounter);
}
@Override
public Context current() {
return delegate.current();
}
@Override
public Context root() {
return delegate.root();
}
private static class MetricsScope implements Scope {
private final Scope delegate;
private final AtomicLong depthCounter;
MetricsScope(Scope delegate, AtomicLong depthCounter) {
this.delegate = delegate;
this.depthCounter = depthCounter;
}
@Override
public void close() {
depthCounter.decrementAndGet();
delegate.close();
}
}
}Interface for providing custom storage implementations.
interface ContextStorageProvider {
ContextStorage get();
}public class CustomStorageProvider implements ContextStorageProvider {
@Override
public ContextStorage get() {
ContextStorage base = ContextStorage.defaultStorage();
// Apply multiple wrappers
return new MetricsContextStorage(
new LoggingContextStorage(
new MdcContextStorage(base)
)
);
}
}
// Register via ServiceLoader
// Create META-INF/services/io.opentelemetry.context.ContextStorageProvider
// Add line: com.example.CustomStorageProviderOpenTelemetry provides a strict context storage implementation for debugging context propagation issues.
Enable strict context checking with JVM argument:
java -Dio.opentelemetry.context.enableStrictContext=true your.ApplicationFeatures:
Usage Example:
// This will trigger warnings in strict mode
Context context = Context.current().with(KEY, "value");
Scope scope = context.makeCurrent();
// BAD: Scope not closed in try-with-resources
performOperation();
scope.close(); // Warning: not closed properly
// BAD: Wrong thread
Thread thread = new Thread(() -> {
scope.close(); // Error: closed on wrong thread
});
thread.start();public class ConditionalStorageWrapper {
public static void configure() {
ContextStorage.addWrapper(storage -> {
// Only add logging in development
if (isDevelopmentMode()) {
storage = new LoggingContextStorage(storage);
}
// Always add metrics
storage = new MetricsContextStorage(storage);
// Add audit trail in production
if (isProductionMode()) {
storage = new AuditContextStorage(storage);
}
return storage;
});
}
}public class MigrationContextStorage implements ContextStorage {
private final ContextStorage newStorage;
private final LegacyContextStorage legacyStorage;
public MigrationContextStorage(ContextStorage newStorage, LegacyContextStorage legacyStorage) {
this.newStorage = newStorage;
this.legacyStorage = legacyStorage;
}
@Override
public Scope attach(Context toAttach) {
// Migrate legacy context values
Context migratedContext = migrateLegacyValues(toAttach);
// Attach to both storages during migration
Scope newScope = newStorage.attach(migratedContext);
Scope legacyScope = legacyStorage.attachLegacy(migratedContext);
return new MigrationScope(newScope, legacyScope);
}
private Context migrateLegacyValues(Context context) {
// Extract legacy values and convert to new format
// Implementation depends on legacy system
return context;
}
}// Good: Minimal overhead wrapper
public class EfficientWrapper implements ContextStorage {
private final ContextStorage delegate;
private static final AtomicLong counter = new AtomicLong();
@Override
public Scope attach(Context toAttach) {
counter.incrementAndGet(); // Fast atomic operation
return new CountingScope(delegate.attach(toAttach));
}
private static class CountingScope implements Scope {
private final Scope delegate;
@Override
public void close() {
counter.decrementAndGet();
delegate.close();
}
}
}Install with Tessl CLI
npx tessl i tessl/maven-io-opentelemetry--opentelemetry-context