diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ComposableSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ComposableSampler.java
new file mode 100644
index 000000000..85891f47d
--- /dev/null
+++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ComposableSampler.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.contrib.sampler.consistent56;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.trace.data.LinkData;
+import java.util.List;
+
+/** An interface for components to be used by composite consistent probability samplers. */
+public interface ComposableSampler {
+
+ /**
+ * Returns the SamplingIntent that is used for the sampling decision. The SamplingIntent includes
+ * the threshold value which will be used for the sampling decision.
+ *
+ *
NOTE: Keep in mind, that in any case the returned threshold value must not depend directly
+ * or indirectly on the random value. In particular this means that the parent sampled flag must
+ * not be used for the calculation of the threshold as the sampled flag depends itself on the
+ * random value.
+ */
+ SamplingIntent getSamplingIntent(
+ Context parentContext,
+ String name,
+ SpanKind spanKind,
+ Attributes attributes,
+ List parentLinks);
+
+ /** Return the string providing a description of the implementation. */
+ String getDescription();
+}
diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSampler.java
index ba4a0869e..2594ef88d 100644
--- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSampler.java
+++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSampler.java
@@ -5,6 +5,13 @@
package io.opentelemetry.contrib.sampler.consistent56;
+import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.trace.data.LinkData;
+import java.util.List;
import javax.annotation.concurrent.Immutable;
@Immutable
@@ -19,8 +26,14 @@ static ConsistentAlwaysOffSampler getInstance() {
}
@Override
- protected long getThreshold(long parentThreshold, boolean isRoot) {
- return ConsistentSamplingUtil.getMaxThreshold();
+ public SamplingIntent getSamplingIntent(
+ Context parentContext,
+ String name,
+ SpanKind spanKind,
+ Attributes attributes,
+ List parentLinks) {
+
+ return () -> getInvalidThreshold();
}
@Override
diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSampler.java
index bae1c4b27..620261aad 100644
--- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSampler.java
+++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSampler.java
@@ -7,6 +7,11 @@
import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMinThreshold;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.trace.data.LinkData;
+import java.util.List;
import javax.annotation.concurrent.Immutable;
@Immutable
@@ -21,8 +26,14 @@ static ConsistentAlwaysOnSampler getInstance() {
}
@Override
- protected long getThreshold(long parentThreshold, boolean isRoot) {
- return getMinThreshold();
+ public SamplingIntent getSamplingIntent(
+ Context parentContext,
+ String name,
+ SpanKind spanKind,
+ Attributes attributes,
+ List parentLinks) {
+
+ return () -> getMinThreshold();
}
@Override
diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOf.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOf.java
new file mode 100644
index 000000000..56add2dfc
--- /dev/null
+++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOf.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.contrib.sampler.consistent56;
+
+import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold;
+import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.isValidThreshold;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.TraceState;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.trace.data.LinkData;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A consistent sampler that queries all its delegate samplers for their sampling threshold, and
+ * uses the minimum threshold value received.
+ */
+@Immutable
+final class ConsistentAnyOf extends ConsistentSampler {
+
+ private final ComposableSampler[] delegates;
+
+ private final String description;
+
+ /**
+ * Constructs a new consistent AnyOf sampler using the provided delegate samplers.
+ *
+ * @param delegates the delegate samplers
+ */
+ ConsistentAnyOf(@Nullable ComposableSampler... delegates) {
+ if (delegates == null || delegates.length == 0) {
+ throw new IllegalArgumentException(
+ "At least one delegate must be specified for ConsistentAnyOf");
+ }
+
+ this.delegates = delegates;
+
+ this.description =
+ Stream.of(delegates)
+ .map(Object::toString)
+ .collect(Collectors.joining(",", "ConsistentAnyOf{", "}"));
+ }
+
+ @Override
+ public SamplingIntent getSamplingIntent(
+ Context parentContext,
+ String name,
+ SpanKind spanKind,
+ Attributes attributes,
+ List parentLinks) {
+
+ SamplingIntent[] intents = new SamplingIntent[delegates.length];
+ int k = 0;
+ long minimumThreshold = getInvalidThreshold();
+ for (ComposableSampler delegate : delegates) {
+ SamplingIntent delegateIntent =
+ delegate.getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks);
+ long delegateThreshold = delegateIntent.getThreshold();
+ if (isValidThreshold(delegateThreshold)) {
+ if (isValidThreshold(minimumThreshold)) {
+ minimumThreshold = Math.min(delegateThreshold, minimumThreshold);
+ } else {
+ minimumThreshold = delegateThreshold;
+ }
+ }
+ intents[k++] = delegateIntent;
+ }
+
+ long resultingThreshold = minimumThreshold;
+
+ return new SamplingIntent() {
+ @Override
+ public long getThreshold() {
+ return resultingThreshold;
+ }
+
+ @Override
+ public Attributes getAttributes() {
+ AttributesBuilder builder = Attributes.builder();
+ for (SamplingIntent intent : intents) {
+ builder = builder.putAll(intent.getAttributes());
+ }
+ return builder.build();
+ }
+
+ @Override
+ public TraceState updateTraceState(TraceState previousState) {
+ for (SamplingIntent intent : intents) {
+ previousState = intent.updateTraceState(previousState);
+ }
+ return previousState;
+ }
+ };
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+}
diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedAndSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedAndSampler.java
deleted file mode 100644
index 40df6c895..000000000
--- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedAndSampler.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package io.opentelemetry.contrib.sampler.consistent56;
-
-import static java.util.Objects.requireNonNull;
-
-import javax.annotation.concurrent.Immutable;
-
-/**
- * A consistent sampler composed of two consistent samplers.
- *
- * This sampler samples if both samplers would sample.
- */
-@Immutable
-final class ConsistentComposedAndSampler extends ConsistentSampler {
-
- private final ConsistentSampler sampler1;
- private final ConsistentSampler sampler2;
- private final String description;
-
- ConsistentComposedAndSampler(ConsistentSampler sampler1, ConsistentSampler sampler2) {
- this.sampler1 = requireNonNull(sampler1);
- this.sampler2 = requireNonNull(sampler2);
- this.description =
- "ConsistentComposedAndSampler{"
- + "sampler1="
- + sampler1.getDescription()
- + ",sampler2="
- + sampler2.getDescription()
- + '}';
- }
-
- @Override
- protected long getThreshold(long parentThreshold, boolean isRoot) {
- long threshold1 = sampler1.getThreshold(parentThreshold, isRoot);
- long threshold2 = sampler2.getThreshold(parentThreshold, isRoot);
- if (ConsistentSamplingUtil.isValidThreshold(threshold1)
- && ConsistentSamplingUtil.isValidThreshold(threshold2)) {
- return Math.max(threshold1, threshold2);
- } else {
- return ConsistentSamplingUtil.getInvalidThreshold();
- }
- }
-
- @Override
- public String getDescription() {
- return description;
- }
-}
diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedOrSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedOrSampler.java
deleted file mode 100644
index b701b5622..000000000
--- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedOrSampler.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package io.opentelemetry.contrib.sampler.consistent56;
-
-import static java.util.Objects.requireNonNull;
-
-import javax.annotation.concurrent.Immutable;
-
-/**
- * A consistent sampler composed of two consistent samplers.
- *
- *
This sampler samples if any of the two samplers would sample.
- */
-@Immutable
-final class ConsistentComposedOrSampler extends ConsistentSampler {
-
- private final ConsistentSampler sampler1;
- private final ConsistentSampler sampler2;
- private final String description;
-
- ConsistentComposedOrSampler(ConsistentSampler sampler1, ConsistentSampler sampler2) {
- this.sampler1 = requireNonNull(sampler1);
- this.sampler2 = requireNonNull(sampler2);
- this.description =
- "ConsistentComposedOrSampler{"
- + "sampler1="
- + sampler1.getDescription()
- + ",sampler2="
- + sampler2.getDescription()
- + '}';
- }
-
- @Override
- protected long getThreshold(long parentThreshold, boolean isRoot) {
- long threshold1 = sampler1.getThreshold(parentThreshold, isRoot);
- long threshold2 = sampler2.getThreshold(parentThreshold, isRoot);
- if (ConsistentSamplingUtil.isValidThreshold(threshold1)) {
- if (ConsistentSamplingUtil.isValidThreshold(threshold2)) {
- return Math.min(threshold1, threshold2);
- }
- return threshold1;
- } else {
- if (ConsistentSamplingUtil.isValidThreshold(threshold2)) {
- return threshold2;
- }
- return ConsistentSamplingUtil.getInvalidThreshold();
- }
- }
-
- @Override
- public String getDescription() {
- return description;
- }
-}
diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSampler.java
index d2e2fc426..f2e92651c 100644
--- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSampler.java
+++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSampler.java
@@ -7,6 +7,14 @@
import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.calculateSamplingProbability;
import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.checkThreshold;
+import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold;
+import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMaxThreshold;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.trace.data.LinkData;
+import java.util.List;
public class ConsistentFixedThresholdSampler extends ConsistentSampler {
@@ -18,7 +26,7 @@ protected ConsistentFixedThresholdSampler(long threshold) {
this.threshold = threshold;
String thresholdString;
- if (threshold == ConsistentSamplingUtil.getMaxThreshold()) {
+ if (threshold == getMaxThreshold()) {
thresholdString = "max";
} else {
thresholdString =
@@ -41,7 +49,18 @@ public String getDescription() {
}
@Override
- protected long getThreshold(long parentThreshold, boolean isRoot) {
- return threshold;
+ public SamplingIntent getSamplingIntent(
+ Context parentContext,
+ String name,
+ SpanKind spanKind,
+ Attributes attributes,
+ List parentLinks) {
+
+ return () -> {
+ if (threshold == getMaxThreshold()) {
+ return getInvalidThreshold();
+ }
+ return threshold;
+ };
}
}
diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentParentBasedSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentParentBasedSampler.java
index bb3f3836a..01e42ddcf 100644
--- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentParentBasedSampler.java
+++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentParentBasedSampler.java
@@ -8,6 +8,14 @@
import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold;
import static java.util.Objects.requireNonNull;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanContext;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.TraceState;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.trace.data.LinkData;
+import java.util.List;
import javax.annotation.concurrent.Immutable;
/**
@@ -17,7 +25,7 @@
@Immutable
final class ConsistentParentBasedSampler extends ConsistentSampler {
- private final ConsistentSampler rootSampler;
+ private final ComposableSampler rootSampler;
private final String description;
@@ -27,19 +35,40 @@ final class ConsistentParentBasedSampler extends ConsistentSampler {
*
* @param rootSampler the root sampler
*/
- ConsistentParentBasedSampler(ConsistentSampler rootSampler) {
+ ConsistentParentBasedSampler(ComposableSampler rootSampler) {
this.rootSampler = requireNonNull(rootSampler);
this.description =
"ConsistentParentBasedSampler{rootSampler=" + rootSampler.getDescription() + '}';
}
@Override
- protected long getThreshold(long parentThreshold, boolean isRoot) {
+ public SamplingIntent getSamplingIntent(
+ Context parentContext,
+ String name,
+ SpanKind spanKind,
+ Attributes attributes,
+ List parentLinks) {
+
+ Span parentSpan = Span.fromContext(parentContext);
+ SpanContext parentSpanContext = parentSpan.getSpanContext();
+ boolean isRoot = !parentSpanContext.isValid();
+
if (isRoot) {
- return rootSampler.getThreshold(getInvalidThreshold(), isRoot);
+ return rootSampler.getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks);
+ }
+
+ TraceState parentTraceState = parentSpanContext.getTraceState();
+ String otelTraceStateString = parentTraceState.get(OtelTraceState.TRACE_STATE_KEY);
+ OtelTraceState otelTraceState = OtelTraceState.parse(otelTraceStateString);
+
+ long parentThreshold;
+ if (otelTraceState.hasValidThreshold()) {
+ parentThreshold = otelTraceState.getThreshold();
} else {
- return parentThreshold;
+ parentThreshold = getInvalidThreshold();
}
+
+ return () -> parentThreshold;
}
@Override
diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSampler.java
index d7622effa..d28161485 100644
--- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSampler.java
+++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSampler.java
@@ -5,11 +5,19 @@
package io.opentelemetry.contrib.sampler.consistent56;
+import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.calculateSamplingProbability;
import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.calculateThreshold;
-import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMinThreshold;
+import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold;
+import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.isValidThreshold;
import static java.util.Objects.requireNonNull;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.TraceState;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.trace.data.LinkData;
import io.opentelemetry.sdk.trace.samplers.Sampler;
+import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.LongSupplier;
import javax.annotation.concurrent.Immutable;
@@ -66,6 +74,24 @@
* {@code decayFactor} corresponds to {@code b(n)}
* {@code adaptationTimeSeconds} corresponds to {@code -1 / ln(1 - a)}
*
+ *
+ *
+ *
+ *
The sampler also keeps track of the average sampling probability delivered by the delegate
+ * sampler, using exponential smoothing. Given the sequence of the observed probabilities {@code
+ * P(k)}, the exponentially smoothed values {@code S(k)} are calculated according to the following
+ * formula:
+ *
+ *
{@code S(0) = 1}
+ *
+ *
{@code S(n) = alpha * P(n) + (1 - alpha) * S(n-1)}, for {@code n > 0}
+ *
+ *
where {@code alpha} is the smoothing factor ({@code 0 < alpha < 1}).
+ *
+ *
The smoothing factor is chosen heuristically to be approximately proportional to the expected
+ * maximum volume of spans sampled within the adaptation time window, i.e.
+ *
+ *
{@code 1 / (adaptationTimeSeconds * targetSpansPerSecondLimit)}
*/
final class ConsistentRateLimitingSampler extends ConsistentSampler {
@@ -75,12 +101,18 @@ final class ConsistentRateLimitingSampler extends ConsistentSampler {
private static final class State {
private final double effectiveWindowCount;
private final double effectiveWindowNanos;
+ private final double effectiveDelegateProbability;
private final long lastNanoTime;
- public State(double effectiveWindowCount, double effectiveWindowNanos, long lastNanoTime) {
+ public State(
+ double effectiveWindowCount,
+ double effectiveWindowNanos,
+ long lastNanoTime,
+ double effectiveDelegateProbability) {
this.effectiveWindowCount = effectiveWindowCount;
this.effectiveWindowNanos = effectiveWindowNanos;
this.lastNanoTime = lastNanoTime;
+ this.effectiveDelegateProbability = effectiveDelegateProbability;
}
}
@@ -88,7 +120,9 @@ public State(double effectiveWindowCount, double effectiveWindowNanos, long last
private final LongSupplier nanoTimeSupplier;
private final double inverseAdaptationTimeNanos;
private final double targetSpansPerNanosecondLimit;
+ private final double probabilitySmoothingFactor;
private final AtomicReference state;
+ private final ComposableSampler delegate;
/**
* Constructor.
@@ -99,10 +133,13 @@ public State(double effectiveWindowCount, double effectiveWindowNanos, long last
* @param nanoTimeSupplier a supplier for the current nano time
*/
ConsistentRateLimitingSampler(
+ ComposableSampler delegate,
double targetSpansPerSecondLimit,
double adaptationTimeSeconds,
LongSupplier nanoTimeSupplier) {
+ this.delegate = requireNonNull(delegate);
+
if (targetSpansPerSecondLimit < 0.0) {
throw new IllegalArgumentException("Limit for sampled spans per second must be nonnegative!");
}
@@ -120,36 +157,114 @@ public State(double effectiveWindowCount, double effectiveWindowNanos, long last
this.inverseAdaptationTimeNanos = NANOS_IN_SECONDS / adaptationTimeSeconds;
this.targetSpansPerNanosecondLimit = NANOS_IN_SECONDS * targetSpansPerSecondLimit;
- this.state = new AtomicReference<>(new State(0, 0, nanoTimeSupplier.getAsLong()));
+ this.probabilitySmoothingFactor =
+ determineProbabilitySmoothingFactor(targetSpansPerSecondLimit, adaptationTimeSeconds);
+
+ this.state = new AtomicReference<>(new State(0, 0, nanoTimeSupplier.getAsLong(), 1.0));
+ }
+
+ private static double determineProbabilitySmoothingFactor(
+ double targetSpansPerSecondLimit, double adaptationTimeSeconds) {
+ // The probability smoothing factor alpha will be the weight for the newly observed
+ // probability P, while (1-alpha) will be the weight for the cumulative average probability
+ // observed so far (newC = P * alpha + oldC * (1 - alpha)). Any smoothing factor
+ // alpha from the interval (0.0, 1.0) is mathematically acceptable.
+ // However, we'd like the weight associated with the newly observed data point to be inversely
+ // proportional to the adaptation time (larger adaptation time will allow longer time for the
+ // cumulative probability to stabilize) and inversely proportional to the order of magnitude of
+ // the data points arriving within a given time unit (because with a lot of data points we can
+ // afford to give a smaller weight to each single one). We do not know the true rate of Spans
+ // coming in to get sampled, but we optimistically assume that the user knows what they are
+ // doing and that the targetSpansPerSecondLimit will be of similar order of magnitude.
+
+ // First approximation of the probability smoothing factor alpha.
+ double t = 1.0 / (targetSpansPerSecondLimit * adaptationTimeSeconds);
+
+ // We expect that t is a small number, but we have to make sure that alpha is smaller than 1.
+ // Therefore we apply a "bending" transformation which almost preserves small values, but makes
+ // sure that the result is within the expected interval.
+ return t / (1.0 + t);
}
- private State updateState(State oldState, long currentNanoTime) {
- if (currentNanoTime <= oldState.lastNanoTime) {
+ private State updateState(State oldState, long currentNanoTime, double delegateProbability) {
+ double currentAverageProbability =
+ oldState.effectiveDelegateProbability * (1.0 - probabilitySmoothingFactor)
+ + delegateProbability * probabilitySmoothingFactor;
+
+ long nanoTimeDelta = currentNanoTime - oldState.lastNanoTime;
+ if (nanoTimeDelta <= 0.0) {
+ // Low clock resolution or clock jumping backwards.
+ // Assume time delta equal to zero.
return new State(
- oldState.effectiveWindowCount + 1, oldState.effectiveWindowNanos, oldState.lastNanoTime);
+ oldState.effectiveWindowCount + 1,
+ oldState.effectiveWindowNanos,
+ oldState.lastNanoTime,
+ currentAverageProbability);
}
- long nanoTimeDelta = currentNanoTime - oldState.lastNanoTime;
+
double decayFactor = Math.exp(-nanoTimeDelta * inverseAdaptationTimeNanos);
double currentEffectiveWindowCount = oldState.effectiveWindowCount * decayFactor + 1;
double currentEffectiveWindowNanos =
oldState.effectiveWindowNanos * decayFactor + nanoTimeDelta;
- return new State(currentEffectiveWindowCount, currentEffectiveWindowNanos, currentNanoTime);
+
+ return new State(
+ currentEffectiveWindowCount,
+ currentEffectiveWindowNanos,
+ currentNanoTime,
+ currentAverageProbability);
}
@Override
- protected long getThreshold(long parentThreshold, boolean isRoot) {
- long currentNanoTime = nanoTimeSupplier.getAsLong();
- State currentState = state.updateAndGet(s -> updateState(s, currentNanoTime));
+ public SamplingIntent getSamplingIntent(
+ Context parentContext,
+ String name,
+ SpanKind spanKind,
+ Attributes attributes,
+ List parentLinks) {
+ double suggestedProbability;
+ long suggestedThreshold;
+
+ SamplingIntent delegateIntent =
+ delegate.getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks);
+ long delegateThreshold = delegateIntent.getThreshold();
+
+ if (isValidThreshold(delegateThreshold)) {
+ double delegateProbability = calculateSamplingProbability(delegateThreshold);
+ long currentNanoTime = nanoTimeSupplier.getAsLong();
+ State currentState =
+ state.updateAndGet(s -> updateState(s, currentNanoTime, delegateProbability));
- double samplingProbability =
- (currentState.effectiveWindowNanos * targetSpansPerNanosecondLimit)
- / currentState.effectiveWindowCount;
+ double targetMaxProbability =
+ (currentState.effectiveWindowNanos * targetSpansPerNanosecondLimit)
+ / currentState.effectiveWindowCount;
- if (samplingProbability >= 1.) {
- return getMinThreshold();
+ if (currentState.effectiveDelegateProbability > targetMaxProbability) {
+ suggestedProbability =
+ targetMaxProbability / currentState.effectiveDelegateProbability * delegateProbability;
+ } else {
+ suggestedProbability = delegateProbability;
+ }
+ suggestedThreshold = calculateThreshold(suggestedProbability);
} else {
- return calculateThreshold(samplingProbability);
+ suggestedThreshold = getInvalidThreshold();
}
+
+ return new SamplingIntent() {
+ @Override
+ public long getThreshold() {
+ return suggestedThreshold;
+ }
+
+ @Override
+ public Attributes getAttributes() {
+ return delegateIntent.getAttributes();
+ }
+
+ @Override
+ public TraceState updateTraceState(TraceState previousState) {
+ return delegateIntent.updateTraceState(previousState);
+ }
+ };
}
@Override
diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSampler.java
new file mode 100644
index 000000000..90e4cb026
--- /dev/null
+++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSampler.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.contrib.sampler.consistent56;
+
+import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.trace.data.LinkData;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A consistent sampler that uses Span categorization and uses a different delegate sampler for each
+ * category. Categorization of Spans is aided by Predicates, which can be combined with
+ * ComposableSamplers into PredicatedSamplers.
+ */
+@Immutable
+final class ConsistentRuleBasedSampler extends ConsistentSampler {
+
+ @Nullable private final SpanKind spanKindToMatch;
+ private final PredicatedSampler[] samplers;
+
+ private final String description;
+
+ ConsistentRuleBasedSampler(
+ @Nullable SpanKind spanKindToMatch, @Nullable PredicatedSampler... samplers) {
+ this.spanKindToMatch = spanKindToMatch;
+ this.samplers = (samplers != null) ? samplers : new PredicatedSampler[0];
+
+ this.description =
+ Stream.of(samplers)
+ .map((s) -> s.getSampler().getDescription())
+ .collect(Collectors.joining(",", "ConsistentRuleBasedSampler{", "}"));
+ }
+
+ @Override
+ public SamplingIntent getSamplingIntent(
+ Context parentContext,
+ String name,
+ SpanKind spanKind,
+ Attributes attributes,
+ List parentLinks) {
+
+ if (spanKindToMatch == null || spanKindToMatch == spanKind) {
+ for (PredicatedSampler delegate : samplers) {
+ if (delegate
+ .getPredicate()
+ .spanMatches(parentContext, name, spanKind, attributes, parentLinks)) {
+ return delegate
+ .getSampler()
+ .getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks);
+ }
+ }
+ }
+
+ return () -> getInvalidThreshold();
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+}
diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSampler.java
index 618728b5c..5592b3215 100644
--- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSampler.java
+++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSampler.java
@@ -6,7 +6,6 @@
package io.opentelemetry.contrib.sampler.consistent56;
import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidRandomValue;
-import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold;
import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.isValidThreshold;
import io.opentelemetry.api.common.Attributes;
@@ -21,9 +20,11 @@
import io.opentelemetry.sdk.trace.samplers.SamplingResult;
import java.util.List;
import java.util.function.LongSupplier;
+import javax.annotation.Nullable;
/** Abstract base class for consistent samplers. */
-public abstract class ConsistentSampler implements Sampler {
+@SuppressWarnings("InconsistentOverloads")
+public abstract class ConsistentSampler implements Sampler, ComposableSampler {
/**
* Returns a {@link ConsistentSampler} that samples all spans.
@@ -60,10 +61,23 @@ public static ConsistentSampler probabilityBased(double samplingProbability) {
*
* @param rootSampler the root sampler
*/
- public static ConsistentSampler parentBased(ConsistentSampler rootSampler) {
+ public static ConsistentSampler parentBased(ComposableSampler rootSampler) {
return new ConsistentParentBasedSampler(rootSampler);
}
+ /**
+ * Constructs a new consistent rule based sampler using the given sequence of Predicates and
+ * delegate Samplers.
+ *
+ * @param spanKindToMatch the SpanKind for which the Sampler applies, null value indicates all
+ * SpanKinds
+ * @param samplers the PredicatedSamplers to evaluate and query
+ */
+ public static ConsistentRuleBasedSampler ruleBased(
+ @Nullable SpanKind spanKindToMatch, PredicatedSampler... samplers) {
+ return new ConsistentRuleBasedSampler(spanKindToMatch, samplers);
+ }
+
/**
* Returns a new {@link ConsistentSampler} that attempts to adjust the sampling probability
* dynamically to meet the target span rate.
@@ -72,9 +86,26 @@ public static ConsistentSampler parentBased(ConsistentSampler rootSampler) {
* @param adaptationTimeSeconds the typical time to adapt to a new load (time constant used for
* exponential smoothing)
*/
- public static ConsistentSampler rateLimited(
+ static ConsistentSampler rateLimited(
double targetSpansPerSecondLimit, double adaptationTimeSeconds) {
- return rateLimited(targetSpansPerSecondLimit, adaptationTimeSeconds, System::nanoTime);
+ return rateLimited(alwaysOn(), targetSpansPerSecondLimit, adaptationTimeSeconds);
+ }
+
+ /**
+ * Returns a new {@link ConsistentSampler} that honors the delegate sampling decision as long as
+ * it seems to meet the target span rate. In case the delegate sampling rate seems to exceed the
+ * target, the sampler attempts to decrease the effective sampling probability dynamically to meet
+ * the target span rate.
+ *
+ * @param delegate the delegate sampler
+ * @param targetSpansPerSecondLimit the desired spans per second limit
+ * @param adaptationTimeSeconds the typical time to adapt to a new load (time constant used for
+ * exponential smoothing)
+ */
+ public static ConsistentSampler rateLimited(
+ ComposableSampler delegate, double targetSpansPerSecondLimit, double adaptationTimeSeconds) {
+ return rateLimited(
+ delegate, targetSpansPerSecondLimit, adaptationTimeSeconds, System::nanoTime);
}
/**
@@ -90,52 +121,46 @@ static ConsistentSampler rateLimited(
double targetSpansPerSecondLimit,
double adaptationTimeSeconds,
LongSupplier nanoTimeSupplier) {
- return new ConsistentRateLimitingSampler(
- targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier);
+ return rateLimited(
+ alwaysOn(), targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier);
}
/**
- * Returns a {@link ConsistentSampler} that samples a span if both this and the other given
- * consistent sampler would sample the span.
- *
- * If the other consistent sampler is the same as this, this consistent sampler will be
- * returned.
- *
- *
The returned sampler takes care of setting the trace state correctly, which would not happen
- * if the {@link #shouldSample(Context, String, String, SpanKind, Attributes, List)} method was
- * called for each sampler individually. Also, the combined sampler is more efficient than
- * evaluating the two samplers individually and combining both results afterwards.
+ * Returns a new {@link ConsistentSampler} that honors the delegate sampling decision as long as
+ * it seems to meet the target span rate. In case the delegate sampling rate seems to exceed the
+ * target, the sampler attempts to decrease the effective sampling probability dynamically to meet
+ * the target span rate.
*
- * @param otherConsistentSampler the other consistent sampler
- * @return the composed consistent sampler
+ * @param delegate the delegate sampler
+ * @param targetSpansPerSecondLimit the desired spans per second limit
+ * @param adaptationTimeSeconds the typical time to adapt to a new load (time constant used for
+ * exponential smoothing)
+ * @param nanoTimeSupplier a supplier for the current nano time
*/
- public ConsistentSampler and(ConsistentSampler otherConsistentSampler) {
- if (otherConsistentSampler == this) {
- return this;
- }
- return new ConsistentComposedAndSampler(this, otherConsistentSampler);
+ static ConsistentSampler rateLimited(
+ ComposableSampler delegate,
+ double targetSpansPerSecondLimit,
+ double adaptationTimeSeconds,
+ LongSupplier nanoTimeSupplier) {
+ return new ConsistentRateLimitingSampler(
+ delegate, targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier);
}
/**
- * Returns a {@link ConsistentSampler} that samples a span if this or the other given consistent
- * sampler would sample the span.
- *
- *
If the other consistent sampler is the same as this, this consistent sampler will be
- * returned.
+ * Returns a {@link ConsistentSampler} that queries its delegate Samplers for their sampling
+ * threshold before determining what threshold to use. The intention is to make a positive
+ * sampling decision if any of the delegates would make a positive decision.
*
*
The returned sampler takes care of setting the trace state correctly, which would not happen
* if the {@link #shouldSample(Context, String, String, SpanKind, Attributes, List)} method was
* called for each sampler individually. Also, the combined sampler is more efficient than
- * evaluating the two samplers individually and combining both results afterwards.
+ * evaluating the samplers individually and combining the results afterwards.
*
- * @param otherConsistentSampler the other consistent sampler
- * @return the composed consistent sampler
+ * @param delegates the delegate samplers, at least one delegate must be specified
+ * @return the ConsistentAnyOf sampler
*/
- public ConsistentSampler or(ConsistentSampler otherConsistentSampler) {
- if (otherConsistentSampler == this) {
- return this;
- }
- return new ConsistentComposedOrSampler(this, otherConsistentSampler);
+ public static ConsistentSampler anyOf(ComposableSampler... delegates) {
+ return new ConsistentAnyOf(delegates);
}
@Override
@@ -146,55 +171,35 @@ public final SamplingResult shouldSample(
SpanKind spanKind,
Attributes attributes,
List parentLinks) {
-
Span parentSpan = Span.fromContext(parentContext);
SpanContext parentSpanContext = parentSpan.getSpanContext();
- boolean isRoot = !parentSpanContext.isValid();
- boolean isParentSampled = parentSpanContext.isSampled();
TraceState parentTraceState = parentSpanContext.getTraceState();
String otelTraceStateString = parentTraceState.get(OtelTraceState.TRACE_STATE_KEY);
OtelTraceState otelTraceState = OtelTraceState.parse(otelTraceStateString);
- long randomValue;
- if (otelTraceState.hasValidRandomValue()) {
- randomValue = otelTraceState.getRandomValue();
- } else {
- randomValue = OtelTraceState.parseHex(traceId, 18, 14, getInvalidRandomValue());
- }
-
- long parentThreshold;
- if (otelTraceState.hasValidThreshold()) {
- long threshold = otelTraceState.getThreshold();
- if ((randomValue >= threshold) == isParentSampled) { // test invariant
- parentThreshold = threshold;
- } else {
- parentThreshold = getInvalidThreshold();
- }
- } else {
- parentThreshold = getInvalidThreshold();
- }
-
- // determine new threshold that is used for the sampling decision
- long threshold = getThreshold(parentThreshold, isRoot);
+ SamplingIntent intent =
+ getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks);
+ long threshold = intent.getThreshold();
// determine sampling decision
boolean isSampled;
if (isValidThreshold(threshold)) {
- isSampled = (randomValue >= threshold);
- if (isSampled) {
- otelTraceState.setThreshold(threshold);
- } else {
- otelTraceState.invalidateThreshold();
- }
+ long randomness = getRandomness(otelTraceState, traceId);
+ isSampled = threshold <= randomness;
+ } else { // DROP
+ isSampled = false;
+ }
+
+ SamplingDecision samplingDecision;
+ if (isSampled) {
+ samplingDecision = SamplingDecision.RECORD_AND_SAMPLE;
+ otelTraceState.setThreshold(threshold);
} else {
- isSampled = isParentSampled;
+ samplingDecision = SamplingDecision.DROP;
otelTraceState.invalidateThreshold();
}
- SamplingDecision samplingDecision =
- isSampled ? SamplingDecision.RECORD_AND_SAMPLE : SamplingDecision.DROP;
-
String newOtTraceState = otelTraceState.serialize();
return new SamplingResult() {
@@ -206,31 +211,23 @@ public SamplingDecision getDecision() {
@Override
public Attributes getAttributes() {
- return Attributes.empty();
+ return intent.getAttributes();
}
@Override
public TraceState getUpdatedTraceState(TraceState parentTraceState) {
- return parentTraceState.toBuilder()
+ return intent.updateTraceState(parentTraceState).toBuilder()
.put(OtelTraceState.TRACE_STATE_KEY, newOtTraceState)
.build();
}
};
}
- /**
- * Returns the threshold that is used for the sampling decision.
- *
- * NOTE: In future, further information like span attributes could be also added as arguments
- * such that the sampling probability could be made dependent on those extra arguments. However,
- * in any case the returned threshold value must not depend directly or indirectly on the random
- * value. In particular this means that the parent sampled flag must not be used for the
- * calculation of the threshold as the sampled flag depends itself on the random value.
- *
- * @param parentThreshold is the threshold (if known) that was used for a consistent sampling
- * decision by the parent
- * @param isRoot is true for the root span
- * @return the threshold to be used for the sampling decision
- */
- protected abstract long getThreshold(long parentThreshold, boolean isRoot);
+ private static long getRandomness(OtelTraceState otelTraceState, String traceId) {
+ if (otelTraceState.hasValidRandomValue()) {
+ return otelTraceState.getRandomValue();
+ } else {
+ return OtelTraceState.parseHex(traceId, 18, 14, getInvalidRandomValue());
+ }
+ }
}
diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/Predicate.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/Predicate.java
new file mode 100644
index 000000000..56ce59c46
--- /dev/null
+++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/Predicate.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.contrib.sampler.consistent56;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanContext;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.trace.data.LinkData;
+import java.util.List;
+
+/** Interface for logical expression that can be matched against Spans to be sampled */
+@FunctionalInterface
+public interface Predicate {
+
+ /*
+ * Return true if the Span context described by the provided arguments matches the predicate
+ */
+ boolean spanMatches(
+ Context parentContext,
+ String name,
+ SpanKind spanKind,
+ Attributes attributes,
+ List parentLinks);
+
+ /*
+ * Return a Predicate that will match ROOT Spans only
+ */
+ static Predicate isRootSpan() {
+ return (parentContext, name, spanKind, attributes, parentLinks) -> {
+ Span parentSpan = Span.fromContext(parentContext);
+ SpanContext parentSpanContext = parentSpan.getSpanContext();
+ return !parentSpanContext.isValid();
+ };
+ }
+
+ /*
+ * Return a Predicate that matches all Spans
+ */
+ static Predicate anySpan() {
+ return (parentContext, name, spanKind, attributes, parentLinks) -> true;
+ }
+
+ /*
+ * Return a Predicate that represents logical AND of the argument predicates
+ */
+ static Predicate and(Predicate p1, Predicate p2) {
+ return (parentContext, name, spanKind, attributes, parentLinks) ->
+ p1.spanMatches(parentContext, name, spanKind, attributes, parentLinks)
+ && p2.spanMatches(parentContext, name, spanKind, attributes, parentLinks);
+ }
+
+ /*
+ * Return a Predicate that represents logical negation of the argument predicate
+ */
+ static Predicate not(Predicate p) {
+ return (parentContext, name, spanKind, attributes, parentLinks) ->
+ !p.spanMatches(parentContext, name, spanKind, attributes, parentLinks);
+ }
+
+ /*
+ * Return a Predicate that represents logical OR of the argument predicates
+ */
+ static Predicate or(Predicate p1, Predicate p2) {
+ return (parentContext, name, spanKind, attributes, parentLinks) ->
+ p1.spanMatches(parentContext, name, spanKind, attributes, parentLinks)
+ || p2.spanMatches(parentContext, name, spanKind, attributes, parentLinks);
+ }
+}
diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/PredicatedSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/PredicatedSampler.java
new file mode 100644
index 000000000..dabec5b74
--- /dev/null
+++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/PredicatedSampler.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.contrib.sampler.consistent56;
+
+import static java.util.Objects.requireNonNull;
+
+/** A class for holding a pair (Predicate, ComposableSampler) */
+public final class PredicatedSampler {
+
+ public static PredicatedSampler onMatch(Predicate predicate, ComposableSampler sampler) {
+ return new PredicatedSampler(predicate, sampler);
+ }
+
+ private final Predicate predicate;
+ private final ComposableSampler sampler;
+
+ private PredicatedSampler(Predicate predicate, ComposableSampler sampler) {
+ this.predicate = requireNonNull(predicate);
+ this.sampler = requireNonNull(sampler);
+ }
+
+ public Predicate getPredicate() {
+ return predicate;
+ }
+
+ public ComposableSampler getSampler() {
+ return sampler;
+ }
+}
diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/SamplingIntent.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/SamplingIntent.java
new file mode 100644
index 000000000..07906ad3b
--- /dev/null
+++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/SamplingIntent.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.contrib.sampler.consistent56;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.TraceState;
+
+/** Interface for declaring sampling intent by Composable Samplers. */
+@SuppressWarnings("CanIgnoreReturnValueSuggester")
+public interface SamplingIntent {
+
+ /**
+ * Returns the suggested rejection threshold value. The returned value must be either from the
+ * interval [0, 2^56) or be equal to ConsistentSamplingUtil.getInvalidThreshold().
+ *
+ * @return a threshold value
+ */
+ long getThreshold();
+
+ /**
+ * Returns a set of Attributes to be added to the Span in case of positive sampling decision.
+ *
+ * @return Attributes
+ */
+ default Attributes getAttributes() {
+ return Attributes.empty();
+ }
+
+ /**
+ * Given an input Tracestate and sampling Decision provide a Tracestate to be associated with the
+ * Span.
+ *
+ * @param parentState the TraceState of the parent Span
+ * @return a TraceState
+ */
+ default TraceState updateTraceState(TraceState parentState) {
+ return parentState;
+ }
+}
diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/CoinFlipSampler.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/CoinFlipSampler.java
new file mode 100644
index 000000000..a3999e954
--- /dev/null
+++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/CoinFlipSampler.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.contrib.sampler.consistent56;
+
+import static java.util.Objects.requireNonNull;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.trace.data.LinkData;
+import java.util.List;
+import java.util.SplittableRandom;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A consistent sampler that delegates the decision randomly, with a predefined probability, to one
+ * of its two delegates. Used by unit tests.
+ */
+@Immutable
+final class CoinFlipSampler extends ConsistentSampler {
+
+ private static final SplittableRandom random = new SplittableRandom(0x160a50a2073e17e6L);
+
+ private final ComposableSampler samplerA;
+ private final ComposableSampler samplerB;
+ private final double probability;
+ private final String description;
+
+ /**
+ * Constructs a new consistent CoinFlipSampler using the given two delegates with equal
+ * probability.
+ *
+ * @param samplerA the first delegate sampler
+ * @param samplerB the second delegate sampler
+ */
+ CoinFlipSampler(ComposableSampler samplerA, ComposableSampler samplerB) {
+ this(samplerA, samplerB, 0.5);
+ }
+
+ /**
+ * Constructs a new consistent CoinFlipSampler using the given two delegates, and the probability
+ * to use the first one.
+ *
+ * @param probability the probability to use the first sampler
+ * @param samplerA the first delegate sampler
+ * @param samplerB the second delegate sampler
+ */
+ CoinFlipSampler(ComposableSampler samplerA, ComposableSampler samplerB, double probability) {
+ this.samplerA = requireNonNull(samplerA);
+ this.samplerB = requireNonNull(samplerB);
+ this.probability = probability;
+ this.description =
+ "CoinFlipSampler{p="
+ + (float) probability
+ + ",samplerA="
+ + samplerA.getDescription()
+ + ','
+ + "samplerB="
+ + samplerB.getDescription()
+ + '}';
+ }
+
+ @Override
+ public SamplingIntent getSamplingIntent(
+ Context parentContext,
+ String name,
+ SpanKind spanKind,
+ Attributes attributes,
+ List parentLinks) {
+
+ if (random.nextDouble() < probability) {
+ return samplerA.getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks);
+ } else {
+ return samplerB.getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks);
+ }
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+}
diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSamplerTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSamplerTest.java
index 04d266ac2..9b5fc050b 100644
--- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSamplerTest.java
+++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSamplerTest.java
@@ -6,7 +6,6 @@
package io.opentelemetry.contrib.sampler.consistent56;
import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold;
-import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMaxThreshold;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
@@ -21,15 +20,10 @@ void testDescription() {
@Test
void testThreshold() {
- assertThat(ConsistentSampler.alwaysOff().getThreshold(getInvalidThreshold(), false))
- .isEqualTo(getMaxThreshold());
- assertThat(ConsistentSampler.alwaysOff().getThreshold(getInvalidThreshold(), true))
- .isEqualTo(getMaxThreshold());
- assertThat(ConsistentSampler.alwaysOff().getThreshold(getMaxThreshold(), false))
- .isEqualTo(getMaxThreshold());
- assertThat(ConsistentSampler.alwaysOff().getThreshold(getMaxThreshold(), true))
- .isEqualTo(getMaxThreshold());
- assertThat(ConsistentSampler.alwaysOff().getThreshold(0, false)).isEqualTo(getMaxThreshold());
- assertThat(ConsistentSampler.alwaysOff().getThreshold(0, true)).isEqualTo(getMaxThreshold());
+ assertThat(
+ ConsistentSampler.alwaysOff()
+ .getSamplingIntent(null, "span_name", null, null, null)
+ .getThreshold())
+ .isEqualTo(getInvalidThreshold());
}
}
diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSamplerTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSamplerTest.java
index 6df53066b..3a6b8531b 100644
--- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSamplerTest.java
+++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSamplerTest.java
@@ -5,7 +5,6 @@
package io.opentelemetry.contrib.sampler.consistent56;
-import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold;
import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMinThreshold;
import static org.assertj.core.api.Assertions.assertThat;
@@ -21,15 +20,10 @@ void testDescription() {
@Test
void testThreshold() {
- assertThat(ConsistentSampler.alwaysOn().getThreshold(getInvalidThreshold(), false))
+ assertThat(
+ ConsistentSampler.alwaysOn()
+ .getSamplingIntent(null, "span_name", null, null, null)
+ .getThreshold())
.isEqualTo(getMinThreshold());
- assertThat(ConsistentSampler.alwaysOn().getThreshold(getInvalidThreshold(), true))
- .isEqualTo(getMinThreshold());
- assertThat(ConsistentSampler.alwaysOn().getThreshold(getMinThreshold(), false))
- .isEqualTo(getMinThreshold());
- assertThat(ConsistentSampler.alwaysOn().getThreshold(getMinThreshold(), true))
- .isEqualTo(getMinThreshold());
- assertThat(ConsistentSampler.alwaysOn().getThreshold(0, false)).isEqualTo(getMinThreshold());
- assertThat(ConsistentSampler.alwaysOn().getThreshold(0, true)).isEqualTo(getMinThreshold());
}
}
diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOfTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOfTest.java
new file mode 100644
index 000000000..720a675e6
--- /dev/null
+++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOfTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.contrib.sampler.consistent56;
+
+import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.api.common.AttributeKey;
+import org.junit.jupiter.api.Test;
+
+class ConsistentAnyOfTest {
+
+ @Test
+ void testMinimumThreshold() {
+ ComposableSampler delegate1 = new ConsistentFixedThresholdSampler(0x80000000000000L);
+ ComposableSampler delegate2 = new ConsistentFixedThresholdSampler(0x30000000000000L);
+ ComposableSampler delegate3 = new ConsistentFixedThresholdSampler(0xa0000000000000L);
+ ComposableSampler sampler = ConsistentSampler.anyOf(delegate1, delegate2, delegate3);
+ SamplingIntent intent = sampler.getSamplingIntent(null, "span_name", null, null, null);
+ assertThat(intent.getThreshold()).isEqualTo(0x30000000000000L);
+ }
+
+ @Test
+ void testAlwaysDrop() {
+ ComposableSampler delegate1 = ConsistentSampler.alwaysOff();
+ ComposableSampler sampler = ConsistentSampler.anyOf(delegate1);
+ SamplingIntent intent = sampler.getSamplingIntent(null, "span_name", null, null, null);
+ assertThat(intent.getThreshold()).isEqualTo(getInvalidThreshold());
+ }
+
+ @Test
+ void testSpanAttributesAdded() {
+ AttributeKey key1 = AttributeKey.stringKey("tag1");
+ AttributeKey key2 = AttributeKey.stringKey("tag2");
+ AttributeKey key3 = AttributeKey.stringKey("tag3");
+ ComposableSampler delegate1 =
+ new MarkingSampler(new ConsistentFixedThresholdSampler(0x30000000000000L), key1, "a");
+ ComposableSampler delegate2 =
+ new MarkingSampler(new ConsistentFixedThresholdSampler(0x50000000000000L), key2, "b");
+ ComposableSampler delegate3 = new MarkingSampler(ConsistentSampler.alwaysOff(), key3, "c");
+ ComposableSampler sampler = ConsistentSampler.anyOf(delegate1, delegate2, delegate3);
+ SamplingIntent intent = sampler.getSamplingIntent(null, "span_name", null, null, null);
+ assertThat(intent.getAttributes().get(key1)).isEqualTo("a");
+ assertThat(intent.getAttributes().get(key2)).isEqualTo("b");
+ assertThat(intent.getAttributes().get(key3)).isEqualTo("c");
+ assertThat(intent.getThreshold()).isEqualTo(0x30000000000000L);
+ }
+
+ @Test
+ void testSpanAttributeOverride() {
+ AttributeKey key1 = AttributeKey.stringKey("shared");
+ ComposableSampler delegate1 =
+ new MarkingSampler(new ConsistentFixedThresholdSampler(0x30000000000000L), key1, "a");
+ ComposableSampler delegate2 =
+ new MarkingSampler(new ConsistentFixedThresholdSampler(0x50000000000000L), key1, "b");
+ ComposableSampler sampler = ConsistentSampler.anyOf(delegate1, delegate2);
+ SamplingIntent intent = sampler.getSamplingIntent(null, "span_name", null, null, null);
+ assertThat(intent.getAttributes().get(key1)).isEqualTo("b");
+ }
+}
diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSamplerTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSamplerTest.java
index 17daaaf6b..79bf064a0 100644
--- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSamplerTest.java
+++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSamplerTest.java
@@ -8,6 +8,7 @@
import static io.opentelemetry.contrib.sampler.consistent56.TestUtil.generateRandomTraceId;
import static org.assertj.core.api.Assertions.assertThat;
+import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;
@@ -28,6 +29,7 @@ class ConsistentRateLimitingSamplerTest {
private long[] nanoTime;
private LongSupplier nanoTimeSupplier;
+ private LongSupplier lowResolutionTimeSupplier;
private Context parentContext;
private String name;
private SpanKind spanKind;
@@ -39,6 +41,7 @@ class ConsistentRateLimitingSamplerTest {
void init() {
nanoTime = new long[] {0L};
nanoTimeSupplier = () -> nanoTime[0];
+ lowResolutionTimeSupplier = () -> (nanoTime[0] / 1000000) * 1000000; // 1ms resolution
parentContext = Context.root();
name = "name";
spanKind = SpanKind.SERVER;
@@ -61,9 +64,52 @@ void testConstantRate() {
double targetSpansPerSecondLimit = 1000;
double adaptationTimeSeconds = 5;
+ ComposableSampler delegate =
+ new CoinFlipSampler(ConsistentSampler.alwaysOff(), ConsistentSampler.probabilityBased(0.8));
ConsistentSampler sampler =
ConsistentSampler.rateLimited(
- targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier);
+ delegate, targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier);
+
+ long nanosBetweenSpans = TimeUnit.MICROSECONDS.toNanos(100);
+ int numSpans = 1000000;
+
+ List spanSampledNanos = new ArrayList<>();
+
+ for (int i = 0; i < numSpans; ++i) {
+ advanceTime(nanosBetweenSpans);
+ SamplingResult samplingResult =
+ sampler.shouldSample(
+ parentContext,
+ generateRandomTraceId(random),
+ name,
+ spanKind,
+ attributes,
+ parentLinks);
+ if (SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult.getDecision())) {
+ spanSampledNanos.add(getCurrentTimeNanos());
+ }
+ }
+
+ long numSampledSpansInLast5Seconds =
+ spanSampledNanos.stream()
+ .filter(x -> x > TimeUnit.SECONDS.toNanos(95) && x <= TimeUnit.SECONDS.toNanos(100))
+ .count();
+
+ assertThat(numSampledSpansInLast5Seconds / 5.)
+ .isCloseTo(targetSpansPerSecondLimit, Percentage.withPercentage(5));
+ }
+
+ @Test
+ void testConstantRateLowResolution() {
+
+ double targetSpansPerSecondLimit = 1000;
+ double adaptationTimeSeconds = 5;
+
+ ComposableSampler delegate =
+ new CoinFlipSampler(ConsistentSampler.alwaysOff(), ConsistentSampler.probabilityBased(0.8));
+ ConsistentSampler sampler =
+ ConsistentSampler.rateLimited(
+ delegate, targetSpansPerSecondLimit, adaptationTimeSeconds, lowResolutionTimeSupplier);
long nanosBetweenSpans = TimeUnit.MICROSECONDS.toNanos(100);
int numSpans = 1000000;
@@ -228,6 +274,89 @@ void testRateDecrease() {
.isCloseTo(targetSpansPerSecondLimit, Percentage.withPercentage(5));
}
+ /**
+ * Generate a random number representing time elapsed between two simulated (root) spans.
+ *
+ * @param averageSpanRatePerSecond number of simulated spans for each simulated second
+ * @return the time in nanos to be used by the simulator
+ */
+ private long randomInterval(long averageSpanRatePerSecond) {
+ // For simulating real traffic, for example as coming from the Internet.
+ // Assuming Poisson distribution of incoming requests, averageNumberOfSpanPerSecond
+ // is the lambda parameter of the distribution. Consequently, the time between requests
+ // has Exponential distribution with the same lambda parameter.
+ double uniform = random.nextDouble();
+ double intervalInSeconds = -Math.log(uniform) / averageSpanRatePerSecond;
+ return (long) (intervalInSeconds * 1e9);
+ }
+
+ @Test
+ void testProportionalBehavior() {
+ // Based on example discussed at https://github.com/open-telemetry/oteps/pull/250
+ // Assume that there are 2 categories A and B of spans.
+ // Assume there are 10,000 spans/s and 50% belong to A and 50% belong to B.
+ // Now we want to sample A with a probability of 60% and B with a probability of 40%.
+ // That means we would sample 30,000 spans/s from A and 20,000 spans/s from B.
+ //
+ // However, if we do not want to sample more than 1000 spans/s overall, our expectation is
+ // that the ratio of the sampled A and B spans will still remain 3:2.
+
+ double targetSpansPerSecondLimit = 1000;
+ double adaptationTimeSeconds = 5;
+ AttributeKey key = AttributeKey.stringKey("category");
+
+ ComposableSampler delegate =
+ new CoinFlipSampler(
+ new MarkingSampler(ConsistentSampler.probabilityBased(0.6), key, "A"),
+ new MarkingSampler(ConsistentSampler.probabilityBased(0.4), key, "B"));
+
+ ConsistentSampler sampler =
+ ConsistentSampler.rateLimited(
+ delegate, targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier);
+
+ long averageRequestRatePerSecond = 10000;
+ int numSpans = 1000000;
+
+ List spanSampledNanos = new ArrayList<>();
+ int catAsampledCount = 0;
+ int catBsampledCount = 0;
+
+ for (int i = 0; i < numSpans; ++i) {
+ advanceTime(randomInterval(averageRequestRatePerSecond));
+ SamplingResult samplingResult =
+ sampler.shouldSample(
+ parentContext,
+ generateRandomTraceId(random),
+ name,
+ spanKind,
+ attributes,
+ parentLinks);
+ if (SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult.getDecision())) {
+ spanSampledNanos.add(getCurrentTimeNanos());
+
+ // ConsistentRateLimiting sampler is expected to provide proportional sampling
+ // at all times, no need to skip the warm-up phase
+ String category = samplingResult.getAttributes().get(key);
+ if ("A".equals(category)) {
+ catAsampledCount++;
+ } else if ("B".equals(category)) {
+ catBsampledCount++;
+ }
+ }
+ }
+
+ double expectedRatio = 0.6 / 0.4;
+ assertThat(catAsampledCount / (double) catBsampledCount)
+ .isCloseTo(expectedRatio, Percentage.withPercentage(2));
+
+ long timeNow = nanoTime[0];
+ long numSampledSpansInLast5Seconds =
+ spanSampledNanos.stream().filter(x -> x > timeNow - 5000000000L && x <= timeNow).count();
+
+ assertThat(numSampledSpansInLast5Seconds / 5.)
+ .isCloseTo(targetSpansPerSecondLimit, Percentage.withPercentage(5));
+ }
+
@Test
void testDescription() {
diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSamplerTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSamplerTest.java
new file mode 100644
index 000000000..bf79de0c5
--- /dev/null
+++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSamplerTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.contrib.sampler.consistent56;
+
+import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.trace.SpanKind;
+import org.junit.jupiter.api.Test;
+
+class ConsistentRuleBasedSamplerTest {
+
+ @Test
+ void testEmptySet() {
+ ComposableSampler sampler = ConsistentSampler.ruleBased(SpanKind.SERVER);
+ SamplingIntent intent =
+ sampler.getSamplingIntent(null, "span_name", SpanKind.SERVER, null, null);
+ assertThat(intent.getThreshold()).isEqualTo(getInvalidThreshold());
+ }
+
+ private static Predicate matchSpanName(String nameToMatch) {
+ return (parentContext, name, spanKind, attributes, parentLinks) -> {
+ return nameToMatch.equals(name);
+ };
+ }
+
+ @Test
+ void testChoice() {
+ // Testing the correct choice by checking both the returned threshold and the marking attribute
+
+ AttributeKey key1 = AttributeKey.stringKey("tag1");
+ AttributeKey key2 = AttributeKey.stringKey("tag2");
+ AttributeKey key3 = AttributeKey.stringKey("tag3");
+
+ ComposableSampler delegate1 =
+ new MarkingSampler(new ConsistentFixedThresholdSampler(0x80000000000000L), key1, "a");
+ ComposableSampler delegate2 =
+ new MarkingSampler(new ConsistentFixedThresholdSampler(0x50000000000000L), key2, "b");
+ ComposableSampler delegate3 =
+ new MarkingSampler(new ConsistentFixedThresholdSampler(0x30000000000000L), key3, "c");
+
+ ComposableSampler sampler =
+ ConsistentSampler.ruleBased(
+ null,
+ PredicatedSampler.onMatch(matchSpanName("A"), delegate1),
+ PredicatedSampler.onMatch(matchSpanName("B"), delegate2),
+ PredicatedSampler.onMatch(matchSpanName("C"), delegate3));
+
+ SamplingIntent intent;
+
+ intent = sampler.getSamplingIntent(null, "A", SpanKind.CLIENT, null, null);
+ assertThat(intent.getThreshold()).isEqualTo(0x80000000000000L);
+ assertThat(intent.getAttributes().get(key1)).isEqualTo("a");
+ assertThat(intent.getAttributes().get(key2)).isEqualTo(null);
+ assertThat(intent.getAttributes().get(key3)).isEqualTo(null);
+
+ intent = sampler.getSamplingIntent(null, "B", SpanKind.PRODUCER, null, null);
+ assertThat(intent.getThreshold()).isEqualTo(0x50000000000000L);
+ assertThat(intent.getAttributes().get(key1)).isEqualTo(null);
+ assertThat(intent.getAttributes().get(key2)).isEqualTo("b");
+ assertThat(intent.getAttributes().get(key3)).isEqualTo(null);
+
+ intent = sampler.getSamplingIntent(null, "C", SpanKind.SERVER, null, null);
+ assertThat(intent.getThreshold()).isEqualTo(0x30000000000000L);
+ assertThat(intent.getAttributes().get(key1)).isEqualTo(null);
+ assertThat(intent.getAttributes().get(key2)).isEqualTo(null);
+ assertThat(intent.getAttributes().get(key3)).isEqualTo("c");
+
+ intent = sampler.getSamplingIntent(null, "D", null, null, null);
+ assertThat(intent.getThreshold()).isEqualTo(getInvalidThreshold());
+ assertThat(intent.getAttributes().get(key1)).isEqualTo(null);
+ assertThat(intent.getAttributes().get(key2)).isEqualTo(null);
+ assertThat(intent.getAttributes().get(key3)).isEqualTo(null);
+ }
+
+ @Test
+ void testSpanKindMatch() {
+ ComposableSampler sampler =
+ ConsistentSampler.ruleBased(
+ SpanKind.CLIENT,
+ PredicatedSampler.onMatch(Predicate.anySpan(), ConsistentSampler.alwaysOn()));
+
+ SamplingIntent intent;
+
+ intent = sampler.getSamplingIntent(null, "span name", SpanKind.CONSUMER, null, null);
+ assertThat(intent.getThreshold()).isEqualTo(getInvalidThreshold());
+
+ intent = sampler.getSamplingIntent(null, "span name", SpanKind.CLIENT, null, null);
+ assertThat(intent.getThreshold()).isEqualTo(0);
+ }
+}
diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/MarkingSampler.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/MarkingSampler.java
new file mode 100644
index 000000000..687cd532a
--- /dev/null
+++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/MarkingSampler.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.contrib.sampler.consistent56;
+
+import static java.util.Objects.requireNonNull;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.TraceState;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.trace.data.LinkData;
+import java.util.List;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A Composable that creates the same sampling intent as the delegate, but it additionally sets a
+ * Span attribute according to the provided attribute key and value. This is used by unit tests, but
+ * could be also offered as a general utility.
+ */
+@Immutable
+final class MarkingSampler implements ComposableSampler {
+
+ private final ComposableSampler delegate;
+ private final AttributeKey attributeKey;
+ private final String attributeValue;
+
+ private final String description;
+
+ /**
+ * Constructs a new MarkingSampler
+ *
+ * @param delegate the delegate sampler
+ * @param attributeKey Span attribute key
+ * @param attributeValue Span attribute value
+ */
+ MarkingSampler(
+ ComposableSampler delegate, AttributeKey attributeKey, String attributeValue) {
+ this.delegate = requireNonNull(delegate);
+ this.attributeKey = requireNonNull(attributeKey);
+ this.attributeValue = requireNonNull(attributeValue);
+ this.description =
+ "MarkingSampler{delegate="
+ + delegate.getDescription()
+ + ",key="
+ + attributeKey
+ + ",value="
+ + attributeValue
+ + '}';
+ }
+
+ @Override
+ public SamplingIntent getSamplingIntent(
+ Context parentContext,
+ String name,
+ SpanKind spanKind,
+ Attributes attributes,
+ List parentLinks) {
+
+ SamplingIntent delegateIntent =
+ delegate.getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks);
+
+ return new SamplingIntent() {
+ @Override
+ public long getThreshold() {
+ return delegateIntent.getThreshold();
+ }
+
+ @Override
+ public Attributes getAttributes() {
+ AttributesBuilder builder = delegateIntent.getAttributes().toBuilder();
+ builder = builder.put(attributeKey, attributeValue);
+ return builder.build();
+ }
+
+ @Override
+ public TraceState updateTraceState(TraceState previousState) {
+ return delegateIntent.updateTraceState(previousState);
+ }
+ };
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+}
diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/UseCaseTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/UseCaseTest.java
new file mode 100644
index 000000000..e164a66fb
--- /dev/null
+++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/UseCaseTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.contrib.sampler.consistent56;
+
+import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSampler.alwaysOff;
+import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSampler.alwaysOn;
+import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold;
+import static io.opentelemetry.contrib.sampler.consistent56.Predicate.anySpan;
+import static io.opentelemetry.contrib.sampler.consistent56.Predicate.isRootSpan;
+import static io.opentelemetry.contrib.sampler.consistent56.PredicatedSampler.onMatch;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.SpanKind;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Testing a "real life" sampler configuration, as provided as an example in
+ * https://github.com/open-telemetry/oteps/pull/250. The example uses many different composite
+ * samplers combining them together to demonstrate the expressiveness and flexibility of the
+ * proposed specification.
+ */
+class UseCaseTest {
+ private static final long[] nanoTime = new long[] {0L};
+
+ private static final long nanoTime() {
+ return nanoTime[0];
+ }
+
+ private static void advanceTime(long nanosIncrement) {
+ nanoTime[0] += nanosIncrement;
+ }
+
+ //
+ // S = ConsistentRateLimiting(
+ // ConsistentAnyOf(
+ // ConsistentParentBased(
+ // ConsistentRuleBased(ROOT, {
+ // (http.target == /healthcheck) => ConsistentAlwaysOff,
+ // (http.target == /checkout) => ConsistentAlwaysOn,
+ // true => ConsistentFixedThreshold(0.25)
+ // }),
+ // ConsistentRuleBased(CLIENT, {
+ // (http.url == /foo) => ConsistentAlwaysOn
+ // }
+ // ),
+ // 1000.0
+ // )
+ //
+ private static final AttributeKey httpTarget = AttributeKey.stringKey("http.target");
+ private static final AttributeKey httpUrl = AttributeKey.stringKey("http.url");
+
+ private static ConsistentSampler buildSampler() {
+ Predicate healthCheck =
+ Predicate.and(
+ isRootSpan(),
+ (parentContext, name, spanKind, attributes, parentLinks) -> {
+ return "/healthCheck".equals(attributes.get(httpTarget));
+ });
+ Predicate checkout =
+ Predicate.and(
+ isRootSpan(),
+ (parentContext, name, spanKind, attributes, parentLinks) -> {
+ return "/checkout".equals(attributes.get(httpTarget));
+ });
+ ComposableSampler s1 =
+ ConsistentSampler.parentBased(
+ ConsistentSampler.ruleBased(
+ null,
+ onMatch(healthCheck, alwaysOff()),
+ onMatch(checkout, alwaysOn()),
+ onMatch(anySpan(), ConsistentSampler.probabilityBased(0.25))));
+ Predicate foo =
+ (parentContext, name, spanKind, attributes, parentLinks) -> {
+ return "/foo".equals(attributes.get(httpUrl));
+ };
+
+ ComposableSampler s2 = ConsistentSampler.ruleBased(SpanKind.CLIENT, onMatch(foo, alwaysOn()));
+ ComposableSampler s3 = ConsistentSampler.anyOf(s1, s2);
+ return ConsistentSampler.rateLimited(s3, 1000.0, 5, UseCaseTest::nanoTime);
+ }
+
+ @Test
+ void testDropHealthcheck() {
+ ConsistentSampler s = buildSampler();
+ Attributes attributes = createAttributes(httpTarget, "/healthCheck");
+ SamplingIntent intent = s.getSamplingIntent(null, "A", SpanKind.SERVER, attributes, null);
+ assertThat(intent.getThreshold()).isEqualTo(getInvalidThreshold());
+ }
+
+ @Test
+ void testSampleCheckout() {
+ ConsistentSampler s = buildSampler();
+ advanceTime(1000000);
+ Attributes attributes = createAttributes(httpTarget, "/checkout");
+ SamplingIntent intent = s.getSamplingIntent(null, "B", SpanKind.SERVER, attributes, null);
+ assertThat(intent.getThreshold()).isEqualTo(0L);
+ advanceTime(1000); // rate limiting should kick in
+ intent = s.getSamplingIntent(null, "B", SpanKind.SERVER, attributes, null);
+ assertThat(intent.getThreshold()).isGreaterThan(0L);
+ }
+
+ @Test
+ void testSampleClient() {
+ ConsistentSampler s = buildSampler();
+ advanceTime(1000000);
+ Attributes attributes = createAttributes(httpUrl, "/foo");
+ SamplingIntent intent = s.getSamplingIntent(null, "C", SpanKind.CLIENT, attributes, null);
+ assertThat(intent.getThreshold()).isEqualTo(0L);
+ }
+
+ @Test
+ void testOtherRoot() {
+ ConsistentSampler s = buildSampler();
+ advanceTime(1000000);
+ Attributes attributes = Attributes.empty();
+ SamplingIntent intent = s.getSamplingIntent(null, "D", SpanKind.SERVER, attributes, null);
+ assertThat(intent.getThreshold()).isEqualTo(0xc0000000000000L);
+ }
+
+ private static Attributes createAttributes(AttributeKey key, String value) {
+ return Attributes.builder().put(key, value).build();
+ }
+}