Skip to content

Common Expression Language (CEL) sampler #1957

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 63 additions & 2 deletions samplers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
The following samplers support [declarative configuration](https://opentelemetry.io/docs/languages/java/configuration/#declarative-configuration):

* `RuleBasedRoutingSampler`
* `CelBasedSampler`

To use:

* Add a dependency on `io.opentelemetry:opentelemetry-sdk-extension-incubator:<version>`
* 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.
* Configure the `.tracer_provider.sampler` to include the `rule_based_routing` sampler.
* Configure the `.tracer_provider.sampler` to include the `rule_based_routing` or `cel_based` sampler.

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

Schema for `rule_based_routing` sampler:

Expand Down Expand Up @@ -59,6 +60,66 @@ tracer_provider:
pattern: /actuator.*
```

## CelBasedSampler

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.

To use:

* Add a dependency on `io.opentelemetry:opentelemetry-sdk-extension-incubator:<version>`
* 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.
* Configure the `.tracer_provider.sampler` to include the `cel_based` sampler.

Schema for `cel_based` sampler:

```yaml
# The fallback sampler to use if no expressions match.
fallback_sampler:
always_on:
# List of CEL expressions to evaluate. Expressions are evaluated in order.
expressions:
# The action to take when the expression evaluates to true. Must be one of: DROP, RECORD_AND_SAMPLE.
- action: DROP
# The CEL expression to evaluate. Must return a boolean.
expression: attribute['url.path'].startsWith('/actuator')
- action: RECORD_AND_SAMPLE
expression: attribute['http.method'] == 'GET' && attribute['http.status_code'] < 400
```

Available variables in CEL expressions:

* `name` (string): The span name
* `spanKind` (string): The span kind (e.g., "SERVER", "CLIENT")
* `attribute` (map): A map of span attributes

Example of using `cel_based` sampler as the root sampler in `parent_based` sampler configuration:

```yaml
tracer_provider:
sampler:
parent_based:
root:
cel_based:
fallback_sampler:
always_on:
expressions:
# Drop health check endpoints
- action: DROP
expression: spanKind == 'SERVER' && attribute['url.path'].startsWith('/health')
# Drop actuator endpoints
- action: DROP
expression: spanKind == 'SERVER' && attribute['url.path'].startsWith('/actuator')
# Sample only HTTP GET requests with successful responses
- action: RECORD_AND_SAMPLE
expression: spanKind == 'SERVER' && attribute['http.method'] == 'GET' && attribute['http.status_code'] < 400
# Selectively sample based on span name
- action: RECORD_AND_SAMPLE
expression: name.contains('checkout') || name.contains('payment')
# Drop spans with specific name patterns
- action: DROP
expression: name.matches('.*internal.*') && spanKind == 'INTERNAL'
```

## Component owners

- [Jack Berg](https://github.com/jack-berg), New Relic
Expand Down
2 changes: 2 additions & 0 deletions samplers/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ otelJava.moduleName.set("io.opentelemetry.contrib.sampler")
dependencies {
api("io.opentelemetry:opentelemetry-sdk")

implementation("dev.cel:cel:0.9.0")

compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
compileOnly("io.opentelemetry:opentelemetry-api-incubator")
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.sampler;

import static java.util.Objects.requireNonNull;

import dev.cel.common.types.CelProtoTypes;
import dev.cel.common.types.SimpleType;
import dev.cel.compiler.CelCompiler;
import dev.cel.compiler.CelCompilerFactory;
import dev.cel.runtime.CelEvaluationException;
import dev.cel.runtime.CelRuntime;
import dev.cel.runtime.CelRuntimeFactory;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.trace.data.LinkData;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import io.opentelemetry.sdk.trace.samplers.SamplingResult;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

/**
* This sampler accepts a list of {@link CelBasedSamplingExpression}s and tries to match every
* proposed span against those rules. Every rule describes a span's attribute, a pattern against
* which to match attribute's value, and a sampler that will make a decision about given span if
* match was successful.
*
* <p>Matching is performed by {@link Pattern}.
*
* <p>Provided span kind is checked first and if differs from the one given to {@link
* #builder(Sampler)}, the default fallback sampler will make a decision.
*
* <p>Note that only attributes that were set on {@link SpanBuilder} will be taken into account,
* attributes set after the span has been started are not used
*
* <p>If none of the rules matched, the default fallback sampler will make a decision.
*/
public final class CelBasedSampler implements Sampler {

private static final Logger logger = Logger.getLogger(CelBasedSampler.class.getName());

public static final CelCompiler celCompiler =
CelCompilerFactory.standardCelCompilerBuilder()
.addVar("name", SimpleType.STRING)
.addVar("traceId", SimpleType.STRING)
.addVar("spanKind", SimpleType.STRING)
.addVar("attribute", CelProtoTypes.createMap(CelProtoTypes.STRING, CelProtoTypes.DYN))
.setResultType(SimpleType.BOOL)
.build();

final CelRuntime celRuntime;

private final List<CelBasedSamplingExpression> expressions;
private final Sampler fallback;

public CelBasedSampler(List<CelBasedSamplingExpression> expressions, Sampler fallback) {
this.expressions = requireNonNull(expressions, "expressions must not be null");
this.expressions.forEach(
expr -> {
if (!expr.abstractSyntaxTree.isChecked()) {
throw new IllegalArgumentException(
"Expression and its AST is not checked: " + expr.expression);
}
});
this.fallback = requireNonNull(fallback);
this.celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build();
}

public static CelBasedSamplerBuilder builder(Sampler fallback) {
return new CelBasedSamplerBuilder(
requireNonNull(fallback, "fallback sampler must not be null"), celCompiler);
}

@Override
public SamplingResult shouldSample(
Context parentContext,
String traceId,
String name,
SpanKind spanKind,
Attributes attributes,
List<LinkData> parentLinks) {

// Prepare the evaluation context with span data
Map<String, Object> evaluationContext = new HashMap<>();
evaluationContext.put("name", name);
evaluationContext.put("traceId", traceId);
evaluationContext.put("spanKind", spanKind.name());
evaluationContext.put("attribute", convertAttributesToMap(attributes));

for (CelBasedSamplingExpression expression : expressions) {
try {
CelRuntime.Program program = celRuntime.createProgram(expression.abstractSyntaxTree);
Object result = program.eval(evaluationContext);
// Happy path: Perform sampling based on the boolean result
if (result instanceof Boolean && ((Boolean) result)) {
return expression.delegate.shouldSample(
parentContext, traceId, name, spanKind, attributes, parentLinks);
}
// If result is not boolean, treat as false
logger.log(
Level.FINE,
"Expression '" + expression.expression + "' returned non-boolean result: " + result);
} catch (CelEvaluationException e) {
logger.log(
Level.FINE,
"Expression '" + expression.expression + "' evaluation error: " + e.getMessage());
}
}

return fallback.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks);
}

/** Convert OpenTelemetry Attributes to a Map that CEL can work with */
private static Map<String, Object> convertAttributesToMap(Attributes attributes) {
Map<String, Object> map = new HashMap<>();
attributes.forEach(
(key, value) -> {
map.put(key.getKey(), value);
});
return map;
}

@Override
public String getDescription() {
return "CelBasedSampler{" + "fallback=" + fallback + ", expressions=" + expressions + '}';
}

@Override
public String toString() {
return getDescription();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.sampler;

import static java.util.Objects.requireNonNull;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import dev.cel.common.CelAbstractSyntaxTree;
import dev.cel.common.CelValidationException;
import dev.cel.compiler.CelCompiler;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import java.util.ArrayList;
import java.util.List;

public final class CelBasedSamplerBuilder {
private final CelCompiler celCompiler;
private final List<CelBasedSamplingExpression> expressions = new ArrayList<>();
private final Sampler defaultDelegate;

CelBasedSamplerBuilder(Sampler defaultDelegate, CelCompiler celCompiler) {
this.defaultDelegate = defaultDelegate;
this.celCompiler = celCompiler;
}

/**
* Use the provided sampler when the value of the provided {@link AttributeKey} matches the
* provided pattern.
*/
@CanIgnoreReturnValue
public CelBasedSamplerBuilder customize(String expression, Sampler sampler)
throws CelValidationException {
CelAbstractSyntaxTree abstractSyntaxTree =
celCompiler.compile(requireNonNull(expression, "expression must not be null")).getAst();

expressions.add(
new CelBasedSamplingExpression(
requireNonNull(abstractSyntaxTree, "abstractSyntaxTree must not be null"),
requireNonNull(sampler, "sampler must not be null")));
return this;
}

/**
* Drop all spans when the value of the provided {@link AttributeKey} matches the provided
* pattern.
*/
@CanIgnoreReturnValue
public CelBasedSamplerBuilder drop(String expression) throws CelValidationException {
return customize(expression, Sampler.alwaysOff());
}

/**
* Record and sample all spans when the value of the provided {@link AttributeKey} matches the
* provided pattern.
*/
@CanIgnoreReturnValue
public CelBasedSamplerBuilder recordAndSample(String expression) throws CelValidationException {
return customize(expression, Sampler.alwaysOn());
}

/** Build the sampler based on the rules provided. */
public CelBasedSampler build() {
return new CelBasedSampler(expressions, defaultDelegate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.sampler;

import static java.util.Objects.requireNonNull;

import dev.cel.common.CelAbstractSyntaxTree;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import java.util.Objects;
import javax.annotation.Nullable;

public class CelBasedSamplingExpression {
final CelAbstractSyntaxTree abstractSyntaxTree;
final String expression;
final Sampler delegate;

CelBasedSamplingExpression(CelAbstractSyntaxTree abstractSyntaxTree, Sampler delegate) {
this.abstractSyntaxTree = requireNonNull(abstractSyntaxTree);
this.expression = abstractSyntaxTree.getSource().getContent().toString();
this.delegate = requireNonNull(delegate);
}

@Override
public String toString() {
return "CelBasedSamplingExpression{"
+ "expression='"
+ expression
+ "', delegate="
+ delegate
+ "}";
}

@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof CelBasedSamplingExpression)) {
return false;
}
CelBasedSamplingExpression that = (CelBasedSamplingExpression) o;
return Objects.equals(abstractSyntaxTree, that.abstractSyntaxTree)
&& Objects.equals(expression, that.expression)
&& Objects.equals(delegate, that.delegate);
}

@Override
public int hashCode() {
return Objects.hash(abstractSyntaxTree, expression, delegate);
}
}
Loading
Loading