diff --git a/sdk-extensions/incubator/build.gradle.kts b/sdk-extensions/incubator/build.gradle.kts index 1bb858d699a..a3947db8715 100644 --- a/sdk-extensions/incubator/build.gradle.kts +++ b/sdk-extensions/incubator/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { testImplementation(project(":exporters:zipkin")) testImplementation(project(":sdk-extensions:jaeger-remote-sampler")) testImplementation(project(":extensions:trace-propagators")) + testImplementation("edu.berkeley.cs.jqf:jqf-fuzz") testImplementation("io.opentelemetry.contrib:opentelemetry-aws-xray-propagator") testImplementation("com.linecorp.armeria:armeria-junit5") diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableAlwaysOffSampler.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableAlwaysOffSampler.java new file mode 100644 index 00000000000..7eb2b2ab209 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableAlwaysOffSampler.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.samplers; + +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.function.Function; + +enum ComposableAlwaysOffSampler implements ComposableSampler { + INSTANCE; + + private static final SamplingIntent INTENT = + SamplingIntent.create( + ImmutableSamplingIntent.INVALID_THRESHOLD, + /* thresholdReliable= */ false, + Attributes.empty(), + Function.identity()); + + @Override + public SamplingIntent getSamplingIntent( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return INTENT; + } + + @Override + public String getDescription() { + return "ComposableAlwaysOffSampler"; + } + + @Override + public String toString() { + return getDescription(); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableAlwaysOnSampler.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableAlwaysOnSampler.java new file mode 100644 index 00000000000..0ccbf3feb98 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableAlwaysOnSampler.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.samplers; + +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.function.Function; + +enum ComposableAlwaysOnSampler implements ComposableSampler { + INSTANCE; + + private static final SamplingIntent INTENT = + SamplingIntent.create( + ImmutableSamplingIntent.MIN_THRESHOLD, + /* thresholdReliable= */ true, + Attributes.empty(), + Function.identity()); + + @Override + public SamplingIntent getSamplingIntent( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return INTENT; + } + + @Override + public String getDescription() { + return "ComposableAlwaysOnSampler"; + } + + @Override + public String toString() { + return getDescription(); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableParentThresholdSampler.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableParentThresholdSampler.java new file mode 100644 index 00000000000..5f3c8830ab4 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableParentThresholdSampler.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.samplers; + +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.INVALID_THRESHOLD; +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.MIN_THRESHOLD; +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.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.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; +import java.util.function.Function; + +final class ComposableParentThresholdSampler implements ComposableSampler { + + private final ComposableSampler rootSampler; + private final String description; + + ComposableParentThresholdSampler(ComposableSampler rootSampler) { + this.rootSampler = rootSampler; + this.description = "ComposableParentThresholdSampler{rootSampler=" + rootSampler + "}"; + } + + @Override + public SamplingIntent getSamplingIntent( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + SpanContext parentSpanContext = Span.fromContext(parentContext).getSpanContext(); + if (!parentSpanContext.isValid()) { + return rootSampler.getSamplingIntent( + parentContext, traceId, name, spanKind, attributes, parentLinks); + } + + OtelTraceState otTraceState = OtelTraceState.parse(parentSpanContext.getTraceState()); + if (isValidThreshold(otTraceState.getThreshold())) { + return ImmutableSamplingIntent.create( + otTraceState.getThreshold(), + /* thresholdReliable= */ true, + Attributes.empty(), + Function.identity()); + } + + long threshold = + parentSpanContext.getTraceFlags().isSampled() ? MIN_THRESHOLD : INVALID_THRESHOLD; + return ImmutableSamplingIntent.create( + threshold, /* thresholdReliable= */ false, Attributes.empty(), Function.identity()); + } + + @Override + public String getDescription() { + return description; + } + + @Override + public String toString() { + return this.getDescription(); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableSampler.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableSampler.java new file mode 100644 index 00000000000..b09edacab4f --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableSampler.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.samplers; + +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; + +/** A sampler that can be composed to make a final sampling decision. */ +public interface ComposableSampler { + /** Returns a {@link ComposableSampler} that does not sample any span. */ + static ComposableSampler alwaysOff() { + return ComposableAlwaysOffSampler.INSTANCE; + } + + /** Returns a {@link ComposableSampler} that samples all spans. */ + static ComposableSampler alwaysOn() { + return ComposableAlwaysOnSampler.INSTANCE; + } + + /** Returns a {@link ComposableSampler} that samples each span with a fixed ratio. */ + static ComposableSampler traceIdRatioBased(double ratio) { + return new ComposableTraceIdRatioBasedSampler(ratio); + } + + /** + * Returns a {@link ComposableSampler} that respects the sampling decision of the parent span or + * falls back to the given sampler if it is a root span. + */ + static ComposableSampler parentThreshold(ComposableSampler rootSampler) { + return new ComposableParentThresholdSampler(rootSampler); + } + + /** Returns the {@link SamplingIntent} to use to make a sampling decision. */ + SamplingIntent getSamplingIntent( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks); + + /** Returns a description of the sampler implementation. */ + String getDescription(); +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableTraceIdRatioBasedSampler.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableTraceIdRatioBasedSampler.java new file mode 100644 index 00000000000..3b9e08effc1 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableTraceIdRatioBasedSampler.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.samplers; + +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.function.Function; + +final class ComposableTraceIdRatioBasedSampler implements ComposableSampler { + private static long calculateThreshold(double ratio) { + return ImmutableSamplingIntent.MAX_THRESHOLD + - Math.round(ratio * (double) ImmutableSamplingIntent.MAX_THRESHOLD); + } + + private final SamplingIntent intent; + private final String description; + + ComposableTraceIdRatioBasedSampler(double ratio) { + long threshold = calculateThreshold(ratio); + String thresholdStr; + if (threshold == ImmutableSamplingIntent.MAX_THRESHOLD) { + thresholdStr = "max"; + + // Same as ComposableAlwaysOffSampler, notably the threshold is not considered reliable. + // The spec mentions returning an instance of ComposableAlwaysOffSampler in this case but + // it seems clearer if the description of the sampler matches the user's request. + this.intent = + SamplingIntent.create( + ImmutableSamplingIntent.INVALID_THRESHOLD, + /* thresholdReliable= */ false, + Attributes.empty(), + Function.identity()); + } else { + StringBuilder sb = new StringBuilder(); + OtelTraceState.serializeTh(threshold, sb); + thresholdStr = sb.toString(); + + this.intent = + SamplingIntent.create( + threshold, /* thresholdReliable= */ true, Attributes.empty(), Function.identity()); + } + this.description = + "ComposableTraceIdRatioBasedSampler{threshold=" + thresholdStr + ", ratio=" + ratio + "}"; + } + + @Override + public SamplingIntent getSamplingIntent( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return intent; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public String toString() { + return this.getDescription(); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/CompositeSampler.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/CompositeSampler.java new file mode 100644 index 00000000000..0a03d66a9fe --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/CompositeSampler.java @@ -0,0 +1,121 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.samplers; + +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.INVALID_THRESHOLD; +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.isValidRandomValue; +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.isValidThreshold; +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.OtelTraceState.OTEL_TRACE_STATE_KEY; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.internal.OtelEncodingUtils; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.internal.RandomSupplier; +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; + +/** + * A sampler that uses a {@link ComposableSampler} to make its sampling decisions while handlign + * tracestate. + */ +public final class CompositeSampler implements Sampler { + /** + * Returns a new composite {@link Sampler} that delegates to the given {@link ComposableSampler}. + */ + public static Sampler wrap(ComposableSampler delegate) { + return new CompositeSampler(delegate); + } + + private final ComposableSampler delegate; + + private CompositeSampler(ComposableSampler delegate) { + this.delegate = delegate; + } + + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + TraceState traceState = Span.fromContext(parentContext).getSpanContext().getTraceState(); + OtelTraceState otelTraceState = OtelTraceState.parse(traceState); + + SamplingIntent intent = + delegate.getSamplingIntent(parentContext, traceId, name, spanKind, attributes, parentLinks); + + boolean thresholdReliable = false; + boolean sampled = false; + if (isValidThreshold(intent.getThreshold())) { + thresholdReliable = intent.isThresholdReliable(); + long randomValue; + if (thresholdReliable) { + if (isValidRandomValue(otelTraceState.getRandomValue())) { + randomValue = otelTraceState.getRandomValue(); + } else { + // Use last 56 bits of trace ID as random value. + randomValue = OtelEncodingUtils.longFromBase16String(traceId, 16) & 0x00FFFFFFFFFFFFFFL; + } + } else { + randomValue = RandomSupplier.platformDefault().get().nextLong() & 0x00FFFFFFFFFFFFFFL; + } + sampled = intent.getThreshold() <= randomValue; + } + + SamplingDecision decision = + sampled ? SamplingDecision.RECORD_AND_SAMPLE : SamplingDecision.DROP; + if (sampled && thresholdReliable) { + otelTraceState = + new OtelTraceState( + otelTraceState.getRandomValue(), intent.getThreshold(), otelTraceState.getRest()); + } else { + otelTraceState = + new OtelTraceState( + otelTraceState.getRandomValue(), INVALID_THRESHOLD, otelTraceState.getRest()); + } + + String serializedState = otelTraceState.serialize(); + return new SamplingResult() { + @Override + public SamplingDecision getDecision() { + return decision; + } + + @Override + public Attributes getAttributes() { + return intent.getAttributes(); + } + + @Override + public TraceState getUpdatedTraceState(TraceState parentTraceState) { + TraceState newTraceState = intent.getTraceStateUpdater().apply(traceState); + if (!serializedState.isEmpty()) { + newTraceState = + newTraceState.toBuilder().put(OTEL_TRACE_STATE_KEY, serializedState).build(); + } + return newTraceState; + } + }; + } + + @Override + public String getDescription() { + return delegate.getDescription(); + } + + @Override + public String toString() { + return this.getDescription(); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ImmutableSamplingIntent.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ImmutableSamplingIntent.java new file mode 100644 index 00000000000..3ca759ed62e --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ImmutableSamplingIntent.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.samplers; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.TraceState; +import java.util.function.Function; + +@AutoValue +abstract class ImmutableSamplingIntent implements SamplingIntent { + private static final int RANDOM_VALUE_BITS = 56; + + static final long INVALID_THRESHOLD = -1; + static final long INVALID_RANDOM_VALUE = -1; + static final long MIN_THRESHOLD = 0; + static final long MAX_THRESHOLD = 1L << RANDOM_VALUE_BITS; + static final long MAX_RANDOM_VALUE = MAX_THRESHOLD - 1; + + static boolean isValidThreshold(long threshold) { + return threshold >= MIN_THRESHOLD && threshold <= MAX_THRESHOLD; + } + + static boolean isValidRandomValue(long randomValue) { + return randomValue >= 0 && randomValue <= MAX_RANDOM_VALUE; + } + + static ImmutableSamplingIntent create( + long threshold, + boolean thresholdReliable, + Attributes attributes, + Function traceStateUpdater) { + return new AutoValue_ImmutableSamplingIntent( + threshold, thresholdReliable, attributes, traceStateUpdater); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/OtelTraceState.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/OtelTraceState.java new file mode 100644 index 00000000000..b108c82f573 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/OtelTraceState.java @@ -0,0 +1,196 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.samplers; + +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.INVALID_RANDOM_VALUE; +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.INVALID_THRESHOLD; +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.MAX_THRESHOLD; +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.isValidRandomValue; +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.isValidThreshold; + +import io.opentelemetry.api.internal.OtelEncodingUtils; +import io.opentelemetry.api.internal.TemporaryBuffers; +import io.opentelemetry.api.trace.TraceState; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +// https://opentelemetry.io/docs/specs/otel/trace/tracestate-handling/ +final class OtelTraceState { + + private static final OtelTraceState EMPTY = + new OtelTraceState(INVALID_RANDOM_VALUE, INVALID_THRESHOLD, Collections.emptyList()); + + private static final int MAX_OTEL_TRACE_STATE_LENGTH = 256; + // visible for testing + static final int MAX_VALUE_LENGTH = 14; // 56 bits, 4 bits per hex digit + + static final String OTEL_TRACE_STATE_KEY = "ot"; + + static OtelTraceState parse(TraceState traceState) { + String ot = traceState.get(OTEL_TRACE_STATE_KEY); + if (ot == null || ot.isEmpty() || ot.length() > MAX_OTEL_TRACE_STATE_LENGTH) { + return EMPTY; + } + + long threshold = INVALID_THRESHOLD; + long randomValue = INVALID_RANDOM_VALUE; + + List rest = Collections.emptyList(); + int idx = 0; + while (idx < ot.length()) { + int delimIdx = ot.indexOf(';', idx); + String member = delimIdx != -1 ? ot.substring(idx, delimIdx) : ot.substring(idx); + if (member.startsWith("th:")) { + threshold = parseTh(member.substring("th:".length())); + } else if (member.startsWith("rv:")) { + randomValue = parseRv(member.substring("rv:".length())); + } else { + if (rest.isEmpty()) { + rest = new ArrayList<>(); + } + rest.add(member); + } + + if (delimIdx == -1) { + break; + } + idx = delimIdx + 1; + } + + return new OtelTraceState(randomValue, threshold, rest); + } + + private static long parseTh(String th) { + if (th.isEmpty() + || th.length() > MAX_VALUE_LENGTH + || !OtelEncodingUtils.isValidBase16String(th)) { + return INVALID_THRESHOLD; + } + + // Fast path for very common all sampling case + if (th.equals("0")) { + return 0; + } + + return OtelEncodingUtils.longFromBase16String(new PaddedValue(th), 0); + } + + private static long parseRv(String rv) { + if (rv.length() != MAX_VALUE_LENGTH || !OtelEncodingUtils.isValidBase16String(rv)) { + return INVALID_RANDOM_VALUE; + } + + return OtelEncodingUtils.longFromBase16String(new PaddedValue(rv), 0); + } + + static void serializeTh(long threshold, StringBuilder sb) { + if (threshold == 0) { + sb.append('0'); + return; + } + // We only need 56 bits, but we can live with one extra byte being encoded. + char[] buf = TemporaryBuffers.chars(16); + OtelEncodingUtils.longToBase16String(threshold, buf, 0); + int startIdx = 2; + int endIdx = 16; + for (; endIdx > startIdx; endIdx--) { + if (buf[endIdx - 1] != '0') { + break; + } + } + sb.append(buf, startIdx, endIdx - startIdx); + } + + private static void serializeRv(long randomValue, StringBuilder sb) { + // We only need 56 bits, but we can live with one extra byte being encoded. + char[] buf = TemporaryBuffers.chars(16); + OtelEncodingUtils.longToBase16String(randomValue, buf, 0); + int startIdx = 2; + int endIdx = 16; + sb.append(buf, startIdx, endIdx - startIdx); + } + + private final long randomValue; + private final long threshold; + private final List rest; + + OtelTraceState(long randomValue, long threshold, List rest) { + this.randomValue = randomValue; + this.threshold = threshold; + this.rest = rest; + } + + long getRandomValue() { + return randomValue; + } + + long getThreshold() { + return threshold; + } + + List getRest() { + return rest; + } + + String serialize() { + if ((!isValidThreshold(threshold) || threshold == MAX_THRESHOLD) + && !isValidRandomValue(randomValue) + && rest.isEmpty()) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + if (isValidThreshold(threshold) && threshold != MAX_THRESHOLD) { + sb.append("th:"); + serializeTh(threshold, sb); + sb.append(';'); + } + if (isValidRandomValue(randomValue)) { + sb.append("rv:"); + serializeRv(randomValue, sb); + sb.append(';'); + } + for (String member : rest) { + sb.append(member); + sb.append(';'); + } + + // Trim trailing semicolon + sb.setLength(sb.length() - 1); + return sb.toString(); + } + + private static class PaddedValue implements CharSequence { + private final String value; + + PaddedValue(String value) { + this.value = value; + } + + @Override + public int length() { + return 16; + } + + @Override + public char charAt(int index) { + if (index < 2) { + return '0'; + } + index -= 2; + if (index < value.length()) { + return value.charAt(index); + } + return '0'; + } + + @Override + public CharSequence subSequence(int start, int end) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/SamplingIntent.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/SamplingIntent.java new file mode 100644 index 00000000000..6e91db5aa34 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/SamplingIntent.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.samplers; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.TraceState; +import java.util.function.Function; + +/** Information to make a sampling decision. */ +public interface SamplingIntent { + + /** Returns a {@link SamplingIntent} with the given data. */ + static SamplingIntent create( + long threshold, + boolean thresholdReliable, + Attributes attributes, + Function traceStateUpdater) { + return ImmutableSamplingIntent.create( + threshold, thresholdReliable, attributes, traceStateUpdater); + } + + /** + * Returns the sampling threshold value. A lower threshold increases the likelihood of sampling. + */ + long getThreshold(); + + /** Returns whether the threshold can be reliably used for Span-to-Metrics estimation. */ + boolean isThresholdReliable(); + + /** Returns any attributes to add to the span to record the sampling result. */ + Attributes getAttributes(); + + /** Returns a function to apply to the tracestate of the span to possibly update it. */ + Function getTraceStateUpdater(); +} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableAlwaysOffSamplerTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableAlwaysOffSamplerTest.java new file mode 100644 index 00000000000..fa8469dcff7 --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableAlwaysOffSamplerTest.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.samplers; + +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.TestUtil.traceIdGenerator; +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.TraceId; +import io.opentelemetry.context.Context; +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.function.Supplier; +import org.junit.jupiter.api.Test; + +class ComposableAlwaysOffSamplerTest { + @Test + void testDescription() { + assertThat(ComposableSampler.alwaysOff().getDescription()) + .isEqualTo("ComposableAlwaysOffSampler"); + assertThat(ComposableSampler.alwaysOff()).hasToString("ComposableAlwaysOffSampler"); + } + + @Test + void testThreshold() { + assertThat( + ComposableSampler.alwaysOff() + .getSamplingIntent( + Context.root(), + TraceId.getInvalid(), + "span", + SpanKind.SERVER, + Attributes.empty(), + Collections.emptyList()) + .getThreshold()) + .isEqualTo(-1); + } + + @Test + void sampling() { + Supplier generator = traceIdGenerator(); + Sampler sampler = CompositeSampler.wrap(ComposableSampler.alwaysOff()); + int numSampled = 0; + for (int i = 0; i < 10000; i++) { + SamplingResult result = + sampler.shouldSample( + Context.root(), + generator.get(), + "span", + SpanKind.SERVER, + Attributes.empty(), + Collections.emptyList()); + if (result.getDecision() == SamplingDecision.RECORD_AND_SAMPLE) { + numSampled++; + } + } + assertThat(numSampled).isZero(); + } +} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableAlwaysOnSamplerTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableAlwaysOnSamplerTest.java new file mode 100644 index 00000000000..5b0751851a6 --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableAlwaysOnSamplerTest.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.samplers; + +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.TestUtil.traceIdGenerator; +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.TraceId; +import io.opentelemetry.context.Context; +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.function.Supplier; +import org.junit.jupiter.api.Test; + +class ComposableAlwaysOnSamplerTest { + @Test + void testDescription() { + assertThat(ComposableSampler.alwaysOn().getDescription()) + .isEqualTo("ComposableAlwaysOnSampler"); + assertThat(ComposableSampler.alwaysOn()).hasToString("ComposableAlwaysOnSampler"); + } + + @Test + void testThreshold() { + assertThat( + ComposableSampler.alwaysOn() + .getSamplingIntent( + Context.root(), + TraceId.getInvalid(), + "span", + SpanKind.SERVER, + Attributes.empty(), + Collections.emptyList()) + .getThreshold()) + .isEqualTo(0); + } + + @Test + void sampling() { + Supplier generator = traceIdGenerator(); + Sampler sampler = CompositeSampler.wrap(ComposableSampler.alwaysOn()); + int numSampled = 0; + for (int i = 0; i < 10000; i++) { + SamplingResult result = + sampler.shouldSample( + Context.root(), + generator.get(), + "span", + SpanKind.SERVER, + Attributes.empty(), + Collections.emptyList()); + if (result.getDecision() == SamplingDecision.RECORD_AND_SAMPLE) { + numSampled++; + } + } + assertThat(numSampled).isEqualTo(10000); + } +} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableParentThresholdSamplerTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableParentThresholdSamplerTest.java new file mode 100644 index 00000000000..f0e4ca30a73 --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableParentThresholdSamplerTest.java @@ -0,0 +1,110 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.samplers; + +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.SpanId; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +public class ComposableParentThresholdSamplerTest { + @Test + void testDescription() { + assertThat(ComposableSampler.parentThreshold(ComposableSampler.alwaysOn()).getDescription()) + .isEqualTo("ComposableParentThresholdSampler{rootSampler=ComposableAlwaysOnSampler}"); + assertThat(ComposableSampler.parentThreshold(ComposableSampler.alwaysOn())) + .hasToString("ComposableParentThresholdSampler{rootSampler=ComposableAlwaysOnSampler}"); + } + + @Test + void rootSpan() { + assertThat( + ComposableSampler.parentThreshold(ComposableSampler.alwaysOn()) + .getSamplingIntent( + Context.root(), + TraceId.getInvalid(), + "span", + SpanKind.SERVER, + Attributes.empty(), + Collections.emptyList()) + .getThreshold()) + .isEqualTo(0); + } + + @Test + void parentWithThreshold() { + SpanContext parentSpanContext = + SpanContext.create( + TraceId.fromLongs(1, 2), + SpanId.fromLong(3), + TraceFlags.getSampled(), + TraceState.builder() + .put("ot", new OtelTraceState(1, 10, Collections.emptyList()).serialize()) + .build()); + assertThat( + ComposableSampler.parentThreshold(ComposableSampler.alwaysOn()) + .getSamplingIntent( + Context.root().with(Span.wrap(parentSpanContext)), + parentSpanContext.getTraceId(), + "span", + SpanKind.SERVER, + Attributes.empty(), + Collections.emptyList()) + .getThreshold()) + .isEqualTo(10); + } + + @Test + void parentWithoutThresholdSampled() { + SpanContext parentSpanContext = + SpanContext.create( + TraceId.fromLongs(1, 2), + SpanId.fromLong(3), + TraceFlags.getSampled(), + TraceState.getDefault()); + SamplingIntent intent = + ComposableSampler.parentThreshold(ComposableSampler.alwaysOn()) + .getSamplingIntent( + Context.root().with(Span.wrap(parentSpanContext)), + parentSpanContext.getTraceId(), + "span", + SpanKind.SERVER, + Attributes.empty(), + Collections.emptyList()); + assertThat(intent.getThreshold()).isZero(); + assertThat(intent.isThresholdReliable()).isFalse(); + } + + @Test + void parentWithoutThresholdNotSampled() { + SpanContext parentSpanContext = + SpanContext.create( + TraceId.fromLongs(1, 2), + SpanId.fromLong(3), + TraceFlags.getDefault(), + TraceState.getDefault()); + SamplingIntent intent = + ComposableSampler.parentThreshold(ComposableSampler.alwaysOn()) + .getSamplingIntent( + Context.root().with(Span.wrap(parentSpanContext)), + parentSpanContext.getTraceId(), + "span", + SpanKind.SERVER, + Attributes.empty(), + Collections.emptyList()); + assertThat(intent.getThreshold()).isEqualTo(-1); + assertThat(intent.isThresholdReliable()).isFalse(); + } +} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableTraceIdRatioBasedSamplerTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableTraceIdRatioBasedSamplerTest.java new file mode 100644 index 00000000000..3322c49f020 --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableTraceIdRatioBasedSamplerTest.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.samplers; + +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.TestUtil.traceIdGenerator; +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.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.Collections; +import java.util.function.Supplier; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ComposableTraceIdRatioBasedSamplerTest { + @Test + void testDescription() { + assertThat(ComposableSampler.traceIdRatioBased(1.0).getDescription()) + .isEqualTo("ComposableTraceIdRatioBasedSampler{threshold=0, ratio=1.0}"); + assertThat(ComposableSampler.traceIdRatioBased(1.0)) + .hasToString("ComposableTraceIdRatioBasedSampler{threshold=0, ratio=1.0}"); + + assertThat(ComposableSampler.traceIdRatioBased(0.5).getDescription()) + .isEqualTo("ComposableTraceIdRatioBasedSampler{threshold=8, ratio=0.5}"); + assertThat(ComposableSampler.traceIdRatioBased(0.25).getDescription()) + .isEqualTo("ComposableTraceIdRatioBasedSampler{threshold=c, ratio=0.25}"); + assertThat(ComposableSampler.traceIdRatioBased(1e-300).getDescription()) + .isEqualTo("ComposableTraceIdRatioBasedSampler{threshold=max, ratio=1.0E-300}"); + assertThat(ComposableSampler.traceIdRatioBased(0).getDescription()) + .isEqualTo("ComposableTraceIdRatioBasedSampler{threshold=max, ratio=0.0}"); + } + + @ParameterizedTest + @CsvSource({ + "1.0, 0", + "0.5, 36028797018963968", + "0.25, 54043195528445952", + "0.125, 63050394783186944", + "0.0, 72057594037927936", + "0.45, 39631676720860364", + "0.2, 57646075230342348", + "0.13, 62690106812997304", + "0.05, 68454714336031539", + }) + void sampling(double ratio, long threshold) { + Supplier generator = traceIdGenerator(); + Sampler sampler = CompositeSampler.wrap(ComposableSampler.traceIdRatioBased(ratio)); + int numSampled = 0; + for (int i = 0; i < 10000; i++) { + SamplingResult result = + sampler.shouldSample( + Context.root(), + generator.get(), + "span", + SpanKind.SERVER, + Attributes.empty(), + Collections.emptyList()); + if (result.getDecision() == SamplingDecision.RECORD_AND_SAMPLE) { + numSampled++; + OtelTraceState otTraceState = + OtelTraceState.parse(result.getUpdatedTraceState(TraceState.getDefault())); + assertThat(otTraceState.getThreshold()).isEqualTo(threshold); + assertThat(otTraceState.getRandomValue()).isEqualTo(-1); + } + } + int expectedNumSampled = (int) Math.round(10000 * ratio); + // NB: It would be better to calculate a standard deviation based on the numbers. + // But our random seed in TestUtil conveniently allows the test to pass so don't bother. + assertThat(Math.abs(numSampled - expectedNumSampled)).isLessThan(50); + } +} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/CompositeSamplerTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/CompositeSamplerTest.java new file mode 100644 index 00000000000..1a79e9d5dee --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/CompositeSamplerTest.java @@ -0,0 +1,278 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.samplers; + +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.INVALID_RANDOM_VALUE; +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.INVALID_THRESHOLD; +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.isValidRandomValue; +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.isValidThreshold; +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.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.OptionalLong; +import org.junit.jupiter.api.Test; + +class CompositeSamplerTest { + 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())); + return isValidThreshold(otelTraceState.getThreshold()) + ? OptionalLong.of(otelTraceState.getThreshold()) + : OptionalLong.empty(); + } + + OptionalLong getRandomValue() { + Span parentSpan = Span.fromContext(parentContext); + OtelTraceState otelTraceState = + OtelTraceState.parse( + samplingResult.getUpdatedTraceState(parentSpan.getSpanContext().getTraceState())); + return isValidRandomValue(otelTraceState.getRandomValue()) + ? OptionalLong.of(otelTraceState.getRandomValue()) + : OptionalLong.empty(); + } + } + + private static TraceState createTraceState(OptionalLong threshold, OptionalLong randomValue) { + long t = threshold.orElse(INVALID_THRESHOLD); + long rv = randomValue.orElse(INVALID_RANDOM_VALUE); + OtelTraceState state = new OtelTraceState(rv, t, Collections.emptyList()); + return TraceState.builder().put("ot", 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, Sampler 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 description() { + assertThat( + CompositeSampler.wrap(ComposableSampler.parentThreshold(ComposableSampler.alwaysOn())) + .getDescription()) + .isEqualTo("ComposableParentThresholdSampler{rootSampler=ComposableAlwaysOnSampler}"); + assertThat( + CompositeSampler.wrap(ComposableSampler.parentThreshold(ComposableSampler.alwaysOn()))) + .hasToString("ComposableParentThresholdSampler{rootSampler=ComposableAlwaysOnSampler}"); + } + + @Test + void testMinThresholdWithoutParentRandomValue() { + Input input = new Input(); + + Sampler sampler = CompositeSampler.wrap(ComposableSampler.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); + + Sampler sampler = CompositeSampler.wrap(ComposableSampler.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(); + + Sampler sampler = CompositeSampler.wrap(ComposableSampler.traceIdRatioBased(0.0)); + + 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 + + Sampler sampler = + CompositeSampler.wrap(ComposableSampler.parentThreshold(ComposableSampler.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(); + + Sampler sampler = + CompositeSampler.wrap(ComposableSampler.parentThreshold(ComposableSampler.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); + + Sampler sampler = CompositeSampler.wrap(ComposableSampler.traceIdRatioBased(0.5)); + + 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); + + Sampler sampler = CompositeSampler.wrap(ComposableSampler.traceIdRatioBased(0.5)); + + 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); + + Sampler sampler = CompositeSampler.wrap(ComposableSampler.traceIdRatioBased(1.0)); + 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/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/OtelTraceStateFuzzTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/OtelTraceStateFuzzTest.java new file mode 100644 index 00000000000..c563ddfc3f1 --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/OtelTraceStateFuzzTest.java @@ -0,0 +1,115 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.samplers; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.pholser.junit.quickcheck.generator.InRange; +import edu.berkeley.cs.jqf.fuzz.Fuzz; +import edu.berkeley.cs.jqf.fuzz.JQF; +import edu.berkeley.cs.jqf.fuzz.junit.GuidedFuzzing; +import edu.berkeley.cs.jqf.fuzz.random.NoGuidance; +import io.opentelemetry.api.trace.TraceState; +import java.util.Collections; +import org.junit.jupiter.api.Test; +import org.junit.runner.Result; +import org.junit.runner.RunWith; + +@SuppressWarnings("SystemOut") +class OtelTraceStateFuzzTest { + + @RunWith(JQF.class) + public static class TestCases { + @Fuzz + public void roundTripRandomValues(long rv, long th) { + OtelTraceState input = new OtelTraceState(rv, th, Collections.emptyList()); + OtelTraceState output = + OtelTraceState.parse(TraceState.builder().put("ot", input.serialize()).build()); + assertState(output, rv, th); + } + + @Fuzz + public void roundTripValidValues( + @InRange(minLong = 0, maxLong = ImmutableSamplingIntent.MAX_RANDOM_VALUE) long rv, + @InRange(minLong = 0, maxLong = ImmutableSamplingIntent.MAX_THRESHOLD) long th) { + OtelTraceState input = new OtelTraceState(rv, th, Collections.emptyList()); + OtelTraceState output = + OtelTraceState.parse(TraceState.builder().put("ot", input.serialize()).build()); + assertState(output, rv, th); + } + + private static void assertState(OtelTraceState state, long rv, long th) { + boolean hasRv = false; + boolean hasTh = false; + if (rv >= 0 && rv <= ImmutableSamplingIntent.MAX_RANDOM_VALUE) { + assertThat(state.getRandomValue()).isEqualTo(rv); + hasRv = true; + } else { + assertThat(state.getRandomValue()).isEqualTo(ImmutableSamplingIntent.INVALID_RANDOM_VALUE); + } + if (th >= 0 && th <= ImmutableSamplingIntent.MAX_THRESHOLD) { + assertThat(state.getThreshold()).isEqualTo(th); + hasTh = state.getThreshold() != ImmutableSamplingIntent.MAX_THRESHOLD; + } else { + assertThat(state.getThreshold()).isEqualTo(ImmutableSamplingIntent.INVALID_THRESHOLD); + } + String[] parts = state.serialize().split(";"); + String thStr = null; + String rvStr = null; + if (hasRv && hasTh) { + assertThat(parts).hasSize(2); + thStr = parts[0]; + rvStr = parts[1]; + } else if (hasRv) { + assertThat(parts).hasSize(1); + rvStr = parts[0]; + } else if (hasTh) { + assertThat(parts).hasSize(1); + thStr = parts[0]; + } + + if (hasRv) { + assertThat(rvStr).startsWith("rv:"); + rvStr = rvStr.substring("rv:".length()); + assertThat(rvStr).hasSize(OtelTraceState.MAX_VALUE_LENGTH); + assertThat(rvStr).isHexadecimal(); + } + + if (hasTh) { + assertThat(thStr).startsWith("th:"); + thStr = thStr.substring("th:".length()); + assertThat(thStr).hasSizeBetween(1, OtelTraceState.MAX_VALUE_LENGTH); + assertThat(thStr).isHexadecimal(); + if (th == 0) { + assertThat(thStr).isEqualTo("0"); + } else { + // No trailing zeros + assertThat(thStr).doesNotMatch("[^0]0+$"); + } + } + } + } + + // driver methods to avoid having to use the vintage junit engine, and to enable increasing the + // number of iterations: + + @Test + void roundTripFuzzing() { + Result result = runTestCase("roundTripRandomValues"); + assertThat(result.wasSuccessful()).isTrue(); + } + + @Test + void roundTripValidValues() { + Result result = runTestCase("roundTripValidValues"); + assertThat(result.wasSuccessful()).isTrue(); + } + + private static Result runTestCase(String testCaseName) { + return GuidedFuzzing.run( + TestCases.class, testCaseName, new NoGuidance(10000, System.out), System.out); + } +} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/OtelTraceStateTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/OtelTraceStateTest.java new file mode 100644 index 00000000000..0d655141481 --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/OtelTraceStateTest.java @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.samplers; + +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.INVALID_RANDOM_VALUE; +import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.MAX_THRESHOLD; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.TraceState; +import java.util.Collections; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class OtelTraceStateTest { + + private static String getXString(int len) { + return Stream.generate(() -> "X").limit(len).collect(Collectors.joining()); + } + + @ParameterizedTest + @CsvSource({ + "'', ''", + "a, a", + "#, #", + ";, ''", + "a;, a", + "a;b;, a;b", + "animal:bear;food:pizza, animal:bear;food:pizza", + "rv:1234567890abcd, rv:1234567890abcd", + "rv:01020304050607, rv:01020304050607", + "rv:1234567890abcde, ''", + "th:1234567890abcd, th:1234567890abcd", + "th:1234567890abcd, th:1234567890abcd", + "th:10000000000000, th:1", + "th:1234500000000, th:12345", + "th:0, th:0", + "th:100000000000000, ''", + "th:1234567890abcde, ''", + "th:, ''", + "th:x, ''", + "th:100000000000000, ''", + "th:10000000000000, th:1", + "th:1000000000000, th:1", + "th:100000000000, th:1", + "th:10000000000, th:1", + "th:1000000000, th:1", + "th:100000000, th:1", + "th:10000000, th:1", + "th:1000000, th:1", + "th:100000, th:1", + "th:10000, th:1", + "th:1000, th:1", + "th:100, th:1", + "th:10, th:1", + "th:1, th:1", + "th:10000000000001, th:10000000000001", + "th:10000000000010, th:1000000000001", + "rv:x, ''", + "rv:xxxxxxxxxxxxxx, ''", + "rv:100000000000000, ''", + "rv:10000000000000, rv:10000000000000", + "rv:1000000000000, ''", + }) + void roundTrip(String input, String output) { + String result = OtelTraceState.parse(TraceState.builder().put("ot", input).build()).serialize(); + assertThat(result).isEqualTo(output); + } + + @Test + void notTooLong() { + String input = "a:" + getXString(214) + ";rv:1234567890abcd;th:1234567890abcd;x:3"; + String result = OtelTraceState.parse(TraceState.builder().put("ot", input).build()).serialize(); + assertThat(result) + .isEqualTo("th:1234567890abcd;rv:1234567890abcd;a:" + getXString(214) + ";x:3"); + } + + @Test + void tooLong() { + String input = "a:" + getXString(215) + ";rv:1234567890abcd;th:1234567890abcd;x:3"; + String result = OtelTraceState.parse(TraceState.builder().put("ot", input).build()).serialize(); + assertThat(result).isEmpty(); + } + + @Test + void missing() { + String result = OtelTraceState.parse(TraceState.getDefault()).serialize(); + assertThat(result).isEmpty(); + } + + @Test + void emptyMaxThreshold() { + String result = + new OtelTraceState(INVALID_RANDOM_VALUE, MAX_THRESHOLD, Collections.emptyList()) + .serialize(); + assertThat(result).isEmpty(); + } +} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/TestUtil.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/TestUtil.java new file mode 100644 index 00000000000..37bdd131dab --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/TestUtil.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.samplers; + +import io.opentelemetry.api.trace.TraceId; +import java.util.Random; +import java.util.function.Supplier; + +final class TestUtil { + + static Supplier traceIdGenerator() { + // Generate a fixed set of random trace IDs for reliable sampling tests. + Random random = new Random(0xabcd1234L); + return () -> { + long a = random.nextLong(); + long b; + do { + b = random.nextLong(); + } while (b == 0); + return TraceId.fromLongs(a, b); + }; + } + + private TestUtil() {} +}