Skip to content

Commit eed644f

Browse files
authored
Common Expression Language (CEL) sampler (#1957)
1 parent 091ebd2 commit eed644f

26 files changed

+1354
-0
lines changed

.github/component_owners.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ components:
2525
baggage-processor:
2626
- mikegoldsmith
2727
- zeitlinger
28+
cel-sampler:
29+
- dol
30+
- trask
31+
- jack-berg
32+
- breedx-splk
2833
cloudfoundry-resources:
2934
- KarstenSchnitter
3035
compressors:

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ feature or via instrumentation, this project is hopefully for you.
2121
| alpha | [AWS X-Ray Propagator](./aws-xray-propagator/README.md) |
2222
| alpha | [Baggage Processors](./baggage-processor/README.md) |
2323
| alpha | [zstd Compressor](./compressors/compressor-zstd/README.md) |
24+
| alpha | [CEL-Based Sampler](./cel-sampler/README.md) |
2425
| alpha | [Consistent Sampling](./consistent-sampling/README.md) |
2526
| alpha | [Disk Buffering](./disk-buffering/README.md) |
2627
| alpha | [GCP Authentication Extension](./gcp-auth-extension/README.md) |

cel-sampler/README.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# CEL-Based Sampler
2+
3+
## Declarative configuration
4+
5+
The `CelBasedSampler` supports [declarative configuration](https://opentelemetry.io/docs/languages/java/configuration/#declarative-configuration).
6+
7+
To use:
8+
9+
* Add a dependency on `io.opentelemetry.contrib:opentelemetry-cel-sampler:<version>`
10+
* Follow the [instructions](https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/incubator/README.md#declarative-configuration) to configure OpenTelemetry with declarative configuration.
11+
* Configure the `.tracer_provider.sampler` to include the `cel_based` sampler.
12+
13+
Support is now available for the java agent, see an [example here](https://github.com/open-telemetry/opentelemetry-java-examples/blob/main/javaagent).
14+
15+
## Overview
16+
17+
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.
18+
19+
## Schema
20+
21+
Schema for `cel_based` sampler:
22+
23+
```yaml
24+
# The fallback sampler to use if no expressions match.
25+
fallback_sampler:
26+
always_on:
27+
# List of CEL expressions to evaluate. Expressions are evaluated in order.
28+
expressions:
29+
# The action to take when the expression evaluates to true. Must be one of: DROP, RECORD_AND_SAMPLE.
30+
- action: DROP
31+
# The CEL expression to evaluate. Must return a boolean.
32+
expression: attribute['url.path'].startsWith('/actuator')
33+
- action: RECORD_AND_SAMPLE
34+
expression: attribute['http.method'] == 'GET' && attribute['http.status_code'] < 400
35+
```
36+
37+
## Available variables
38+
39+
Available variables in CEL expressions:
40+
41+
* `name` (string): The span name
42+
* `spanKind` (string): The span kind (e.g., "SERVER", "CLIENT")
43+
* `attribute` (map): A map of span attributes
44+
45+
## Example configuration
46+
47+
Example of using `cel_based` sampler as the root sampler in `parent_based` sampler configuration:
48+
49+
```yaml
50+
tracer_provider:
51+
sampler:
52+
parent_based:
53+
root:
54+
cel_based:
55+
fallback_sampler:
56+
always_on:
57+
expressions:
58+
# Drop health check endpoints
59+
- action: DROP
60+
expression: spanKind == 'SERVER' && attribute['url.path'].startsWith('/health')
61+
# Drop actuator endpoints
62+
- action: DROP
63+
expression: spanKind == 'SERVER' && attribute['url.path'].startsWith('/actuator')
64+
# Sample only HTTP GET requests with successful responses
65+
- action: RECORD_AND_SAMPLE
66+
expression: spanKind == 'SERVER' && attribute['http.method'] == 'GET' && attribute['http.status_code'] < 400
67+
# Selectively sample based on span name
68+
- action: RECORD_AND_SAMPLE
69+
expression: name.contains('checkout') || name.contains('payment')
70+
# Drop spans with specific name patterns
71+
- action: DROP
72+
expression: name.matches('.*internal.*') && spanKind == 'INTERNAL'
73+
```
74+
75+
## Component owners
76+
77+
* [Dominic Lüchinger](https://github.com/dol), SIX Group
78+
* [Jack Berg](https://github.com/jack-berg), New Relic
79+
* [Jason Plumb](https://github.com/breedx-splk), Splunk
80+
* [Trask Stalnaker](https://github.com/trask), Microsoft
81+
82+
Learn more about component owners in [component_owners.yml](../.github/component_owners.yml).

cel-sampler/build.gradle.kts

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

0 commit comments

Comments
 (0)