Skip to content

Commit 48dd6b4

Browse files
authored
[dynamic control] Use composable samplers and add sampler initialization (#2752)
1 parent 47b401d commit 48dd6b4

4 files changed

Lines changed: 166 additions & 8 deletions

File tree

dynamic-control/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ dependencies {
2424

2525
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
2626
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
27+
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator")
2728

2829
testCompileOnly("com.google.auto.service:auto-service-annotations")
2930
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
@@ -32,6 +33,7 @@ dependencies {
3233

3334
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
3435
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
36+
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator")
3537
testImplementation("org.assertj:assertj-core")
3638
testImplementation("org.mockito:mockito-inline")
3739
testImplementation("org.mockito:mockito-junit-jupiter")

dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/tracesampling/TraceSamplingRatePolicy.java

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,67 @@
66
package io.opentelemetry.contrib.dynamic.policy.tracesampling;
77

88
import io.opentelemetry.contrib.dynamic.policy.TelemetryPolicy;
9+
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer;
10+
import io.opentelemetry.sdk.extension.incubator.trace.samplers.ComposableSampler;
11+
import io.opentelemetry.sdk.extension.incubator.trace.samplers.CompositeSampler;
12+
import io.opentelemetry.sdk.trace.samplers.Sampler;
13+
import java.util.Objects;
14+
import javax.annotation.Nullable;
915

1016
public final class TraceSamplingRatePolicy extends TelemetryPolicy {
1117
public static final String POLICY_TYPE = "trace-sampling";
1218

19+
@Nullable private static volatile DelegatingSampler initializedSampler;
20+
1321
private final double probability;
1422

1523
public TraceSamplingRatePolicy(double probability) {
1624
super(POLICY_TYPE);
25+
this.probability = normalizeProbability(probability);
26+
}
27+
28+
public double getProbability() {
29+
return probability;
30+
}
31+
32+
/**
33+
* Initializes runtime wiring for this policy type.
34+
*
35+
* <p>If the extension is configured to use this policy, this installs an opinionated sampler that
36+
* overrides any other sampler
37+
*/
38+
public static void initialize(AutoConfigurationCustomizer autoConfiguration) {
39+
Objects.requireNonNull(autoConfiguration, "autoConfiguration cannot be null");
40+
Sampler initialDelegate = createSampler(1.0);
41+
DelegatingSampler delegatingSampler = new DelegatingSampler(initialDelegate);
42+
initializedSampler = delegatingSampler;
43+
autoConfiguration.addSamplerCustomizer((sampler, config) -> delegatingSampler);
44+
}
45+
46+
/**
47+
* Creates the composed sampler used for this policy probability.
48+
*
49+
* @param probability sampling probability in the inclusive range {@code [0.0, 1.0]}
50+
* @return a sampler equivalent to the configured probability with parent-based behavior
51+
* @throws IllegalArgumentException if probability is NaN or outside {@code [0.0, 1.0]}
52+
*/
53+
public static Sampler createSampler(double probability) {
54+
probability = normalizeProbability(probability);
55+
return CompositeSampler.wrap(
56+
ComposableSampler.parentThreshold(ComposableSampler.probability(probability)));
57+
}
58+
59+
private static double normalizeProbability(double probability) {
1760
if (Double.isNaN(probability) || probability < 0.0 || probability > 1.0) {
1861
throw new IllegalArgumentException("probability must be within [0.0, 1.0]");
1962
}
20-
this.probability = probability;
63+
// Normalize -0.0 to +0.0 so equality/hash behavior stays intuitive.
64+
return probability == 0.0 ? 0.0 : probability;
2165
}
2266

23-
public double getProbability() {
24-
return probability;
67+
@Nullable
68+
public static DelegatingSampler getInitializedSampler() {
69+
return initializedSampler;
2570
}
2671

2772
@Override

dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/tracesampling/TraceSamplingRatePolicyImplementer.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,18 @@
1212
import java.util.Collections;
1313
import java.util.List;
1414
import java.util.Objects;
15+
import java.util.logging.Logger;
1516

1617
/**
1718
* Implements the {@code trace-sampling} policy by updating a {@link DelegatingSampler}.
1819
*
1920
* <p>This implementer listens for validated {@link TelemetryPolicy} updates of type {@code
2021
* "trace-sampling"} and applies {@link TraceSamplingRatePolicy#getProbability()} to the delegate
21-
* sampler using {@link Sampler#traceIdRatioBased(double)} wrapped by {@link
22-
* Sampler#parentBased(Sampler)}.
22+
* sampler via {@link TraceSamplingRatePolicy#createSampler(double)}.
2323
*
2424
* <p>If a type-only {@link TelemetryPolicy} of type {@code "trace-sampling"} is received, it is
25-
* treated as policy removal and the delegate falls back to {@link Sampler#alwaysOn()}.
25+
* treated as policy removal and the delegate is reset using {@code
26+
* TraceSamplingRatePolicy.createSampler(1.0)}.
2627
*
2728
* <p>Validation is performed by {@link TraceSamplingValidator}; this implementer only consumes
2829
* policies produced by that validator.
@@ -31,6 +32,8 @@
3132
* with sampling operations on the associated {@link DelegatingSampler}.
3233
*/
3334
public final class TraceSamplingRatePolicyImplementer implements PolicyImplementer {
35+
private static final Logger logger =
36+
Logger.getLogger(TraceSamplingRatePolicyImplementer.class.getName());
3437

3538
private static final List<PolicyValidator> VALIDATORS =
3639
Collections.<PolicyValidator>singletonList(new TraceSamplingValidator());
@@ -60,12 +63,14 @@ public void onPoliciesChanged(List<TelemetryPolicy> policies) {
6063
}
6164
if (!(policy instanceof TraceSamplingRatePolicy)) {
6265
// Type-only policy represents removing trace-sampling config.
63-
delegatingSampler.setDelegate(Sampler.alwaysOn());
66+
delegatingSampler.setDelegate(TraceSamplingRatePolicy.createSampler(1.0));
67+
logger.info("Applied trace sampling policy reset: probability reset to 1.0");
6468
continue;
6569
}
6670
double ratio = ((TraceSamplingRatePolicy) policy).getProbability();
67-
Sampler sampler = Sampler.parentBased(Sampler.traceIdRatioBased(ratio));
71+
Sampler sampler = TraceSamplingRatePolicy.createSampler(ratio);
6872
delegatingSampler.setDelegate(sampler);
73+
logger.info("Applied trace sampling policy update: probability=" + ratio);
6974
}
7075
}
7176
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.dynamic.policy.tracesampling;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
10+
import static org.mockito.ArgumentMatchers.any;
11+
import static org.mockito.Mockito.mock;
12+
import static org.mockito.Mockito.verify;
13+
14+
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer;
15+
import io.opentelemetry.sdk.trace.samplers.Sampler;
16+
import org.junit.jupiter.api.Test;
17+
18+
class TraceSamplingRatePolicyTest {
19+
20+
@Test
21+
void constructorStoresProbabilityAndType() {
22+
TraceSamplingRatePolicy policy = new TraceSamplingRatePolicy(0.25);
23+
24+
assertThat(policy.getProbability()).isEqualTo(0.25);
25+
assertThat(policy.getType()).isEqualTo(TraceSamplingRatePolicy.POLICY_TYPE);
26+
}
27+
28+
@Test
29+
void constructorNormalizesNegativeZeroToPositiveZero() {
30+
TraceSamplingRatePolicy negativeZero = new TraceSamplingRatePolicy(-0.0);
31+
TraceSamplingRatePolicy positiveZero = new TraceSamplingRatePolicy(0.0);
32+
33+
assertThat(negativeZero.getProbability()).isEqualTo(0.0);
34+
assertThat(Double.doubleToRawLongBits(negativeZero.getProbability()))
35+
.isEqualTo(Double.doubleToRawLongBits(0.0));
36+
assertThat(negativeZero).isEqualTo(positiveZero);
37+
assertThat(negativeZero.hashCode()).isEqualTo(positiveZero.hashCode());
38+
}
39+
40+
@Test
41+
void constructorRejectsOutOfRangeOrNaNProbabilities() {
42+
assertThatThrownBy(() -> new TraceSamplingRatePolicy(Double.NaN))
43+
.isInstanceOf(IllegalArgumentException.class)
44+
.hasMessage("probability must be within [0.0, 1.0]");
45+
assertThatThrownBy(() -> new TraceSamplingRatePolicy(-0.001))
46+
.isInstanceOf(IllegalArgumentException.class)
47+
.hasMessage("probability must be within [0.0, 1.0]");
48+
assertThatThrownBy(() -> new TraceSamplingRatePolicy(1.001))
49+
.isInstanceOf(IllegalArgumentException.class)
50+
.hasMessage("probability must be within [0.0, 1.0]");
51+
}
52+
53+
@Test
54+
void equalsAndHashCodeUseProbability() {
55+
TraceSamplingRatePolicy a = new TraceSamplingRatePolicy(0.5);
56+
TraceSamplingRatePolicy b = new TraceSamplingRatePolicy(0.5);
57+
TraceSamplingRatePolicy c = new TraceSamplingRatePolicy(0.75);
58+
59+
assertThat(a).isEqualTo(b);
60+
assertThat(a.hashCode()).isEqualTo(b.hashCode());
61+
assertThat(a).isNotEqualTo(c);
62+
assertThat(a).isNotEqualTo(null);
63+
assertThat(a).isNotEqualTo("not-a-policy");
64+
}
65+
66+
@Test
67+
void initializeRejectsNullCustomizer() {
68+
assertThatThrownBy(() -> TraceSamplingRatePolicy.initialize(null))
69+
.isInstanceOf(NullPointerException.class)
70+
.hasMessage("autoConfiguration cannot be null");
71+
}
72+
73+
@Test
74+
void initializeStoresDelegatingSamplerAndRegistersCustomizer() {
75+
AutoConfigurationCustomizer customizer = mock(AutoConfigurationCustomizer.class);
76+
77+
TraceSamplingRatePolicy.initialize(customizer);
78+
79+
assertThat(TraceSamplingRatePolicy.getInitializedSampler()).isNotNull();
80+
verify(customizer).addSamplerCustomizer(any());
81+
}
82+
83+
@Test
84+
void createSamplerAcceptsBoundaryProbabilities() {
85+
Sampler zero = TraceSamplingRatePolicy.createSampler(0.0);
86+
Sampler negativeZero = TraceSamplingRatePolicy.createSampler(-0.0);
87+
Sampler one = TraceSamplingRatePolicy.createSampler(1.0);
88+
89+
assertThat(zero).isNotNull();
90+
assertThat(negativeZero).isNotNull();
91+
assertThat(one).isNotNull();
92+
}
93+
94+
@Test
95+
void createSamplerRejectsOutOfRangeOrNaNProbabilities() {
96+
assertThatThrownBy(() -> TraceSamplingRatePolicy.createSampler(Double.NaN))
97+
.isInstanceOf(IllegalArgumentException.class)
98+
.hasMessage("probability must be within [0.0, 1.0]");
99+
assertThatThrownBy(() -> TraceSamplingRatePolicy.createSampler(-0.01))
100+
.isInstanceOf(IllegalArgumentException.class)
101+
.hasMessage("probability must be within [0.0, 1.0]");
102+
assertThatThrownBy(() -> TraceSamplingRatePolicy.createSampler(1.01))
103+
.isInstanceOf(IllegalArgumentException.class)
104+
.hasMessage("probability must be within [0.0, 1.0]");
105+
}
106+
}

0 commit comments

Comments
 (0)