Skip to content

Commit d24c8d7

Browse files
committed
New CEL based sampler with declarative config support
1 parent 0a37bb9 commit d24c8d7

14 files changed

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

0 commit comments

Comments
 (0)