diff --git a/components/context/build.gradle.kts b/components/context/build.gradle.kts
new file mode 100644
index 00000000000..4dca7fc3036
--- /dev/null
+++ b/components/context/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ id("me.champeau.jmh")
+}
+
+apply(from = "$rootDir/gradle/java.gradle")
+
+jmh {
+ version = "1.28"
+}
diff --git a/components/context/src/main/java/datadog/context/Context.java b/components/context/src/main/java/datadog/context/Context.java
new file mode 100644
index 00000000000..fcdc4d418d1
--- /dev/null
+++ b/components/context/src/main/java/datadog/context/Context.java
@@ -0,0 +1,100 @@
+package datadog.context;
+
+import static datadog.context.ContextProviders.binder;
+import static datadog.context.ContextProviders.manager;
+
+import javax.annotation.Nullable;
+
+/** Immutable context scoped to an execution unit or carrier object. */
+public interface Context {
+
+ /** Returns the root context. */
+ static Context root() {
+ return manager().root();
+ }
+
+ /**
+ * Returns the context attached to the current execution unit.
+ *
+ * @return Attached context; {@link #root()} if there is none
+ */
+ static Context current() {
+ return manager().current();
+ }
+
+ /**
+ * Attaches this context to the current execution unit.
+ *
+ * @return Scope to be closed when the context is invalid.
+ */
+ default ContextScope attach() {
+ return manager().attach(this);
+ }
+
+ /**
+ * Swaps this context with the one attached to current execution unit.
+ *
+ * @return Previously attached context; {@link #root()} if there was none
+ */
+ default Context swap() {
+ return manager().swap(this);
+ }
+
+ /**
+ * Detaches the context attached to the current execution unit, leaving it context-less.
+ *
+ *
WARNING: prefer {@link ContextScope#close()} to properly restore the surrounding context.
+ *
+ * @return Previously attached context; {@link #root()} if there was none
+ */
+ static Context detach() {
+ return manager().detach();
+ }
+
+ /**
+ * Returns the context attached to the given carrier object.
+ *
+ * @return Attached context; {@link #root()} if there is none
+ */
+ static Context from(Object carrier) {
+ return binder().from(carrier);
+ }
+
+ /** Attaches this context to the given carrier object. */
+ default void attachTo(Object carrier) {
+ binder().attachTo(carrier, this);
+ }
+
+ /**
+ * Detaches the context attached to the given carrier object, leaving it context-less.
+ *
+ * @return Previously attached context; {@link #root()} if there was none
+ */
+ static Context detachFrom(Object carrier) {
+ return binder().detachFrom(carrier);
+ }
+
+ /**
+ * Gets the value stored in this context under the given key.
+ *
+ * @return Value stored under the key; {@code null} if there is no value.
+ */
+ @Nullable
+ T get(ContextKey key);
+
+ /**
+ * Creates a new context with the given key-value mapping.
+ *
+ * @return New context with the key-value mapping.
+ */
+ Context with(ContextKey key, T value);
+
+ /**
+ * Creates a new context with a value that has its own implicit key.
+ *
+ * @return New context with the implicitly keyed value.
+ */
+ default Context with(ImplicitContextKeyed value) {
+ return value.storeInto(this);
+ }
+}
diff --git a/components/context/src/main/java/datadog/context/ContextBinder.java b/components/context/src/main/java/datadog/context/ContextBinder.java
new file mode 100644
index 00000000000..bfcc85a887a
--- /dev/null
+++ b/components/context/src/main/java/datadog/context/ContextBinder.java
@@ -0,0 +1,34 @@
+package datadog.context;
+
+/** Binds context to carrier objects. */
+public interface ContextBinder {
+
+ /**
+ * Returns the context attached to the given carrier object.
+ *
+ * @return Attached context; {@link Context#root()} if there is none
+ */
+ Context from(Object carrier);
+
+ /** Attaches the given context to the given carrier object. */
+ void attachTo(Object carrier, Context context);
+
+ /**
+ * Detaches the context attached to the given carrier object, leaving it context-less.
+ *
+ * @return Previously attached context; {@link Context#root()} if there was none
+ */
+ Context detachFrom(Object carrier);
+
+ /** Requests use of a custom {@link ContextBinder}. */
+ static void register(ContextBinder binder) {
+ ContextProviders.customBinder = binder;
+ }
+
+ final class Provided {
+ static final ContextBinder INSTANCE =
+ null != ContextProviders.customBinder
+ ? ContextProviders.customBinder
+ : new WeakMapContextBinder();
+ }
+}
diff --git a/components/context/src/main/java/datadog/context/ContextKey.java b/components/context/src/main/java/datadog/context/ContextKey.java
new file mode 100644
index 00000000000..41b79a42c52
--- /dev/null
+++ b/components/context/src/main/java/datadog/context/ContextKey.java
@@ -0,0 +1,47 @@
+package datadog.context;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Key for indexing values of type {@link T} stored in a {@link Context}.
+ *
+ *
Keys are compared by identity rather than by name. Each stored context type should either
+ * share its key for re-use or implement {@link ImplicitContextKeyed} to keep its key private.
+ */
+public final class ContextKey {
+ private static final AtomicInteger NEXT_INDEX = new AtomicInteger(0);
+
+ private final String name;
+ final int index;
+
+ private ContextKey(String name) {
+ this.name = name;
+ this.index = NEXT_INDEX.getAndIncrement();
+ }
+
+ /** Creates a new key with the given name. */
+ public static ContextKey named(String name) {
+ return new ContextKey<>(name);
+ }
+
+ @Override
+ public int hashCode() {
+ return index;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ } else if (o == null || getClass() != o.getClass()) {
+ return false;
+ } else {
+ return index == ((ContextKey>) o).index;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}
diff --git a/components/context/src/main/java/datadog/context/ContextManager.java b/components/context/src/main/java/datadog/context/ContextManager.java
new file mode 100644
index 00000000000..e8d45a8ec86
--- /dev/null
+++ b/components/context/src/main/java/datadog/context/ContextManager.java
@@ -0,0 +1,50 @@
+package datadog.context;
+
+/** Manages context across execution units. */
+public interface ContextManager {
+
+ /** Returns the root context. */
+ Context root();
+
+ /**
+ * Returns the context attached to the current execution unit.
+ *
+ * @return Attached context; {@link #root()} if there is none
+ */
+ Context current();
+
+ /**
+ * Attaches the given context to the current execution unit.
+ *
+ * @return Scope to be closed when the context is invalid.
+ */
+ ContextScope attach(Context context);
+
+ /**
+ * Swaps the given context with the one attached to current execution unit.
+ *
+ * @return Previously attached context; {@link #root()} if there was none
+ */
+ Context swap(Context context);
+
+ /**
+ * Detaches the context attached to the current execution unit, leaving it context-less.
+ *
+ *
WARNING: prefer {@link ContextScope#close()} to properly restore the surrounding context.
+ *
+ * @return Previously attached context; {@link #root()} if there was none
+ */
+ Context detach();
+
+ /** Requests use of a custom {@link ContextManager}. */
+ static void register(ContextManager manager) {
+ ContextProviders.customManager = manager;
+ }
+
+ final class Provided {
+ static final ContextManager INSTANCE =
+ null != ContextProviders.customManager
+ ? ContextProviders.customManager
+ : new ThreadLocalContextManager();
+ }
+}
diff --git a/components/context/src/main/java/datadog/context/ContextProviders.java b/components/context/src/main/java/datadog/context/ContextProviders.java
new file mode 100644
index 00000000000..e43d989bedc
--- /dev/null
+++ b/components/context/src/main/java/datadog/context/ContextProviders.java
@@ -0,0 +1,16 @@
+package datadog.context;
+
+/** Provides {@link ContextManager} and {@link ContextBinder} implementations. */
+final class ContextProviders {
+
+ static volatile ContextManager customManager;
+ static volatile ContextBinder customBinder;
+
+ static ContextManager manager() {
+ return ContextManager.Provided.INSTANCE; // may be overridden by instrumentation
+ }
+
+ static ContextBinder binder() {
+ return ContextBinder.Provided.INSTANCE; // may be overridden by instrumentation
+ }
+}
diff --git a/components/context/src/main/java/datadog/context/ContextScope.java b/components/context/src/main/java/datadog/context/ContextScope.java
new file mode 100644
index 00000000000..3048d00b37a
--- /dev/null
+++ b/components/context/src/main/java/datadog/context/ContextScope.java
@@ -0,0 +1,12 @@
+package datadog.context;
+
+/** Controls the validity of context attached to an execution unit. */
+public interface ContextScope extends AutoCloseable {
+
+ /** Returns the context controlled by this scope. */
+ Context context();
+
+ /** Detaches the context from the execution unit. */
+ @Override
+ void close();
+}
diff --git a/components/context/src/main/java/datadog/context/EmptyContext.java b/components/context/src/main/java/datadog/context/EmptyContext.java
new file mode 100644
index 00000000000..0ce7b53a68d
--- /dev/null
+++ b/components/context/src/main/java/datadog/context/EmptyContext.java
@@ -0,0 +1,16 @@
+package datadog.context;
+
+/** {@link Context} containing no values. */
+final class EmptyContext implements Context {
+ static final Context INSTANCE = new EmptyContext();
+
+ @Override
+ public T get(ContextKey key) {
+ return null;
+ }
+
+ @Override
+ public Context with(ContextKey key, T value) {
+ return new SingleContext(key.index, value);
+ }
+}
diff --git a/components/context/src/main/java/datadog/context/ImplicitContextKeyed.java b/components/context/src/main/java/datadog/context/ImplicitContextKeyed.java
new file mode 100644
index 00000000000..a822158984b
--- /dev/null
+++ b/components/context/src/main/java/datadog/context/ImplicitContextKeyed.java
@@ -0,0 +1,12 @@
+package datadog.context;
+
+/** {@link Context} value that has its own implicit {@link ContextKey}. */
+public interface ImplicitContextKeyed {
+
+ /**
+ * Creates a new context with this value under its chosen key.
+ *
+ * @return New context with the implicitly keyed value.
+ */
+ Context storeInto(Context context);
+}
diff --git a/components/context/src/main/java/datadog/context/IndexedContext.java b/components/context/src/main/java/datadog/context/IndexedContext.java
new file mode 100644
index 00000000000..89bd29b6200
--- /dev/null
+++ b/components/context/src/main/java/datadog/context/IndexedContext.java
@@ -0,0 +1,46 @@
+package datadog.context;
+
+import static java.lang.Math.max;
+import static java.util.Arrays.copyOfRange;
+
+import java.util.Arrays;
+
+/** {@link Context} containing many values. */
+final class IndexedContext implements Context {
+ private final Object[] store;
+
+ IndexedContext(Object[] store) {
+ this.store = store;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public T get(ContextKey key) {
+ int index = key.index;
+ return index < store.length ? (T) store[index] : null;
+ }
+
+ @Override
+ public Context with(ContextKey key, T value) {
+ int index = key.index;
+ Object[] newStore = copyOfRange(store, 0, max(store.length, index + 1));
+ newStore[index] = value;
+
+ return new IndexedContext(newStore);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ IndexedContext that = (IndexedContext) o;
+ return Arrays.equals(store, that.store);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 31;
+ result = 31 * result + Arrays.hashCode(store);
+ return result;
+ }
+}
diff --git a/components/context/src/main/java/datadog/context/SingleContext.java b/components/context/src/main/java/datadog/context/SingleContext.java
new file mode 100644
index 00000000000..d54f10d73c2
--- /dev/null
+++ b/components/context/src/main/java/datadog/context/SingleContext.java
@@ -0,0 +1,51 @@
+package datadog.context;
+
+import static java.lang.Math.max;
+
+import java.util.Objects;
+
+/** {@link Context} containing a single value. */
+final class SingleContext implements Context {
+ private final int index;
+ private final Object value;
+
+ SingleContext(int index, Object value) {
+ this.index = index;
+ this.value = value;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public V get(ContextKey key) {
+ return index == key.index ? (V) value : null;
+ }
+
+ @Override
+ public Context with(ContextKey secondKey, V secondValue) {
+ int secondIndex = secondKey.index;
+ if (index == secondIndex) {
+ return new SingleContext(index, secondValue);
+ } else {
+ Object[] store = new Object[max(index, secondIndex) + 1];
+ store[index] = value;
+ store[secondIndex] = secondValue;
+ return new IndexedContext(store);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SingleContext that = (SingleContext) o;
+ return index == that.index && Objects.equals(value, that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 31;
+ result = 31 * result + index;
+ result = 31 * result + Objects.hashCode(value);
+ return result;
+ }
+}
diff --git a/components/context/src/main/java/datadog/context/ThreadLocalContextManager.java b/components/context/src/main/java/datadog/context/ThreadLocalContextManager.java
new file mode 100644
index 00000000000..d8fab846111
--- /dev/null
+++ b/components/context/src/main/java/datadog/context/ThreadLocalContextManager.java
@@ -0,0 +1,56 @@
+package datadog.context;
+
+/** {@link ContextManager} that uses a {@link ThreadLocal} to track context per thread. */
+final class ThreadLocalContextManager implements ContextManager {
+
+ private static final ThreadLocal CURRENT_HOLDER =
+ ThreadLocal.withInitial(() -> new Context[] {EmptyContext.INSTANCE});
+
+ @Override
+ public Context root() {
+ return EmptyContext.INSTANCE;
+ }
+
+ @Override
+ public Context current() {
+ return CURRENT_HOLDER.get()[0];
+ }
+
+ @Override
+ public ContextScope attach(Context context) {
+
+ Context[] holder = CURRENT_HOLDER.get();
+ Context previous = holder[0];
+ holder[0] = context;
+
+ return new ContextScope() {
+ private boolean closed;
+
+ @Override
+ public Context context() {
+ return context;
+ }
+
+ @Override
+ public void close() {
+ if (!closed && context == holder[0]) {
+ holder[0] = previous;
+ closed = true;
+ }
+ }
+ };
+ }
+
+ @Override
+ public Context swap(Context context) {
+ Context[] holder = CURRENT_HOLDER.get();
+ Context previous = holder[0];
+ holder[0] = context;
+ return previous;
+ }
+
+ @Override
+ public Context detach() {
+ return swap(root());
+ }
+}
diff --git a/components/context/src/main/java/datadog/context/WeakMapContextBinder.java b/components/context/src/main/java/datadog/context/WeakMapContextBinder.java
new file mode 100644
index 00000000000..144a62c1c35
--- /dev/null
+++ b/components/context/src/main/java/datadog/context/WeakMapContextBinder.java
@@ -0,0 +1,29 @@
+package datadog.context;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/** {@link ContextBinder} that uses a global weak map of carriers to contexts. */
+final class WeakMapContextBinder implements ContextBinder {
+
+ private static final Map