From e46643159a573ce00f40a9064a89abeb5166fad4 Mon Sep 17 00:00:00 2001 From: Otmar Ertl Date: Wed, 18 Oct 2023 15:37:55 +0200 Subject: [PATCH] consistent sampler prototypes using 56 bits of randomness (#1063) --- consistent-sampling/build.gradle.kts | 2 +- .../ConsistentAlwaysOffSampler.java | 26 ++ .../ConsistentAlwaysOnSampler.java | 28 ++ .../ConsistentComposedAndSampler.java | 56 +++ .../ConsistentComposedOrSampler.java | 61 ++++ .../ConsistentFixedThresholdSampler.java | 49 +++ .../ConsistentParentBasedSampler.java | 51 +++ .../ConsistentRateLimitingSampler.java | 160 +++++++++ .../consistent56/ConsistentSampler.java | 319 ++++++++++++++++++ .../consistent56/ConsistentSamplingUtil.java | 147 ++++++++ .../sampler/consistent56/OtelTraceState.java | 256 ++++++++++++++ .../consistent56/RandomValueGenerator.java | 24 ++ .../consistent56/RandomValueGenerators.java | 25 ++ .../ConsistentAlwaysOffSamplerTest.java | 31 ++ .../ConsistentAlwaysOnSamplerTest.java | 35 ++ .../ConsistentFixedThresholdSamplerTest.java | 114 +++++++ .../ConsistentRateLimitingSamplerTest.java | 231 +++++++++++++ .../consistent56/ConsistentSamplerTest.java | 261 ++++++++++++++ .../ConsistentSamplingUtilTest.java | 114 +++++++ .../consistent56/OtelTraceStateTest.java | 75 ++++ .../RandomValueGeneratorsTest.java | 22 ++ 21 files changed, 2086 insertions(+), 1 deletion(-) create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSampler.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSampler.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedAndSampler.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedOrSampler.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSampler.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentParentBasedSampler.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSampler.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSampler.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplingUtil.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/OtelTraceState.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGenerator.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGenerators.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSamplerTest.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSamplerTest.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSamplerTest.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSamplerTest.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplerTest.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplingUtilTest.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/OtelTraceStateTest.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGeneratorsTest.java diff --git a/consistent-sampling/build.gradle.kts b/consistent-sampling/build.gradle.kts index b6700f1ff..06b330c2f 100644 --- a/consistent-sampling/build.gradle.kts +++ b/consistent-sampling/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } description = "Sampler and exporter implementations for consistent sampling" -otelJava.moduleName.set("io.opentelemetry.contrib.sampler.consistent") +otelJava.moduleName.set("io.opentelemetry.contrib.sampler") dependencies { api("io.opentelemetry:opentelemetry-sdk-trace") 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 new file mode 100644 index 000000000..2b95c19ce --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSampler.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import javax.annotation.concurrent.Immutable; + +@Immutable +final class ConsistentAlwaysOffSampler extends ConsistentSampler { + + ConsistentAlwaysOffSampler(RandomValueGenerator randomValueGenerator) { + super(randomValueGenerator); + } + + @Override + protected long getThreshold(long parentThreshold, boolean isRoot) { + return 0; + } + + @Override + public String getDescription() { + return "ConsistentAlwaysOffSampler"; + } +} 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 new file mode 100644 index 000000000..93db59560 --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSampler.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMaxThreshold; + +import javax.annotation.concurrent.Immutable; + +@Immutable +final class ConsistentAlwaysOnSampler extends ConsistentSampler { + + ConsistentAlwaysOnSampler(RandomValueGenerator randomValueGenerator) { + super(randomValueGenerator); + } + + @Override + protected long getThreshold(long parentThreshold, boolean isRoot) { + return getMaxThreshold(); + } + + @Override + public String getDescription() { + return "ConsistentAlwaysOnSampler"; + } +} 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 new file mode 100644 index 000000000..de22a7056 --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedAndSampler.java @@ -0,0 +1,56 @@ +/* + * 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, + RandomValueGenerator randomValueGenerator) { + super(randomValueGenerator); + 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.min(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 new file mode 100644 index 000000000..542cc65bc --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedOrSampler.java @@ -0,0 +1,61 @@ +/* + * 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, + RandomValueGenerator randomValueGenerator) { + super(randomValueGenerator); + 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.max(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 new file mode 100644 index 000000000..dd160f387 --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSampler.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.calculateSamplingProbability; +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.checkThreshold; + +public class ConsistentFixedThresholdSampler extends ConsistentSampler { + + private final long threshold; + private final String description; + + protected ConsistentFixedThresholdSampler( + long threshold, RandomValueGenerator randomValueGenerator) { + super(randomValueGenerator); + checkThreshold(threshold); + this.threshold = threshold; + + String thresholdString; + if (threshold == ConsistentSamplingUtil.getMaxThreshold()) { + thresholdString = "max"; + } else { + thresholdString = + ConsistentSamplingUtil.appendLast56BitHexEncodedWithoutTrailingZeros( + new StringBuilder(), threshold) + .toString(); + } + + this.description = + "ConsistentFixedThresholdSampler{threshold=" + + thresholdString + + ", sampling probability=" + + calculateSamplingProbability(threshold) + + "}"; + } + + @Override + public String getDescription() { + return description; + } + + @Override + protected long getThreshold(long parentThreshold, boolean isRoot) { + 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 new file mode 100644 index 000000000..9d40793d9 --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentParentBasedSampler.java @@ -0,0 +1,51 @@ +/* + * 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 that makes the same sampling decision as the parent. For root spans the + * sampling decision is delegated to the root sampler. + */ +@Immutable +final class ConsistentParentBasedSampler extends ConsistentSampler { + + private final ConsistentSampler rootSampler; + + private final String description; + + /** + * Constructs a new consistent parent based sampler using the given root sampler and the given + * thread-safe random generator. + * + * @param rootSampler the root sampler + * @param randomValueGenerator the function to use for generating the r-value + */ + ConsistentParentBasedSampler( + ConsistentSampler rootSampler, RandomValueGenerator randomValueGenerator) { + super(randomValueGenerator); + this.rootSampler = requireNonNull(rootSampler); + this.description = + "ConsistentParentBasedSampler{rootSampler=" + rootSampler.getDescription() + '}'; + } + + @Override + protected long getThreshold(long parentThreshold, boolean isRoot) { + if (isRoot) { + return rootSampler.getThreshold(ConsistentSamplingUtil.getInvalidThreshold(), isRoot); + } else { + return parentThreshold; + } + } + + @Override + public String getDescription() { + return description; + } +} 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 new file mode 100644 index 000000000..3e741df8d --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSampler.java @@ -0,0 +1,160 @@ +/* + * 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.sdk.trace.samplers.Sampler; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.LongSupplier; +import javax.annotation.concurrent.Immutable; + +/** + * This consistent {@link Sampler} adjusts the sampling probability dynamically to limit the rate of + * sampled spans. + * + *

This sampler uses exponential smoothing to estimate on irregular data (compare Wright, David + * J. "Forecasting data published at irregular time intervals using an extension of Holt's method." + * Management science 32.4 (1986): 499-510.) to estimate the average waiting time between spans + * which further allows to estimate the current rate of spans. In the paper, Eq. 2 defines the + * weighted average of a sequence of data + * + *

{@code ..., X(n-2), X(n-1), X(n)} + * + *

at irregular times + * + *

{@code ..., t(n-2), t(n-1), t(n)} + * + *

as + * + *

{@code E(X(n)) := A(n) * V(n)}. + * + *

{@code A(n)} and {@code V(n)} are computed recursively using Eq. 5 and Eq. 6 given by + * + *

{@code A(n) = b(n) * A(n-1) + X(n)} and {@code V(n) = V(n-1) / (b(n) + V(n-1))} + * + *

where + * + *

{@code b(n) := (1 - a)^(t(n) - t(n-1)) = exp((t(n) - t(n-1)) * ln(1 - a))}. + * + *

Introducing + * + *

{@code C(n) := 1 / V(n)} + * + *

the recursion can be rewritten as + * + *

{@code A(n) = b(n) * A(n-1) + X(n)} and {@code C(n) = b(n) * C(n-1) + 1}. + * + *

+ * + *

Since we want to estimate the average waiting time, our data is given by + * + *

{@code X(n) := t(n) - t(n-1)}. + * + *

+ * + *

The following correspondence is used for the implementation: + * + *

+ */ +final class ConsistentRateLimitingSampler extends ConsistentSampler { + + private static final double NANOS_IN_SECONDS = 1e-9; + + @Immutable + private static final class State { + private final double effectiveWindowCount; + private final double effectiveWindowNanos; + private final long lastNanoTime; + + public State(double effectiveWindowCount, double effectiveWindowNanos, long lastNanoTime) { + this.effectiveWindowCount = effectiveWindowCount; + this.effectiveWindowNanos = effectiveWindowNanos; + this.lastNanoTime = lastNanoTime; + } + } + + private final String description; + private final LongSupplier nanoTimeSupplier; + private final double inverseAdaptationTimeNanos; + private final double targetSpansPerNanosecondLimit; + private final AtomicReference state; + + /** + * Constructor. + * + * @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 randomValueGenerator the function to use for generating the r-value + * @param nanoTimeSupplier a supplier for the current nano time + */ + ConsistentRateLimitingSampler( + double targetSpansPerSecondLimit, + double adaptationTimeSeconds, + RandomValueGenerator randomValueGenerator, + LongSupplier nanoTimeSupplier) { + super(randomValueGenerator); + + if (targetSpansPerSecondLimit < 0.0) { + throw new IllegalArgumentException("Limit for sampled spans per second must be nonnegative!"); + } + if (adaptationTimeSeconds < 0.0) { + throw new IllegalArgumentException("Adaptation rate must be nonnegative!"); + } + this.description = + "ConsistentRateLimitingSampler{targetSpansPerSecondLimit=" + + targetSpansPerSecondLimit + + ", adaptationTimeSeconds=" + + adaptationTimeSeconds + + "}"; + this.nanoTimeSupplier = requireNonNull(nanoTimeSupplier); + + this.inverseAdaptationTimeNanos = NANOS_IN_SECONDS / adaptationTimeSeconds; + this.targetSpansPerNanosecondLimit = NANOS_IN_SECONDS * targetSpansPerSecondLimit; + + this.state = new AtomicReference<>(new State(0, 0, nanoTimeSupplier.getAsLong())); + } + + private State updateState(State oldState, long currentNanoTime) { + if (currentNanoTime <= oldState.lastNanoTime) { + return new State( + oldState.effectiveWindowCount + 1, oldState.effectiveWindowNanos, oldState.lastNanoTime); + } + 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); + } + + @Override + protected long getThreshold(long parentThreshold, boolean isRoot) { + long currentNanoTime = nanoTimeSupplier.getAsLong(); + State currentState = state.updateAndGet(s -> updateState(s, currentNanoTime)); + + double samplingProbability = + (currentState.effectiveWindowNanos * targetSpansPerNanosecondLimit) + / currentState.effectiveWindowCount; + + if (samplingProbability >= 1.) { + return ConsistentSamplingUtil.getMaxThreshold(); + } else { + return ConsistentSamplingUtil.calculateThreshold(samplingProbability); + } + } + + @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 new file mode 100644 index 000000000..005c22d90 --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSampler.java @@ -0,0 +1,319 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +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.getMaxThreshold; +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.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 io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.List; +import java.util.function.LongSupplier; + +/** Abstract base class for consistent samplers. */ +public abstract class ConsistentSampler implements Sampler { + + /** + * Returns a {@link ConsistentSampler} that samples all spans. + * + * @return a sampler + */ + public static ConsistentSampler alwaysOn() { + return alwaysOn(RandomValueGenerators.getDefault()); + } + + /** + * Returns a {@link ConsistentSampler} that samples all spans. + * + * @param randomValueGenerator the function to use for generating the random value + * @return a sampler + */ + public static ConsistentSampler alwaysOn(RandomValueGenerator randomValueGenerator) { + return new ConsistentAlwaysOnSampler(randomValueGenerator); + } + + /** + * Returns a {@link ConsistentSampler} that does not sample any span. + * + * @return a sampler + */ + public static ConsistentSampler alwaysOff() { + return alwaysOff(RandomValueGenerators.getDefault()); + } + + /** + * Returns a {@link ConsistentSampler} that does not sample any span. + * + * @param randomValueGenerator the function to use for generating the random value + * @return a sampler + */ + public static ConsistentSampler alwaysOff(RandomValueGenerator randomValueGenerator) { + return new ConsistentAlwaysOffSampler(randomValueGenerator); + } + + /** + * Returns a {@link ConsistentSampler} that samples each span with a fixed probability. + * + * @param samplingProbability the sampling probability + * @return a sampler + */ + public static ConsistentSampler probabilityBased(double samplingProbability) { + return probabilityBased(samplingProbability, RandomValueGenerators.getDefault()); + } + + /** + * Returns a {@link ConsistentSampler} that samples each span with a fixed probability. + * + * @param samplingProbability the sampling probability + * @param randomValueGenerator the function to use for generating the r-value + * @return a sampler + */ + public static ConsistentSampler probabilityBased( + double samplingProbability, RandomValueGenerator randomValueGenerator) { + long threshold = ConsistentSamplingUtil.calculateThreshold(samplingProbability); + return new ConsistentFixedThresholdSampler(threshold, randomValueGenerator); + } + + /** + * Returns a new {@link ConsistentSampler} that respects the sampling decision of the parent span + * or falls-back to the given sampler if it is a root span. + * + * @param rootSampler the root sampler + */ + public static ConsistentSampler parentBased(ConsistentSampler rootSampler) { + return parentBased(rootSampler, RandomValueGenerators.getDefault()); + } + + /** + * Returns a new {@link ConsistentSampler} that respects the sampling decision of the parent span + * or falls-back to the given sampler if it is a root span. + * + * @param rootSampler the root sampler + * @param randomValueGenerator the function to use for generating the random value + */ + public static ConsistentSampler parentBased( + ConsistentSampler rootSampler, RandomValueGenerator randomValueGenerator) { + return new ConsistentParentBasedSampler(rootSampler, randomValueGenerator); + } + + /** + * Returns a new {@link ConsistentSampler} that attempts to adjust the sampling probability + * dynamically to meet the target span rate. + * + * @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( + double targetSpansPerSecondLimit, double adaptationTimeSeconds) { + return rateLimited( + targetSpansPerSecondLimit, adaptationTimeSeconds, RandomValueGenerators.getDefault()); + } + + /** + * Returns a new {@link ConsistentSampler} that attempts to adjust the sampling probability + * dynamically to meet the target span rate. + * + * @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 randomValueGenerator the function to use for generating the random value + */ + public static ConsistentSampler rateLimited( + double targetSpansPerSecondLimit, + double adaptationTimeSeconds, + RandomValueGenerator randomValueGenerator) { + return rateLimited( + targetSpansPerSecondLimit, adaptationTimeSeconds, randomValueGenerator, System::nanoTime); + } + + /** + * Returns a new {@link ConsistentSampler} that attempts to adjust the sampling probability + * dynamically to meet the target span rate. + * + * @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 randomValueGenerator the function to use for generating the random value + * @param nanoTimeSupplier a supplier for the current nano time + */ + static ConsistentSampler rateLimited( + double targetSpansPerSecondLimit, + double adaptationTimeSeconds, + RandomValueGenerator randomValueGenerator, + LongSupplier nanoTimeSupplier) { + return new ConsistentRateLimitingSampler( + targetSpansPerSecondLimit, adaptationTimeSeconds, randomValueGenerator, 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. + * + * @param otherConsistentSampler the other consistent sampler + * @return the composed consistent sampler + */ + public ConsistentSampler and(ConsistentSampler otherConsistentSampler) { + if (otherConsistentSampler == this) { + return this; + } + return new ConsistentComposedAndSampler( + this, otherConsistentSampler, RandomValueGenerators.getDefault()); + } + + /** + * 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. + * + *

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. + * + * @param otherConsistentSampler the other consistent sampler + * @return the composed consistent sampler + */ + public ConsistentSampler or(ConsistentSampler otherConsistentSampler) { + if (otherConsistentSampler == this) { + return this; + } + return new ConsistentComposedOrSampler( + this, otherConsistentSampler, RandomValueGenerators.getDefault()); + } + + private final RandomValueGenerator randomValueGenerator; + + protected ConsistentSampler(RandomValueGenerator randomValueGenerator) { + this.randomValueGenerator = requireNonNull(randomValueGenerator); + } + + @Override + public final SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + + Span parentSpan = Span.fromContext(parentContext); + SpanContext parentSpanContext = parentSpan.getSpanContext(); + boolean isRoot = !parentSpanContext.isValid(); + boolean isParentSampled = parentSpanContext.isSampled(); + + boolean isRandomTraceIdFlagSet = false; // TODO in future get the random trace ID flag, compare + // https://www.w3.org/TR/trace-context-2/#random-trace-id-flag + + 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 if (isRandomTraceIdFlagSet) { + randomValue = OtelTraceState.parseHex(traceId, 18, 14, getInvalidRandomValue()); + } else { + randomValue = randomValueGenerator.generate(traceId); + otelTraceState.invalidateThreshold(); + otelTraceState.setRandomValue(randomValue); + } + + long parentThreshold; + if (otelTraceState.hasValidThreshold()) { + long threshold = otelTraceState.getThreshold(); + if (((randomValue < threshold) == isParentSampled) || threshold == 0) { + parentThreshold = threshold; + } else { + parentThreshold = getInvalidThreshold(); + } + } else if (isParentSampled) { + parentThreshold = getMaxThreshold(); + } else { + parentThreshold = 0; + } + + // determine new threshold that is used for the sampling decision + long threshold = getThreshold(parentThreshold, isRoot); + + // determine sampling decision + boolean isSampled; + if (isValidThreshold(threshold)) { + isSampled = (randomValue < threshold); + if (0 < threshold && threshold < getMaxThreshold()) { + otelTraceState.setThreshold(threshold); + } else { + otelTraceState.invalidateThreshold(); + } + } else { + isSampled = isParentSampled; + otelTraceState.invalidateThreshold(); + } + + SamplingDecision samplingDecision = + isSampled ? SamplingDecision.RECORD_AND_SAMPLE : SamplingDecision.DROP; + + String newOtTraceState = otelTraceState.serialize(); + + return new SamplingResult() { + + @Override + public SamplingDecision getDecision() { + return samplingDecision; + } + + @Override + public Attributes getAttributes() { + return Attributes.empty(); + } + + @Override + public TraceState getUpdatedTraceState(TraceState parentTraceState) { + return 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); +} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplingUtil.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplingUtil.java new file mode 100644 index 000000000..80a6bffd2 --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplingUtil.java @@ -0,0 +1,147 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; + +public final class ConsistentSamplingUtil { + + private static final int RANDOM_VALUE_BITS = 56; + private static final long MAX_THRESHOLD = 1L << RANDOM_VALUE_BITS; + private static final long MAX_RANDOM_VALUE = MAX_THRESHOLD - 1; + private static final long INVALID_THRESHOLD = -1; + private static final long INVALID_RANDOM_VALUE = -1; + + private ConsistentSamplingUtil() {} + + /** + * Returns for a given threshold the corresponding sampling probability. + * + *

The returned value does not always exactly match the applied sampling probability, since + * some least significant binary digits may not be represented by double-precision floating point + * numbers. + * + * @param threshold the threshold + * @return the sampling probability + */ + public static double calculateSamplingProbability(long threshold) { + checkThreshold(threshold); + return threshold * 0x1p-56; + } + + /** + * Returns the closest sampling threshold that can be used to realize sampling with the given + * probability. + * + * @param samplingProbability the sampling probability + * @return the threshold + */ + public static long calculateThreshold(double samplingProbability) { + checkProbability(samplingProbability); + return Math.round(samplingProbability * 0x1p56); + } + + /** + * Calculates the adjusted count from a given threshold. + * + *

Returns 1, if the threshold is invalid. + * + *

Returns 0, if the threshold is 0. A span with zero threshold is only sampled due to a + * non-probabilistic sampling decision and therefore does not contribute to the adjusted count. + * + * @param threshold the threshold + * @return the adjusted count + */ + public static double calculateAdjustedCount(long threshold) { + if (isValidThreshold(threshold)) { + if (threshold > 0) { + return 0x1p56 / threshold; + } else { + return 0; + } + } else { + return 1.; + } + } + + /** + * Returns an invalid random value. + * + *

{@code isValidRandomValue(getInvalidRandomValue())} will always return true. + * + * @return an invalid random value + */ + public static long getInvalidRandomValue() { + return INVALID_RANDOM_VALUE; + } + + /** + * Returns an invalid threshold. + * + *

{@code isValidThreshold(getInvalidThreshold())} will always return true. + * + * @return an invalid threshold value + */ + public static long getInvalidThreshold() { + return INVALID_THRESHOLD; + } + + public static long getMaxRandomValue() { + return MAX_RANDOM_VALUE; + } + + public static long getMaxThreshold() { + return MAX_THRESHOLD; + } + + public static boolean isValidRandomValue(long randomValue) { + return 0 <= randomValue && randomValue <= getMaxRandomValue(); + } + + public static boolean isValidThreshold(long threshold) { + return 0 <= threshold && threshold <= getMaxThreshold(); + } + + public static boolean isValidProbability(double probability) { + return 0 <= probability && probability <= 1; + } + + static void checkThreshold(long threshold) { + if (!isValidThreshold(threshold)) { + throw new IllegalArgumentException("The threshold must be in the range [0,2^56]!"); + } + } + + static void checkProbability(double probability) { + if (!isValidProbability(probability)) { + throw new IllegalArgumentException("The probability must be in the range [0,1]!"); + } + } + + private static final char[] HEX_DIGITS = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + @CanIgnoreReturnValue + static StringBuilder appendLast56BitHexEncoded(StringBuilder sb, long l) { + return appendLast56BitHexEncodedHelper(sb, l, 0); + } + + @CanIgnoreReturnValue + static StringBuilder appendLast56BitHexEncodedWithoutTrailingZeros(StringBuilder sb, long l) { + int numTrailingBits = Long.numberOfTrailingZeros(l | 0x80000000000000L); + return appendLast56BitHexEncodedHelper(sb, l, numTrailingBits); + } + + @CanIgnoreReturnValue + private static StringBuilder appendLast56BitHexEncodedHelper( + StringBuilder sb, long l, int numTrailingZeroBits) { + for (int i = 52; i >= numTrailingZeroBits - 3; i -= 4) { + sb.append(HEX_DIGITS[(int) ((l >>> i) & 0xf)]); + } + return sb; + } +} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/OtelTraceState.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/OtelTraceState.java new file mode 100644 index 000000000..e546b184b --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/OtelTraceState.java @@ -0,0 +1,256 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +final class OtelTraceState { + + public static final String TRACE_STATE_KEY = "ot"; + + private static final String SUBKEY_RANDOM_VALUE = "rv"; + private static final String SUBKEY_THRESHOLD = "th"; + private static final int TRACE_STATE_SIZE_LIMIT = 256; + + private long randomValue; // valid in the interval [0, MAX_RANDOM_VALUE] + private long threshold; // valid in the interval [0, MAX_THRESHOLD] + + private final List otherKeyValuePairs; + + private OtelTraceState(long randomValue, long threshold, List otherKeyValuePairs) { + this.randomValue = randomValue; + this.threshold = threshold; + this.otherKeyValuePairs = otherKeyValuePairs; + } + + private OtelTraceState() { + this( + ConsistentSamplingUtil.getInvalidRandomValue(), + ConsistentSamplingUtil.getInvalidThreshold(), + Collections.emptyList()); + } + + public long getRandomValue() { + return randomValue; + } + + public long getThreshold() { + return threshold; + } + + public boolean hasValidRandomValue() { + return ConsistentSamplingUtil.isValidRandomValue(randomValue); + } + + public boolean hasValidThreshold() { + return ConsistentSamplingUtil.isValidThreshold(threshold); + } + + public void invalidateRandomValue() { + randomValue = ConsistentSamplingUtil.getInvalidRandomValue(); + } + + public void invalidateThreshold() { + threshold = ConsistentSamplingUtil.getInvalidThreshold(); + } + + /** + * Sets a new th-value. + * + *

If the given th-value is invalid, the current th-value is invalidated. + * + * @param threshold the new th-value + */ + public void setThreshold(long threshold) { + if (ConsistentSamplingUtil.isValidThreshold(threshold)) { + this.threshold = threshold; + } else { + invalidateThreshold(); + } + } + + /** + * Sets a new rv-value. + * + *

If the given rv-value is invalid, the current rv-value is invalidated. + * + * @param randomValue the new rv-value + */ + public void setRandomValue(long randomValue) { + if (ConsistentSamplingUtil.isValidRandomValue(randomValue)) { + this.randomValue = randomValue; + } else { + invalidateRandomValue(); + } + } + + /** + * Returns a string representing this state. + * + * @return a string + */ + public String serialize() { + StringBuilder sb = new StringBuilder(); + if (hasValidThreshold() && threshold < ConsistentSamplingUtil.getMaxThreshold()) { + sb.append(SUBKEY_THRESHOLD).append(':'); + ConsistentSamplingUtil.appendLast56BitHexEncodedWithoutTrailingZeros(sb, threshold); + } + if (hasValidRandomValue()) { + if (sb.length() > 0) { + sb.append(';'); + } + sb.append(SUBKEY_RANDOM_VALUE).append(':'); + ConsistentSamplingUtil.appendLast56BitHexEncoded(sb, randomValue); + } + for (String pair : otherKeyValuePairs) { + int ex = sb.length(); + if (ex != 0) { + ex += 1; + } + if (ex + pair.length() > TRACE_STATE_SIZE_LIMIT) { + break; + } + if (sb.length() > 0) { + sb.append(';'); + } + sb.append(pair); + } + return sb.toString(); + } + + private static boolean isValueByte(char c) { + return isLowerCaseAlphaNum(c) || isUpperCaseAlpha(c) || c == '.' || c == '_' || c == '-'; + } + + private static boolean isLowerCaseAlphaNum(char c) { + return isLowerCaseAlpha(c) || isDigit(c); + } + + private static boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } + + private static boolean isLowerCaseAlpha(char c) { + return c >= 'a' && c <= 'z'; + } + + private static boolean isUpperCaseAlpha(char c) { + return c >= 'A' && c <= 'Z'; + } + + private static long parseRandomValue(String s, int startIncl, int endIncl) { + int len = endIncl - startIncl; + if (len != 14) { + return ConsistentSamplingUtil.getInvalidRandomValue(); + } + return parseHex(s, startIncl, len, ConsistentSamplingUtil.getInvalidRandomValue()); + } + + private static long parseThreshold(String s, int startIncl, int endIncl) { + int len = endIncl - startIncl; + if (len > 14) { + return ConsistentSamplingUtil.getInvalidThreshold(); + } + return parseHex(s, startIncl, len, ConsistentSamplingUtil.getInvalidThreshold()); + } + + static long parseHex(String s, int startIncl, int len, long invalidReturnValue) { + long r = 0; + for (int i = 0; i < len; ++i) { + long c = s.charAt(startIncl + i); + long x; + if (c >= '0' && c <= '9') { + x = c - '0'; + } else if (c >= 'a' && c <= 'f') { + x = c - 'a' + 10; + } else { + return invalidReturnValue; + } + r |= x << (52 - (i << 2)); + } + return r; + } + + /** + * Parses the trace state from a given string. + * + *

If the string cannot be successfully parsed, a new empty {@code OtelTraceState2} is + * returned. + * + * @param ts the string + * @return the parsed trace state or an empty trace state in case of parsing errors + */ + public static OtelTraceState parse(@Nullable String ts) { + List otherKeyValuePairs = null; + long threshold = ConsistentSamplingUtil.getInvalidThreshold(); + long randomValue = ConsistentSamplingUtil.getInvalidRandomValue(); + + if (ts == null || ts.isEmpty()) { + return new OtelTraceState(); + } + + if (ts.length() > TRACE_STATE_SIZE_LIMIT) { + return new OtelTraceState(); + } + + int startPos = 0; + int len = ts.length(); + + while (true) { + int colonPos = startPos; + for (; colonPos < len; colonPos++) { + char c = ts.charAt(colonPos); + if (!isLowerCaseAlpha(c) && (!isDigit(c) || colonPos == startPos)) { + break; + } + } + if (colonPos == startPos || colonPos == len || ts.charAt(colonPos) != ':') { + return new OtelTraceState(); + } + + int separatorPos = colonPos + 1; + while (separatorPos < len && isValueByte(ts.charAt(separatorPos))) { + separatorPos++; + } + + if (colonPos - startPos == SUBKEY_THRESHOLD.length() + && ts.startsWith(SUBKEY_THRESHOLD, startPos)) { + threshold = parseThreshold(ts, colonPos + 1, separatorPos); + } else if (colonPos - startPos == SUBKEY_RANDOM_VALUE.length() + && ts.startsWith(SUBKEY_RANDOM_VALUE, startPos)) { + randomValue = parseRandomValue(ts, colonPos + 1, separatorPos); + } else { + if (otherKeyValuePairs == null) { + otherKeyValuePairs = new ArrayList<>(); + } + otherKeyValuePairs.add(ts.substring(startPos, separatorPos)); + } + + if (separatorPos < len && ts.charAt(separatorPos) != ';') { + return new OtelTraceState(); + } + + if (separatorPos == len) { + break; + } + + startPos = separatorPos + 1; + + // test for a trailing ; + if (startPos == len) { + return new OtelTraceState(); + } + } + + return new OtelTraceState( + randomValue, + threshold, + (otherKeyValuePairs != null) ? otherKeyValuePairs : Collections.emptyList()); + } +} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGenerator.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGenerator.java new file mode 100644 index 000000000..39ba47b03 --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGenerator.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +/** + * A function for generating random values. + * + *

The distribution of random values generated by this function must be uniform over the range + * [0,2^56-1] + */ +@FunctionalInterface +public interface RandomValueGenerator { + + /** + * Returns a 56-bit uniformly distributed random value. + * + * @param traceId the trace ID + * @return the random value + */ + long generate(String traceId); +} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGenerators.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGenerators.java new file mode 100644 index 000000000..fcd0f6f5f --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGenerators.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMaxRandomValue; + +import java.util.concurrent.ThreadLocalRandom; + +final class RandomValueGenerators { + + private static final RandomValueGenerator DEFAULT = createDefault(); + + static RandomValueGenerator getDefault() { + return DEFAULT; + } + + private static RandomValueGenerator createDefault() { + return s -> ThreadLocalRandom.current().nextLong() & getMaxRandomValue(); + } + + private RandomValueGenerators() {} +} 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 new file mode 100644 index 000000000..60f97ac23 --- /dev/null +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSamplerTest.java @@ -0,0 +1,31 @@ +/* + * 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.getMaxThreshold; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class ConsistentAlwaysOffSamplerTest { + + @Test + void testDescription() { + assertThat(ConsistentSampler.alwaysOff().getDescription()) + .isEqualTo("ConsistentAlwaysOffSampler"); + } + + @Test + void testThreshold() { + assertThat(ConsistentSampler.alwaysOff().getThreshold(getInvalidThreshold(), false)).isZero(); + assertThat(ConsistentSampler.alwaysOff().getThreshold(getInvalidThreshold(), true)).isZero(); + assertThat(ConsistentSampler.alwaysOff().getThreshold(getMaxThreshold(), false)).isZero(); + assertThat(ConsistentSampler.alwaysOff().getThreshold(getMaxThreshold(), true)).isZero(); + assertThat(ConsistentSampler.alwaysOff().getThreshold(0, false)).isZero(); + assertThat(ConsistentSampler.alwaysOff().getThreshold(0, true)).isZero(); + } +} 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 new file mode 100644 index 000000000..96943be5e --- /dev/null +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSamplerTest.java @@ -0,0 +1,35 @@ +/* + * 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.getMaxThreshold; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class ConsistentAlwaysOnSamplerTest { + + @Test + void testDescription() { + assertThat(ConsistentSampler.alwaysOn().getDescription()) + .isEqualTo("ConsistentAlwaysOnSampler"); + } + + @Test + void testThreshold() { + assertThat(ConsistentSampler.alwaysOn().getThreshold(getInvalidThreshold(), false)) + .isEqualTo(getMaxThreshold()); + assertThat(ConsistentSampler.alwaysOn().getThreshold(getInvalidThreshold(), true)) + .isEqualTo(getMaxThreshold()); + assertThat(ConsistentSampler.alwaysOn().getThreshold(getMaxThreshold(), false)) + .isEqualTo(getMaxThreshold()); + assertThat(ConsistentSampler.alwaysOn().getThreshold(getMaxThreshold(), true)) + .isEqualTo(getMaxThreshold()); + assertThat(ConsistentSampler.alwaysOn().getThreshold(0, false)).isEqualTo(getMaxThreshold()); + assertThat(ConsistentSampler.alwaysOn().getThreshold(0, true)).isEqualTo(getMaxThreshold()); + } +} diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSamplerTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSamplerTest.java new file mode 100644 index 000000000..340f6641f --- /dev/null +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSamplerTest.java @@ -0,0 +1,114 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.calculateThreshold; +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMaxRandomValue; +import static org.assertj.core.api.Assertions.assertThat; + +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 io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.Collections; +import java.util.List; +import java.util.SplittableRandom; +import org.hipparchus.stat.inference.AlternativeHypothesis; +import org.hipparchus.stat.inference.BinomialTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class ConsistentFixedThresholdSamplerTest { + + private Context parentContext; + private String traceId; + private String name; + private SpanKind spanKind; + private Attributes attributes; + private List parentLinks; + + @BeforeEach + public void init() { + + parentContext = Context.root(); + traceId = "0123456789abcdef0123456789abcdef"; + name = "name"; + spanKind = SpanKind.SERVER; + attributes = Attributes.empty(); + parentLinks = Collections.emptyList(); + } + + private void testSampling(SplittableRandom rng, double samplingProbability) { + int numSpans = 10000; + + Sampler sampler = + ConsistentSampler.probabilityBased( + samplingProbability, s -> rng.nextLong() & getMaxRandomValue()); + + int numSampled = 0; + for (long i = 0; i < numSpans; ++i) { + SamplingResult samplingResult = + sampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); + if (samplingResult.getDecision() == SamplingDecision.RECORD_AND_SAMPLE) { + String traceStateString = + samplingResult + .getUpdatedTraceState(TraceState.getDefault()) + .get(OtelTraceState.TRACE_STATE_KEY); + OtelTraceState traceState = OtelTraceState.parse(traceStateString); + assertThat(traceState.hasValidRandomValue()).isTrue(); + if (samplingProbability == 1.) { + assertThat(traceState.hasValidThreshold()).isFalse(); + } else { + assertThat(traceState.hasValidThreshold()).isTrue(); + assertThat(traceState.getThreshold()).isEqualTo(calculateThreshold(samplingProbability)); + } + + numSampled += 1; + } + } + + assertThat( + new BinomialTest() + .binomialTest( + numSpans, numSampled, samplingProbability, AlternativeHypothesis.TWO_SIDED)) + .isGreaterThan(0.005); + } + + @Test + public void testSampling() { + + // fix seed to get reproducible results + SplittableRandom random = new SplittableRandom(0); + + testSampling(random, 1.); + testSampling(random, 0.5); + testSampling(random, 0.25); + testSampling(random, 0.125); + testSampling(random, 0.0); + testSampling(random, 0.45); + testSampling(random, 0.2); + testSampling(random, 0.13); + testSampling(random, 0.05); + } + + @Test + public void testDescription() { + assertThat(ConsistentSampler.probabilityBased(1.0).getDescription()) + .isEqualTo("ConsistentFixedThresholdSampler{threshold=max, sampling probability=1.0}"); + assertThat(ConsistentSampler.probabilityBased(0.5).getDescription()) + .isEqualTo("ConsistentFixedThresholdSampler{threshold=8, sampling probability=0.5}"); + assertThat(ConsistentSampler.probabilityBased(0.25).getDescription()) + .isEqualTo("ConsistentFixedThresholdSampler{threshold=4, sampling probability=0.25}"); + assertThat(ConsistentSampler.probabilityBased(1e-300).getDescription()) + .isEqualTo("ConsistentFixedThresholdSampler{threshold=0, sampling probability=0.0}"); + assertThat(ConsistentSampler.probabilityBased(0).getDescription()) + .isEqualTo("ConsistentFixedThresholdSampler{threshold=0, sampling probability=0.0}"); + } +} 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 new file mode 100644 index 000000000..24c70e4e5 --- /dev/null +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSamplerTest.java @@ -0,0 +1,231 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMaxRandomValue; +import static org.assertj.core.api.Assertions.assertThat; + +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 io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.SplittableRandom; +import java.util.concurrent.TimeUnit; +import java.util.function.LongSupplier; +import org.assertj.core.data.Percentage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ConsistentRateLimitingSamplerTest { + + private long[] nanoTime; + private LongSupplier nanoTimeSupplier; + private Context parentContext; + private String traceId; + private String name; + private SpanKind spanKind; + private Attributes attributes; + private List parentLinks; + + private static RandomValueGenerator randomValueGenerator() { + SplittableRandom random = new SplittableRandom(0L); + return s -> random.nextLong() & getMaxRandomValue(); + } + + @BeforeEach + void init() { + nanoTime = new long[] {0L}; + nanoTimeSupplier = () -> nanoTime[0]; + parentContext = Context.root(); + traceId = "0123456789abcdef0123456789abcdef"; + name = "name"; + spanKind = SpanKind.SERVER; + attributes = Attributes.empty(); + parentLinks = Collections.emptyList(); + } + + private void advanceTime(long nanosIncrement) { + nanoTime[0] += nanosIncrement; + } + + private long getCurrentTimeNanos() { + return nanoTime[0]; + } + + @Test + void testConstantRate() { + + double targetSpansPerSecondLimit = 1000; + double adaptationTimeSeconds = 5; + + ConsistentSampler sampler = + ConsistentSampler.rateLimited( + targetSpansPerSecondLimit, + adaptationTimeSeconds, + randomValueGenerator(), + 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, traceId, 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 testRateIncrease() { + + double targetSpansPerSecondLimit = 1000; + double adaptationTimeSeconds = 5; + + ConsistentSampler sampler = + ConsistentSampler.rateLimited( + targetSpansPerSecondLimit, + adaptationTimeSeconds, + randomValueGenerator(), + nanoTimeSupplier); + + long nanosBetweenSpans1 = TimeUnit.MICROSECONDS.toNanos(100); + long nanosBetweenSpans2 = TimeUnit.MICROSECONDS.toNanos(10); + int numSpans1 = 500000; + int numSpans2 = 5000000; + + List spanSampledNanos = new ArrayList<>(); + + for (int i = 0; i < numSpans1; ++i) { + advanceTime(nanosBetweenSpans1); + SamplingResult samplingResult = + sampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); + if (SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult.getDecision())) { + spanSampledNanos.add(getCurrentTimeNanos()); + } + } + for (int i = 0; i < numSpans2; ++i) { + advanceTime(nanosBetweenSpans2); + SamplingResult samplingResult = + sampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); + if (SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult.getDecision())) { + spanSampledNanos.add(getCurrentTimeNanos()); + } + } + + long numSampledSpansWithin5SecondsBeforeChange = + spanSampledNanos.stream() + .filter(x -> x > TimeUnit.SECONDS.toNanos(45) && x <= TimeUnit.SECONDS.toNanos(50)) + .count(); + long numSampledSpansWithin5SecondsAfterChange = + spanSampledNanos.stream() + .filter(x -> x > TimeUnit.SECONDS.toNanos(50) && x <= TimeUnit.SECONDS.toNanos(55)) + .count(); + long numSampledSpansInLast5Seconds = + spanSampledNanos.stream() + .filter(x -> x > TimeUnit.SECONDS.toNanos(95) && x <= TimeUnit.SECONDS.toNanos(100)) + .count(); + + assertThat(numSampledSpansWithin5SecondsBeforeChange / 5.) + .isCloseTo(targetSpansPerSecondLimit, Percentage.withPercentage(5)); + assertThat(numSampledSpansWithin5SecondsAfterChange / 5.) + .isGreaterThan(2. * targetSpansPerSecondLimit); + assertThat(numSampledSpansInLast5Seconds / 5.) + .isCloseTo(targetSpansPerSecondLimit, Percentage.withPercentage(5)); + } + + @Test + void testRateDecrease() { + + double targetSpansPerSecondLimit = 1000; + double adaptationTimeSeconds = 5; + + ConsistentSampler sampler = + ConsistentSampler.rateLimited( + targetSpansPerSecondLimit, + adaptationTimeSeconds, + randomValueGenerator(), + nanoTimeSupplier); + + long nanosBetweenSpans1 = TimeUnit.MICROSECONDS.toNanos(10); + long nanosBetweenSpans2 = TimeUnit.MICROSECONDS.toNanos(100); + int numSpans1 = 5000000; + int numSpans2 = 500000; + + List spanSampledNanos = new ArrayList<>(); + + for (int i = 0; i < numSpans1; ++i) { + advanceTime(nanosBetweenSpans1); + SamplingResult samplingResult = + sampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); + if (SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult.getDecision())) { + spanSampledNanos.add(getCurrentTimeNanos()); + } + } + for (int i = 0; i < numSpans2; ++i) { + advanceTime(nanosBetweenSpans2); + SamplingResult samplingResult = + sampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); + if (SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult.getDecision())) { + spanSampledNanos.add(getCurrentTimeNanos()); + } + } + + long numSampledSpansWithin5SecondsBeforeChange = + spanSampledNanos.stream() + .filter(x -> x > TimeUnit.SECONDS.toNanos(45) && x <= TimeUnit.SECONDS.toNanos(50)) + .count(); + long numSampledSpansWithin5SecondsAfterChange = + spanSampledNanos.stream() + .filter(x -> x > TimeUnit.SECONDS.toNanos(50) && x <= TimeUnit.SECONDS.toNanos(55)) + .count(); + long numSampledSpansInLast5Seconds = + spanSampledNanos.stream() + .filter(x -> x > TimeUnit.SECONDS.toNanos(95) && x <= TimeUnit.SECONDS.toNanos(100)) + .count(); + + assertThat(numSampledSpansWithin5SecondsBeforeChange / 5.) + .isCloseTo(targetSpansPerSecondLimit, Percentage.withPercentage(5)); + assertThat(numSampledSpansWithin5SecondsAfterChange / 5.) + .isLessThan(0.5 * targetSpansPerSecondLimit); + assertThat(numSampledSpansInLast5Seconds / 5.) + .isCloseTo(targetSpansPerSecondLimit, Percentage.withPercentage(5)); + } + + @Test + void testDescription() { + + double targetSpansPerSecondLimit = 123.456; + double adaptationTimeSeconds = 7.89; + ConsistentSampler sampler = + ConsistentSampler.rateLimited(targetSpansPerSecondLimit, adaptationTimeSeconds); + + assertThat(sampler.getDescription()) + .isEqualTo( + "ConsistentRateLimitingSampler{targetSpansPerSecondLimit=" + + targetSpansPerSecondLimit + + ", adaptationTimeSeconds=" + + adaptationTimeSeconds + + "}"); + } +} diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplerTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplerTest.java new file mode 100644 index 000000000..e36868a56 --- /dev/null +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplerTest.java @@ -0,0 +1,261 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMaxRandomValue; +import static org.assertj.core.api.Assertions.assertThat; + +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.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.Collections; +import java.util.List; +import java.util.OptionalLong; +import java.util.SplittableRandom; +import org.junit.jupiter.api.Test; + +class ConsistentSamplerTest { + + private static class Input { + private static final String traceId = "00112233445566778800000000000000"; + private static final String spanId = "0123456789abcdef"; + private static final String name = "name"; + private static final SpanKind spanKind = SpanKind.SERVER; + private static final Attributes attributes = Attributes.empty(); + private static final List parentLinks = Collections.emptyList(); + private boolean parentSampled = true; + + private OptionalLong parentThreshold = OptionalLong.empty(); + private OptionalLong parentRandomValue = OptionalLong.empty(); + + private final SplittableRandom random = new SplittableRandom(0L); + + public void setParentSampled(boolean parentSampled) { + this.parentSampled = parentSampled; + } + + public void setParentThreshold(long parentThreshold) { + assertThat(parentThreshold).isBetween(0L, 0xffffffffffffffL); + this.parentThreshold = OptionalLong.of(parentThreshold); + } + + public void setParentRandomValue(long parentRandomValue) { + assertThat(parentRandomValue).isBetween(0L, 0xffffffffffffffL); + this.parentRandomValue = OptionalLong.of(parentRandomValue); + } + + public Context getParentContext() { + return createParentContext( + traceId, spanId, parentThreshold, parentRandomValue, parentSampled); + } + + public static String getTraceId() { + return traceId; + } + + public static String getName() { + return name; + } + + public static SpanKind getSpanKind() { + return spanKind; + } + + public static Attributes getAttributes() { + return attributes; + } + + public static List getParentLinks() { + return parentLinks; + } + + public RandomValueGenerator getRandomValueGenerator() { + return s -> random.nextLong() & getMaxRandomValue(); + } + } + + private static class Output { + + private final SamplingResult samplingResult; + private final Context parentContext; + + Output(SamplingResult samplingResult, Context parentContext) { + this.samplingResult = samplingResult; + this.parentContext = parentContext; + } + + boolean getSampledFlag() { + return SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult.getDecision()); + } + + OptionalLong getThreshold() { + Span parentSpan = Span.fromContext(parentContext); + OtelTraceState otelTraceState = + OtelTraceState.parse( + samplingResult + .getUpdatedTraceState(parentSpan.getSpanContext().getTraceState()) + .get(OtelTraceState.TRACE_STATE_KEY)); + return otelTraceState.hasValidThreshold() + ? OptionalLong.of(otelTraceState.getThreshold()) + : OptionalLong.empty(); + } + + OptionalLong getRandomValue() { + Span parentSpan = Span.fromContext(parentContext); + OtelTraceState otelTraceState = + OtelTraceState.parse( + samplingResult + .getUpdatedTraceState(parentSpan.getSpanContext().getTraceState()) + .get(OtelTraceState.TRACE_STATE_KEY)); + return otelTraceState.hasValidRandomValue() + ? OptionalLong.of(otelTraceState.getRandomValue()) + : OptionalLong.empty(); + } + } + + private static TraceState createTraceState(OptionalLong threshold, OptionalLong randomValue) { + OtelTraceState state = OtelTraceState.parse(""); + threshold.ifPresent(x -> state.setThreshold(x)); + randomValue.ifPresent(x -> state.setRandomValue(x)); + return TraceState.builder().put(OtelTraceState.TRACE_STATE_KEY, state.serialize()).build(); + } + + private static Context createParentContext( + String traceId, + String spanId, + OptionalLong threshold, + OptionalLong randomValue, + boolean sampled) { + TraceState parentTraceState = createTraceState(threshold, randomValue); + TraceFlags traceFlags = sampled ? TraceFlags.getSampled() : TraceFlags.getDefault(); + SpanContext parentSpanContext = + SpanContext.create(traceId, spanId, traceFlags, parentTraceState); + Span parentSpan = Span.wrap(parentSpanContext); + return parentSpan.storeInContext(Context.root()); + } + + private static Output sample(Input input, ConsistentSampler sampler) { + + Context parentContext = input.getParentContext(); + SamplingResult samplingResult = + sampler.shouldSample( + parentContext, + Input.getTraceId(), + Input.getName(), + Input.getSpanKind(), + Input.getAttributes(), + Input.getParentLinks()); + return new Output(samplingResult, parentContext); + } + + @Test + void testMaxThresholdWithoutParentRandomValue() { + + Input input = new Input(); + + ConsistentSampler sampler = ConsistentSampler.alwaysOn(input.getRandomValueGenerator()); + + Output output = sample(input, sampler); + + assertThat(output.samplingResult.getDecision()).isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + assertThat(output.getThreshold()).isEmpty(); + assertThat(output.getRandomValue()).hasValue(0x20a8397b1dcdafL); + assertThat(output.getSampledFlag()).isTrue(); + } + + @Test + void testMaxThresholdWithParentRandomValue() { + + long parentRandomValue = 0x7f99aa40c02744L; + + Input input = new Input(); + input.setParentRandomValue(parentRandomValue); + + ConsistentSampler sampler = ConsistentSampler.alwaysOn(input.getRandomValueGenerator()); + + Output output = sample(input, sampler); + + assertThat(output.samplingResult.getDecision()).isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + assertThat(output.getThreshold()).isEmpty(); + assertThat(output.getRandomValue()).hasValue(parentRandomValue); + assertThat(output.getSampledFlag()).isTrue(); + } + + @Test + void testMinThreshold() { + + Input input = new Input(); + + ConsistentSampler sampler = + new ConsistentFixedThresholdSampler(0L, input.getRandomValueGenerator()); + + Output output = sample(input, sampler); + + assertThat(output.samplingResult.getDecision()).isEqualTo(SamplingDecision.DROP); + assertThat(output.getThreshold()).isEmpty(); + assertThat(output.getRandomValue()).hasValue(0x20a8397b1dcdafL); + assertThat(output.getSampledFlag()).isFalse(); + } + + @Test + void testHalfThresholdNotSampled() { + + Input input = new Input(); + input.setParentRandomValue(0x8000000000000L); + + ConsistentSampler sampler = + new ConsistentFixedThresholdSampler(0x8000000000000L, input.getRandomValueGenerator()); + + Output output = sample(input, sampler); + + assertThat(output.samplingResult.getDecision()).isEqualTo(SamplingDecision.DROP); + assertThat(output.getThreshold()).hasValue(0x8000000000000L); + assertThat(output.getRandomValue()).hasValue(0x8000000000000L); + assertThat(output.getSampledFlag()).isFalse(); + } + + @Test + void testHalfThresholdSampled() { + + Input input = new Input(); + input.setParentRandomValue(0x7ffffffffffffL); + + ConsistentSampler sampler = + new ConsistentFixedThresholdSampler(0x8000000000000L, input.getRandomValueGenerator()); + + Output output = sample(input, sampler); + + assertThat(output.samplingResult.getDecision()).isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + assertThat(output.getThreshold()).hasValue(0x8000000000000L); + assertThat(output.getRandomValue()).hasValue(0x7ffffffffffffL); + assertThat(output.getSampledFlag()).isTrue(); + } + + @Test + void testParentExtraordinarySampledButChildNotSampled() { + + Input input = new Input(); + input.setParentThreshold(0L); + input.setParentSampled(true); + + ConsistentSampler sampler = + new ConsistentFixedThresholdSampler(0x0L, input.getRandomValueGenerator()); + Output output = sample(input, sampler); + + assertThat(output.samplingResult.getDecision()).isEqualTo(SamplingDecision.DROP); + + assertThat(output.getThreshold()).isEmpty(); + assertThat(output.getRandomValue()).isNotEmpty(); + assertThat(output.getSampledFlag()).isFalse(); + } +} diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplingUtilTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplingUtilTest.java new file mode 100644 index 000000000..944ace75b --- /dev/null +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplingUtilTest.java @@ -0,0 +1,114 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.calculateAdjustedCount; +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.getInvalidRandomValue; +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold; +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.isValidRandomValue; +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.isValidThreshold; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import org.junit.jupiter.api.Test; + +public class ConsistentSamplingUtilTest { + + @Test + void testCalculateSamplingProbability() { + assertThat(calculateSamplingProbability(0L)).isEqualTo(0.); + assertThat(calculateSamplingProbability(0x40000000000000L)).isEqualTo(0.25); + assertThat(calculateSamplingProbability(0x80000000000000L)).isEqualTo(0.5); + assertThat(calculateSamplingProbability(0x100000000000000L)).isEqualTo(1.); + assertThatIllegalArgumentException().isThrownBy(() -> calculateSamplingProbability(-1)); + assertThatIllegalArgumentException() + .isThrownBy(() -> calculateSamplingProbability(0x100000000000001L)); + } + + @Test + void testCalculateThreshold() { + assertThat(calculateThreshold(0.)).isEqualTo(0L); + assertThat(calculateThreshold(0.25)).isEqualTo(0x40000000000000L); + assertThat(calculateThreshold(0.5)).isEqualTo(0x80000000000000L); + assertThat(calculateThreshold(1.)).isEqualTo(0x100000000000000L); + assertThatIllegalArgumentException().isThrownBy(() -> calculateThreshold(Math.nextDown(0.))); + assertThatIllegalArgumentException().isThrownBy(() -> calculateThreshold(Math.nextUp(1.))); + assertThatIllegalArgumentException() + .isThrownBy(() -> calculateThreshold(Double.POSITIVE_INFINITY)); + assertThatIllegalArgumentException() + .isThrownBy(() -> calculateThreshold(Double.NEGATIVE_INFINITY)); + assertThatIllegalArgumentException().isThrownBy(() -> calculateThreshold(Double.NaN)); + } + + @Test + void testGetInvalidRandomValue() { + assertThat(isValidRandomValue(getInvalidRandomValue())).isFalse(); + } + + @Test + void testGetInvalidThreshold() { + assertThat(isValidThreshold(getInvalidThreshold())).isFalse(); + } + + @Test + void testGetMaxThreshold() { + assertThat(ConsistentSamplingUtil.getMaxThreshold()).isEqualTo(0x100000000000000L); + } + + @Test + void testGetMaxRandomValue() { + assertThat(ConsistentSamplingUtil.getMaxRandomValue()).isEqualTo(0xFFFFFFFFFFFFFFL); + } + + @Test + void testCalculateAdjustedCount() { + assertThat(calculateAdjustedCount(0L)).isZero(); + assertThat(calculateAdjustedCount(0x40000000000000L)).isEqualTo(4.); + assertThat(calculateAdjustedCount(0x80000000000000L)).isEqualTo(2.); + assertThat(calculateAdjustedCount(0x100000000000000L)).isOne(); + assertThat(calculateAdjustedCount(-1)).isOne(); + assertThat(calculateAdjustedCount(0x100000000000001L)).isOne(); + } + + @Test + void testAppendLast56BitHexEncoded() { + assertThat( + ConsistentSamplingUtil.appendLast56BitHexEncoded(new StringBuilder(), 0x3a436f7842456L)) + .hasToString("03a436f7842456"); + assertThat( + ConsistentSamplingUtil.appendLast56BitHexEncoded( + new StringBuilder(), 0x3a436f7842456abL)) + .hasToString("a436f7842456ab"); + assertThat(ConsistentSamplingUtil.appendLast56BitHexEncoded(new StringBuilder(), 0L)) + .hasToString("00000000000000"); + } + + @Test + void testAppendLast56BitHexEncodedWithoutTrailingZeros() { + assertThat( + ConsistentSamplingUtil.appendLast56BitHexEncodedWithoutTrailingZeros( + new StringBuilder(), 0x3a436f7842456L)) + .hasToString("03a436f7842456"); + assertThat( + ConsistentSamplingUtil.appendLast56BitHexEncodedWithoutTrailingZeros( + new StringBuilder(), 0x3a436f7842456abL)) + .hasToString("a436f7842456ab"); + assertThat( + ConsistentSamplingUtil.appendLast56BitHexEncodedWithoutTrailingZeros( + new StringBuilder(), 0x80000000000000L)) + .hasToString("8"); + assertThat( + ConsistentSamplingUtil.appendLast56BitHexEncodedWithoutTrailingZeros( + new StringBuilder(), 0x11000000000000L)) + .hasToString("11"); + assertThat( + ConsistentSamplingUtil.appendLast56BitHexEncodedWithoutTrailingZeros( + new StringBuilder(), 0L)) + .hasToString("0"); + } +} diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/OtelTraceStateTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/OtelTraceStateTest.java new file mode 100644 index 000000000..a131e9b78 --- /dev/null +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/OtelTraceStateTest.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; + +public class OtelTraceStateTest { + + private static String getXString(int len) { + return Stream.generate(() -> "X").limit(len).collect(Collectors.joining()); + } + + @Test + public void test() { + + assertEquals("", OtelTraceState.parse("").serialize()); + assertEquals("", OtelTraceState.parse("").serialize()); + + assertEquals("", OtelTraceState.parse("a").serialize()); + assertEquals("", OtelTraceState.parse("#").serialize()); + assertEquals("", OtelTraceState.parse(" ").serialize()); + + assertEquals("rv:1234567890abcd", OtelTraceState.parse("rv:1234567890abcd").serialize()); + assertEquals("rv:01020304050607", OtelTraceState.parse("rv:01020304050607").serialize()); + assertEquals("", OtelTraceState.parse("rv:1234567890abcde").serialize()); + + assertEquals("th:1234567890abcd", OtelTraceState.parse("th:1234567890abcd").serialize()); + assertEquals("th:01020304050607", OtelTraceState.parse("th:01020304050607").serialize()); + assertEquals("th:1", OtelTraceState.parse("th:10000000000000").serialize()); + assertEquals("th:12345", OtelTraceState.parse("th:1234500000000").serialize()); + assertEquals("th:0", OtelTraceState.parse("th:0").serialize()); // TODO + assertEquals("", OtelTraceState.parse("th:100000000000000").serialize()); + assertEquals("", OtelTraceState.parse("th:1234567890abcde").serialize()); + + assertEquals( + "th:1234567890abcd;rv:1234567890abcd;a:" + getXString(214) + ";x:3", + OtelTraceState.parse("a:" + getXString(214) + ";rv:1234567890abcd;th:1234567890abcd;x:3") + .serialize()); + assertEquals( + "", + OtelTraceState.parse("a:" + getXString(215) + ";rv:1234567890abcd;th:1234567890abcd;x:3") + .serialize()); + + assertEquals("", OtelTraceState.parse("th:x").serialize()); + assertEquals("", OtelTraceState.parse("th:100000000000000").serialize()); + assertEquals("th:1", OtelTraceState.parse("th:10000000000000").serialize()); + assertEquals("th:1", OtelTraceState.parse("th:1000000000000").serialize()); + assertEquals("th:1", OtelTraceState.parse("th:100000000000").serialize()); + assertEquals("th:1", OtelTraceState.parse("th:10000000000").serialize()); + assertEquals("th:1", OtelTraceState.parse("th:1000000000").serialize()); + assertEquals("th:1", OtelTraceState.parse("th:100000000").serialize()); + assertEquals("th:1", OtelTraceState.parse("th:10000000").serialize()); + assertEquals("th:1", OtelTraceState.parse("th:1000000").serialize()); + assertEquals("th:1", OtelTraceState.parse("th:100000").serialize()); + assertEquals("th:1", OtelTraceState.parse("th:10000").serialize()); + assertEquals("th:1", OtelTraceState.parse("th:1000").serialize()); + assertEquals("th:1", OtelTraceState.parse("th:100").serialize()); + assertEquals("th:1", OtelTraceState.parse("th:10").serialize()); + assertEquals("th:1", OtelTraceState.parse("th:1").serialize()); + + assertEquals("th:10000000000001", OtelTraceState.parse("th:10000000000001").serialize()); + assertEquals("th:1000000000001", OtelTraceState.parse("th:10000000000010").serialize()); + assertEquals("", OtelTraceState.parse("rv:x").serialize()); + assertEquals("", OtelTraceState.parse("rv:100000000000000").serialize()); + assertEquals("rv:10000000000000", OtelTraceState.parse("rv:10000000000000").serialize()); + assertEquals("", OtelTraceState.parse("rv:1000000000000").serialize()); + } +} diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGeneratorsTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGeneratorsTest.java new file mode 100644 index 000000000..ab7d378b6 --- /dev/null +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGeneratorsTest.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMaxRandomValue; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class RandomValueGeneratorsTest { + @Test + void testRandomRange() { + int attempts = 10000; + for (int i = 0; i < attempts; ++i) { + assertThat(RandomValueGenerators.getDefault().generate("")) + .isBetween(0L, getMaxRandomValue()); + } + } +}