diff --git a/dd-trace-core/src/main/java/datadog/trace/common/sampling/ParentBasedAlwaysOnSampler.java b/dd-trace-core/src/main/java/datadog/trace/common/sampling/ParentBasedAlwaysOnSampler.java
new file mode 100644
index 00000000000..6e8e42dc2d8
--- /dev/null
+++ b/dd-trace-core/src/main/java/datadog/trace/common/sampling/ParentBasedAlwaysOnSampler.java
@@ -0,0 +1,24 @@
+package datadog.trace.common.sampling;
+
+import datadog.trace.api.sampling.PrioritySampling;
+import datadog.trace.api.sampling.SamplingMechanism;
+import datadog.trace.core.CoreSpan;
+
+/**
+ * Implements the OpenTelemetry {@code parentbased_always_on} sampler.
+ *
+ *
Root spans are always sampled. Child spans inherit the sampling decision from their parent,
+ * which is handled by the context propagation layer.
+ */
+public class ParentBasedAlwaysOnSampler implements Sampler, PrioritySampler {
+
+ @Override
+ public > boolean sample(final T span) {
+ return true;
+ }
+
+ @Override
+ public > void setSamplingPriority(final T span) {
+ span.setSamplingPriority(PrioritySampling.SAMPLER_KEEP, SamplingMechanism.DEFAULT);
+ }
+}
diff --git a/dd-trace-core/src/main/java/datadog/trace/common/sampling/Sampler.java b/dd-trace-core/src/main/java/datadog/trace/common/sampling/Sampler.java
index 7cecdf40ed9..af1045e39df 100644
--- a/dd-trace-core/src/main/java/datadog/trace/common/sampling/Sampler.java
+++ b/dd-trace-core/src/main/java/datadog/trace/common/sampling/Sampler.java
@@ -80,6 +80,7 @@ public static Sampler forConfig(final Config config, final TraceConfig traceConf
log.error("Invalid sampler configuration. Using AllSampler", e);
sampler = new AllSampler();
}
+ // TODO: if OTLP trace export enabled, select ParentBasedAlwaysOnSampler here
} else if (config.isPrioritySamplingEnabled()) {
if (KEEP.equalsIgnoreCase(config.getPrioritySamplingForce())) {
log.debug("Force Sampling Priority to: SAMPLER_KEEP.");
diff --git a/dd-trace-core/src/test/java/datadog/trace/common/sampling/ParentBasedAlwaysOnSamplerTest.java b/dd-trace-core/src/test/java/datadog/trace/common/sampling/ParentBasedAlwaysOnSamplerTest.java
new file mode 100644
index 00000000000..e515975edfe
--- /dev/null
+++ b/dd-trace-core/src/test/java/datadog/trace/common/sampling/ParentBasedAlwaysOnSamplerTest.java
@@ -0,0 +1,117 @@
+package datadog.trace.common.sampling;
+
+import static datadog.trace.api.TracePropagationStyle.DATADOG;
+import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import datadog.trace.api.DDTraceId;
+import datadog.trace.common.writer.ListWriter;
+import datadog.trace.common.writer.RemoteResponseListener;
+import datadog.trace.core.CoreTracer;
+import datadog.trace.core.DDSpan;
+import datadog.trace.core.propagation.ExtractedContext;
+import datadog.trace.core.propagation.PropagationTags;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.tabletest.junit.TableTest;
+
+class ParentBasedAlwaysOnSamplerTest {
+
+ private final ListWriter writer = new ListWriter();
+ private CoreTracer tracer;
+
+ @AfterEach
+ void tearDown() {
+ if (tracer != null) {
+ tracer.close();
+ }
+ }
+
+ private CoreTracer buildTracer(ParentBasedAlwaysOnSampler sampler) {
+ tracer = CoreTracer.builder().writer(writer).sampler(sampler).build();
+ return tracer;
+ }
+
+ @Test
+ void alwaysSamplesSpans() {
+ ParentBasedAlwaysOnSampler sampler = new ParentBasedAlwaysOnSampler();
+ CoreTracer tracer = buildTracer(sampler);
+
+ DDSpan span = (DDSpan) tracer.buildSpan("test").start();
+ try {
+ assertTrue(sampler.sample(span));
+ } finally {
+ span.finish();
+ }
+ }
+
+ @Test
+ void setsSamplingPriorityToSamplerKeep() {
+ ParentBasedAlwaysOnSampler sampler = new ParentBasedAlwaysOnSampler();
+ CoreTracer tracer = buildTracer(sampler);
+
+ DDSpan span = (DDSpan) tracer.buildSpan("test").start();
+ try {
+ sampler.setSamplingPriority(span);
+ assertEquals(SAMPLER_KEEP, span.getSamplingPriority());
+ } finally {
+ span.finish();
+ }
+ }
+
+ @Test
+ void childSpanInheritsSamplingPriorityFromLocalParent() {
+ ParentBasedAlwaysOnSampler sampler = new ParentBasedAlwaysOnSampler();
+ CoreTracer tracer = buildTracer(sampler);
+
+ DDSpan rootSpan = (DDSpan) tracer.buildSpan("root").start();
+ sampler.setSamplingPriority(rootSpan);
+ DDSpan childSpan = (DDSpan) tracer.buildSpan("child").asChildOf(rootSpan.context()).start();
+ try {
+ assertEquals(SAMPLER_KEEP, rootSpan.getSamplingPriority());
+ assertEquals(SAMPLER_KEEP, childSpan.getSamplingPriority());
+ } finally {
+ childSpan.finish();
+ rootSpan.finish();
+ }
+ }
+
+ @TableTest({
+ "scenario | parentPriority",
+ "sampler keep | 1 ",
+ "sampler drop | 0 ",
+ "user keep | 2 ",
+ "user drop | -1 "
+ })
+ @ParameterizedTest(name = "child span inherits sampling decision from remote parent [{index}]")
+ void childSpanInheritsSamplingDecisionFromRemoteParent(int parentPriority) {
+ ParentBasedAlwaysOnSampler sampler = new ParentBasedAlwaysOnSampler();
+ CoreTracer tracer = buildTracer(sampler);
+
+ ExtractedContext extractedContext =
+ new ExtractedContext(
+ DDTraceId.ONE, 2, parentPriority, null, PropagationTags.factory().empty(), DATADOG);
+
+ DDSpan span = (DDSpan) tracer.buildSpan("child").asChildOf(extractedContext).start();
+ try {
+ assertEquals(parentPriority, span.getSamplingPriority());
+ } finally {
+ span.finish();
+ }
+ }
+
+ @Test
+ void isNotARemoteResponseListener() {
+ ParentBasedAlwaysOnSampler sampler = new ParentBasedAlwaysOnSampler();
+ assertFalse(sampler instanceof RemoteResponseListener);
+ }
+
+ @Test
+ void implementsPrioritySampler() {
+ ParentBasedAlwaysOnSampler sampler = new ParentBasedAlwaysOnSampler();
+ assertTrue(sampler instanceof PrioritySampler);
+ }
+}