Skip to content

Commit 4446bd1

Browse files
committed
New CEL based sampler with declarative config support
1 parent f3fe52b commit 4446bd1

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)