From b42b0e242bb0a558d515ad7eddefed983c447c92 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 23 Apr 2026 13:35:39 -0700 Subject: [PATCH] consistent-sampling: rebase on upstream ComposableSampler API --- CHANGELOG.md | 33 + consistent-sampling/README.md | 60 +- consistent-sampling/build.gradle.kts | 2 + .../ConsistentAlwaysOffSampler.java | 26 - .../consistent/ConsistentAlwaysOnSampler.java | 26 - .../ConsistentAnyOf.java | 76 ++- .../ConsistentComposedAndSampler.java | 53 -- .../ConsistentComposedOrSampler.java | 59 -- .../ConsistentParentBasedSampler.java | 50 -- .../ConsistentProbabilityBasedSampler.java | 66 -- .../ConsistentRateLimitingSampler.java | 182 ++++-- .../sampler/consistent/ConsistentSampler.java | 421 ++----------- .../ConsistentSamplingUtil.java | 2 +- .../ConsistentVariableThresholdSampler.java | 85 +++ ...dConsistentProbabilitySamplerProvider.java | 5 +- .../sampler/consistent/RValueGenerator.java | 41 -- .../sampler/consistent/RValueGenerators.java | 22 - .../sampler/consistent56/Composable.java | 35 -- .../ConsistentAlwaysOffSampler.java | 53 -- .../ConsistentAlwaysOnSampler.java | 43 -- .../ConsistentFixedThresholdSampler.java | 33 - .../ConsistentParentBasedSampler.java | 117 ---- .../ConsistentRateLimitingSampler.java | 279 --------- .../ConsistentRuleBasedSampler.java | 71 --- .../consistent56/ConsistentSampler.java | 257 -------- .../ConsistentThresholdSampler.java | 60 -- .../ConsistentVariableThresholdSampler.java | 60 -- .../sampler/consistent56/OtelTraceState.java | 256 -------- .../sampler/consistent56/Predicate.java | 84 --- .../consistent56/PredicatedSampler.java | 32 - .../consistent56/RandomValueGenerator.java | 24 - .../consistent56/RandomValueGenerators.java | 25 - .../sampler/consistent56/SamplingIntent.java | 49 -- .../CoinFlipSampler.java | 21 +- .../consistent/ConsistentAnyOfTest.java | 130 ++++ ...ConsistentProbabilityBasedSamplerTest.java | 149 ----- .../ConsistentRateLimitingSamplerTest.java | 410 ++++++++++++- ...entReservoirSamplingSpanProcessorTest.java | 166 ++++- ...eservoirSamplingSpanProcessorTestUtil.java | 77 +++ .../consistent/ConsistentSamplerTest.java | 262 -------- .../ConsistentSamplingUtilTest.java | 20 +- ...onsistentVariableThresholdSamplerTest.java | 6 +- .../MarkingSampler.java | 39 +- .../contrib/sampler/consistent/TestUtil.java | 71 +-- .../sampler/consistent/UseCaseTest.java | 147 +++++ .../ConsistentAlwaysOffSamplerTest.java | 29 - .../ConsistentAlwaysOnSamplerTest.java | 29 - .../consistent56/ConsistentAnyOfTest.java | 127 ---- .../ConsistentFixedThresholdSamplerTest.java | 107 ---- .../ConsistentRateLimitingSamplerTest.java | 574 ------------------ .../ConsistentRuleBasedSamplerTest.java | 95 --- .../consistent56/ConsistentSamplerTest.java | 274 --------- .../consistent56/OtelTraceStateTest.java | 83 --- .../RandomValueGeneratorsTest.java | 22 - .../sampler/consistent56/TestUtil.java | 28 - .../sampler/consistent56/UseCaseTest.java | 128 ---- 56 files changed, 1321 insertions(+), 4360 deletions(-) delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentAlwaysOffSampler.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentAlwaysOnSampler.java rename consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/{consistent56 => consistent}/ConsistentAnyOf.java (57%) delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentComposedAndSampler.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentComposedOrSampler.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentParentBasedSampler.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentProbabilityBasedSampler.java rename consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/{consistent56 => consistent}/ConsistentSamplingUtil.java (98%) create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentVariableThresholdSampler.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/RValueGenerator.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/RValueGenerators.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/Composable.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSampler.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSampler.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSampler.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentParentBasedSampler.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSampler.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSampler.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSampler.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentThresholdSampler.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentVariableThresholdSampler.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/OtelTraceState.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/Predicate.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/PredicatedSampler.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGenerator.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGenerators.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/SamplingIntent.java rename consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/{consistent56 => consistent}/CoinFlipSampler.java (72%) create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentAnyOfTest.java delete mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentProbabilityBasedSamplerTest.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentReservoirSamplingSpanProcessorTestUtil.java delete mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentSamplerTest.java rename consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/{consistent56 => consistent}/ConsistentSamplingUtilTest.java (82%) rename consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/{consistent56 => consistent}/ConsistentVariableThresholdSamplerTest.java (86%) rename consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/{consistent56 => consistent}/MarkingSampler.java (65%) create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/UseCaseTest.java delete mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSamplerTest.java delete mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSamplerTest.java delete mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOfTest.java delete mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSamplerTest.java delete mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSamplerTest.java delete mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSamplerTest.java delete mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplerTest.java delete mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/OtelTraceStateTest.java delete mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGeneratorsTest.java delete mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/TestUtil.java delete mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/UseCaseTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fcc35271e..4835b243fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,39 @@ ## Unreleased +### Consistent sampling + +- **Breaking change**: the consistent sampling module has been rebuilt on top + of the OpenTelemetry SDK incubator composable-samplers API in + `opentelemetry-sdk-extension-incubator`. Both the legacy OTEP 4673 implementation in + `io.opentelemetry.contrib.sampler.consistent` and the newer `consistent56` + package have been collapsed into a single `io.opentelemetry.contrib.sampler.consistent` + package that uses the SDK incubator implementations where available. + Removed classes (use the SDK incubator equivalents instead): + `ConsistentProbabilityBasedSampler`, `ConsistentComposedAndSampler`, + `ConsistentComposedOrSampler`, `ConsistentAlwaysOnSampler`, + `ConsistentAlwaysOffSampler`, `ConsistentParentBasedSampler`, + `ConsistentFixedThresholdSampler`, `ConsistentRuleBasedSampler`, + `Composable`, `SamplingIntent`, `RandomValueGenerator`, + `RandomValueGenerators`, `Predicate`, `PredicatedSampler`, + `RValueGenerator`, `RValueGenerators`. The remaining contrib-only samplers + (`ConsistentRateLimitingSampler`, `ConsistentVariableThresholdSampler`, + `ConsistentAnyOf`) now implement the SDK incubator + `io.opentelemetry.sdk.extension.incubator.trace.samplers.ComposableSampler` + interface, and `ConsistentReservoirSamplingSpanProcessor` is unchanged. + Use `CompositeSampler.wrap(composable)` to turn a composable sampler into + a `Sampler`. +- The `parentbased_consistent_probability` autoconfigure sampler name is + retained; the provider now builds its sampler on top of the SDK incubator + `ComposableSampler` / `CompositeSampler` API and produces a sampler + equivalent to + `CompositeSampler.wrap(ComposableSampler.parentThreshold(ComposableSampler.probability(p)))`. +- Migration mapping for the removed `ConsistentSampler` factories: + - `ConsistentSampler.alwaysOn()` -> `ComposableSampler.alwaysOn()` + - `ConsistentSampler.alwaysOff()` -> `ComposableSampler.alwaysOff()` + - `ConsistentSampler.probabilityBased(p)` -> `ComposableSampler.probability(p)` + - `ConsistentSampler.parentBased(root)` -> `ComposableSampler.parentThreshold(root)` + ### Dynamic control - Add SourceFormat string to enum conversion diff --git a/consistent-sampling/README.md b/consistent-sampling/README.md index 4f67765ac9..30a6f031cd 100644 --- a/consistent-sampling/README.md +++ b/consistent-sampling/README.md @@ -1,59 +1,27 @@ # Consistent sampling -There are two major components included here. - -## Original proposal implementation - -The original specification for consistent probability sampling is defined by - -and . -It supports sampling probabilities that are power of 2 (1, 1/2, 1/4, ...), and uses 8-bit `r-value` and 8-bit `p-value` in tracestate. - -The implementation of this proposal is contained by the package `io/opentelemetry/contrib/sampler/consistent` in this repository and provides various Sampler implementations. - -* **ConsistentSampler**: - abstract base class of all consistent sampler implementations below -* **ConsistentAlwaysOffSampler**: - see -* **ConsistentAlwaysOnSampler**: - see -* **ConsistentComposedAndSampler**: - allows combining two consistent samplers and samples when both samplers would sample -* **ConsistentComposedOrSampler**: - allows combining two consistent sampler and samples when at least one of both samplers would sample, - see -* **ConsistentParentBasedSampler**: - see -* **ConsistentProbabilityBasedSampler**: - see -* **ConsistentRateLimitingSampler**: - a rate limiting sampler based on exponential smoothing that dynamically adjusts the sampling - probability based on the estimated rate of spans occurring to satisfy a given rate of sampled spans - -## Current proposal implementation - The current version of the specification for consistent probability sampling is described by . It uses **56** bits for representing _rejection threshold_, which corresponds to a much wider range of sampling probabilities than the original proposal. -The implementation of the current proposal is contained by the package `io/opentelemetry/contrib/sampler/consistent56` in this repository and provides implementation for a number of different Samplers. +The implementation is contained by the package `io/opentelemetry/contrib/sampler/consistent` in this repository. +The OpenTelemetry SDK incubator module provides the composable-samplers building blocks +(`ComposableSampler`, `CompositeSampler`, and the `alwaysOn`/`alwaysOff`/`probability`/ +`parentThreshold`/`ruleBasedBuilder`/`annotating` factories) in +`opentelemetry-sdk-extension-incubator` under +`io.opentelemetry.sdk.extension.incubator.trace.samplers`. This module adds the following +contrib-only APIs: -* **ConsistentSampler** - abstract base class for all consistent sampler implementations -* **ComposableSampler**: - interface used to build hierarchies of Samplers, see [Composite Samplers](https://github.com/open-telemetry/oteps/pull/250) -* **ConsistentAlwaysOffSampler**: -* **ConsistentAlwaysOnSampler**: -* **ConsistentAnyOfSampler**: - allows combining several consistent samplers; it samples when at least one of them would sample, -* **ConsistentParentBasedSampler**: -* **ConsistentFixedThresholdSampler**: - consistent probability sampler that uses a predefined sampling probability +* **ConsistentSampler**: + convenience factory for the contrib-only samplers below; for the common samplers use + `ComposableSampler`'s static factories directly +* **ConsistentAnyOf**: + allows combining several composable samplers; it samples when at least one of them would sample * **ConsistentRateLimitingSampler**: a rate limiting sampler based on exponential smoothing that dynamically adjusts the sampling probability based on the estimated rate of spans occurring to satisfy a given rate of sampled spans -* **ConsistentRuleBasedSampler** - a sampler that performs stratified sampling by evaluating qualifying conditions and propagating the sampling decision from one of its delegate samplers +* **ConsistentVariableThresholdSampler**: + consistent probability sampler whose sampling probability can be updated at runtime ## Component owners diff --git a/consistent-sampling/build.gradle.kts b/consistent-sampling/build.gradle.kts index eb7c9da0fd..3440b114c9 100644 --- a/consistent-sampling/build.gradle.kts +++ b/consistent-sampling/build.gradle.kts @@ -9,6 +9,8 @@ otelJava.moduleName.set("io.opentelemetry.contrib.sampler") dependencies { api("io.opentelemetry:opentelemetry-sdk-trace") api("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") + api("io.opentelemetry:opentelemetry-sdk-extension-incubator") + testImplementation("org.hipparchus:hipparchus-core:4.0.3") testImplementation("org.hipparchus:hipparchus-stat:4.0.3") } diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentAlwaysOffSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentAlwaysOffSampler.java deleted file mode 100644 index 07a1b86791..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentAlwaysOffSampler.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent; - -import javax.annotation.concurrent.Immutable; - -@Immutable -final class ConsistentAlwaysOffSampler extends ConsistentSampler { - - ConsistentAlwaysOffSampler(RValueGenerator rValueGenerator) { - super(rValueGenerator); - } - - @Override - protected int getP(int parentP, boolean isRoot) { - return OtelTraceState.getMaxP(); - } - - @Override - public String getDescription() { - return "ConsistentAlwaysOffSampler"; - } -} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentAlwaysOnSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentAlwaysOnSampler.java deleted file mode 100644 index 68c9183b1a..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentAlwaysOnSampler.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent; - -import javax.annotation.concurrent.Immutable; - -@Immutable -final class ConsistentAlwaysOnSampler extends ConsistentSampler { - - ConsistentAlwaysOnSampler(RValueGenerator rValueGenerator) { - super(rValueGenerator); - } - - @Override - protected int getP(int parentP, boolean isRoot) { - return 0; - } - - @Override - public String getDescription() { - return "ConsistentAlwaysOnSampler"; - } -} 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/consistent/ConsistentAnyOf.java similarity index 57% rename from consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOf.java rename to consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentAnyOf.java index 8cb7c028f8..6a910768e9 100644 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOf.java +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentAnyOf.java @@ -3,18 +3,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.contrib.sampler.consistent56; +package io.opentelemetry.contrib.sampler.consistent; -import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold; -import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.isValidThreshold; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.getInvalidThreshold; +import static io.opentelemetry.contrib.sampler.consistent.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.extension.incubator.trace.samplers.ComposableSampler; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.SamplingIntent; import io.opentelemetry.sdk.trace.data.LinkData; import java.util.List; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; @@ -25,9 +28,9 @@ * uses the minimum threshold value received. */ @Immutable -final class ConsistentAnyOf extends ConsistentSampler { +final class ConsistentAnyOf implements ComposableSampler { - private final Composable[] delegates; + private final ComposableSampler[] delegates; private final String description; @@ -36,7 +39,7 @@ final class ConsistentAnyOf extends ConsistentSampler { * * @param delegates the delegate samplers */ - ConsistentAnyOf(@Nullable Composable... delegates) { + ConsistentAnyOf(@Nullable ComposableSampler... delegates) { if (delegates == null || delegates.length == 0) { throw new IllegalArgumentException( "At least one delegate must be specified for ConsistentAnyOf"); @@ -53,6 +56,7 @@ final class ConsistentAnyOf extends ConsistentSampler { @Override public SamplingIntent getSamplingIntent( Context parentContext, + String traceId, String name, SpanKind spanKind, Attributes attributes, @@ -65,64 +69,54 @@ public SamplingIntent getSamplingIntent( long minimumThreshold = getInvalidThreshold(); // If any of the delegates returning the threshold value equal to T returns true upon calling - // its IsAdjustedCountReliable() method, the resulting isAdjustedCountReliable is true, + // its isThresholdReliable() method, the resulting isThresholdReliable is true, // otherwise it is false. - boolean isAdjustedCountCorrect = false; + boolean isThresholdReliable = false; int k = 0; - for (Composable delegate : delegates) { + for (ComposableSampler delegate : delegates) { SamplingIntent delegateIntent = - delegate.getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks); + delegate.getSamplingIntent( + parentContext, traceId, name, spanKind, attributes, parentLinks); long delegateThreshold = delegateIntent.getThreshold(); if (isValidThreshold(delegateThreshold)) { if (isValidThreshold(minimumThreshold)) { if (delegateThreshold == minimumThreshold) { - if (delegateIntent.isAdjustedCountReliable()) { - isAdjustedCountCorrect = true; + if (delegateIntent.isThresholdReliable()) { + isThresholdReliable = true; } } else if (delegateThreshold < minimumThreshold) { minimumThreshold = delegateThreshold; - isAdjustedCountCorrect = delegateIntent.isAdjustedCountReliable(); + isThresholdReliable = delegateIntent.isThresholdReliable(); } } else { minimumThreshold = delegateThreshold; - isAdjustedCountCorrect = delegateIntent.isAdjustedCountReliable(); + isThresholdReliable = delegateIntent.isThresholdReliable(); } } intents[k++] = delegateIntent; } long resultingThreshold = minimumThreshold; - boolean isResultingAdjustedCountCorrect = isAdjustedCountCorrect; + boolean resultingThresholdReliable = isThresholdReliable; - return new SamplingIntent() { - @Override - public long getThreshold() { - return resultingThreshold; - } - - @Override - public boolean isAdjustedCountReliable() { - return isResultingAdjustedCountCorrect; - } + AttributesBuilder builder = Attributes.builder(); + for (SamplingIntent intent : intents) { + builder = builder.putAll(intent.getAttributes()); + } + Attributes mergedAttributes = builder.build(); - @Override - public Attributes getAttributes() { - AttributesBuilder builder = Attributes.builder(); - for (SamplingIntent intent : intents) { - builder = builder.putAll(intent.getAttributes()); - } - return builder.build(); - } + Function composedUpdater = + previousState -> { + TraceState state = previousState; + for (SamplingIntent intent : intents) { + state = intent.getTraceStateUpdater().apply(state); + } + return state; + }; - @Override - public TraceState updateTraceState(TraceState previousState) { - for (SamplingIntent intent : intents) { - previousState = intent.updateTraceState(previousState); - } - return previousState; - } - }; + return SamplingIntent.create( + resultingThreshold, resultingThresholdReliable, mergedAttributes, composedUpdater); } @Override diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentComposedAndSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentComposedAndSampler.java deleted file mode 100644 index efc7235be8..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentComposedAndSampler.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent; - -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, RValueGenerator rValueGenerator) { - super(rValueGenerator); - this.sampler1 = requireNonNull(sampler1); - this.sampler2 = requireNonNull(sampler2); - this.description = - "ConsistentComposedAndSampler{" - + "sampler1=" - + sampler1.getDescription() - + ",sampler2=" - + sampler2.getDescription() - + '}'; - } - - @Override - protected int getP(int parentP, boolean isRoot) { - int p1 = sampler1.getP(parentP, isRoot); - int p2 = sampler2.getP(parentP, isRoot); - if (OtelTraceState.isValidP(p1) && OtelTraceState.isValidP(p2)) { - return Math.max(p1, p2); - } else { - return OtelTraceState.getInvalidP(); - } - } - - @Override - public String getDescription() { - return description; - } -} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentComposedOrSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentComposedOrSampler.java deleted file mode 100644 index 78b101a39d..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentComposedOrSampler.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent; - -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, RValueGenerator rValueGenerator) { - super(rValueGenerator); - this.sampler1 = requireNonNull(sampler1); - this.sampler2 = requireNonNull(sampler2); - this.description = - "ConsistentComposedOrSampler{" - + "sampler1=" - + sampler1.getDescription() - + ",sampler2=" - + sampler2.getDescription() - + '}'; - } - - @Override - protected int getP(int parentP, boolean isRoot) { - int p1 = sampler1.getP(parentP, isRoot); - int p2 = sampler2.getP(parentP, isRoot); - if (OtelTraceState.isValidP(p1)) { - if (OtelTraceState.isValidP(p2)) { - return Math.min(p1, p2); - } - return p1; - } else { - if (OtelTraceState.isValidP(p2)) { - return p2; - } - return OtelTraceState.getInvalidP(); - } - } - - @Override - public String getDescription() { - return description; - } -} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentParentBasedSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentParentBasedSampler.java deleted file mode 100644 index 0a3c93c283..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentParentBasedSampler.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent; - -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 rValueGenerator the function to use for generating the r-value - */ - ConsistentParentBasedSampler(ConsistentSampler rootSampler, RValueGenerator rValueGenerator) { - super(rValueGenerator); - this.rootSampler = requireNonNull(rootSampler); - this.description = - "ConsistentParentBasedSampler{rootSampler=" + rootSampler.getDescription() + '}'; - } - - @Override - protected int getP(int parentP, boolean isRoot) { - if (isRoot) { - return rootSampler.getP(OtelTraceState.getInvalidP(), isRoot); - } else { - return parentP; - } - } - - @Override - public String getDescription() { - return description; - } -} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentProbabilityBasedSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentProbabilityBasedSampler.java deleted file mode 100644 index cb3f2da8be..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentProbabilityBasedSampler.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent; - -import java.util.Locale; -import javax.annotation.concurrent.Immutable; - -/** A consistent sampler that samples with a fixed probability. */ -@Immutable -final class ConsistentProbabilityBasedSampler extends ConsistentSampler { - - private final int lowerPValue; - private final int upperPValue; - private final double probabilityToUseLowerPValue; - private final String description; - private final RandomGenerator randomGenerator; - - /** - * Constructor. - * - * @param samplingProbability the sampling probability - * @param rValueGenerator the function to use for generating the r-value - */ - ConsistentProbabilityBasedSampler( - double samplingProbability, - RValueGenerator rValueGenerator, - RandomGenerator randomGenerator) { - super(rValueGenerator); - if (samplingProbability < 0.0 || samplingProbability > 1.0) { - throw new IllegalArgumentException("Sampling probability must be in range [0.0, 1.0]!"); - } - this.description = - String.format(Locale.ROOT, "ConsistentProbabilityBasedSampler{%.6f}", samplingProbability); - this.randomGenerator = randomGenerator; - - lowerPValue = getLowerBoundP(samplingProbability); - upperPValue = getUpperBoundP(samplingProbability); - - if (lowerPValue == upperPValue) { - probabilityToUseLowerPValue = 1; - } else { - double upperSamplingProbability = getSamplingProbability(lowerPValue); - double lowerSamplingProbability = getSamplingProbability(upperPValue); - probabilityToUseLowerPValue = - (samplingProbability - lowerSamplingProbability) - / (upperSamplingProbability - lowerSamplingProbability); - } - } - - @Override - protected int getP(int parentP, boolean isRoot) { - if (randomGenerator.nextBoolean(probabilityToUseLowerPValue)) { - return lowerPValue; - } else { - return upperPValue; - } - } - - @Override - public String getDescription() { - return description; - } -} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentRateLimitingSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentRateLimitingSampler.java index 14d6dfee4a..1dbc26d303 100644 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentRateLimitingSampler.java +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentRateLimitingSampler.java @@ -5,17 +5,26 @@ package io.opentelemetry.contrib.sampler.consistent; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.calculateSamplingProbability; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.calculateThreshold; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.getInvalidThreshold; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.isValidThreshold; import static java.util.Objects.requireNonNull; -import io.opentelemetry.sdk.trace.samplers.Sampler; -import java.util.Locale; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.ComposableSampler; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.SamplingIntent; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; 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 consistent {@link ComposableSampler} 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." @@ -65,19 +74,45 @@ *

  • {@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 { +final class ConsistentRateLimitingSampler implements ComposableSampler { + + 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 double effectiveDelegateProbability; private final long lastNanoTime; - State(double effectiveWindowCount, double effectiveWindowNanos, long lastNanoTime) { + State( + double effectiveWindowCount, + double effectiveWindowNanos, + long lastNanoTime, + double effectiveDelegateProbability) { this.effectiveWindowCount = effectiveWindowCount; this.effectiveWindowNanos = effectiveWindowNanos; this.lastNanoTime = lastNanoTime; + this.effectiveDelegateProbability = effectiveDelegateProbability; } } @@ -85,8 +120,9 @@ private static final class State { private final LongSupplier nanoTimeSupplier; private final double inverseAdaptationTimeNanos; private final double targetSpansPerNanosecondLimit; + private final double probabilitySmoothingFactor; private final AtomicReference state; - private final RandomGenerator randomGenerator; + private final ComposableSampler delegate; /** * Constructor. @@ -94,17 +130,15 @@ private static final class State { * @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 rValueGenerator the function to use for generating the r-value - * @param randomGenerator a random generator * @param nanoTimeSupplier a supplier for the current nano time */ ConsistentRateLimitingSampler( + ComposableSampler delegate, double targetSpansPerSecondLimit, double adaptationTimeSeconds, - RValueGenerator rValueGenerator, - RandomGenerator randomGenerator, LongSupplier nanoTimeSupplier) { - super(rValueGenerator); + + this.delegate = requireNonNull(delegate); if (targetSpansPerSecondLimit < 0.0) { throw new IllegalArgumentException("Limit for sampled spans per second must be nonnegative!"); @@ -113,64 +147,114 @@ private static final class State { throw new IllegalArgumentException("Adaptation rate must be nonnegative!"); } this.description = - String.format( - Locale.ROOT, - "ConsistentRateLimitingSampler{%.6f, %.6f}", - targetSpansPerSecondLimit, - adaptationTimeSeconds); + "ConsistentRateLimitingSampler{targetSpansPerSecondLimit=" + + targetSpansPerSecondLimit + + ", adaptationTimeSeconds=" + + adaptationTimeSeconds + + "}"; this.nanoTimeSupplier = requireNonNull(nanoTimeSupplier); - this.inverseAdaptationTimeNanos = 1e-9 / adaptationTimeSeconds; - this.targetSpansPerNanosecondLimit = 1e-9 * targetSpansPerSecondLimit; + this.inverseAdaptationTimeNanos = NANOS_IN_SECONDS / adaptationTimeSeconds; + this.targetSpansPerNanosecondLimit = NANOS_IN_SECONDS * targetSpansPerSecondLimit; + + 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. - this.state = new AtomicReference<>(new State(0, 0, nanoTimeSupplier.getAsLong())); + // First approximation of the probability smoothing factor alpha. + double t = 1.0 / (targetSpansPerSecondLimit * adaptationTimeSeconds); - this.randomGenerator = randomGenerator; + // 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 int getP(int parentP, 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 0; - } + public SamplingIntent getSamplingIntent( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + double suggestedProbability; + long suggestedThreshold; - int lowerPValue = getLowerBoundP(samplingProbability); - int upperPValue = getUpperBoundP(samplingProbability); + SamplingIntent delegateIntent = + delegate.getSamplingIntent(parentContext, traceId, name, spanKind, attributes, parentLinks); + long delegateThreshold = delegateIntent.getThreshold(); - if (lowerPValue == upperPValue) { - return lowerPValue; - } + if (isValidThreshold(delegateThreshold)) { + double delegateProbability = calculateSamplingProbability(delegateThreshold); + long currentNanoTime = nanoTimeSupplier.getAsLong(); + State currentState = + state.updateAndGet(s -> updateState(s, currentNanoTime, delegateProbability)); - double upperSamplingRate = getSamplingProbability(lowerPValue); - double lowerSamplingRate = getSamplingProbability(upperPValue); - double probabilityToUseLowerPValue = - (samplingProbability - lowerSamplingRate) / (upperSamplingRate - lowerSamplingRate); + double targetMaxProbability = + (currentState.effectiveWindowNanos * targetSpansPerNanosecondLimit) + / currentState.effectiveWindowCount; - if (randomGenerator.nextBoolean(probabilityToUseLowerPValue)) { - return lowerPValue; + if (currentState.effectiveDelegateProbability > targetMaxProbability) { + suggestedProbability = + targetMaxProbability / currentState.effectiveDelegateProbability * delegateProbability; + } else { + suggestedProbability = delegateProbability; + } + suggestedThreshold = calculateThreshold(suggestedProbability); } else { - return upperPValue; + suggestedThreshold = getInvalidThreshold(); } + + return SamplingIntent.create( + suggestedThreshold, + delegateIntent.isThresholdReliable(), + delegateIntent.getAttributes(), + delegateIntent.getTraceStateUpdater()); } @Override diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentSampler.java index 77d2d9de20..90fa4c5857 100644 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentSampler.java +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentSampler.java @@ -5,401 +5,94 @@ package io.opentelemetry.contrib.sampler.consistent; -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 io.opentelemetry.sdk.extension.incubator.trace.samplers.ComposableSampler; 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(RValueGenerators.getDefault()); - } - - /** - * Returns a {@link ConsistentSampler} that samples all spans. - * - * @param rValueGenerator the function to use for generating the r-value - * @return a sampler - */ - public static ConsistentSampler alwaysOn(RValueGenerator rValueGenerator) { - return new ConsistentAlwaysOnSampler(rValueGenerator); - } - - /** - * Returns a {@link ConsistentSampler} that does not sample any span. - * - * @return a sampler - */ - public static ConsistentSampler alwaysOff() { - return alwaysOff(RValueGenerators.getDefault()); - } - - /** - * Returns a {@link ConsistentSampler} that does not sample any span. - * - * @param rValueGenerator the function to use for generating the r-value - * @return a sampler - */ - public static ConsistentSampler alwaysOff(RValueGenerator rValueGenerator) { - return new ConsistentAlwaysOffSampler(rValueGenerator); - } - - /** - * 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, RValueGenerators.getDefault()); - } - - /** - * Returns a {@link ConsistentSampler} that samples each span with a fixed probability. - * - * @param samplingProbability the sampling probability - * @param rValueGenerator the function to use for generating the r-value - * @return a sampler - */ - public static ConsistentSampler probabilityBased( - double samplingProbability, RValueGenerator rValueGenerator) { - return new ConsistentProbabilityBasedSampler( - samplingProbability, rValueGenerator, RandomGenerator.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 - */ - public static ConsistentSampler parentBased(ConsistentSampler rootSampler) { - return parentBased(rootSampler, RValueGenerators.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 rValueGenerator the function to use for generating the r-value - */ - public static ConsistentSampler parentBased( - ConsistentSampler rootSampler, RValueGenerator rValueGenerator) { - return new ConsistentParentBasedSampler(rootSampler, rValueGenerator); - } +/** + * Factory entry points for the contrib-only consistent probability samplers that are not part of + * the upstream {@link ComposableSampler} API. + * + *

      + *
    • {@link ConsistentRateLimitingSampler} — adaptive rate limiting + *
    • {@link ConsistentVariableThresholdSampler} — fixed probability that can be updated at + * runtime + *
    • {@link ConsistentAnyOf} — the minimum-threshold combination of several composable + * samplers + *
    + * + *

    For the common samplers (always-on/off, fixed probability, parent-based, rule-based, + * annotating) use {@link ComposableSampler}'s static factories directly. To turn a {@link + * ComposableSampler} into a {@link io.opentelemetry.sdk.trace.samplers.Sampler} use {@link + * io.opentelemetry.sdk.extension.incubator.trace.samplers.CompositeSampler#wrap(ComposableSampler)}. + */ +public final class ConsistentSampler { /** - * 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) + * Returns a {@link ComposableSampler} that attempts to adjust the sampling probability + * dynamically to meet the target span rate. Spans are first passed to {@link + * ComposableSampler#alwaysOn()} and then rate-limited. */ - public static ConsistentSampler rateLimited( + static ComposableSampler rateLimited( double targetSpansPerSecondLimit, double adaptationTimeSeconds) { return rateLimited( - targetSpansPerSecondLimit, adaptationTimeSeconds, RValueGenerators.getDefault()); + ComposableSampler.alwaysOn(), targetSpansPerSecondLimit, adaptationTimeSeconds); } /** - * 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 rValueGenerator the function to use for generating the r-value + * Returns a {@link ComposableSampler} that honors the delegate's sampling decision as long as it + * seems to meet the target span rate. In case the delegate's sampling rate seems to exceed the + * target, the sampler attempts to decrease the effective sampling probability dynamically. */ - public static ConsistentSampler rateLimited( - double targetSpansPerSecondLimit, - double adaptationTimeSeconds, - RValueGenerator rValueGenerator) { + @SuppressWarnings("InconsistentOverloads") + public static ComposableSampler rateLimited( + ComposableSampler delegate, double targetSpansPerSecondLimit, double adaptationTimeSeconds) { return rateLimited( - targetSpansPerSecondLimit, adaptationTimeSeconds, rValueGenerator, System::nanoTime); + delegate, targetSpansPerSecondLimit, adaptationTimeSeconds, 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 rValueGenerator the function to use for generating the r-value - * @param nanoTimeSupplier a supplier for the current nano time - */ - static ConsistentSampler rateLimited( + // Package-private overloads exposing the nanoTimeSupplier for tests. + + static ComposableSampler rateLimited( double targetSpansPerSecondLimit, double adaptationTimeSeconds, - RValueGenerator rValueGenerator, LongSupplier nanoTimeSupplier) { - return new ConsistentRateLimitingSampler( + return rateLimited( + ComposableSampler.alwaysOn(), targetSpansPerSecondLimit, adaptationTimeSeconds, - rValueGenerator, - RandomGenerator.getDefault(), 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, RValueGenerators.getDefault()); - } - - /** - * Returns a {@link ConsistentSampler} that samples a span if either 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, RValueGenerators.getDefault()); - } - - private final RValueGenerator rValueGenerator; - - protected ConsistentSampler(RValueGenerator rValueGenerator) { - this.rValueGenerator = requireNonNull(rValueGenerator); - } - - private static boolean isInvariantViolated( - OtelTraceState otelTraceState, boolean isParentSampled) { - if (otelTraceState.hasValidR() && otelTraceState.hasValidP()) { - // if valid p- and r-values are given, they must be consistent with the isParentSampled flag - // see - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/tracestate-probability-sampling.md#sampled-flag - int p = otelTraceState.getP(); - int r = otelTraceState.getR(); - int maxP = OtelTraceState.getMaxP(); - boolean isInvariantTrue = ((p <= r) == isParentSampled) || (isParentSampled && (p == maxP)); - return !isInvariantTrue; - } else { - return false; - } - } - - @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(); - - TraceState parentTraceState = parentSpanContext.getTraceState(); - String otelTraceStateString = parentTraceState.get(OtelTraceState.TRACE_STATE_KEY); - OtelTraceState otelTraceState = OtelTraceState.parse(otelTraceStateString); - - if (!otelTraceState.hasValidR() || isInvariantViolated(otelTraceState, isParentSampled)) { - // unset p-value in case of an invalid r-value or in case of any invariant violation - otelTraceState.invalidateP(); - } - - // generate new r-value if not available - if (!otelTraceState.hasValidR()) { - otelTraceState.setR(Math.min(rValueGenerator.generate(traceId), OtelTraceState.getMaxR())); - } - - // determine and set new p-value that is used for the sampling decision - int newP = getP(otelTraceState.getP(), isRoot); - otelTraceState.setP(newP); - - // determine sampling decision - boolean isSampled; - if (otelTraceState.hasValidP()) { - isSampled = (otelTraceState.getP() <= otelTraceState.getR()); - } else { - // if new p-value is invalid, respect sampling decision of parent - isSampled = isParentSampled; - } - SamplingDecision samplingDecision = - isSampled ? SamplingDecision.RECORD_AND_SAMPLE : SamplingDecision.DROP; - - // invalidate p-value if not sampled - if (!isSampled) { - otelTraceState.invalidateP(); - } - - 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(); - } - }; + @SuppressWarnings("InconsistentOverloads") + static ComposableSampler rateLimited( + ComposableSampler delegate, + double targetSpansPerSecondLimit, + double adaptationTimeSeconds, + LongSupplier nanoTimeSupplier) { + return new ConsistentRateLimitingSampler( + delegate, targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier); } /** - * Returns the p-value that is used for the sampling decision. - * - *

    The returned p-value is translated into corresponding sampling probabilities as given in the - * following: - * - *

    p-value = 0 => sampling probability = 1 - * - *

    p-value = 1 => sampling probability = 1/2 - * - *

    p-value = 2 => sampling probability = 1/4 - * - *

    ... - * - *

    p-value = (z-2) => sampling probability = 1/2^(z-2) - * - *

    p-value = (z-1) => sampling probability = 1/2^(z-1) - * - *

    p-value = z => sampling probability = 0 - * - *

    Here z denotes OtelTraceState.getMaxP(). - * - *

    Any other p-values have no meaning and will lead to inconsistent sampling decisions. The - * parent sampled flag will define the sampling decision in this case. - * - *

    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 p-value must not depend directly or indirectly on the r-value. In - * particular this means that the parent sampled flag must not be used for the calculation of the - * p-value as the sampled flag depends itself on the r-value. - * - * @param parentP is the p-value (if known) that was used for a consistent sampling decision by - * the parent - * @param isRoot is true for the root span - * @return this Builder + * Returns a {@link ComposableSampler} with a fixed sampling probability that can be updated at + * runtime via {@link ConsistentVariableThresholdSampler#setSamplingProbability(double)}. */ - protected abstract int getP(int parentP, boolean isRoot); - - /** - * Returns the sampling probability for a given p-value. - * - * @param p the p-value - * @return the sampling probability in the range [0,1] - * @throws IllegalArgumentException if the given p-value is invalid - */ - protected static double getSamplingProbability(int p) { - if (OtelTraceState.isValidP(p)) { - if (p == OtelTraceState.getMaxP()) { - return 0.0; - } else { - return Double.longBitsToDouble((0x3FFL - p) << 52); - } - } else { - throw new IllegalArgumentException("Invalid p-value!"); - } + public static ConsistentVariableThresholdSampler updateableProbabilityBased( + double samplingProbability) { + return new ConsistentVariableThresholdSampler(samplingProbability); } - private static final double SMALLEST_POSITIVE_SAMPLING_PROBABILITY = - getSamplingProbability(OtelTraceState.getMaxP() - 1); - /** - * Returns the largest p-value for which {@code getSamplingProbability(p) >= samplingProbability}. + * Returns a {@link ComposableSampler} that queries all its delegates for their sampling + * threshold. The intention is to make a positive sampling decision if any of the delegates would + * make a positive decision. The returned sampler uses the minimum threshold value found among all + * delegates. * - * @param samplingProbability the sampling probability - * @return the p-value + * @param delegates the delegate samplers, at least one delegate must be specified */ - protected static int getLowerBoundP(double samplingProbability) { - if (!(samplingProbability >= 0.0 && samplingProbability <= 1.0)) { - throw new IllegalArgumentException(); - } - if (samplingProbability == 0.) { - return OtelTraceState.getMaxP(); - } else if (samplingProbability <= SMALLEST_POSITIVE_SAMPLING_PROBABILITY) { - return OtelTraceState.getMaxP() - 1; - } else { - long longSamplingProbability = Double.doubleToRawLongBits(samplingProbability); - long mantissa = longSamplingProbability & 0x000FFFFFFFFFFFFFL; - long exponent = longSamplingProbability >>> 52; // compare - // https://en.wikipedia.org/wiki/Double-precision_floating-point_format#Exponent_encoding - return (int) (0x3FFL - exponent) - (mantissa != 0 ? 1 : 0); - } + public static ComposableSampler anyOf(ComposableSampler... delegates) { + return new ConsistentAnyOf(delegates); } - /** - * Returns the smallest p-value for which {@code getSamplingProbability(p) <= - * samplingProbability}. - * - * @param samplingProbability the sampling probability - * @return the p-value - */ - protected static int getUpperBoundP(double samplingProbability) { - if (!(samplingProbability >= 0.0 && samplingProbability <= 1.0)) { - throw new IllegalArgumentException(); - } - if (samplingProbability <= SMALLEST_POSITIVE_SAMPLING_PROBABILITY) { - return OtelTraceState.getMaxP(); - } else { - long longSamplingProbability = Double.doubleToRawLongBits(samplingProbability); - long exponent = longSamplingProbability >>> 52; // compare - // https://en.wikipedia.org/wiki/Double-precision_floating-point_format#Exponent_encoding - return (int) (0x3FFL - exponent); - } - } + private ConsistentSampler() {} } 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/consistent/ConsistentSamplingUtil.java similarity index 98% rename from consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplingUtil.java rename to consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentSamplingUtil.java index 4abe4bf155..fd254424b8 100644 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplingUtil.java +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentSamplingUtil.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.contrib.sampler.consistent56; +package io.opentelemetry.contrib.sampler.consistent; import com.google.errorprone.annotations.CanIgnoreReturnValue; diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentVariableThresholdSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentVariableThresholdSampler.java new file mode 100644 index 0000000000..5515e16566 --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ConsistentVariableThresholdSampler.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent; + +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.calculateSamplingProbability; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.calculateThreshold; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.checkThreshold; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.getInvalidThreshold; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.getMaxThreshold; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.ComposableSampler; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.SamplingIntent; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; +import java.util.function.Function; + +public class ConsistentVariableThresholdSampler implements ComposableSampler { + + private volatile long threshold; + private volatile String description = ""; + + protected ConsistentVariableThresholdSampler(double samplingProbability) { + updateSamplingProbability(samplingProbability); + } + + @Override + public SamplingIntent getSamplingIntent( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + long threshold = this.threshold; + if (threshold == getMaxThreshold()) { + return SamplingIntent.create( + getInvalidThreshold(), false, Attributes.empty(), Function.identity()); + } + return SamplingIntent.create(threshold, true, Attributes.empty(), Function.identity()); + } + + @Override + public String getDescription() { + return description; + } + + public long getThreshold() { + return threshold; + } + + public void setSamplingProbability(double samplingProbability) { + updateSamplingProbability(samplingProbability); + } + + private void updateSamplingProbability(double samplingProbability) { + long threshold = calculateThreshold(samplingProbability); + checkThreshold(threshold); + this.threshold = threshold; + + String thresholdString; + if (threshold == getMaxThreshold()) { + thresholdString = "max"; + } else { + thresholdString = + ConsistentSamplingUtil.appendLast56BitHexEncodedWithoutTrailingZeros( + new StringBuilder(), threshold) + .toString(); + } + + // tiny eventual consistency where the description would be out of date with the threshold, + // but this doesn't really matter + this.description = + "ConsistentVariableThresholdSampler{threshold=" + + thresholdString + + ", sampling probability=" + + calculateSamplingProbability(threshold) + + "}"; + } +} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ParentBasedConsistentProbabilitySamplerProvider.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ParentBasedConsistentProbabilitySamplerProvider.java index 0ca7e17a9d..55ab3d4e24 100644 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ParentBasedConsistentProbabilitySamplerProvider.java +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/ParentBasedConsistentProbabilitySamplerProvider.java @@ -7,6 +7,8 @@ import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.ComposableSampler; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.CompositeSampler; import io.opentelemetry.sdk.trace.samplers.Sampler; public final class ParentBasedConsistentProbabilitySamplerProvider @@ -15,7 +17,8 @@ public final class ParentBasedConsistentProbabilitySamplerProvider @Override public Sampler createSampler(ConfigProperties config) { double samplingProbability = config.getDouble("otel.traces.sampler.arg", 1.0d); - return ConsistentSampler.parentBased(ConsistentSampler.probabilityBased(samplingProbability)); + return CompositeSampler.wrap( + ComposableSampler.parentThreshold(ComposableSampler.probability(samplingProbability))); } @Override diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/RValueGenerator.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/RValueGenerator.java deleted file mode 100644 index 21dae49eed..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/RValueGenerator.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent; - -/** - * A function for generating r-values. - * - *

    The distribution of r-values generated by this function must satisfy the table below. - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
    Required distribution of r-values
    r-valueProbability of r-value
    01/2
    11/4
    21/8
    31/16
    ……
    0 <= r <= 612**-(r+1)
    ……
    592**-60
    602**-61
    612**-62
    >=622**-62
    - * - * For more info see Methods - * for generating R-values. - */ -@FunctionalInterface -public interface RValueGenerator { - - int generate(String traceId); -} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/RValueGenerators.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/RValueGenerators.java deleted file mode 100644 index c556988e71..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent/RValueGenerators.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent; - -final class RValueGenerators { - - private static final RValueGenerator DEFAULT = createDefault(); - - static RValueGenerator getDefault() { - return DEFAULT; - } - - private static RValueGenerator createDefault() { - RandomGenerator randomGenerator = RandomGenerator.getDefault(); - return s -> randomGenerator.numberOfLeadingZerosOfRandomLong(); - } - - private RValueGenerators() {} -} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/Composable.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/Composable.java deleted file mode 100644 index 9a4443bb0c..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/Composable.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 Composable { - - /** - * 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 deleted file mode 100644 index 0ab08aef80..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSampler.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 javax.annotation.concurrent.Immutable; - -@Immutable -final class ConsistentAlwaysOffSampler extends ConsistentSampler { - - private static final ConsistentAlwaysOffSampler INSTANCE = new ConsistentAlwaysOffSampler(); - - private ConsistentAlwaysOffSampler() {} - - static ConsistentAlwaysOffSampler getInstance() { - return INSTANCE; - } - - @Override - public SamplingIntent getSamplingIntent( - Context parentContext, - String name, - SpanKind spanKind, - Attributes attributes, - List parentLinks) { - - return new SamplingIntent() { - @Override - public long getThreshold() { - return getInvalidThreshold(); - } - - @Override - public boolean isAdjustedCountReliable() { - return false; - } - }; - } - - @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 deleted file mode 100644 index 620261aadf..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSampler.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent56; - -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 -final class ConsistentAlwaysOnSampler extends ConsistentSampler { - - private static final ConsistentAlwaysOnSampler INSTANCE = new ConsistentAlwaysOnSampler(); - - private ConsistentAlwaysOnSampler() {} - - static ConsistentAlwaysOnSampler getInstance() { - return INSTANCE; - } - - @Override - public SamplingIntent getSamplingIntent( - Context parentContext, - String name, - SpanKind spanKind, - Attributes attributes, - List parentLinks) { - - return () -> getMinThreshold(); - } - - @Override - public String getDescription() { - return "ConsistentAlwaysOnSampler"; - } -} 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 deleted file mode 100644 index 253edf7090..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSampler.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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; - -public class ConsistentFixedThresholdSampler extends ConsistentThresholdSampler { - - private final long threshold; - private final String description; - - protected ConsistentFixedThresholdSampler(long threshold) { - this.threshold = getThreshold(threshold); - this.description = getThresholdDescription(threshold); - } - - protected ConsistentFixedThresholdSampler(double samplingProbability) { - this(calculateThreshold(samplingProbability)); - } - - @Override - public String getDescription() { - return description; - } - - @Override - public long getThreshold() { - 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 deleted file mode 100644 index 88583eacf9..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentParentBasedSampler.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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.getMinThreshold; -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; - -/** - * 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 -public class ConsistentParentBasedSampler extends ConsistentSampler { - - private final Composable 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 - */ - protected ConsistentParentBasedSampler(Composable rootSampler) { - this.rootSampler = requireNonNull(rootSampler); - this.description = - "ConsistentParentBasedSampler{rootSampler=" + rootSampler.getDescription() + '}'; - } - - @Override - public final 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.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; - boolean isParentAdjustedCountCorrect; - if (otelTraceState.hasValidThreshold()) { - parentThreshold = otelTraceState.getThreshold(); - isParentAdjustedCountCorrect = true; - } else { - // If no threshold, look at the sampled flag - parentThreshold = parentSpanContext.isSampled() ? getMinThreshold() : getInvalidThreshold(); - isParentAdjustedCountCorrect = false; - } - - return new SamplingIntent() { - @Override - public long getThreshold() { - return parentThreshold; - } - - @Override - public boolean isAdjustedCountReliable() { - return isParentAdjustedCountCorrect; - } - - @Override - public Attributes getAttributes() { - if (parentSpanContext.isRemote()) { - return getAttributesWhenParentRemote(name, spanKind, attributes, parentLinks); - } else { - return getAttributesWhenParentLocal(name, spanKind, attributes, parentLinks); - } - } - - @Override - public TraceState updateTraceState(TraceState parentState) { - return parentState; - } - }; - } - - protected Attributes getAttributesWhenParentLocal( - String name, SpanKind spanKind, Attributes attributes, List parentLinks) { - return Attributes.empty(); - } - - protected Attributes getAttributesWhenParentRemote( - String name, SpanKind spanKind, Attributes attributes, List parentLinks) { - return Attributes.empty(); - } - - @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 deleted file mode 100644 index 0075c5692b..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSampler.java +++ /dev/null @@ -1,279 +0,0 @@ -/* - * 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.calculateThreshold; -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; - -/** - * 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: - * - *

      - *
    • {@code effectiveWindowNanos} corresponds to {@code A(n)} - *
    • {@code effectiveWindowCount} corresponds to {@code C(n)} - *
    • {@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 { - - 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 double effectiveDelegateProbability; - private final long lastNanoTime; - - State( - double effectiveWindowCount, - double effectiveWindowNanos, - long lastNanoTime, - double effectiveDelegateProbability) { - this.effectiveWindowCount = effectiveWindowCount; - this.effectiveWindowNanos = effectiveWindowNanos; - this.lastNanoTime = lastNanoTime; - this.effectiveDelegateProbability = effectiveDelegateProbability; - } - } - - private final String description; - private final LongSupplier nanoTimeSupplier; - private final double inverseAdaptationTimeNanos; - private final double targetSpansPerNanosecondLimit; - private final double probabilitySmoothingFactor; - private final AtomicReference state; - private final Composable delegate; - - /** - * 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 nanoTimeSupplier a supplier for the current nano time - */ - ConsistentRateLimitingSampler( - Composable 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!"); - } - 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.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, 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, - currentAverageProbability); - } - - double decayFactor = Math.exp(-nanoTimeDelta * inverseAdaptationTimeNanos); - double currentEffectiveWindowCount = oldState.effectiveWindowCount * decayFactor + 1; - double currentEffectiveWindowNanos = - oldState.effectiveWindowNanos * decayFactor + nanoTimeDelta; - - return new State( - currentEffectiveWindowCount, - currentEffectiveWindowNanos, - currentNanoTime, - currentAverageProbability); - } - - @Override - 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 targetMaxProbability = - (currentState.effectiveWindowNanos * targetSpansPerNanosecondLimit) - / currentState.effectiveWindowCount; - - if (currentState.effectiveDelegateProbability > targetMaxProbability) { - suggestedProbability = - targetMaxProbability / currentState.effectiveDelegateProbability * delegateProbability; - } else { - suggestedProbability = delegateProbability; - } - suggestedThreshold = calculateThreshold(suggestedProbability); - } else { - suggestedThreshold = getInvalidThreshold(); - } - - return new SamplingIntent() { - @Override - public long getThreshold() { - return suggestedThreshold; - } - - @Override - public boolean isAdjustedCountReliable() { - return delegateIntent.isAdjustedCountReliable(); - } - - @Override - public Attributes getAttributes() { - return delegateIntent.getAttributes(); - } - - @Override - public TraceState updateTraceState(TraceState previousState) { - return delegateIntent.updateTraceState(previousState); - } - }; - } - - @Override - public String getDescription() { - return description; - } -} 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 deleted file mode 100644 index 4d38a22127..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSampler.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 Composables - * 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 deleted file mode 100644 index 22ee83b8c7..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSampler.java +++ /dev/null @@ -1,257 +0,0 @@ -/* - * 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.isValidThreshold; - -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; -import javax.annotation.Nullable; - -/** Abstract base class for consistent samplers. */ -@SuppressWarnings("InconsistentOverloads") -public abstract class ConsistentSampler implements Sampler, Composable { - - /** - * Returns a {@link ConsistentSampler} that samples all spans. - * - * @return a sampler - */ - public static ConsistentSampler alwaysOn() { - return ConsistentAlwaysOnSampler.getInstance(); - } - - /** - * Returns a {@link ConsistentSampler} that does not sample any span. - * - * @return a sampler - */ - public static ConsistentSampler alwaysOff() { - return ConsistentAlwaysOffSampler.getInstance(); - } - - /** - * 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) { - long threshold = ConsistentSamplingUtil.calculateThreshold(samplingProbability); - return new ConsistentFixedThresholdSampler(threshold); - } - - /** - * Returns a {@link ConsistentSampler} that samples each span with a known probability, where the - * probablity can be dynamically updated. - * - * @param samplingProbability the sampling probability - * @return a sampler - */ - public static ConsistentSampler updateableProbabilityBased(double samplingProbability) { - return new ConsistentVariableThresholdSampler(samplingProbability); - } - - /** - * 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(Composable 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. - * - * @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) - */ - static ConsistentSampler rateLimited( - double targetSpansPerSecondLimit, double adaptationTimeSeconds) { - 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( - Composable delegate, double targetSpansPerSecondLimit, double adaptationTimeSeconds) { - return rateLimited( - delegate, targetSpansPerSecondLimit, adaptationTimeSeconds, 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 nanoTimeSupplier a supplier for the current nano time - */ - static ConsistentSampler rateLimited( - double targetSpansPerSecondLimit, - double adaptationTimeSeconds, - LongSupplier nanoTimeSupplier) { - return rateLimited( - alwaysOn(), targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier); - } - - /** - * 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) - * @param nanoTimeSupplier a supplier for the current nano time - */ - static ConsistentSampler rateLimited( - Composable delegate, - double targetSpansPerSecondLimit, - double adaptationTimeSeconds, - LongSupplier nanoTimeSupplier) { - return new ConsistentRateLimitingSampler( - delegate, targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier); - } - - /** - * 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 samplers individually and combining the results afterwards. - * - * @param delegates the delegate samplers, at least one delegate must be specified - * @return the ConsistentAnyOf sampler - */ - public static ConsistentSampler anyOf(Composable... delegates) { - return new ConsistentAnyOf(delegates); - } - - @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(); - - TraceState parentTraceState = parentSpanContext.getTraceState(); - String otelTraceStateString = parentTraceState.get(OtelTraceState.TRACE_STATE_KEY); - OtelTraceState otelTraceState = OtelTraceState.parse(otelTraceStateString); - - SamplingIntent intent = - getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks); - long threshold = intent.getThreshold(); - - // determine sampling decision - boolean isSampled; - boolean isAdjustedCountCorrect; - if (isValidThreshold(threshold)) { - isAdjustedCountCorrect = intent.isAdjustedCountReliable(); - // determine the randomness value to use - long randomness; - if (isAdjustedCountCorrect) { - randomness = getRandomness(otelTraceState, traceId); - } else { - // We cannot assume any particular distribution of the provided trace randomness, - // because the sampling decision may depend directly or indirectly on the randomness value; - // however, we still want to sample with probability corresponding to the obtained threshold - randomness = RandomValueGenerators.getDefault().generate(traceId); - } - isSampled = threshold <= randomness; - } else { // invalid threshold, DROP - isSampled = false; - isAdjustedCountCorrect = false; - } - - SamplingDecision samplingDecision = - isSampled ? SamplingDecision.RECORD_AND_SAMPLE : SamplingDecision.DROP; - - // determine tracestate changes - if (isSampled && isAdjustedCountCorrect) { - otelTraceState.setThreshold(threshold); - } else { - otelTraceState.invalidateThreshold(); - } - - String newOtTraceState = otelTraceState.serialize(); - - return new SamplingResult() { - - @Override - public SamplingDecision getDecision() { - return samplingDecision; - } - - @Override - public Attributes getAttributes() { - return intent.getAttributes(); - } - - @Override - public TraceState getUpdatedTraceState(TraceState parentTraceState) { - return intent.updateTraceState(parentTraceState).toBuilder() - .put(OtelTraceState.TRACE_STATE_KEY, newOtTraceState) - .build(); - } - }; - } - - 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/ConsistentThresholdSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentThresholdSampler.java deleted file mode 100644 index 63c1dbeaa4..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentThresholdSampler.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent56; - -import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.appendLast56BitHexEncodedWithoutTrailingZeros; -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 abstract class ConsistentThresholdSampler extends ConsistentSampler { - - protected abstract long getThreshold(); - - protected static long getThreshold(long threshold) { - checkThreshold(threshold); - return threshold; - } - - protected static String getThresholdDescription(long threshold) { - String thresholdString; - if (threshold == getMaxThreshold()) { - thresholdString = "max"; - } else { - thresholdString = - appendLast56BitHexEncodedWithoutTrailingZeros(new StringBuilder(), threshold).toString(); - } - - return "ConsistentFixedThresholdSampler{threshold=" - + thresholdString - + ", sampling probability=" - + calculateSamplingProbability(threshold) - + "}"; - } - - @Override - public SamplingIntent getSamplingIntent( - Context parentContext, - String name, - SpanKind spanKind, - Attributes attributes, - List parentLinks) { - - return () -> { - if (getThreshold() == getMaxThreshold()) { - return getInvalidThreshold(); - } - return getThreshold(); - }; - } -} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentVariableThresholdSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentVariableThresholdSampler.java deleted file mode 100644 index 9d356d49b5..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentVariableThresholdSampler.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.calculateThreshold; -import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.checkThreshold; -import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMaxThreshold; - -public class ConsistentVariableThresholdSampler extends ConsistentThresholdSampler { - - private volatile long threshold; - private volatile String description = ""; - - protected ConsistentVariableThresholdSampler(double samplingProbability) { - updateSamplingProbability(samplingProbability); - } - - @Override - public String getDescription() { - return description; - } - - @Override - public long getThreshold() { - return threshold; - } - - public void setSamplingProbability(double samplingProbability) { - updateSamplingProbability(samplingProbability); - } - - private void updateSamplingProbability(double samplingProbability) { - long threshold = calculateThreshold(samplingProbability); - checkThreshold(threshold); - this.threshold = threshold; - - String thresholdString; - if (threshold == getMaxThreshold()) { - thresholdString = "max"; - } else { - thresholdString = - ConsistentSamplingUtil.appendLast56BitHexEncodedWithoutTrailingZeros( - new StringBuilder(), threshold) - .toString(); - } - - // tiny eventual consistency where the description would be out of date with the threshold, - // but this doesn't really matter - this.description = - "ConsistentVariableThresholdSampler{threshold=" - + thresholdString - + ", sampling probability=" - + calculateSamplingProbability(threshold) - + "}"; - } -} 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 deleted file mode 100644 index e546b184b1..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/OtelTraceState.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * 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/Predicate.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/Predicate.java deleted file mode 100644 index 7cbd706d69..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/Predicate.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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 will only match Spans with local parent - */ - static Predicate hasLocalParent() { - return (parentContext, name, spanKind, attributes, parentLinks) -> { - Span parentSpan = Span.fromContext(parentContext); - SpanContext parentSpanContext = parentSpan.getSpanContext(); - return !parentSpanContext.isValid() || !parentSpanContext.isRemote(); - }; - } - - /* - * 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 deleted file mode 100644 index ac3bc7fa1f..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/PredicatedSampler.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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, Composable) */ -public final class PredicatedSampler { - - public static PredicatedSampler onMatch(Predicate predicate, Composable sampler) { - return new PredicatedSampler(predicate, sampler); - } - - private final Predicate predicate; - private final Composable sampler; - - private PredicatedSampler(Predicate predicate, Composable sampler) { - this.predicate = requireNonNull(predicate); - this.sampler = requireNonNull(sampler); - } - - public Predicate getPredicate() { - return predicate; - } - - public Composable getSampler() { - return sampler; - } -} 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 deleted file mode 100644 index 39ba47b033..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGenerator.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 deleted file mode 100644 index fcd0f6f5fe..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGenerators.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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/main/java/io/opentelemetry/contrib/sampler/consistent56/SamplingIntent.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/SamplingIntent.java deleted file mode 100644 index 50a9b67164..0000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/SamplingIntent.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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(); - - /* - * Return true if the adjusted count (calculated as reciprocal of the sampling probability) can be faithfully used to estimate span metrics. - */ - default boolean isAdjustedCountReliable() { - return true; - } - - /** - * 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/consistent/CoinFlipSampler.java similarity index 72% rename from consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/CoinFlipSampler.java rename to consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/CoinFlipSampler.java index c49b6b0e29..239222b237 100644 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/CoinFlipSampler.java +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/CoinFlipSampler.java @@ -3,13 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.contrib.sampler.consistent56; +package io.opentelemetry.contrib.sampler.consistent; 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.extension.incubator.trace.samplers.ComposableSampler; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.SamplingIntent; import io.opentelemetry.sdk.trace.data.LinkData; import java.util.List; import java.util.SplittableRandom; @@ -20,12 +22,12 @@ * of its two delegates. Used by unit tests. */ @Immutable -final class CoinFlipSampler extends ConsistentSampler { +final class CoinFlipSampler implements ComposableSampler { private static final SplittableRandom random = new SplittableRandom(0x160a50a2073e17e6L); - private final Composable samplerA; - private final Composable samplerB; + private final ComposableSampler samplerA; + private final ComposableSampler samplerB; private final double probability; private final String description; @@ -36,7 +38,7 @@ final class CoinFlipSampler extends ConsistentSampler { * @param samplerA the first delegate sampler * @param samplerB the second delegate sampler */ - CoinFlipSampler(Composable samplerA, Composable samplerB) { + CoinFlipSampler(ComposableSampler samplerA, ComposableSampler samplerB) { this(samplerA, samplerB, 0.5); } @@ -48,7 +50,7 @@ final class CoinFlipSampler extends ConsistentSampler { * @param samplerA the first delegate sampler * @param samplerB the second delegate sampler */ - CoinFlipSampler(Composable samplerA, Composable samplerB, double probability) { + CoinFlipSampler(ComposableSampler samplerA, ComposableSampler samplerB, double probability) { this.samplerA = requireNonNull(samplerA); this.samplerB = requireNonNull(samplerB); this.probability = probability; @@ -66,15 +68,18 @@ final class CoinFlipSampler extends ConsistentSampler { @Override public SamplingIntent getSamplingIntent( Context parentContext, + String traceId, String name, SpanKind spanKind, Attributes attributes, List parentLinks) { if (random.nextDouble() < probability) { - return samplerA.getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks); + return samplerA.getSamplingIntent( + parentContext, traceId, name, spanKind, attributes, parentLinks); } else { - return samplerB.getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks); + return samplerB.getSamplingIntent( + parentContext, traceId, name, spanKind, attributes, parentLinks); } } diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentAnyOfTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentAnyOfTest.java new file mode 100644 index 0000000000..55acc2d442 --- /dev/null +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentAnyOfTest.java @@ -0,0 +1,130 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent; + +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.getInvalidThreshold; +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; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.ComposableSampler; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.SamplingIntent; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; +import java.util.function.Function; +import org.junit.jupiter.api.Test; + +class ConsistentAnyOfTest { + + static class TestSampler implements ComposableSampler { + private final long threshold; + private final boolean thresholdReliable; + + public TestSampler(long threshold, boolean thresholdReliable) { + this.threshold = threshold; + this.thresholdReliable = thresholdReliable; + } + + @Override + public SamplingIntent getSamplingIntent( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + + return SamplingIntent.create( + threshold, thresholdReliable, Attributes.empty(), Function.identity()); + } + + @Override + public String getDescription() { + return "MockSampler for tests"; + } + } + + @Test + void testMinimumThresholdReliable() { + ComposableSampler delegate1 = + new TestSampler(0x80000000000000L, /* thresholdReliable= */ false); + ComposableSampler delegate2 = new TestSampler(0x30000000000000L, /* thresholdReliable= */ true); + ComposableSampler delegate3 = + new TestSampler(0xa0000000000000L, /* thresholdReliable= */ false); + ComposableSampler delegate4 = + new TestSampler(0x30000000000000L, /* thresholdReliable= */ false); + + ComposableSampler sampler = ConsistentSampler.anyOf(delegate1, delegate2, delegate3, delegate4); + SamplingIntent intent = sampler.getSamplingIntent(null, "tid", "span_name", null, null, null); + assertThat(intent.getThreshold()).isEqualTo(0x30000000000000L); + assertThat(intent.isThresholdReliable()).isTrue(); + + // Change the delegate order + sampler = ConsistentSampler.anyOf(delegate1, delegate4, delegate3, delegate2); + intent = sampler.getSamplingIntent(null, "tid", "span_name", null, null, null); + assertThat(intent.getThreshold()).isEqualTo(0x30000000000000L); + assertThat(intent.isThresholdReliable()).isTrue(); + } + + @Test + void testMinimumThresholdUnreliable() { + ComposableSampler delegate1 = new TestSampler(0x80000000000000L, /* thresholdReliable= */ true); + ComposableSampler delegate2 = + new TestSampler(0x30000000000000L, /* thresholdReliable= */ false); + ComposableSampler delegate3 = new TestSampler(0xa0000000000000L, /* thresholdReliable= */ true); + + ComposableSampler sampler = ConsistentSampler.anyOf(delegate1, delegate2, delegate3); + SamplingIntent intent = sampler.getSamplingIntent(null, "tid", "span_name", null, null, null); + assertThat(intent.getThreshold()).isEqualTo(0x30000000000000L); + assertThat(intent.isThresholdReliable()).isFalse(); + } + + @Test + void testAlwaysDrop() { + ComposableSampler delegate1 = ComposableSampler.alwaysOff(); + ComposableSampler sampler = ConsistentSampler.anyOf(delegate1); + SamplingIntent intent = sampler.getSamplingIntent(null, "tid", "span_name", null, null, null); + assertThat(intent.getThreshold()).isEqualTo(getInvalidThreshold()); + assertThat(intent.isThresholdReliable()).isFalse(); + } + + @Test + void testSpanAttributesAdded() { + AttributeKey key1 = AttributeKey.stringKey("tag1"); + AttributeKey key2 = AttributeKey.stringKey("tag2"); + AttributeKey key3 = AttributeKey.stringKey("tag3"); + ComposableSampler delegate1 = + new MarkingSampler( + new TestSampler(0x30000000000000L, /* thresholdReliable= */ true), key1, "a"); + ComposableSampler delegate2 = + new MarkingSampler( + new TestSampler(0x50000000000000L, /* thresholdReliable= */ true), key2, "b"); + ComposableSampler delegate3 = new MarkingSampler(ComposableSampler.alwaysOff(), key3, "c"); + ComposableSampler sampler = ConsistentSampler.anyOf(delegate1, delegate2, delegate3); + SamplingIntent intent = sampler.getSamplingIntent(null, "tid", "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); + assertThat(intent.isThresholdReliable()).isTrue(); + } + + @Test + void testSpanAttributeOverride() { + AttributeKey key1 = AttributeKey.stringKey("shared"); + ComposableSampler delegate1 = + new MarkingSampler( + new TestSampler(0x30000000000000L, /* thresholdReliable= */ true), key1, "a"); + ComposableSampler delegate2 = + new MarkingSampler( + new TestSampler(0x50000000000000L, /* thresholdReliable= */ true), key1, "b"); + ComposableSampler sampler = ConsistentSampler.anyOf(delegate1, delegate2); + SamplingIntent intent = sampler.getSamplingIntent(null, "tid", "span_name", null, null, null); + assertThat(intent.getAttributes().get(key1)).isEqualTo("b"); + } +} diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentProbabilityBasedSamplerTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentProbabilityBasedSamplerTest.java deleted file mode 100644 index a05506bc84..0000000000 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentProbabilityBasedSamplerTest.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent; - -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.HashMap; -import java.util.List; -import java.util.Map; -import java.util.SplittableRandom; -import org.hipparchus.stat.inference.GTest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class ConsistentProbabilityBasedSamplerTest { - - 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 test(SplittableRandom rng, double samplingProbability) { - int numSpans = 1000000; - - Sampler sampler = - ConsistentSampler.probabilityBased( - samplingProbability, - s -> RandomGenerator.create(rng::nextLong).numberOfLeadingZerosOfRandomLong()); - - Map observedPvalues = new HashMap<>(); - 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.hasValidR()).isTrue(); - assertThat(traceState.hasValidP()).isTrue(); - observedPvalues.merge(traceState.getP(), 1L, Long::sum); - } - } - verifyObservedPvaluesUsingGtest(numSpans, observedPvalues, samplingProbability); - } - - @Test - void test() { - - // fix seed to get reproducible results - SplittableRandom random = new SplittableRandom(0); - - test(random, 1.); - test(random, 0.5); - test(random, 0.25); - test(random, 0.125); - test(random, 0.0); - test(random, 0.45); - test(random, 0.2); - test(random, 0.13); - test(random, 0.05); - } - - private static void verifyObservedPvaluesUsingGtest( - long originalNumberOfSpans, Map observedPvalues, double samplingProbability) { - - Object notSampled = - new Object() { - @Override - public String toString() { - return "NOT SAMPLED"; - } - }; - - Map expectedProbabilities = new HashMap<>(); - if (samplingProbability >= 1.) { - expectedProbabilities.put(0, 1.); - } else if (samplingProbability <= 0.) { - expectedProbabilities.put(notSampled, 1.); - } else { - int exponent = 0; - while (true) { - if (Math.pow(0.5, exponent + 1) < samplingProbability - && Math.pow(0.5, exponent) >= samplingProbability) { - break; - } - exponent += 1; - } - if (samplingProbability == Math.pow(0.5, exponent)) { - expectedProbabilities.put(notSampled, 1 - samplingProbability); - expectedProbabilities.put(exponent, samplingProbability); - } else { - expectedProbabilities.put(notSampled, 1 - samplingProbability); - expectedProbabilities.put(exponent, 2 * samplingProbability - Math.pow(0.5, exponent)); - expectedProbabilities.put(exponent + 1, Math.pow(0.5, exponent) - samplingProbability); - } - } - - Map extendedObservedAdjustedCounts = new HashMap<>(observedPvalues); - long numberOfSpansNotSampled = - originalNumberOfSpans - observedPvalues.values().stream().mapToLong(i -> i).sum(); - if (numberOfSpansNotSampled > 0) { - extendedObservedAdjustedCounts.put(notSampled, numberOfSpansNotSampled); - } - - double[] expectedValues = new double[expectedProbabilities.size()]; - long[] observedValues = new long[expectedProbabilities.size()]; - - int counter = 0; - for (Object key : expectedProbabilities.keySet()) { - observedValues[counter] = extendedObservedAdjustedCounts.getOrDefault(key, 0L); - double p = expectedProbabilities.get(key); - expectedValues[counter] = p * originalNumberOfSpans; - counter += 1; - } - - if (expectedProbabilities.size() > 1) { - assertThat(new GTest().gTest(expectedValues, observedValues)).isGreaterThan(0.01); - } else { - assertThat((double) observedValues[0]).isEqualTo(expectedValues[0]); - } - } -} diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentRateLimitingSamplerTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentRateLimitingSamplerTest.java index 5097fdd2a5..0676565c5c 100644 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentRateLimitingSamplerTest.java +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentRateLimitingSamplerTest.java @@ -5,12 +5,22 @@ package io.opentelemetry.contrib.sampler.consistent; +import static io.opentelemetry.contrib.sampler.consistent.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.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.extension.incubator.trace.samplers.ComposableSampler; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.CompositeSampler; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.SamplingIntent; 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.ArrayList; @@ -27,23 +37,25 @@ class ConsistentRateLimitingSamplerTest { private long[] nanoTime; private LongSupplier nanoTimeSupplier; + private LongSupplier lowResolutionTimeSupplier; private Context parentContext; - private String traceId; private String name; private SpanKind spanKind; private Attributes attributes; private List parentLinks; + private SplittableRandom random; @BeforeEach void init() { nanoTime = new long[] {0L}; nanoTimeSupplier = () -> nanoTime[0]; + lowResolutionTimeSupplier = () -> (nanoTime[0] / 1000000) * 1000000; // 1ms resolution parentContext = Context.root(); - traceId = "0123456789abcdef0123456789abcdef"; name = "name"; spanKind = SpanKind.SERVER; attributes = Attributes.empty(); parentLinks = Collections.emptyList(); + random = new SplittableRandom(0L); } private void advanceTime(long nanosIncrement) { @@ -60,9 +72,12 @@ void testConstantRate() { double targetSpansPerSecondLimit = 1000; double adaptationTimeSeconds = 5; - ConsistentSampler sampler = - ConsistentSampler.rateLimited( - targetSpansPerSecondLimit, adaptationTimeSeconds, rValueGenerator(), nanoTimeSupplier); + ComposableSampler delegate = + new CoinFlipSampler(ComposableSampler.alwaysOff(), ComposableSampler.probability(0.8)); + Sampler sampler = + CompositeSampler.wrap( + ConsistentSampler.rateLimited( + delegate, targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier)); long nanosBetweenSpans = TimeUnit.MICROSECONDS.toNanos(100); int numSpans = 1000000; @@ -72,7 +87,58 @@ void testConstantRate() { for (int i = 0; i < numSpans; ++i) { advanceTime(nanosBetweenSpans); SamplingResult samplingResult = - sampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); + 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(ComposableSampler.alwaysOff(), ComposableSampler.probability(0.8)); + Sampler sampler = + CompositeSampler.wrap( + ConsistentSampler.rateLimited( + delegate, + targetSpansPerSecondLimit, + adaptationTimeSeconds, + lowResolutionTimeSupplier)); + + 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()); } @@ -93,9 +159,10 @@ void testRateIncrease() { double targetSpansPerSecondLimit = 1000; double adaptationTimeSeconds = 5; - ConsistentSampler sampler = - ConsistentSampler.rateLimited( - targetSpansPerSecondLimit, adaptationTimeSeconds, rValueGenerator(), nanoTimeSupplier); + Sampler sampler = + CompositeSampler.wrap( + ConsistentSampler.rateLimited( + targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier)); long nanosBetweenSpans1 = TimeUnit.MICROSECONDS.toNanos(100); long nanosBetweenSpans2 = TimeUnit.MICROSECONDS.toNanos(10); @@ -107,7 +174,13 @@ void testRateIncrease() { for (int i = 0; i < numSpans1; ++i) { advanceTime(nanosBetweenSpans1); SamplingResult samplingResult = - sampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); + sampler.shouldSample( + parentContext, + generateRandomTraceId(random), + name, + spanKind, + attributes, + parentLinks); if (SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult.getDecision())) { spanSampledNanos.add(getCurrentTimeNanos()); } @@ -115,7 +188,13 @@ void testRateIncrease() { for (int i = 0; i < numSpans2; ++i) { advanceTime(nanosBetweenSpans2); SamplingResult samplingResult = - sampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); + sampler.shouldSample( + parentContext, + generateRandomTraceId(random), + name, + spanKind, + attributes, + parentLinks); if (SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult.getDecision())) { spanSampledNanos.add(getCurrentTimeNanos()); } @@ -148,9 +227,10 @@ void testRateDecrease() { double targetSpansPerSecondLimit = 1000; double adaptationTimeSeconds = 5; - ConsistentSampler sampler = - ConsistentSampler.rateLimited( - targetSpansPerSecondLimit, adaptationTimeSeconds, rValueGenerator(), nanoTimeSupplier); + Sampler sampler = + CompositeSampler.wrap( + ConsistentSampler.rateLimited( + targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier)); long nanosBetweenSpans1 = TimeUnit.MICROSECONDS.toNanos(10); long nanosBetweenSpans2 = TimeUnit.MICROSECONDS.toNanos(100); @@ -162,7 +242,13 @@ void testRateDecrease() { for (int i = 0; i < numSpans1; ++i) { advanceTime(nanosBetweenSpans1); SamplingResult samplingResult = - sampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); + sampler.shouldSample( + parentContext, + generateRandomTraceId(random), + name, + spanKind, + attributes, + parentLinks); if (SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult.getDecision())) { spanSampledNanos.add(getCurrentTimeNanos()); } @@ -170,7 +256,13 @@ void testRateDecrease() { for (int i = 0; i < numSpans2; ++i) { advanceTime(nanosBetweenSpans2); SamplingResult samplingResult = - sampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); + sampler.shouldSample( + parentContext, + generateRandomTraceId(random), + name, + spanKind, + attributes, + parentLinks); if (SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult.getDecision())) { spanSampledNanos.add(getCurrentTimeNanos()); } @@ -197,9 +289,287 @@ void testRateDecrease() { .isCloseTo(targetSpansPerSecondLimit, Percentage.withPercentage(5)); } - private static RValueGenerator rValueGenerator() { - SplittableRandom random = new SplittableRandom(0L); - RandomGenerator randomGenerator = RandomGenerator.create(random::nextLong); - return s -> randomGenerator.numberOfLeadingZerosOfRandomLong(); + /** + * 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(ComposableSampler.probability(0.6), key, "A"), + new MarkingSampler(ComposableSampler.probability(0.4), key, "B")); + + Sampler sampler = + CompositeSampler.wrap( + 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 testUnstableDelegate() { + // Assume there are 10,000 spans/s and the delegate samples 50% of them with probability 100%, + // and unconditionally rejects the rest. + // + // Now, if we do not want to sample more than 1000 spans/s overall, the rate limiting + // sampler should calculate the effective threshold correctly. + + double targetSpansPerSecondLimit = 1000; + double adaptationTimeSeconds = 5; + + ComposableSampler delegate = + new CoinFlipSampler(ComposableSampler.alwaysOff(), ComposableSampler.alwaysOn()); + + Sampler sampler = + CompositeSampler.wrap( + ConsistentSampler.rateLimited( + delegate, targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier)); + + long averageRequestRatePerSecond = 10000; + int numSpans = 1000000; + + List spanSampledNanos = new ArrayList<>(); + + 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()); + } + } + + 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 testLegacyCase() { + // This test makes sure that the issue + // https://github.com/open-telemetry/opentelemetry-java-contrib/issues/2007 + // is resolved. + + long averageRequestRatePerSecond = 10000; + + // Assume the following setup: + // The root span is sampled by the legacy sampler AlwaysOn. + // One of its descendant spans, which we will call "parent" span, is sampled with + // stage1: ConsistentRateLimitingSampler(legacy-like root sampler, 5000/s). + // This will sample approximately 50% of the spans. + + // Its "child" is similarly sampled by + // stage2: ConsistentRateLimitingSampler(parentThreshold(alwaysOff), 2500/s). + + // This sampler will generate the same output as the root span described above: + // - the threshold will be 0, so all spans will be sampled + // - thresholdReliable will be false + // - there will be no threshold in TraceState, but the sampling flag will be set + ComposableSampler mockRootSampler = new LegacyLikeComposable(ComposableSampler.alwaysOn()); + + double targetSpansPerSecondLimit = 2500; // for stage2 + double adaptationTimeSeconds = 5; + + // The sampler for "parent" spans + Sampler stage1 = + CompositeSampler.wrap( + ConsistentSampler.rateLimited( + mockRootSampler, + 2 * targetSpansPerSecondLimit, + adaptationTimeSeconds, + nanoTimeSupplier)); + + // The sampler for "child" spans (it will never see root spans) + Sampler stage2 = + CompositeSampler.wrap( + ConsistentSampler.rateLimited( + ComposableSampler.parentThreshold(ComposableSampler.alwaysOff()), + targetSpansPerSecondLimit, + adaptationTimeSeconds, + nanoTimeSupplier)); + + int numSpans = 1000000; + int stage1SampledCount = 0; + int stage2SampledCount = 0; + + for (int i = 0; i < numSpans; ++i) { + advanceTime(randomInterval(averageRequestRatePerSecond)); + String traceId = generateRandomTraceId(random); + + // Stage 1 sampling, the "parent" + SamplingResult samplingResult1 = + stage1.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); + + boolean isSampled = SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult1.getDecision()); + if (isSampled) { + stage1SampledCount++; + } + + // Prepare the context for the child span, pass parent's TraceState to the child + Span parentSpan = Span.fromContext(parentContext); + SpanContext parentSpanContext = parentSpan.getSpanContext(); + TraceState parentSamplingTraceState = + samplingResult1.getUpdatedTraceState(parentSpanContext.getTraceState()); + + SpanContext childSpanContext = + SpanContext.create( + traceId, + "1000badbadbad000", + isSampled ? TraceFlags.getSampled() : TraceFlags.getDefault(), + parentSamplingTraceState); + Span childSpan = Span.wrap(childSpanContext); + Context childContext = childSpan.storeInContext(parentContext); + + // Stage 2 sampling, the "child" + SamplingResult samplingResult2 = + stage2.shouldSample(childContext, traceId, name, spanKind, attributes, parentLinks); + + if (SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult2.getDecision())) { + stage2SampledCount++; + } + } + + long timeNow = nanoTime[0]; + double duration = timeNow / 1000000000.0; // in seconds + assertThat(duration) + .isCloseTo(numSpans / (double) averageRequestRatePerSecond, Percentage.withPercentage(2)); + + assertThat(stage1SampledCount / duration) + .isCloseTo(2 * targetSpansPerSecondLimit, Percentage.withPercentage(2)); + + assertThat(stage2SampledCount / duration) + .isCloseTo(targetSpansPerSecondLimit, Percentage.withPercentage(2)); + } + + /* + * An auxiliary class used to simulate the behavior of a legacy (non consistent-probability) + * sampler, just for testing mixed environment + */ + static class LegacyLikeComposable implements ComposableSampler { + + private final ComposableSampler delegate; + + public LegacyLikeComposable(ComposableSampler delegate) { + this.delegate = delegate; + } + + @Override + public SamplingIntent getSamplingIntent( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + + SamplingIntent delegateIntent = + delegate.getSamplingIntent( + parentContext, traceId, name, spanKind, attributes, parentLinks); + + // Forcing "legacy" behavior, no threshold will be put into TraceState + return SamplingIntent.create( + delegateIntent.getThreshold(), + /* thresholdReliable= */ false, + delegateIntent.getAttributes(), + delegateIntent.getTraceStateUpdater()); + } + + @Override + public String getDescription() { + return "LegacyLike(" + delegate.getDescription() + ")"; + } + } + + @Test + void testDescription() { + + double targetSpansPerSecondLimit = 123.456; + double adaptationTimeSeconds = 7.89; + ComposableSampler 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/consistent/ConsistentReservoirSamplingSpanProcessorTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentReservoirSamplingSpanProcessorTest.java index 1415d0f6fc..2ca218aecc 100644 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentReservoirSamplingSpanProcessorTest.java +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentReservoirSamplingSpanProcessorTest.java @@ -6,7 +6,7 @@ package io.opentelemetry.contrib.sampler.consistent; import static io.opentelemetry.contrib.sampler.consistent.ConsistentReservoirSamplingSpanProcessor.DEFAULT_EXPORT_TIMEOUT_NANOS; -import static io.opentelemetry.contrib.sampler.consistent.TestUtil.verifyObservedPvaluesUsingGtest; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentReservoirSamplingSpanProcessorTestUtil.verifyObservedPvaluesUsingGtest; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @@ -19,15 +19,22 @@ import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; +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.api.trace.Tracer; +import io.opentelemetry.context.Context; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; import io.opentelemetry.sdk.trace.samplers.SamplingResult; import java.util.ArrayList; import java.util.Arrays; @@ -343,7 +350,7 @@ public void continuesIfExporterTimesOut() throws InterruptedException { when(mockSpanExporter.export(argThat(containsSpanName(SPAN_NAME_2, exportedAgain::countDown)))) .thenReturn(CompletableResultCode.ofSuccess()); createEndedSpan(SPAN_NAME_2, sdkTracerProvider); - exported.await(); + exportedAgain.await(); awaitReservoirEmpty(processor); shutdown(sdkTracerProvider); @@ -473,7 +480,7 @@ void fullReservoir() { SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() - .setSampler(ConsistentSampler.alwaysOn()) + .setSampler(Sampler.alwaysOn()) .addSpanProcessor(processor) .build(); @@ -531,9 +538,7 @@ private void testConsistentSampling( RandomGenerator randomGenerator = RandomGenerator.create(asThreadSafeLongSupplier(rng2)); SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() - .setSampler( - ConsistentSampler.probabilityBased( - samplingProbability, s -> randomGenerator.numberOfLeadingZerosOfRandomLong())) + .setSampler(probabilityBased(samplingProbability, randomGenerator)) .addSpanProcessor(processor) .build(); @@ -690,4 +695,153 @@ void testConsistentSampling() { testConsistentSampling( 0xc41d327fd1a6866aL, 1000000, 5, 4, 1.0, EnumSet.of(Tests.VERIFY_ORDER_INDEPENDENCE)); } + + private static final double SMALLEST_POSITIVE_SAMPLING_PROBABILITY = + getSamplingProbability(OtelTraceState.getMaxP() - 1); + + private static Sampler probabilityBased( + double samplingProbability, RandomGenerator rValueGenerator) { + int lowerPValue = getLowerBoundP(samplingProbability); + int upperPValue = getUpperBoundP(samplingProbability); + + double probabilityToUseLowerPValue; + if (lowerPValue == upperPValue) { + probabilityToUseLowerPValue = 1; + } else { + double upperSamplingProbability = getSamplingProbability(lowerPValue); + double lowerSamplingProbability = getSamplingProbability(upperPValue); + probabilityToUseLowerPValue = + (samplingProbability - lowerSamplingProbability) + / (upperSamplingProbability - lowerSamplingProbability); + } + RandomGenerator pValueGenerator = RandomGenerator.getDefault(); + + return new Sampler() { + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + Span parentSpan = Span.fromContext(parentContext); + SpanContext parentSpanContext = parentSpan.getSpanContext(); + boolean isParentSampled = parentSpanContext.isSampled(); + + TraceState parentTraceState = parentSpanContext.getTraceState(); + String otelTraceStateString = parentTraceState.get(OtelTraceState.TRACE_STATE_KEY); + OtelTraceState otelTraceState = OtelTraceState.parse(otelTraceStateString); + + if (!otelTraceState.hasValidR() || isInvariantViolated(otelTraceState, isParentSampled)) { + otelTraceState.invalidateP(); + } + + if (!otelTraceState.hasValidR()) { + otelTraceState.setR( + Math.min( + rValueGenerator.numberOfLeadingZerosOfRandomLong(), OtelTraceState.getMaxR())); + } + + if (pValueGenerator.nextBoolean(probabilityToUseLowerPValue)) { + otelTraceState.setP(lowerPValue); + } else { + otelTraceState.setP(upperPValue); + } + + boolean isSampled; + if (otelTraceState.hasValidP()) { + isSampled = otelTraceState.getP() <= otelTraceState.getR(); + } else { + isSampled = isParentSampled; + } + SamplingDecision samplingDecision = + isSampled ? SamplingDecision.RECORD_AND_SAMPLE : SamplingDecision.DROP; + + if (!isSampled) { + otelTraceState.invalidateP(); + } + + 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(); + } + }; + } + + @Override + public String getDescription() { + return "TestProbabilityBasedSampler{" + samplingProbability + "}"; + } + }; + } + + private static boolean isInvariantViolated( + OtelTraceState otelTraceState, boolean isParentSampled) { + if (otelTraceState.hasValidR() && otelTraceState.hasValidP()) { + int p = otelTraceState.getP(); + int r = otelTraceState.getR(); + int maxP = OtelTraceState.getMaxP(); + boolean isInvariantTrue = ((p <= r) == isParentSampled) || (isParentSampled && (p == maxP)); + return !isInvariantTrue; + } else { + return false; + } + } + + private static double getSamplingProbability(int p) { + if (OtelTraceState.isValidP(p)) { + if (p == OtelTraceState.getMaxP()) { + return 0.0; + } else { + return Double.longBitsToDouble((0x3FFL - p) << 52); + } + } else { + throw new IllegalArgumentException("Invalid p-value!"); + } + } + + private static int getLowerBoundP(double samplingProbability) { + if (!(samplingProbability >= 0.0 && samplingProbability <= 1.0)) { + throw new IllegalArgumentException(); + } + if (samplingProbability == 0.) { + return OtelTraceState.getMaxP(); + } else if (samplingProbability <= SMALLEST_POSITIVE_SAMPLING_PROBABILITY) { + return OtelTraceState.getMaxP() - 1; + } else { + long longSamplingProbability = Double.doubleToRawLongBits(samplingProbability); + long mantissa = longSamplingProbability & 0x000FFFFFFFFFFFFFL; + long exponent = longSamplingProbability >>> 52; + return (int) (0x3FFL - exponent) - (mantissa != 0 ? 1 : 0); + } + } + + private static int getUpperBoundP(double samplingProbability) { + if (!(samplingProbability >= 0.0 && samplingProbability <= 1.0)) { + throw new IllegalArgumentException(); + } + if (samplingProbability <= SMALLEST_POSITIVE_SAMPLING_PROBABILITY) { + return OtelTraceState.getMaxP(); + } else { + long longSamplingProbability = Double.doubleToRawLongBits(samplingProbability); + long exponent = longSamplingProbability >>> 52; + return (int) (0x3FFL - exponent); + } + } } diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentReservoirSamplingSpanProcessorTestUtil.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentReservoirSamplingSpanProcessorTestUtil.java new file mode 100644 index 0000000000..5b8d839b30 --- /dev/null +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentReservoirSamplingSpanProcessorTestUtil.java @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashMap; +import java.util.Map; +import org.hipparchus.stat.inference.GTest; + +public final class ConsistentReservoirSamplingSpanProcessorTestUtil { + + private ConsistentReservoirSamplingSpanProcessorTestUtil() {} + + public static void verifyObservedPvaluesUsingGtest( + long originalNumberOfSpans, Map observedPvalues, double samplingProbability) { + + Object notSampled = + new Object() { + @Override + public String toString() { + return "NOT SAMPLED"; + } + }; + + Map expectedProbabilities = new HashMap<>(); + if (samplingProbability >= 1.) { + expectedProbabilities.put(0, 1.); + } else if (samplingProbability <= 0.) { + expectedProbabilities.put(notSampled, 1.); + } else { + int exponent = 0; + while (true) { + if (Math.pow(0.5, exponent + 1) < samplingProbability + && Math.pow(0.5, exponent) >= samplingProbability) { + break; + } + exponent += 1; + } + if (samplingProbability == Math.pow(0.5, exponent)) { + expectedProbabilities.put(notSampled, 1 - samplingProbability); + expectedProbabilities.put(exponent, samplingProbability); + } else { + expectedProbabilities.put(notSampled, 1 - samplingProbability); + expectedProbabilities.put(exponent, 2 * samplingProbability - Math.pow(0.5, exponent)); + expectedProbabilities.put(exponent + 1, Math.pow(0.5, exponent) - samplingProbability); + } + } + + Map extendedObservedAdjustedCounts = new HashMap<>(observedPvalues); + long numberOfSpansNotSampled = + originalNumberOfSpans - observedPvalues.values().stream().mapToLong(i -> i).sum(); + if (numberOfSpansNotSampled > 0) { + extendedObservedAdjustedCounts.put(notSampled, numberOfSpansNotSampled); + } + + double[] expectedValues = new double[expectedProbabilities.size()]; + long[] observedValues = new long[expectedProbabilities.size()]; + + int counter = 0; + for (Object key : expectedProbabilities.keySet()) { + observedValues[counter] = extendedObservedAdjustedCounts.getOrDefault(key, 0L); + double p = expectedProbabilities.get(key); + expectedValues[counter] = p * originalNumberOfSpans; + counter += 1; + } + + if (expectedProbabilities.size() > 1) { + assertThat(new GTest().gTest(expectedValues, observedValues)).isGreaterThan(0.01); + } else { + assertThat((double) observedValues[0]).isEqualTo(expectedValues[0]); + } + } +} diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentSamplerTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentSamplerTest.java deleted file mode 100644 index 1a61868c85..0000000000 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentSamplerTest.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent; - -import static io.opentelemetry.contrib.sampler.consistent.OtelTraceState.getInvalidP; -import static io.opentelemetry.contrib.sampler.consistent.OtelTraceState.getInvalidR; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -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.OptionalInt; -import java.util.SplittableRandom; -import org.junit.jupiter.api.Test; - -class ConsistentSamplerTest { - - @Test - void testGetSamplingRate() { - assertThatThrownBy(() -> ConsistentSampler.getSamplingProbability(-1)) - .isInstanceOf(IllegalArgumentException.class); - for (int i = 0; i < OtelTraceState.getMaxP() - 1; i += 1) { - assertThat(ConsistentSampler.getSamplingProbability(i)).isEqualTo(Math.pow(0.5, i)); - } - assertThat(ConsistentSampler.getSamplingProbability(OtelTraceState.getMaxP())).isEqualTo(0.); - assertThatThrownBy(() -> ConsistentSampler.getSamplingProbability(OtelTraceState.getMaxP() + 1)) - .isInstanceOf(IllegalArgumentException.class); - } - - @Test - void testGetLowerBoundP() { - assertThat(ConsistentSampler.getLowerBoundP(1.0)).isEqualTo(0); - assertThat(ConsistentSampler.getLowerBoundP(Math.nextDown(1.0))).isEqualTo(0); - for (int i = 1; i < OtelTraceState.getMaxP() - 1; i += 1) { - double samplingProbability = Math.pow(0.5, i); - assertThat(ConsistentSampler.getLowerBoundP(samplingProbability)).isEqualTo(i); - assertThat(ConsistentSampler.getLowerBoundP(Math.nextUp(samplingProbability))) - .isEqualTo(i - 1); - assertThat(ConsistentSampler.getLowerBoundP(Math.nextDown(samplingProbability))).isEqualTo(i); - } - assertThat(ConsistentSampler.getLowerBoundP(Double.MIN_NORMAL)) - .isEqualTo(OtelTraceState.getMaxP() - 1); - assertThat(ConsistentSampler.getLowerBoundP(Double.MIN_VALUE)) - .isEqualTo(OtelTraceState.getMaxP() - 1); - assertThat(ConsistentSampler.getLowerBoundP(0.0)).isEqualTo(OtelTraceState.getMaxP()); - } - - @Test - void testGetUpperBoundP() { - assertThat(ConsistentSampler.getUpperBoundP(1.0)).isEqualTo(0); - assertThat(ConsistentSampler.getUpperBoundP(Math.nextDown(1.0))).isEqualTo(1); - for (int i = 1; i < OtelTraceState.getMaxP() - 1; i += 1) { - double samplingProbability = Math.pow(0.5, i); - assertThat(ConsistentSampler.getUpperBoundP(samplingProbability)).isEqualTo(i); - assertThat(ConsistentSampler.getUpperBoundP(Math.nextUp(samplingProbability))).isEqualTo(i); - assertThat(ConsistentSampler.getUpperBoundP(Math.nextDown(samplingProbability))) - .isEqualTo(i + 1); - } - assertThat(ConsistentSampler.getUpperBoundP(Double.MIN_NORMAL)) - .isEqualTo(OtelTraceState.getMaxP()); - assertThat(ConsistentSampler.getUpperBoundP(Double.MIN_VALUE)) - .isEqualTo(OtelTraceState.getMaxP()); - assertThat(ConsistentSampler.getUpperBoundP(0.0)).isEqualTo(OtelTraceState.getMaxP()); - } - - @Test - void testRandomValues() { - int numCycles = 1000; - SplittableRandom random = new SplittableRandom(0L); - for (int i = 0; i < numCycles; ++i) { - double samplingProbability = Math.exp(-1. / random.nextDouble()); - int pmin = ConsistentSampler.getLowerBoundP(samplingProbability); - int pmax = ConsistentSampler.getUpperBoundP(samplingProbability); - assertThat(ConsistentSampler.getSamplingProbability(pmin)) - .isGreaterThanOrEqualTo(samplingProbability); - assertThat(ConsistentSampler.getSamplingProbability(pmax)) - .isLessThanOrEqualTo(samplingProbability); - } - } - - private static ConsistentSampler createConsistentSampler(int p, int r) { - long randomLong = ~(0xFFFFFFFFFFFFFFFFL << r); - RandomGenerator randomGenerator = RandomGenerator.create(() -> randomLong); - - return new ConsistentSampler(s -> randomGenerator.numberOfLeadingZerosOfRandomLong()) { - @Override - public String getDescription() { - throw new UnsupportedOperationException(); - } - - @Override - protected int getP(int parentP, boolean isRoot) { - return p; - } - }; - } - - private static TraceState createTraceState(int p, int r) { - OtelTraceState state = OtelTraceState.parse(""); - state.setP(p); - state.setR(r); - return TraceState.builder().put(OtelTraceState.TRACE_STATE_KEY, state.serialize()).build(); - } - - private static Context createParentContext( - String traceId, String spanId, int p, int r, boolean sampled) { - TraceState parentTraceState = createTraceState(p, r); - 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 boolean getSampledFlag(SamplingResult samplingResult) { - return SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult.getDecision()); - } - - private static OptionalInt getP(SamplingResult samplingResult, Context parentContext) { - Span parentSpan = Span.fromContext(parentContext); - OtelTraceState otelTraceState = - OtelTraceState.parse( - samplingResult - .getUpdatedTraceState(parentSpan.getSpanContext().getTraceState()) - .get(OtelTraceState.TRACE_STATE_KEY)); - return otelTraceState.hasValidP() ? OptionalInt.of(otelTraceState.getP()) : OptionalInt.empty(); - } - - private static OptionalInt getR(SamplingResult samplingResult, Context parentContext) { - Span parentSpan = Span.fromContext(parentContext); - OtelTraceState otelTraceState = - OtelTraceState.parse( - samplingResult - .getUpdatedTraceState(parentSpan.getSpanContext().getTraceState()) - .get(OtelTraceState.TRACE_STATE_KEY)); - return otelTraceState.hasValidR() ? OptionalInt.of(otelTraceState.getR()) : OptionalInt.empty(); - } - - private static void assertConsistentSampling( - int parentP, - int parentR, - boolean parentSampled, - int samplerP, - int generatedR, - int expectedP, - int expectedR, - boolean expectSampled) { - - String traceId = "0123456789abcdef0123456789abcdef"; - String spanId = "0123456789abcdef"; - String name = "name"; - SpanKind spanKind = SpanKind.SERVER; - Attributes attributes = Attributes.empty(); - List parentLinks = Collections.emptyList(); - - Context parentContext = createParentContext(traceId, spanId, parentP, parentR, parentSampled); - ConsistentSampler sampler = createConsistentSampler(samplerP, generatedR); - SamplingResult samplingResult = - sampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); - - assertThat(getSampledFlag(samplingResult)).isEqualTo(expectSampled); - OptionalInt p = getP(samplingResult, parentContext); - if (OtelTraceState.isValidP(expectedP)) { - assertThat(p.getAsInt()).isEqualTo(expectedP); - } else { - assertThat(p.isPresent()).isFalse(); - } - OptionalInt r = getR(samplingResult, parentContext); - if (OtelTraceState.isValidR(expectedR)) { - assertThat(r.getAsInt()).isEqualTo(expectedR); - } else { - assertThat(r.isPresent()).isFalse(); - } - } - - private static final boolean NOT_SAMPLED = false; - private static final boolean SAMPLED = true; - - @Test - void testUndefinedParentTraceState() { - assertConsistentSampling(getInvalidP(), getInvalidR(), NOT_SAMPLED, 0, 0, 0, 0, SAMPLED); - assertConsistentSampling(getInvalidP(), getInvalidR(), NOT_SAMPLED, 2, 3, 2, 3, SAMPLED); - assertConsistentSampling( - getInvalidP(), getInvalidR(), NOT_SAMPLED, 3, 2, getInvalidP(), 2, NOT_SAMPLED); - assertConsistentSampling(getInvalidP(), getInvalidR(), NOT_SAMPLED, 0, 1, 0, 1, SAMPLED); - assertConsistentSampling(getInvalidP(), getInvalidR(), NOT_SAMPLED, 0, 2, 0, 2, SAMPLED); - assertConsistentSampling( - getInvalidP(), getInvalidR(), NOT_SAMPLED, 1, 0, getInvalidP(), 0, NOT_SAMPLED); - assertConsistentSampling( - getInvalidP(), getInvalidR(), NOT_SAMPLED, 2, 0, getInvalidP(), 0, NOT_SAMPLED); - assertConsistentSampling(getInvalidP(), getInvalidR(), SAMPLED, 0, 0, 0, 0, SAMPLED); - assertConsistentSampling(getInvalidP(), getInvalidR(), SAMPLED, 2, 3, 2, 3, SAMPLED); - assertConsistentSampling( - getInvalidP(), getInvalidR(), SAMPLED, 3, 2, getInvalidP(), 2, NOT_SAMPLED); - } - - @Test - void testParentTraceStateWithDefinedPOnly() { - assertConsistentSampling(6, getInvalidR(), NOT_SAMPLED, 0, 0, 0, 0, SAMPLED); - assertConsistentSampling(7, getInvalidR(), NOT_SAMPLED, 2, 3, 2, 3, SAMPLED); - assertConsistentSampling(4, getInvalidR(), NOT_SAMPLED, 3, 2, getInvalidP(), 2, NOT_SAMPLED); - assertConsistentSampling(3, getInvalidR(), NOT_SAMPLED, 0, 1, 0, 1, SAMPLED); - assertConsistentSampling(2, getInvalidR(), NOT_SAMPLED, 0, 2, 0, 2, SAMPLED); - assertConsistentSampling(6, getInvalidR(), NOT_SAMPLED, 1, 0, getInvalidP(), 0, NOT_SAMPLED); - assertConsistentSampling(7, getInvalidR(), NOT_SAMPLED, 2, 0, getInvalidP(), 0, NOT_SAMPLED); - assertConsistentSampling(5, getInvalidR(), NOT_SAMPLED, 8, 7, getInvalidP(), 7, NOT_SAMPLED); - assertConsistentSampling(5, getInvalidR(), NOT_SAMPLED, 6, 7, 6, 7, SAMPLED); - assertConsistentSampling(12, getInvalidR(), SAMPLED, 0, 0, 0, 0, SAMPLED); - assertConsistentSampling(15, getInvalidR(), SAMPLED, 2, 3, 2, 3, SAMPLED); - assertConsistentSampling(18, getInvalidR(), SAMPLED, 3, 2, getInvalidP(), 2, NOT_SAMPLED); - } - - @Test - void testParentTraceStateWithDefinedROnly() { - assertConsistentSampling(getInvalidP(), 0, NOT_SAMPLED, 0, 5, 0, 0, SAMPLED); - assertConsistentSampling(getInvalidP(), 3, NOT_SAMPLED, 2, 0, 2, 3, SAMPLED); - assertConsistentSampling(getInvalidP(), 2, NOT_SAMPLED, 3, 1, getInvalidP(), 2, NOT_SAMPLED); - assertConsistentSampling(getInvalidP(), 1, NOT_SAMPLED, 0, 0, 0, 1, SAMPLED); - assertConsistentSampling(getInvalidP(), 2, NOT_SAMPLED, 0, 5, 0, 2, SAMPLED); - assertConsistentSampling(getInvalidP(), 0, NOT_SAMPLED, 1, 8, getInvalidP(), 0, NOT_SAMPLED); - assertConsistentSampling(getInvalidP(), 0, NOT_SAMPLED, 2, 5, getInvalidP(), 0, NOT_SAMPLED); - assertConsistentSampling(getInvalidP(), 0, SAMPLED, 0, 11, 0, 0, SAMPLED); - assertConsistentSampling(getInvalidP(), 3, SAMPLED, 2, 9, 2, 3, SAMPLED); - assertConsistentSampling(getInvalidP(), 2, SAMPLED, 3, 1, getInvalidP(), 2, NOT_SAMPLED); - } - - @Test - void testConsistentParentTraceState() { - // ( (r >= p) <=> sampled) is satisfied - assertConsistentSampling(3, 5, SAMPLED, 6, 7, getInvalidP(), 5, NOT_SAMPLED); - assertConsistentSampling(3, 5, SAMPLED, 2, 7, 2, 5, SAMPLED); - assertConsistentSampling(5, 3, NOT_SAMPLED, 6, 7, getInvalidP(), 3, NOT_SAMPLED); - } - - @Test - void testInconsistentParentTraceState() { - // ( (r >= p) <=> sampled) is not satisfied - assertConsistentSampling(5, 3, SAMPLED, 6, 7, getInvalidP(), 3, NOT_SAMPLED); - assertConsistentSampling(3, 5, NOT_SAMPLED, 6, 7, getInvalidP(), 5, NOT_SAMPLED); - assertConsistentSampling(5, 3, SAMPLED, 1, 7, 1, 3, SAMPLED); - assertConsistentSampling(3, 5, NOT_SAMPLED, 2, 7, 2, 5, SAMPLED); - } - - @Test - void testInvalidSamplerP() { - assertConsistentSampling(3, 5, SAMPLED, getInvalidP(), 7, getInvalidP(), 5, SAMPLED); - assertConsistentSampling(5, 3, NOT_SAMPLED, getInvalidP(), 7, getInvalidP(), 3, NOT_SAMPLED); - } -} 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/consistent/ConsistentSamplingUtilTest.java similarity index 82% rename from consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplingUtilTest.java rename to consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentSamplingUtilTest.java index d612f9e0a5..7d70343737 100644 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplingUtilTest.java +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentSamplingUtilTest.java @@ -3,17 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.contrib.sampler.consistent56; +package io.opentelemetry.contrib.sampler.consistent; -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.getMaxThreshold; -import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMinThreshold; -import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.isValidRandomValue; -import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.isValidThreshold; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.calculateAdjustedCount; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.calculateSamplingProbability; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.calculateThreshold; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.getInvalidRandomValue; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.getInvalidThreshold; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.getMaxThreshold; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.getMinThreshold; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.isValidRandomValue; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.isValidThreshold; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentVariableThresholdSamplerTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentVariableThresholdSamplerTest.java similarity index 86% rename from consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentVariableThresholdSamplerTest.java rename to consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentVariableThresholdSamplerTest.java index 90428fad99..cfea507f97 100644 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentVariableThresholdSamplerTest.java +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/ConsistentVariableThresholdSamplerTest.java @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.contrib.sampler.consistent56; +package io.opentelemetry.contrib.sampler.consistent; -import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.calculateThreshold; -import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMaxThreshold; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.calculateThreshold; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.getMaxThreshold; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; 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/consistent/MarkingSampler.java similarity index 65% rename from consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/MarkingSampler.java rename to consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/MarkingSampler.java index 3e691da9c2..9453fc2029 100644 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/MarkingSampler.java +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/MarkingSampler.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.contrib.sampler.consistent56; +package io.opentelemetry.contrib.sampler.consistent; import static java.util.Objects.requireNonNull; @@ -11,8 +11,9 @@ 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.extension.incubator.trace.samplers.ComposableSampler; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.SamplingIntent; import io.opentelemetry.sdk.trace.data.LinkData; import java.util.List; import javax.annotation.concurrent.Immutable; @@ -23,9 +24,9 @@ * could be also offered as a general utility. */ @Immutable -final class MarkingSampler implements Composable { +final class MarkingSampler implements ComposableSampler { - private final Composable delegate; + private final ComposableSampler delegate; private final AttributeKey attributeKey; private final String attributeValue; @@ -38,7 +39,8 @@ final class MarkingSampler implements Composable { * @param attributeKey Span attribute key * @param attributeValue Span attribute value */ - MarkingSampler(Composable delegate, AttributeKey attributeKey, String attributeValue) { + MarkingSampler( + ComposableSampler delegate, AttributeKey attributeKey, String attributeValue) { this.delegate = requireNonNull(delegate); this.attributeKey = requireNonNull(attributeKey); this.attributeValue = requireNonNull(attributeValue); @@ -55,32 +57,23 @@ final class MarkingSampler implements Composable { @Override public SamplingIntent getSamplingIntent( Context parentContext, + String traceId, String name, SpanKind spanKind, Attributes attributes, List parentLinks) { SamplingIntent delegateIntent = - delegate.getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks); + delegate.getSamplingIntent(parentContext, traceId, name, spanKind, attributes, parentLinks); - return new SamplingIntent() { - @Override - public long getThreshold() { - return delegateIntent.getThreshold(); - } + AttributesBuilder builder = delegateIntent.getAttributes().toBuilder(); + builder = builder.put(attributeKey, attributeValue); - @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); - } - }; + return SamplingIntent.create( + delegateIntent.getThreshold(), + delegateIntent.isThresholdReliable(), + builder.build(), + delegateIntent.getTraceStateUpdater()); } @Override diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/TestUtil.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/TestUtil.java index d1ed0643be..418322bdce 100644 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/TestUtil.java +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/TestUtil.java @@ -5,73 +5,24 @@ package io.opentelemetry.contrib.sampler.consistent; -import static org.assertj.core.api.Assertions.assertThat; +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.HEX_DIGITS; -import java.util.HashMap; -import java.util.Map; -import org.hipparchus.stat.inference.GTest; +import java.util.SplittableRandom; public final class TestUtil { private TestUtil() {} - public static void verifyObservedPvaluesUsingGtest( - long originalNumberOfSpans, Map observedPvalues, double samplingProbability) { - - Object notSampled = - new Object() { - @Override - public String toString() { - return "NOT SAMPLED"; - } - }; - - Map expectedProbabilities = new HashMap<>(); - if (samplingProbability >= 1.) { - expectedProbabilities.put(0, 1.); - } else if (samplingProbability <= 0.) { - expectedProbabilities.put(notSampled, 1.); - } else { - int exponent = 0; - while (true) { - if (Math.pow(0.5, exponent + 1) < samplingProbability - && Math.pow(0.5, exponent) >= samplingProbability) { - break; - } - exponent += 1; - } - if (samplingProbability == Math.pow(0.5, exponent)) { - expectedProbabilities.put(notSampled, 1 - samplingProbability); - expectedProbabilities.put(exponent, samplingProbability); - } else { - expectedProbabilities.put(notSampled, 1 - samplingProbability); - expectedProbabilities.put(exponent, 2 * samplingProbability - Math.pow(0.5, exponent)); - expectedProbabilities.put(exponent + 1, Math.pow(0.5, exponent) - samplingProbability); - } - } - - Map extendedObservedAdjustedCounts = new HashMap<>(observedPvalues); - long numberOfSpansNotSampled = - originalNumberOfSpans - observedPvalues.values().stream().mapToLong(i -> i).sum(); - if (numberOfSpansNotSampled > 0) { - extendedObservedAdjustedCounts.put(notSampled, numberOfSpansNotSampled); + static String generateRandomTraceId(SplittableRandom random) { + StringBuilder sb = new StringBuilder(16); + long hi = random.nextLong(); + long lo = random.nextLong(); + for (int i = 0; i < 64; i += 4) { + sb.append(HEX_DIGITS[(int) (hi >>> i) & 0xF]); } - - double[] expectedValues = new double[expectedProbabilities.size()]; - long[] observedValues = new long[expectedProbabilities.size()]; - - int counter = 0; - for (Object key : expectedProbabilities.keySet()) { - observedValues[counter] = extendedObservedAdjustedCounts.getOrDefault(key, 0L); - double p = expectedProbabilities.get(key); - expectedValues[counter] = p * originalNumberOfSpans; - counter += 1; - } - - if (expectedProbabilities.size() > 1) { - assertThat(new GTest().gTest(expectedValues, observedValues)).isGreaterThan(0.01); - } else { - assertThat((double) observedValues[0]).isEqualTo(expectedValues[0]); + for (int i = 0; i < 64; i += 4) { + sb.append(HEX_DIGITS[(int) (lo >>> i) & 0xF]); } + return sb.toString(); } } diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/UseCaseTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/UseCaseTest.java new file mode 100644 index 0000000000..62542fe78e --- /dev/null +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent/UseCaseTest.java @@ -0,0 +1,147 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent; + +import static io.opentelemetry.contrib.sampler.consistent.ConsistentSamplingUtil.getInvalidThreshold; +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.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.ComposableSampler; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.SamplingIntent; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.SamplingPredicate; +import java.util.Collections; +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( + // parentThreshold( + // ruleBased(ROOT, { + // (http.target == /healthcheck) => alwaysOff, + // (http.target == /checkout) => alwaysOn, + // true => probability(0.25) + // }), + // ruleBased(CLIENT, { + // (http.url == /foo) => alwaysOn + // } + // ), + // 1000.0 + // ) + // + private static final AttributeKey httpTarget = AttributeKey.stringKey("http.target"); + private static final AttributeKey httpUrl = AttributeKey.stringKey("http.url"); + + private static ComposableSampler buildSampler() { + SamplingPredicate healthCheck = + (parentContext, traceId, name, spanKind, attributes, parentLinks) -> { + return isRootSpan(parentContext) && "/healthCheck".equals(attributes.get(httpTarget)); + }; + SamplingPredicate checkout = + (parentContext, traceId, name, spanKind, attributes, parentLinks) -> { + return isRootSpan(parentContext) && "/checkout".equals(attributes.get(httpTarget)); + }; + + ComposableSampler s1 = + ComposableSampler.parentThreshold( + ComposableSampler.ruleBasedBuilder() + .add(healthCheck, ComposableSampler.alwaysOff()) + .add(checkout, ComposableSampler.alwaysOn()) + .add( + (parentContext, traceId, name, spanKind, attributes, parentLinks) -> true, + ComposableSampler.probability(0.25)) + .build()); + + SamplingPredicate foo = + (parentContext, traceId, name, spanKind, attributes, parentLinks) -> { + return spanKind == SpanKind.CLIENT && "/foo".equals(attributes.get(httpUrl)); + }; + + ComposableSampler s2 = + ComposableSampler.ruleBasedBuilder().add(foo, ComposableSampler.alwaysOn()).build(); + ComposableSampler s3 = ConsistentSampler.anyOf(s1, s2); + return ConsistentSampler.rateLimited(s3, 1000.0, 5, UseCaseTest::nanoTime); + } + + @Test + void testDropHealthcheck() { + ComposableSampler s = buildSampler(); + Attributes attributes = createAttributes(httpTarget, "/healthCheck"); + SamplingIntent intent = + s.getSamplingIntent( + Context.root(), "A", "span_name", SpanKind.SERVER, attributes, Collections.emptyList()); + assertThat(intent.getThreshold()).isEqualTo(getInvalidThreshold()); + } + + @Test + void testSampleCheckout() { + ComposableSampler s = buildSampler(); + advanceTime(1000000); + Attributes attributes = createAttributes(httpTarget, "/checkout"); + SamplingIntent intent = + s.getSamplingIntent( + Context.root(), "B", "span_name", SpanKind.SERVER, attributes, Collections.emptyList()); + assertThat(intent.getThreshold()).isEqualTo(0L); + advanceTime(1000); // rate limiting should kick in + intent = + s.getSamplingIntent( + Context.root(), "B", "span_name", SpanKind.SERVER, attributes, Collections.emptyList()); + assertThat(intent.getThreshold()).isGreaterThan(0L); + } + + @Test + void testSampleClient() { + ComposableSampler s = buildSampler(); + advanceTime(1000000); + Attributes attributes = createAttributes(httpUrl, "/foo"); + SamplingIntent intent = + s.getSamplingIntent( + Context.root(), "C", "span_name", SpanKind.CLIENT, attributes, Collections.emptyList()); + assertThat(intent.getThreshold()).isEqualTo(0L); + } + + @Test + void testOtherRoot() { + ComposableSampler s = buildSampler(); + advanceTime(1000000); + Attributes attributes = Attributes.empty(); + SamplingIntent intent = + s.getSamplingIntent( + Context.root(), "D", "span_name", SpanKind.SERVER, attributes, Collections.emptyList()); + assertThat(intent.getThreshold()).isEqualTo(0xc0000000000000L); + } + + private static boolean isRootSpan(Context parentContext) { + Span parentSpan = Span.fromContext(parentContext); + SpanContext parentSpanContext = parentSpan.getSpanContext(); + return !parentSpanContext.isValid(); + } + + private static Attributes createAttributes(AttributeKey key, String value) { + return Attributes.builder().put(key, value).build(); + } +} 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 deleted file mode 100644 index d0425aa0b6..0000000000 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSamplerTest.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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 org.junit.jupiter.api.Test; - -class ConsistentAlwaysOffSamplerTest { - - @Test - void testDescription() { - assertThat(ConsistentSampler.alwaysOff().getDescription()) - .isEqualTo("ConsistentAlwaysOffSampler"); - } - - @Test - void testThreshold() { - 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 deleted file mode 100644 index 115c39c41a..0000000000 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSamplerTest.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent56; - -import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMinThreshold; -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Test; - -class ConsistentAlwaysOnSamplerTest { - - @Test - void testDescription() { - assertThat(ConsistentSampler.alwaysOn().getDescription()) - .isEqualTo("ConsistentAlwaysOnSampler"); - } - - @Test - void testThreshold() { - assertThat( - ConsistentSampler.alwaysOn() - .getSamplingIntent(null, "span_name", null, null, null) - .getThreshold()) - .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 deleted file mode 100644 index 873ed04d31..0000000000 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOfTest.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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.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 org.junit.jupiter.api.Test; - -class ConsistentAnyOfTest { - - static class TestSampler implements Composable { - private final long threshold; - private final boolean isAdjustedCountCorrect; - - public TestSampler(long threshold, boolean isAdjustedCountCorrect) { - this.threshold = threshold; - this.isAdjustedCountCorrect = isAdjustedCountCorrect; - } - - @Override - public SamplingIntent getSamplingIntent( - Context parentContext, - String name, - SpanKind spanKind, - Attributes attributes, - List parentLinks) { - - return new SamplingIntent() { - @Override - public long getThreshold() { - return threshold; - } - - @Override - public boolean isAdjustedCountReliable() { - return isAdjustedCountCorrect; - } - }; - } - - @Override - public String getDescription() { - return "MockSampler for tests"; - } - } - - @Test - void testMinimumThresholdWithAdjustedCount() { - Composable delegate1 = new TestSampler(0x80000000000000L, /* isAdjustedCountCorrect= */ false); - Composable delegate2 = new TestSampler(0x30000000000000L, /* isAdjustedCountCorrect= */ true); - Composable delegate3 = new TestSampler(0xa0000000000000L, /* isAdjustedCountCorrect= */ false); - Composable delegate4 = new TestSampler(0x30000000000000L, /* isAdjustedCountCorrect= */ false); - - Composable sampler = ConsistentSampler.anyOf(delegate1, delegate2, delegate3, delegate4); - SamplingIntent intent = sampler.getSamplingIntent(null, "span_name", null, null, null); - assertThat(intent.getThreshold()).isEqualTo(0x30000000000000L); - assertThat(intent.isAdjustedCountReliable()).isTrue(); - - // Change the delegate order - sampler = ConsistentSampler.anyOf(delegate1, delegate4, delegate3, delegate2); - intent = sampler.getSamplingIntent(null, "span_name", null, null, null); - assertThat(intent.getThreshold()).isEqualTo(0x30000000000000L); - assertThat(intent.isAdjustedCountReliable()).isTrue(); - } - - @Test - void testMinimumThresholdWithoutAdjustedCount() { - Composable delegate1 = new TestSampler(0x80000000000000L, /* isAdjustedCountCorrect= */ true); - Composable delegate2 = new TestSampler(0x30000000000000L, /* isAdjustedCountCorrect= */ false); - Composable delegate3 = new TestSampler(0xa0000000000000L, /* isAdjustedCountCorrect= */ true); - - Composable sampler = ConsistentSampler.anyOf(delegate1, delegate2, delegate3); - SamplingIntent intent = sampler.getSamplingIntent(null, "span_name", null, null, null); - assertThat(intent.getThreshold()).isEqualTo(0x30000000000000L); - assertThat(intent.isAdjustedCountReliable()).isFalse(); - } - - @Test - void testAlwaysDrop() { - Composable delegate1 = ConsistentSampler.alwaysOff(); - Composable sampler = ConsistentSampler.anyOf(delegate1); - SamplingIntent intent = sampler.getSamplingIntent(null, "span_name", null, null, null); - assertThat(intent.getThreshold()).isEqualTo(getInvalidThreshold()); - assertThat(intent.isAdjustedCountReliable()).isFalse(); - } - - @Test - void testSpanAttributesAdded() { - AttributeKey key1 = AttributeKey.stringKey("tag1"); - AttributeKey key2 = AttributeKey.stringKey("tag2"); - AttributeKey key3 = AttributeKey.stringKey("tag3"); - Composable delegate1 = - new MarkingSampler(new ConsistentFixedThresholdSampler(0x30000000000000L), key1, "a"); - Composable delegate2 = - new MarkingSampler(new ConsistentFixedThresholdSampler(0x50000000000000L), key2, "b"); - Composable delegate3 = new MarkingSampler(ConsistentSampler.alwaysOff(), key3, "c"); - Composable 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); - assertThat(intent.isAdjustedCountReliable()).isTrue(); - } - - @Test - void testSpanAttributeOverride() { - AttributeKey key1 = AttributeKey.stringKey("shared"); - Composable delegate1 = - new MarkingSampler(new ConsistentFixedThresholdSampler(0x30000000000000L), key1, "a"); - Composable delegate2 = - new MarkingSampler(new ConsistentFixedThresholdSampler(0x50000000000000L), key1, "b"); - Composable 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/ConsistentFixedThresholdSamplerTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSamplerTest.java deleted file mode 100644 index 3d78de81ac..0000000000 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSamplerTest.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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.TestUtil.generateRandomTraceId; -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; - -class ConsistentFixedThresholdSamplerTest { - - private Context parentContext; - private String name; - private SpanKind spanKind; - private Attributes attributes; - private List parentLinks; - - @BeforeEach - public void init() { - - parentContext = Context.root(); - 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); - - int numSampled = 0; - for (long i = 0; i < numSpans; ++i) { - SamplingResult samplingResult = - sampler.shouldSample( - parentContext, generateRandomTraceId(rng), 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()).isFalse(); - 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 - 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 - void testDescription() { - assertThat(ConsistentSampler.probabilityBased(1.0).getDescription()) - .isEqualTo("ConsistentFixedThresholdSampler{threshold=0, 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=c, sampling probability=0.25}"); - assertThat(ConsistentSampler.probabilityBased(1e-300).getDescription()) - .isEqualTo("ConsistentFixedThresholdSampler{threshold=max, sampling probability=0.0}"); - assertThat(ConsistentSampler.probabilityBased(0).getDescription()) - .isEqualTo("ConsistentFixedThresholdSampler{threshold=max, 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 deleted file mode 100644 index d5cb6b640d..0000000000 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSamplerTest.java +++ /dev/null @@ -1,574 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent56; - -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.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.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 LongSupplier lowResolutionTimeSupplier; - private Context parentContext; - private String name; - private SpanKind spanKind; - private Attributes attributes; - private List parentLinks; - private SplittableRandom random; - - @BeforeEach - void init() { - nanoTime = new long[] {0L}; - nanoTimeSupplier = () -> nanoTime[0]; - lowResolutionTimeSupplier = () -> (nanoTime[0] / 1000000) * 1000000; // 1ms resolution - parentContext = Context.root(); - name = "name"; - spanKind = SpanKind.SERVER; - attributes = Attributes.empty(); - parentLinks = Collections.emptyList(); - random = new SplittableRandom(0L); - } - - private void advanceTime(long nanosIncrement) { - nanoTime[0] += nanosIncrement; - } - - private long getCurrentTimeNanos() { - return nanoTime[0]; - } - - @Test - void testConstantRate() { - - double targetSpansPerSecondLimit = 1000; - double adaptationTimeSeconds = 5; - - Composable delegate = - new CoinFlipSampler(ConsistentSampler.alwaysOff(), ConsistentSampler.probabilityBased(0.8)); - ConsistentSampler sampler = - ConsistentSampler.rateLimited( - 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; - - Composable 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; - - 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 testRateIncrease() { - - double targetSpansPerSecondLimit = 1000; - double adaptationTimeSeconds = 5; - - ConsistentSampler sampler = - ConsistentSampler.rateLimited( - targetSpansPerSecondLimit, adaptationTimeSeconds, 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, - generateRandomTraceId(random), - 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, - generateRandomTraceId(random), - 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, 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, - generateRandomTraceId(random), - 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, - generateRandomTraceId(random), - 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)); - } - - /** - * 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"); - - Composable 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 testUnstableDelegate() { - // Assume there are 10,000 spans/s and the delegate samples 50% of them with probability 100%, - // and unconditionally rejects the rest. - // - // Now, if we do not want to sample more than 1000 spans/s overall, the rate limiting - // sampler should calculate the effective threshold correctly. - - double targetSpansPerSecondLimit = 1000; - double adaptationTimeSeconds = 5; - - Composable delegate = - new CoinFlipSampler(ConsistentSampler.alwaysOff(), ConsistentSampler.alwaysOn()); - - ConsistentSampler sampler = - ConsistentSampler.rateLimited( - delegate, targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier); - - long averageRequestRatePerSecond = 10000; - int numSpans = 1000000; - - List spanSampledNanos = new ArrayList<>(); - - 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()); - } - } - - 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 testLegacyCase() { - // This test makes sure that the issue - // https://github.com/open-telemetry/opentelemetry-java-contrib/issues/2007 - // is resolved. - - long averageRequestRatePerSecond = 10000; - - // Assume the following setup: - // The root span is sampled by the legacy sampler AlwaysOn. - // One of its descendant spans, which we will call "parent" span, is sampled with - // stage1: ConsistentRateLimitingSampler(ConsistentParentBasedSampler, 5000/s). - // This will sample approximately 50% of the spans. - - // Its "child" is similarly sampled by - // stage2: ConsistentRateLimitingSampler(ConsistentParentBasedSampler, 2500/s). - - // This sampler will generate the same output as the root span described above: - // - the threshold will be 0, so all spans will be sampled - // - isAdjustedCountReliable will be false - // - there will be no threshold in TraceState, but the sampling flag will be set - Composable mockRootSampler = new LegacyLikeComposable(ConsistentSampler.alwaysOn()); - - double targetSpansPerSecondLimit = 2500; // for stage2 - double adaptationTimeSeconds = 5; - - // The sampler for "parent" spans - ConsistentSampler stage1 = - ConsistentSampler.rateLimited( - mockRootSampler, - 2 * targetSpansPerSecondLimit, - adaptationTimeSeconds, - nanoTimeSupplier); - - // The sampler for "child" spans (it will never see root spans) - ConsistentSampler stage2 = - ConsistentSampler.rateLimited( - ConsistentSampler.parentBased(ConsistentSampler.alwaysOff()), - targetSpansPerSecondLimit, - adaptationTimeSeconds, - nanoTimeSupplier); - - int numSpans = 1000000; - int stage1SampledCount = 0; - int stage2SampledCount = 0; - - for (int i = 0; i < numSpans; ++i) { - advanceTime(randomInterval(averageRequestRatePerSecond)); - String traceId = generateRandomTraceId(random); - - // Stage 1 sampling, the "parent" - SamplingResult samplingResult1 = - stage1.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); - - boolean isSampled = SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult1.getDecision()); - if (isSampled) { - stage1SampledCount++; - } - - // Prepare the context for the child span, pass parent's TraceState to the child - Span parentSpan = Span.fromContext(parentContext); - SpanContext parentSpanContext = parentSpan.getSpanContext(); - TraceState parentSamplingTraceState = - samplingResult1.getUpdatedTraceState(parentSpanContext.getTraceState()); - - SpanContext childSpanContext = - SpanContext.create( - traceId, - "1000badbadbad000", - isSampled ? TraceFlags.getSampled() : TraceFlags.getDefault(), - parentSamplingTraceState); - Span childSpan = Span.wrap(childSpanContext); - Context childContext = childSpan.storeInContext(parentContext); - - // Stage 2 sampling, the "child" - SamplingResult samplingResult2 = - stage2.shouldSample(childContext, traceId, name, spanKind, attributes, parentLinks); - - if (SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult2.getDecision())) { - stage2SampledCount++; - } - } - - long timeNow = nanoTime[0]; - double duration = timeNow / 1000000000.0; // in seconds - assertThat(duration) - .isCloseTo(numSpans / (double) averageRequestRatePerSecond, Percentage.withPercentage(2)); - - assertThat(stage1SampledCount / duration) - .isCloseTo(2 * targetSpansPerSecondLimit, Percentage.withPercentage(2)); - - assertThat(stage2SampledCount / duration) - .isCloseTo(targetSpansPerSecondLimit, Percentage.withPercentage(2)); - } - - /* - * An auxiliary class used to simulate the behavior of a legacy (non consistent-probability) - * sampler, just for testing mixed environment - */ - static class LegacyLikeComposable implements Composable { - - private final Composable delegate; - - public LegacyLikeComposable(Composable delegate) { - this.delegate = delegate; - } - - @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 boolean isAdjustedCountReliable() { - // Forcing "legacy" behavior, no threshold will be put into TraceState - return false; - } - - @Override - public Attributes getAttributes() { - return delegateIntent.getAttributes(); - } - - @Override - public TraceState updateTraceState(TraceState previousState) { - return delegateIntent.updateTraceState(previousState); - } - }; - } - - @Override - public String getDescription() { - return "LegacyLike(" + delegate.getDescription() + ")"; - } - } - - @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/ConsistentRuleBasedSamplerTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSamplerTest.java deleted file mode 100644 index 44d015d715..0000000000 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSamplerTest.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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() { - Composable 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"); - - Composable delegate1 = - new MarkingSampler(new ConsistentFixedThresholdSampler(0x80000000000000L), key1, "a"); - Composable delegate2 = - new MarkingSampler(new ConsistentFixedThresholdSampler(0x50000000000000L), key2, "b"); - Composable delegate3 = - new MarkingSampler(new ConsistentFixedThresholdSampler(0x30000000000000L), key3, "c"); - - Composable 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() { - Composable 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/ConsistentSamplerTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplerTest.java deleted file mode 100644 index 7725bb57a6..0000000000 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSamplerTest.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * 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 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 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(); - - void setParentSampled(boolean parentSampled) { - this.parentSampled = parentSampled; - } - - void setParentThreshold(long parentThreshold) { - assertThat(parentThreshold).isBetween(0L, 0xffffffffffffffL); - this.parentThreshold = OptionalLong.of(parentThreshold); - } - - void setParentRandomValue(long parentRandomValue) { - assertThat(parentRandomValue).isBetween(0L, 0xffffffffffffffL); - this.parentRandomValue = OptionalLong.of(parentRandomValue); - } - - Context getParentContext() { - return createParentContext( - traceId, spanId, parentThreshold, parentRandomValue, parentSampled); - } - - static String getTraceId() { - return traceId; - } - - static String getName() { - return name; - } - - static SpanKind getSpanKind() { - return spanKind; - } - - static Attributes getAttributes() { - return attributes; - } - - static List getParentLinks() { - return parentLinks; - } - } - - private static class Output { - - private final SamplingResult samplingResult; - private final Context parentContext; - - Output(SamplingResult samplingResult, Context parentContext) { - this.samplingResult = samplingResult; - this.parentContext = parentContext; - } - - 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 testMinThresholdWithoutParentRandomValue() { - - Input input = new Input(); - - ConsistentSampler sampler = ConsistentSampler.alwaysOn(); - - Output output = sample(input, sampler); - - assertThat(output.samplingResult.getDecision()).isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); - assertThat(output.getThreshold()).hasValue(0); - assertThat(output.getRandomValue()).isNotPresent(); - } - - @Test - void testMinThresholdWithParentRandomValue() { - - long parentRandomValue = 0x7f99aa40c02744L; - - Input input = new Input(); - input.setParentRandomValue(parentRandomValue); - - ConsistentSampler sampler = ConsistentSampler.alwaysOn(); - - Output output = sample(input, sampler); - - assertThat(output.samplingResult.getDecision()).isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); - assertThat(output.getThreshold()).hasValue(0); - assertThat(output.getRandomValue()).hasValue(parentRandomValue); - } - - @Test - void testMaxThreshold() { - - Input input = new Input(); - - ConsistentSampler sampler = new ConsistentFixedThresholdSampler(getMaxThreshold()); - - Output output = sample(input, sampler); - - assertThat(output.samplingResult.getDecision()).isEqualTo(SamplingDecision.DROP); - assertThat(output.getThreshold()).isNotPresent(); - assertThat(output.getRandomValue()).isNotPresent(); - } - - @Test - void testParentBasedInConsistentMode() { - - long parentRandomValue = 0x7f99aa40c02744L; - - Input input = new Input(); - input.setParentRandomValue(parentRandomValue); - input.setParentThreshold(parentRandomValue); - input.setParentSampled(false); // should be ignored - - ConsistentSampler sampler = ConsistentSampler.parentBased(ConsistentSampler.alwaysOn()); - - Output output = sample(input, sampler); - - assertThat(output.samplingResult.getDecision()).isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); - assertThat(output.getThreshold()).hasValue(parentRandomValue); - assertThat(output.getRandomValue()).hasValue(parentRandomValue); - } - - @Test - void testParentBasedInLegacyMode() { - - // No parent threshold present - Input input = new Input(); - - ConsistentSampler sampler = ConsistentSampler.parentBased(ConsistentSampler.alwaysOn()); - - Output output = sample(input, sampler); - - assertThat(output.samplingResult.getDecision()).isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); - assertThat(output.getThreshold()).isNotPresent(); - assertThat(output.getRandomValue()).isNotPresent(); - } - - @Test - void testHalfThresholdNotSampled() { - - Input input = new Input(); - input.setParentRandomValue(0x7FFFFFFFFFFFFFL); - - ConsistentSampler sampler = new ConsistentFixedThresholdSampler(0x80000000000000L); - - Output output = sample(input, sampler); - - assertThat(output.samplingResult.getDecision()).isEqualTo(SamplingDecision.DROP); - assertThat(output.getThreshold()).isNotPresent(); - assertThat(output.getRandomValue()).hasValue(0x7FFFFFFFFFFFFFL); - } - - @Test - void testHalfThresholdSampled() { - - Input input = new Input(); - input.setParentRandomValue(0x80000000000000L); - - ConsistentSampler sampler = new ConsistentFixedThresholdSampler(0x80000000000000L); - - Output output = sample(input, sampler); - - assertThat(output.samplingResult.getDecision()).isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); - assertThat(output.getThreshold()).hasValue(0x80000000000000L); - assertThat(output.getRandomValue()).hasValue(0x80000000000000L); - } - - @Test - void testParentViolatingInvariant() { - - Input input = new Input(); - input.setParentThreshold(0x80000000000000L); - input.setParentRandomValue(0x80000000000000L); - input.setParentSampled(false); - - ConsistentSampler sampler = new ConsistentFixedThresholdSampler(0x0); - Output output = sample(input, sampler); - - assertThat(output.samplingResult.getDecision()).isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); - assertThat(output.getThreshold()).hasValue(0x0L); - assertThat(output.getRandomValue()).hasValue(0x80000000000000L); - } -} 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 deleted file mode 100644 index 8e8b3ef96d..0000000000 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/OtelTraceStateTest.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent56; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.junit.jupiter.api.Test; - -class OtelTraceStateTest { - - private static String getXString(int len) { - return Stream.generate(() -> "X").limit(len).collect(Collectors.joining()); - } - - @Test - void test() { - - assertThat(OtelTraceState.parse("").serialize()).isEqualTo(""); - assertThat(OtelTraceState.parse("").serialize()).isEqualTo(""); - - assertThat(OtelTraceState.parse("a").serialize()).isEqualTo(""); - assertThat(OtelTraceState.parse("#").serialize()).isEqualTo(""); - assertThat(OtelTraceState.parse(" ").serialize()).isEqualTo(""); - - assertThat(OtelTraceState.parse("rv:1234567890abcd").serialize()) - .isEqualTo("rv:1234567890abcd"); - assertThat(OtelTraceState.parse("rv:01020304050607").serialize()) - .isEqualTo("rv:01020304050607"); - assertThat(OtelTraceState.parse("rv:1234567890abcde").serialize()).isEqualTo(""); - - assertThat(OtelTraceState.parse("th:1234567890abcd").serialize()) - .isEqualTo("th:1234567890abcd"); - assertThat(OtelTraceState.parse("th:01020304050607").serialize()) - .isEqualTo("th:01020304050607"); - assertThat(OtelTraceState.parse("th:10000000000000").serialize()).isEqualTo("th:1"); - assertThat(OtelTraceState.parse("th:1234500000000").serialize()).isEqualTo("th:12345"); - assertThat(OtelTraceState.parse("th:0").serialize()).isEqualTo("th:0"); // TODO - assertThat(OtelTraceState.parse("th:100000000000000").serialize()).isEqualTo(""); - assertThat(OtelTraceState.parse("th:1234567890abcde").serialize()).isEqualTo(""); - - assertThat( - OtelTraceState.parse( - "a:" + getXString(214) + ";rv:1234567890abcd;th:1234567890abcd;x:3") - .serialize()) - .isEqualTo("th:1234567890abcd;rv:1234567890abcd;a:" + getXString(214) + ";x:3"); - assertThat( - OtelTraceState.parse( - "a:" + getXString(215) + ";rv:1234567890abcd;th:1234567890abcd;x:3") - .serialize()) - .isEqualTo(""); - - assertThat(OtelTraceState.parse("th:x").serialize()).isEqualTo(""); - assertThat(OtelTraceState.parse("th:100000000000000").serialize()).isEqualTo(""); - assertThat(OtelTraceState.parse("th:10000000000000").serialize()).isEqualTo("th:1"); - assertThat(OtelTraceState.parse("th:1000000000000").serialize()).isEqualTo("th:1"); - assertThat(OtelTraceState.parse("th:100000000000").serialize()).isEqualTo("th:1"); - assertThat(OtelTraceState.parse("th:10000000000").serialize()).isEqualTo("th:1"); - assertThat(OtelTraceState.parse("th:1000000000").serialize()).isEqualTo("th:1"); - assertThat(OtelTraceState.parse("th:100000000").serialize()).isEqualTo("th:1"); - assertThat(OtelTraceState.parse("th:10000000").serialize()).isEqualTo("th:1"); - assertThat(OtelTraceState.parse("th:1000000").serialize()).isEqualTo("th:1"); - assertThat(OtelTraceState.parse("th:100000").serialize()).isEqualTo("th:1"); - assertThat(OtelTraceState.parse("th:10000").serialize()).isEqualTo("th:1"); - assertThat(OtelTraceState.parse("th:1000").serialize()).isEqualTo("th:1"); - assertThat(OtelTraceState.parse("th:100").serialize()).isEqualTo("th:1"); - assertThat(OtelTraceState.parse("th:10").serialize()).isEqualTo("th:1"); - assertThat(OtelTraceState.parse("th:1").serialize()).isEqualTo("th:1"); - - assertThat(OtelTraceState.parse("th:10000000000001").serialize()) - .isEqualTo("th:10000000000001"); - assertThat(OtelTraceState.parse("th:10000000000010").serialize()).isEqualTo("th:1000000000001"); - assertThat(OtelTraceState.parse("rv:x").serialize()).isEqualTo(""); - assertThat(OtelTraceState.parse("rv:100000000000000").serialize()).isEqualTo(""); - assertThat(OtelTraceState.parse("rv:10000000000000").serialize()) - .isEqualTo("rv:10000000000000"); - assertThat(OtelTraceState.parse("rv:1000000000000").serialize()).isEqualTo(""); - } -} 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 deleted file mode 100644 index d9a34255f6..0000000000 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/RandomValueGeneratorsTest.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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; - -class RandomValueGeneratorsTest { - @Test - void testRandomRange() { - int attempts = 10000; - for (int i = 0; i < attempts; ++i) { - assertThat(RandomValueGenerators.getDefault().generate("")) - .isBetween(0L, getMaxRandomValue()); - } - } -} diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/TestUtil.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/TestUtil.java deleted file mode 100644 index 1ffede0a02..0000000000 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/TestUtil.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent56; - -import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.HEX_DIGITS; - -import java.util.SplittableRandom; - -public final class TestUtil { - - private TestUtil() {} - - static String generateRandomTraceId(SplittableRandom random) { - StringBuilder sb = new StringBuilder(16); - long hi = random.nextLong(); - long lo = random.nextLong(); - for (int i = 0; i < 64; i += 4) { - sb.append(HEX_DIGITS[(int) (hi >>> i) & 0xF]); - } - for (int i = 0; i < 64; i += 4) { - sb.append(HEX_DIGITS[(int) (lo >>> i) & 0xF]); - } - return sb.toString(); - } -} 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 deleted file mode 100644 index 5a41324786..0000000000 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/UseCaseTest.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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)); - }); - Composable 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)); - }; - - Composable s2 = ConsistentSampler.ruleBased(SpanKind.CLIENT, onMatch(foo, alwaysOn())); - Composable 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(); - } -}