From 9691526507a83fd4ad28f4dd2502b1d3e6cf398b Mon Sep 17 00:00:00 2001 From: Yang Song Date: Fri, 31 May 2019 07:59:49 -0700 Subject: [PATCH] Add ProbabilitySampler in SDK. (#337) * Add ProbabilitySampler in SDK. * Add more tests. * Fix merging error. --- .../trace/samplers/ProbabilitySampler.java | 135 +++++++++ .../samplers/ProbabilitySamplerTest.java | 273 ++++++++++++++++++ 2 files changed, 408 insertions(+) create mode 100644 sdk/src/main/java/io/opentelemetry/sdk/trace/samplers/ProbabilitySampler.java create mode 100644 sdk/src/test/java/io/opentelemetry/sdk/trace/samplers/ProbabilitySamplerTest.java diff --git a/sdk/src/main/java/io/opentelemetry/sdk/trace/samplers/ProbabilitySampler.java b/sdk/src/main/java/io/opentelemetry/sdk/trace/samplers/ProbabilitySampler.java new file mode 100644 index 00000000000..1b0cf69cc1e --- /dev/null +++ b/sdk/src/main/java/io/opentelemetry/sdk/trace/samplers/ProbabilitySampler.java @@ -0,0 +1,135 @@ +/* + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.trace.samplers; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Preconditions; +import io.opentelemetry.trace.AttributeValue; +import io.opentelemetry.trace.Sampler; +import io.opentelemetry.trace.Span; +import io.opentelemetry.trace.SpanContext; +import io.opentelemetry.trace.SpanId; +import io.opentelemetry.trace.TraceId; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * We assume the lower 64 bits of the traceId's are randomly distributed around the whole (long) + * range. We convert an incoming probability into an upper bound on that value, such that we can + * just compare the absolute value of the id and the bound to see if we are within the desired + * probability range. Using the low bits of the traceId also ensures that systems that only use 64 + * bit ID's will also work with this sampler. + */ +@AutoValue +@Immutable +public abstract class ProbabilitySampler implements Sampler { + + ProbabilitySampler() {} + + abstract double getProbability(); + + abstract long getIdUpperBound(); + + /** + * Returns a new {@link ProbabilitySampler}. The probability of sampling a trace is equal to that + * of the specified probability. + * + * @param probability The desired probability of sampling. Must be within [0.0, 1.0]. + * @return a new {@link ProbabilitySampler}. + * @throws IllegalArgumentException if {@code probability} is out of range + */ + public static ProbabilitySampler create(double probability) { + Preconditions.checkArgument( + probability >= 0.0 && probability <= 1.0, "probability must be in range [0.0, 1.0]"); + long idUpperBound; + // Special case the limits, to avoid any possible issues with lack of precision across + // double/long boundaries. For probability == 0.0, we use Long.MIN_VALUE as this guarantees + // that we will never sample a trace, even in the case where the id == Long.MIN_VALUE, since + // Math.Abs(Long.MIN_VALUE) == Long.MIN_VALUE. + if (probability == 0.0) { + idUpperBound = Long.MIN_VALUE; + } else if (probability == 1.0) { + idUpperBound = Long.MAX_VALUE; + } else { + idUpperBound = (long) (probability * Long.MAX_VALUE); + } + return new AutoValue_ProbabilitySampler(probability, idUpperBound); + } + + @Override + public final Decision shouldSample( + @Nullable SpanContext parentContext, + @Nullable Boolean hasRemoteParent, + TraceId traceId, + SpanId spanId, + String name, + @Nullable List parentLinks) { + // If the parent is sampled keep the sampling decision. + if (parentContext != null && parentContext.getTraceOptions().isSampled()) { + return new SimpleDecision(true); + } + if (parentLinks != null) { + // If any parent link is sampled keep the sampling decision. + for (Span parentLink : parentLinks) { + if (parentLink.getContext().getTraceOptions().isSampled()) { + return new SimpleDecision(true); + } + } + } + // Always sample if we are within probability range. This is true even for child spans (that + // may have had a different sampling decision made) to allow for different sampling policies, + // and dynamic increases to sampling probabilities for debugging purposes. + // Note use of '<' for comparison. This ensures that we never sample for probability == 0.0, + // while allowing for a (very) small chance of *not* sampling if the id == Long.MAX_VALUE. + // This is considered a reasonable tradeoff for the simplicity/performance requirements (this + // code is executed in-line for every Span creation). + return new SimpleDecision(Math.abs(traceId.getLowerLong()) < getIdUpperBound()); + } + + @Override + public final String getDescription() { + return String.format("ProbabilitySampler{%.6f}", getProbability()); + } + + /** Sampling decision without attributes. */ + private static final class SimpleDecision implements Decision { + + private final boolean decision; + + /** + * Creates sampling decision without attributes. + * + * @param decision sampling decision + */ + SimpleDecision(boolean decision) { + this.decision = decision; + } + + @Override + public boolean isSampled() { + return decision; + } + + @Override + public Map attributes() { + return Collections.emptyMap(); + } + } +} diff --git a/sdk/src/test/java/io/opentelemetry/sdk/trace/samplers/ProbabilitySamplerTest.java b/sdk/src/test/java/io/opentelemetry/sdk/trace/samplers/ProbabilitySamplerTest.java new file mode 100644 index 00000000000..97df669bc07 --- /dev/null +++ b/sdk/src/test/java/io/opentelemetry/sdk/trace/samplers/ProbabilitySamplerTest.java @@ -0,0 +1,273 @@ +/* + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.trace.samplers; + +import static com.google.common.truth.Truth.assertThat; + +import io.opentelemetry.trace.AttributeValue; +import io.opentelemetry.trace.Event; +import io.opentelemetry.trace.Link; +import io.opentelemetry.trace.Sampler; +import io.opentelemetry.trace.Span; +import io.opentelemetry.trace.SpanContext; +import io.opentelemetry.trace.SpanId; +import io.opentelemetry.trace.Status; +import io.opentelemetry.trace.TraceId; +import io.opentelemetry.trace.TraceOptions; +import io.opentelemetry.trace.Tracestate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link ProbabilitySampler}. */ +@RunWith(JUnit4.class) +public class ProbabilitySamplerTest { + + private static final String SPAN_NAME = "MySpanName"; + private static final int NUM_SAMPLE_TRIES = 1000; + private final TraceId traceId = generateRandomTraceId(); + private final SpanId parentSpanId = generateRandomSpanId(); + private final Tracestate tracestate = Tracestate.builder().build(); + private final SpanContext sampledSpanContext = + SpanContext.create( + traceId, parentSpanId, TraceOptions.builder().setIsSampled(true).build(), tracestate); + private final SpanContext notSampledSpanContext = + SpanContext.create(traceId, parentSpanId, TraceOptions.DEFAULT, tracestate); + private final Span sampledSpan = + new Span() { + @Override + public void setAttribute(String key, String value) {} + + @Override + public void setAttribute(String key, long value) {} + + @Override + public void setAttribute(String key, double value) {} + + @Override + public void setAttribute(String key, boolean value) {} + + @Override + public void setAttribute(String key, AttributeValue value) {} + + @Override + public void addEvent(String name) {} + + @Override + public void addEvent(String name, Map attributes) {} + + @Override + public void addEvent(Event event) {} + + @Override + public void addLink(Link link) {} + + @Override + public void setStatus(Status status) {} + + @Override + public void updateName(String name) {} + + @Override + public void end() {} + + @Override + public SpanContext getContext() { + return sampledSpanContext; + } + + @Override + public boolean isRecordingEvents() { + return true; + } + }; + + @Rule public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void probabilitySampler_outOfRangeHighProbability() { + thrown.expect(IllegalArgumentException.class); + ProbabilitySampler.create(1.01); + } + + @Test + public void probabilitySampler_outOfRangeLowProbability() { + thrown.expect(IllegalArgumentException.class); + ProbabilitySampler.create(-0.00001); + } + + @Test + public void probabilitySampler_getDescription() { + assertThat(ProbabilitySampler.create(0.5).getDescription()) + .isEqualTo(String.format("ProbabilitySampler{%.6f}", 0.5)); + } + + @Test + public void probabilitySampler_ToString() { + assertThat(ProbabilitySampler.create(0.5).toString()).contains("0.5"); + } + + // Applies the given sampler to NUM_SAMPLE_TRIES random traceId/spanId pairs. + private static void assertSamplerSamplesWithProbability( + Sampler sampler, SpanContext parent, List parentLinks, double probability) { + int count = 0; // Count of spans with sampling enabled + for (int i = 0; i < NUM_SAMPLE_TRIES; i++) { + if (sampler + .shouldSample( + parent, + false, + generateRandomTraceId(), + generateRandomSpanId(), + SPAN_NAME, + parentLinks) + .isSampled()) { + count++; + } + } + double proportionSampled = (double) count / NUM_SAMPLE_TRIES; + // Allow for a large amount of slop (+/- 10%) in number of sampled traces, to avoid flakiness. + assertThat(proportionSampled < probability + 0.1 && proportionSampled > probability - 0.1) + .isTrue(); + } + + @Test + public void probabilitySampler_DifferentProbabilities_NotSampledParent() { + final Sampler fiftyPercentSample = ProbabilitySampler.create(0.5); + assertSamplerSamplesWithProbability( + fiftyPercentSample, notSampledSpanContext, Collections.emptyList(), 0.5); + final Sampler twentyPercentSample = ProbabilitySampler.create(0.2); + assertSamplerSamplesWithProbability( + twentyPercentSample, notSampledSpanContext, Collections.emptyList(), 0.2); + final Sampler twoThirdsSample = ProbabilitySampler.create(2.0 / 3.0); + assertSamplerSamplesWithProbability( + twoThirdsSample, notSampledSpanContext, Collections.emptyList(), 2.0 / 3.0); + } + + @Test + public void probabilitySampler_DifferentProbabilities_SampledParent() { + final Sampler fiftyPercentSample = ProbabilitySampler.create(0.5); + assertSamplerSamplesWithProbability( + fiftyPercentSample, sampledSpanContext, Collections.emptyList(), 1.0); + final Sampler twentyPercentSample = ProbabilitySampler.create(0.2); + assertSamplerSamplesWithProbability( + twentyPercentSample, sampledSpanContext, Collections.emptyList(), 1.0); + final Sampler twoThirdsSample = ProbabilitySampler.create(2.0 / 3.0); + assertSamplerSamplesWithProbability( + twoThirdsSample, sampledSpanContext, Collections.emptyList(), 1.0); + } + + @Test + public void probabilitySampler_DifferentProbabilities_SampledParentLink() { + final Sampler fiftyPercentSample = ProbabilitySampler.create(0.5); + assertSamplerSamplesWithProbability( + fiftyPercentSample, notSampledSpanContext, Arrays.asList(sampledSpan), 1.0); + final Sampler twentyPercentSample = ProbabilitySampler.create(0.2); + assertSamplerSamplesWithProbability( + twentyPercentSample, notSampledSpanContext, Arrays.asList(sampledSpan), 1.0); + final Sampler twoThirdsSample = ProbabilitySampler.create(2.0 / 3.0); + assertSamplerSamplesWithProbability( + twoThirdsSample, notSampledSpanContext, Arrays.asList(sampledSpan), 1.0); + } + + @Test + public void probabilitySampler_SampleBasedOnTraceId() { + final Sampler defaultProbability = ProbabilitySampler.create(0.0001); + // This traceId will not be sampled by the ProbabilitySampler because the first 8 bytes as long + // is not less than probability * Long.MAX_VALUE; + TraceId notSampledtraceId = + TraceId.fromBytes( + new byte[] { + (byte) 0x8F, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + }, + 0); + assertThat( + defaultProbability + .shouldSample( + null, + false, + notSampledtraceId, + generateRandomSpanId(), + SPAN_NAME, + Collections.emptyList()) + .isSampled()) + .isFalse(); + // This traceId will be sampled by the ProbabilitySampler because the first 8 bytes as long + // is less than probability * Long.MAX_VALUE; + TraceId sampledtraceId = + TraceId.fromBytes( + new byte[] { + (byte) 0x00, + (byte) 0x00, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + }, + 0); + assertThat( + defaultProbability + .shouldSample( + null, + false, + sampledtraceId, + generateRandomSpanId(), + SPAN_NAME, + Collections.emptyList()) + .isSampled()) + .isTrue(); + } + + private static TraceId generateRandomTraceId() { + return TraceId.fromLowerBase16(UUID.randomUUID().toString().replace("-", ""), 0); + } + + private static SpanId generateRandomSpanId() { + return SpanId.fromLowerBase16(UUID.randomUUID().toString().replace("-", ""), 0); + } +}