Skip to content

Commit 9ebd9d3

Browse files
committed
Add incubator ComposableRuleBasedSampler
1 parent 18780a3 commit 9ebd9d3

File tree

9 files changed

+347
-9
lines changed

9 files changed

+347
-9
lines changed

sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableAlwaysOffSampler.java

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,17 @@
55

66
package io.opentelemetry.sdk.extension.incubator.trace.samplers;
77

8+
import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.NON_SAMPLING_INTENT;
9+
810
import io.opentelemetry.api.common.Attributes;
911
import io.opentelemetry.api.trace.SpanKind;
1012
import io.opentelemetry.context.Context;
1113
import io.opentelemetry.sdk.trace.data.LinkData;
1214
import java.util.List;
13-
import java.util.function.Function;
1415

1516
enum ComposableAlwaysOffSampler implements ComposableSampler {
1617
INSTANCE;
1718

18-
private static final SamplingIntent INTENT =
19-
SamplingIntent.create(
20-
ImmutableSamplingIntent.INVALID_THRESHOLD,
21-
/* thresholdReliable= */ false,
22-
Attributes.empty(),
23-
Function.identity());
24-
2519
@Override
2620
public SamplingIntent getSamplingIntent(
2721
Context parentContext,
@@ -30,7 +24,7 @@ public SamplingIntent getSamplingIntent(
3024
SpanKind spanKind,
3125
Attributes attributes,
3226
List<LinkData> parentLinks) {
33-
return INTENT;
27+
return NON_SAMPLING_INTENT;
3428
}
3529

3630
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.extension.incubator.trace.samplers;
7+
8+
import static io.opentelemetry.sdk.extension.incubator.trace.samplers.ImmutableSamplingIntent.NON_SAMPLING_INTENT;
9+
10+
import io.opentelemetry.api.common.Attributes;
11+
import io.opentelemetry.api.trace.SpanKind;
12+
import io.opentelemetry.context.Context;
13+
import io.opentelemetry.sdk.trace.data.LinkData;
14+
import java.util.List;
15+
16+
final class ComposableRuleBasedSampler implements ComposableSampler {
17+
18+
private final SamplingRule[] rules;
19+
private final String description;
20+
21+
ComposableRuleBasedSampler(List<SamplingRule> rules) {
22+
this.rules = rules.toArray(new SamplingRule[0]);
23+
24+
StringBuilder description = new StringBuilder("ComposableRuleBasedSampler{[");
25+
if (this.rules.length > 0) {
26+
for (SamplingRule rule : this.rules) {
27+
description.append('(');
28+
description.append(rule.predicate().getDescription());
29+
description.append(':');
30+
description.append(rule.sampler().getDescription());
31+
description.append(')');
32+
description.append(',');
33+
}
34+
// Remove trailing comma
35+
description.setLength(description.length() - 1);
36+
}
37+
description.append("]}");
38+
this.description = description.toString();
39+
}
40+
41+
@Override
42+
public SamplingIntent getSamplingIntent(
43+
Context parentContext,
44+
String traceId,
45+
String name,
46+
SpanKind spanKind,
47+
Attributes attributes,
48+
List<LinkData> parentLinks) {
49+
for (SamplingRule rule : rules) {
50+
if (rule.predicate()
51+
.matches(parentContext, traceId, name, spanKind, attributes, parentLinks)) {
52+
return rule.sampler()
53+
.getSamplingIntent(parentContext, traceId, name, spanKind, attributes, parentLinks);
54+
}
55+
}
56+
return NON_SAMPLING_INTENT;
57+
}
58+
59+
@Override
60+
public String getDescription() {
61+
return description;
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.extension.incubator.trace.samplers;
7+
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
11+
/** A builder for a composable rule-based sampler. */
12+
public final class ComposableRuleBasedSamplerBuilder {
13+
private final List<SamplingRule> rules = new ArrayList<>();
14+
15+
/**
16+
* Adds a rule to use the given {@link ComposableSampler} if the {@link SamplingPredicate}
17+
* matches.
18+
*/
19+
public ComposableRuleBasedSamplerBuilder add(
20+
SamplingPredicate predicate, ComposableSampler sampler) {
21+
rules.add(ImmutableSamplingRule.create(predicate, sampler));
22+
return this;
23+
}
24+
25+
/** Returns a {@link ComposableSampler} with the rules in this builder. */
26+
public ComposableSampler build() {
27+
return new ComposableRuleBasedSampler(rules);
28+
}
29+
}

sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableSampler.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ static ComposableSampler parentThreshold(ComposableSampler rootSampler) {
3636
return new ComposableParentThresholdSampler(rootSampler);
3737
}
3838

39+
/**
40+
* Returns a {@link ComposableRuleBasedSamplerBuilder} to create a composable rule-based sampler.
41+
* Rules will be tested in order, and the first to match will have its {@link ComposableSampler}
42+
* used for a sampling decision. If no rule matches, the span will be dropped.
43+
*/
44+
static ComposableRuleBasedSamplerBuilder ruleBasedBuilder() {
45+
return new ComposableRuleBasedSamplerBuilder();
46+
}
47+
3948
/** Returns the {@link SamplingIntent} to use to make a sampling decision. */
4049
SamplingIntent getSamplingIntent(
4150
Context parentContext,

sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ImmutableSamplingIntent.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ abstract class ImmutableSamplingIntent implements SamplingIntent {
2020
static final long MAX_THRESHOLD = 1L << RANDOM_VALUE_BITS;
2121
static final long MAX_RANDOM_VALUE = MAX_THRESHOLD - 1;
2222

23+
static final SamplingIntent NON_SAMPLING_INTENT =
24+
create(
25+
ImmutableSamplingIntent.INVALID_THRESHOLD,
26+
/* thresholdReliable= */ false,
27+
Attributes.empty(),
28+
Function.identity());
29+
2330
static boolean isValidThreshold(long threshold) {
2431
return threshold >= MIN_THRESHOLD && threshold <= MAX_THRESHOLD;
2532
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.extension.incubator.trace.samplers;
7+
8+
import com.google.auto.value.AutoValue;
9+
10+
@AutoValue
11+
abstract class ImmutableSamplingRule implements SamplingRule {
12+
static final ImmutableSamplingRule create(
13+
SamplingPredicate predicate, ComposableSampler sampler) {
14+
return new AutoValue_ImmutableSamplingRule(predicate, sampler);
15+
}
16+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.extension.incubator.trace.samplers;
7+
8+
import io.opentelemetry.api.common.Attributes;
9+
import io.opentelemetry.api.trace.SpanKind;
10+
import io.opentelemetry.context.Context;
11+
import io.opentelemetry.sdk.trace.data.LinkData;
12+
import java.util.List;
13+
14+
/** A predicate for a composable sampler, indicating whether a set of sampling arguments matches. */
15+
public interface SamplingPredicate {
16+
/** Returns whether this {@link SamplingPredicate} matches the given sampling arguments. */
17+
boolean matches(
18+
Context parentContext,
19+
String traceId,
20+
String name,
21+
SpanKind spanKind,
22+
Attributes attributes,
23+
List<LinkData> parentLinks);
24+
25+
/**
26+
* Returns a description of the {@link SamplingPredicate}. This may be displayed on debug pages or
27+
* in the logs.
28+
*/
29+
String getDescription();
30+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.extension.incubator.trace.samplers;
7+
8+
/** A rule which returns a {@link ComposableSampler} to use when a predicate matches. */
9+
public interface SamplingRule {
10+
11+
/** The {@link SamplingPredicate} which indicates whether to use {@link #sampler()}. */
12+
SamplingPredicate predicate();
13+
14+
/** The {@link ComposableSampler} to use when the rule matches. */
15+
ComposableSampler sampler();
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.extension.incubator.trace.samplers;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
import io.opentelemetry.api.common.AttributeKey;
11+
import io.opentelemetry.api.common.Attributes;
12+
import io.opentelemetry.api.trace.Span;
13+
import io.opentelemetry.api.trace.SpanContext;
14+
import io.opentelemetry.api.trace.SpanId;
15+
import io.opentelemetry.api.trace.SpanKind;
16+
import io.opentelemetry.api.trace.TraceFlags;
17+
import io.opentelemetry.api.trace.TraceId;
18+
import io.opentelemetry.api.trace.TraceState;
19+
import io.opentelemetry.context.Context;
20+
import io.opentelemetry.sdk.trace.data.LinkData;
21+
import io.opentelemetry.sdk.trace.samplers.Sampler;
22+
import io.opentelemetry.sdk.trace.samplers.SamplingDecision;
23+
import java.util.Collections;
24+
import java.util.List;
25+
import org.junit.jupiter.api.Test;
26+
27+
class ComposableRuleBasedSamplerTest {
28+
29+
private static final AttributeKey<String> HTTP_ROUTE = AttributeKey.stringKey("http.route");
30+
31+
private static final class AttributePredicate<T> implements SamplingPredicate {
32+
33+
private final AttributeKey<T> key;
34+
private final String description;
35+
36+
private AttributePredicate(AttributeKey<T> key, T value) {
37+
this.key = key;
38+
this.description = key.getKey() + "=" + value;
39+
}
40+
41+
@Override
42+
public boolean matches(
43+
Context parentContext,
44+
String traceId,
45+
String name,
46+
SpanKind spanKind,
47+
Attributes attributes,
48+
List<LinkData> parentLinks) {
49+
return "/health".equals(attributes.get(key));
50+
}
51+
52+
@Override
53+
public String getDescription() {
54+
return description;
55+
}
56+
57+
@Override
58+
public String toString() {
59+
return getDescription();
60+
}
61+
}
62+
63+
private enum IsRootPredicate implements SamplingPredicate {
64+
INSTANCE;
65+
66+
@Override
67+
public boolean matches(
68+
Context parentContext,
69+
String traceId,
70+
String name,
71+
SpanKind spanKind,
72+
Attributes attributes,
73+
List<LinkData> parentLinks) {
74+
return !Span.fromContext(parentContext).getSpanContext().isValid();
75+
}
76+
77+
@Override
78+
public String getDescription() {
79+
return "isRoot";
80+
}
81+
}
82+
83+
@Test
84+
void testDescription() {
85+
assertThat(ComposableSampler.ruleBasedBuilder().build().getDescription())
86+
.isEqualTo("ComposableRuleBasedSampler{[]}");
87+
assertThat(
88+
ComposableSampler.ruleBasedBuilder()
89+
.add(new AttributePredicate<>(HTTP_ROUTE, "/health"), ComposableSampler.alwaysOff())
90+
.build()
91+
.getDescription())
92+
.isEqualTo("ComposableRuleBasedSampler{[(http.route=/health:ComposableAlwaysOffSampler)]}");
93+
assertThat(
94+
ComposableSampler.ruleBasedBuilder()
95+
.add(new AttributePredicate<>(HTTP_ROUTE, "/health"), ComposableSampler.alwaysOff())
96+
.add(IsRootPredicate.INSTANCE, ComposableSampler.alwaysOn())
97+
.build()
98+
.getDescription())
99+
.isEqualTo(
100+
"ComposableRuleBasedSampler{[(http.route=/health:ComposableAlwaysOffSampler),(isRoot:ComposableAlwaysOnSampler)]}");
101+
}
102+
103+
@Test
104+
void noRules() {
105+
Sampler sampler = CompositeSampler.wrap(ComposableSampler.ruleBasedBuilder().build());
106+
assertThat(
107+
sampler
108+
.shouldSample(
109+
Context.root(),
110+
TraceId.fromLongs(1, 2),
111+
SpanId.fromLong(3),
112+
SpanKind.SERVER,
113+
Attributes.empty(),
114+
Collections.emptyList())
115+
.getDecision())
116+
.isEqualTo(SamplingDecision.DROP);
117+
}
118+
119+
@Test
120+
void rules() {
121+
Sampler sampler =
122+
CompositeSampler.wrap(
123+
ComposableSampler.ruleBasedBuilder()
124+
.add(new AttributePredicate<>(HTTP_ROUTE, "/health"), ComposableSampler.alwaysOff())
125+
.add(IsRootPredicate.INSTANCE, ComposableSampler.alwaysOn())
126+
.build());
127+
128+
// root health check
129+
assertThat(
130+
sampler
131+
.shouldSample(
132+
Context.root(),
133+
TraceId.fromLongs(1, 2),
134+
SpanId.fromLong(3),
135+
SpanKind.SERVER,
136+
Attributes.of(HTTP_ROUTE, "/health"),
137+
Collections.emptyList())
138+
.getDecision())
139+
.isEqualTo(SamplingDecision.DROP);
140+
141+
// root
142+
assertThat(
143+
sampler
144+
.shouldSample(
145+
Context.root(),
146+
TraceId.fromLongs(1, 2),
147+
SpanId.fromLong(3),
148+
SpanKind.SERVER,
149+
Attributes.empty(),
150+
Collections.emptyList())
151+
.getDecision())
152+
.isEqualTo(SamplingDecision.RECORD_AND_SAMPLE);
153+
154+
// no match
155+
assertThat(
156+
sampler
157+
.shouldSample(
158+
Context.root()
159+
.with(
160+
Span.wrap(
161+
SpanContext.create(
162+
TraceId.fromLongs(1, 2),
163+
SpanId.fromLong(2),
164+
TraceFlags.getSampled(),
165+
TraceState.getDefault()))),
166+
TraceId.fromLongs(1, 2),
167+
SpanId.fromLong(3),
168+
SpanKind.SERVER,
169+
Attributes.empty(),
170+
Collections.emptyList())
171+
.getDecision())
172+
.isEqualTo(SamplingDecision.DROP);
173+
}
174+
}

0 commit comments

Comments
 (0)