From 11f2111d088ea824a844ac818c66e827c5f0fa79 Mon Sep 17 00:00:00 2001 From: Peter Findeisen Date: Mon, 16 Sep 2024 08:21:46 -0700 Subject: [PATCH] Composite Samplers prototype (#1443) Co-authored-by: Otmar Ertl --- .../consistent56/ComposableSampler.java | 35 ++++ .../ConsistentAlwaysOffSampler.java | 17 +- .../ConsistentAlwaysOnSampler.java | 15 +- .../sampler/consistent56/ConsistentAnyOf.java | 109 +++++++++++ .../ConsistentComposedAndSampler.java | 52 ------ .../ConsistentComposedOrSampler.java | 57 ------ .../ConsistentFixedThresholdSampler.java | 25 ++- .../ConsistentParentBasedSampler.java | 39 +++- .../ConsistentRateLimitingSampler.java | 149 +++++++++++++-- .../ConsistentRuleBasedSampler.java | 71 +++++++ .../consistent56/ConsistentSampler.java | 173 +++++++++--------- .../sampler/consistent56/Predicate.java | 73 ++++++++ .../consistent56/PredicatedSampler.java | 32 ++++ .../sampler/consistent56/SamplingIntent.java | 42 +++++ .../sampler/consistent56/CoinFlipSampler.java | 85 +++++++++ .../ConsistentAlwaysOffSamplerTest.java | 16 +- .../ConsistentAlwaysOnSamplerTest.java | 14 +- .../consistent56/ConsistentAnyOfTest.java | 63 +++++++ .../ConsistentRateLimitingSamplerTest.java | 131 ++++++++++++- .../ConsistentRuleBasedSamplerTest.java | 95 ++++++++++ .../sampler/consistent56/MarkingSampler.java | 91 +++++++++ .../sampler/consistent56/UseCaseTest.java | 128 +++++++++++++ 22 files changed, 1264 insertions(+), 248 deletions(-) create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ComposableSampler.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOf.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedAndSampler.java delete 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/ConsistentRuleBasedSampler.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/Predicate.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/PredicatedSampler.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/SamplingIntent.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/CoinFlipSampler.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOfTest.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSamplerTest.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/MarkingSampler.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/UseCaseTest.java 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(); + } +}