Skip to content

Commit 776abf3

Browse files
committed
feat(Samplers): New CEL based sampler with declarative config support
Adds new CelBasedSampler to the OpenTelemetry Java SDK, enabling advanced sampling rules using the Common Expression Language (CEL). It also includes updates to the existing RuleBasedRoutingSampler for consistency and clarity.
1 parent c4f12d9 commit 776abf3

16 files changed

+1057
-29
lines changed

samplers/README.md

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
The following samplers support [declarative configuration](https://opentelemetry.io/docs/languages/java/configuration/#declarative-configuration):
66

77
* `RuleBasedRoutingSampler`
8+
* `CelBasedSampler`
89

910
To use:
1011

1112
* Add a dependency on `io.opentelemetry:opentelemetry-sdk-extension-incubator:<version>`
1213
* Follow the [instructions](https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/incubator/README.md#file-configuration) to configure OpenTelemetry with declarative configuration.
13-
* Configure the `.tracer_provider.sampler` to include the `rule_based_routing` sampler.
14+
* Configure the `.tracer_provider.sampler` to include the `rule_based_routing` or `cel_based` sampler.
1415

15-
NOTE: Not yet available for use with the OTEL java agent, but should be in the near future. Please check back for updates.
16+
## RuleBasedRoutingSampler
1617

1718
Schema for `rule_based_routing` sampler:
1819

@@ -59,6 +60,66 @@ tracer_provider:
5960
pattern: /actuator.*
6061
```
6162

63+
## CelBasedSampler
64+
65+
The `CelBasedSampler` uses [Common Expression Language (CEL)](https://github.com/google/cel-spec) to create advanced sampling rules based on span attributes. CEL provides a powerful, yet simple expression language that allows you to create complex matching conditions.
66+
67+
To use:
68+
69+
* Add a dependency on `io.opentelemetry:opentelemetry-sdk-extension-incubator:<version>`
70+
* Follow the [instructions](https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/incubator/README.md#file-configuration) to configure OpenTelemetry with declarative configuration.
71+
* Configure the `.tracer_provider.sampler` to include the `cel_based` sampler.
72+
73+
Schema for `cel_based` sampler:
74+
75+
```yaml
76+
# The fallback sampler to use if no expressions match.
77+
fallback_sampler:
78+
always_on:
79+
# List of CEL expressions to evaluate. Expressions are evaluated in order.
80+
expressions:
81+
# The action to take when the expression evaluates to true. Must be one of: DROP, RECORD_AND_SAMPLE.
82+
- action: DROP
83+
# The CEL expression to evaluate. Must return a boolean.
84+
expression: attribute['url.path'].startsWith('/actuator')
85+
- action: RECORD_AND_SAMPLE
86+
expression: attribute['http.method'] == 'GET' && attribute['http.status_code'] < 400
87+
```
88+
89+
Available variables in CEL expressions:
90+
91+
* `name` (string): The span name
92+
* `spanKind` (string): The span kind (e.g., "SERVER", "CLIENT")
93+
* `attribute` (map): A map of span attributes
94+
95+
Example of using `cel_based` sampler as the root sampler in `parent_based` sampler configuration:
96+
97+
```yaml
98+
tracer_provider:
99+
sampler:
100+
parent_based:
101+
root:
102+
cel_based:
103+
fallback_sampler:
104+
always_on:
105+
expressions:
106+
# Drop health check endpoints
107+
- action: DROP
108+
expression: spanKind == 'SERVER' && attribute['url.path'].startsWith('/health')
109+
# Drop actuator endpoints
110+
- action: DROP
111+
expression: spanKind == 'SERVER' && attribute['url.path'].startsWith('/actuator')
112+
# Sample only HTTP GET requests with successful responses
113+
- action: RECORD_AND_SAMPLE
114+
expression: spanKind == 'SERVER' && attribute['http.method'] == 'GET' && attribute['http.status_code'] < 400
115+
# Selectively sample based on span name
116+
- action: RECORD_AND_SAMPLE
117+
expression: name.contains('checkout') || name.contains('payment')
118+
# Drop spans with specific name patterns
119+
- action: DROP
120+
expression: name.matches('.*internal.*') && spanKind == 'INTERNAL'
121+
```
122+
62123
## Component owners
63124

64125
- [Jack Berg](https://github.com/jack-berg), New Relic

samplers/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ otelJava.moduleName.set("io.opentelemetry.contrib.sampler")
99
dependencies {
1010
api("io.opentelemetry:opentelemetry-sdk")
1111

12+
implementation("dev.cel:cel:0.9.0")
13+
1214
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
1315
compileOnly("io.opentelemetry:opentelemetry-api-incubator")
1416
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator")
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.sampler;
7+
8+
import static java.util.Objects.requireNonNull;
9+
10+
import dev.cel.common.types.CelProtoTypes;
11+
import dev.cel.common.types.SimpleType;
12+
import dev.cel.compiler.CelCompiler;
13+
import dev.cel.compiler.CelCompilerFactory;
14+
import dev.cel.runtime.CelEvaluationException;
15+
import dev.cel.runtime.CelRuntime;
16+
import dev.cel.runtime.CelRuntimeFactory;
17+
import io.opentelemetry.api.common.Attributes;
18+
import io.opentelemetry.api.trace.SpanBuilder;
19+
import io.opentelemetry.api.trace.SpanKind;
20+
import io.opentelemetry.context.Context;
21+
import io.opentelemetry.sdk.trace.data.LinkData;
22+
import io.opentelemetry.sdk.trace.samplers.Sampler;
23+
import io.opentelemetry.sdk.trace.samplers.SamplingResult;
24+
import java.util.HashMap;
25+
import java.util.List;
26+
import java.util.Map;
27+
import java.util.logging.Level;
28+
import java.util.logging.Logger;
29+
import java.util.regex.Pattern;
30+
31+
/**
32+
* This sampler accepts a list of {@link CelBasedSamplingExpression}s and tries to match every
33+
* proposed span against those rules. Every rule describes a span's attribute, a pattern against
34+
* which to match attribute's value, and a sampler that will make a decision about given span if
35+
* match was successful.
36+
*
37+
* <p>Matching is performed by {@link Pattern}.
38+
*
39+
* <p>Provided span kind is checked first and if differs from the one given to {@link
40+
* #builder(Sampler)}, the default fallback sampler will make a decision.
41+
*
42+
* <p>Note that only attributes that were set on {@link SpanBuilder} will be taken into account,
43+
* attributes set after the span has been started are not used
44+
*
45+
* <p>If none of the rules matched, the default fallback sampler will make a decision.
46+
*/
47+
public final class CelBasedSampler implements Sampler {
48+
49+
private static final Logger logger = Logger.getLogger(CelBasedSampler.class.getName());
50+
51+
public static final CelCompiler celCompiler =
52+
CelCompilerFactory.standardCelCompilerBuilder()
53+
.addVar("name", SimpleType.STRING)
54+
.addVar("traceId", SimpleType.STRING)
55+
.addVar("spanKind", SimpleType.STRING)
56+
.addVar("attribute", CelProtoTypes.createMap(CelProtoTypes.STRING, CelProtoTypes.DYN))
57+
.setResultType(SimpleType.BOOL)
58+
.build();
59+
60+
final CelRuntime celRuntime;
61+
62+
private final List<CelBasedSamplingExpression> expressions;
63+
private final Sampler fallback;
64+
65+
public CelBasedSampler(List<CelBasedSamplingExpression> expressions, Sampler fallback) {
66+
this.expressions = requireNonNull(expressions, "expressions must not be null");
67+
this.expressions.forEach(
68+
expr -> {
69+
if (!expr.abstractSyntaxTree.isChecked()) {
70+
throw new IllegalArgumentException(
71+
"Expression and its AST is not checked: " + expr.expression);
72+
}
73+
});
74+
this.fallback = requireNonNull(fallback);
75+
this.celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build();
76+
}
77+
78+
public static CelBasedSamplerBuilder builder(Sampler fallback) {
79+
return new CelBasedSamplerBuilder(
80+
requireNonNull(fallback, "fallback sampler must not be null"), celCompiler);
81+
}
82+
83+
@Override
84+
public SamplingResult shouldSample(
85+
Context parentContext,
86+
String traceId,
87+
String name,
88+
SpanKind spanKind,
89+
Attributes attributes,
90+
List<LinkData> parentLinks) {
91+
92+
// Prepare the evaluation context with span data
93+
Map<String, Object> evaluationContext = new HashMap<>();
94+
evaluationContext.put("name", name);
95+
evaluationContext.put("traceId", traceId);
96+
evaluationContext.put("spanKind", spanKind.name());
97+
evaluationContext.put("attribute", convertAttributesToMap(attributes));
98+
99+
for (CelBasedSamplingExpression expression : expressions) {
100+
try {
101+
CelRuntime.Program program = celRuntime.createProgram(expression.abstractSyntaxTree);
102+
Object result = program.eval(evaluationContext);
103+
// Happy path: Perform sampling based on the boolean result
104+
if (result instanceof Boolean && ((Boolean) result)) {
105+
return expression.delegate.shouldSample(
106+
parentContext, traceId, name, spanKind, attributes, parentLinks);
107+
}
108+
// If result is not boolean, treat as false
109+
logger.log(
110+
Level.FINE,
111+
"Expression '" + expression.expression + "' returned non-boolean result: " + result);
112+
} catch (CelEvaluationException e) {
113+
logger.log(
114+
Level.FINE,
115+
"Expression '" + expression.expression + "' evaluation error: " + e.getMessage());
116+
}
117+
}
118+
119+
return fallback.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks);
120+
}
121+
122+
/** Convert OpenTelemetry Attributes to a Map that CEL can work with */
123+
private static Map<String, Object> convertAttributesToMap(Attributes attributes) {
124+
Map<String, Object> map = new HashMap<>();
125+
attributes.forEach(
126+
(key, value) -> {
127+
map.put(key.getKey(), value);
128+
});
129+
return map;
130+
}
131+
132+
@Override
133+
public String getDescription() {
134+
return "CelBasedSampler{" + "fallback=" + fallback + ", expressions=" + expressions + '}';
135+
}
136+
137+
@Override
138+
public String toString() {
139+
return getDescription();
140+
}
141+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.sampler;
7+
8+
import static java.util.Objects.requireNonNull;
9+
10+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
11+
import dev.cel.common.CelAbstractSyntaxTree;
12+
import dev.cel.common.CelValidationException;
13+
import dev.cel.compiler.CelCompiler;
14+
import io.opentelemetry.api.common.AttributeKey;
15+
import io.opentelemetry.sdk.trace.samplers.Sampler;
16+
import java.util.ArrayList;
17+
import java.util.List;
18+
19+
public final class CelBasedSamplerBuilder {
20+
private final CelCompiler celCompiler;
21+
private final List<CelBasedSamplingExpression> expressions = new ArrayList<>();
22+
private final Sampler defaultDelegate;
23+
24+
CelBasedSamplerBuilder(Sampler defaultDelegate, CelCompiler celCompiler) {
25+
this.defaultDelegate = defaultDelegate;
26+
this.celCompiler = celCompiler;
27+
}
28+
29+
/**
30+
* Use the provided sampler when the value of the provided {@link AttributeKey} matches the
31+
* provided pattern.
32+
*/
33+
@CanIgnoreReturnValue
34+
public CelBasedSamplerBuilder customize(String expression, Sampler sampler)
35+
throws CelValidationException {
36+
CelAbstractSyntaxTree abstractSyntaxTree =
37+
celCompiler.compile(requireNonNull(expression, "expression must not be null")).getAst();
38+
39+
expressions.add(
40+
new CelBasedSamplingExpression(
41+
requireNonNull(abstractSyntaxTree, "abstractSyntaxTree must not be null"),
42+
requireNonNull(sampler, "sampler must not be null")));
43+
return this;
44+
}
45+
46+
/**
47+
* Drop all spans when the value of the provided {@link AttributeKey} matches the provided
48+
* pattern.
49+
*/
50+
@CanIgnoreReturnValue
51+
public CelBasedSamplerBuilder drop(String expression) throws CelValidationException {
52+
return customize(expression, Sampler.alwaysOff());
53+
}
54+
55+
/**
56+
* Record and sample all spans when the value of the provided {@link AttributeKey} matches the
57+
* provided pattern.
58+
*/
59+
@CanIgnoreReturnValue
60+
public CelBasedSamplerBuilder recordAndSample(String expression) throws CelValidationException {
61+
return customize(expression, Sampler.alwaysOn());
62+
}
63+
64+
/** Build the sampler based on the rules provided. */
65+
public CelBasedSampler build() {
66+
return new CelBasedSampler(expressions, defaultDelegate);
67+
}
68+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.sampler;
7+
8+
import static java.util.Objects.requireNonNull;
9+
10+
import dev.cel.common.CelAbstractSyntaxTree;
11+
import io.opentelemetry.sdk.trace.samplers.Sampler;
12+
import java.util.Objects;
13+
import javax.annotation.Nullable;
14+
15+
public class CelBasedSamplingExpression {
16+
final CelAbstractSyntaxTree abstractSyntaxTree;
17+
final String expression;
18+
final Sampler delegate;
19+
20+
CelBasedSamplingExpression(CelAbstractSyntaxTree abstractSyntaxTree, Sampler delegate) {
21+
this.abstractSyntaxTree = requireNonNull(abstractSyntaxTree);
22+
this.expression = abstractSyntaxTree.getSource().getContent().toString();
23+
this.delegate = requireNonNull(delegate);
24+
}
25+
26+
@Override
27+
public String toString() {
28+
return "CelBasedSamplingExpression{"
29+
+ "expression='"
30+
+ expression
31+
+ "', delegate="
32+
+ delegate
33+
+ "}";
34+
}
35+
36+
@Override
37+
public boolean equals(@Nullable Object o) {
38+
if (this == o) {
39+
return true;
40+
}
41+
if (!(o instanceof CelBasedSamplingExpression)) {
42+
return false;
43+
}
44+
CelBasedSamplingExpression that = (CelBasedSamplingExpression) o;
45+
return Objects.equals(abstractSyntaxTree, that.abstractSyntaxTree)
46+
&& Objects.equals(expression, that.expression)
47+
&& Objects.equals(delegate, that.delegate);
48+
}
49+
50+
@Override
51+
public int hashCode() {
52+
return Objects.hash(abstractSyntaxTree, expression, delegate);
53+
}
54+
}

0 commit comments

Comments
 (0)