Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions .github/component_owners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ components:
baggage-processor:
- mikegoldsmith
- zeitlinger
cel-sampler:
- dol
cloudfoundry-resources:
- KarstenSchnitter
compressors:
Expand Down
80 changes: 80 additions & 0 deletions cel-sampler/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# CEL-Based Sampler

## Declarative configuration

The `CelBasedSampler` supports [declarative configuration](https://opentelemetry.io/docs/languages/java/configuration/#declarative-configuration).

To use:

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

Support is now available for the java agent, see an [example here](https://github.com/open-telemetry/opentelemetry-java-examples/blob/main/javaagent).

## Overview

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.

## Schema

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
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 configuration

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

* [Dominic Lüchinger](https://github.com/dol), SIX Group
* TBD

Learn more about component owners in [component_owners.yml](../.github/component_owners.yml).
20 changes: 20 additions & 0 deletions cel-sampler/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
plugins {
id("otel.java-conventions")
id("otel.publish-conventions")
}

description = "Sampler which makes its decision based on semantic attributes values"
otelJava.moduleName.set("io.opentelemetry.contrib.sampler.cel")

dependencies {
api("io.opentelemetry:opentelemetry-sdk")

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

compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator")

testImplementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating")
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.sampler.cel;

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;

/**
* 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 CEL expression evaluation.
*
* <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());

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();

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

/**
* Creates a new CEL-based sampler.
*
* @param expressions The list of CEL expressions to evaluate
* @param fallback The fallback sampler to use when no expressions match
*/
public CelBasedSampler(List<CelBasedSamplingExpression> expressions, Sampler fallback) {
this.expressions = requireNonNull(expressions, "expressions must not be null");
this.expressions.forEach(
expr -> {
if (!expr.getAbstractSyntaxTree().isChecked()) {
throw new IllegalArgumentException(
"Expression and its AST is not checked: " + expr.getExpression());
}
});
this.fallback = requireNonNull(fallback, "fallback must not be null");
this.celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build();
}

/**
* Creates a new builder for CEL-based sampler.
*
* @param fallback The fallback sampler to use when no expressions match
* @return A new builder instance
*/
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.getAbstractSyntaxTree());
Object result = program.eval(evaluationContext);
// Happy path: Perform sampling based on the boolean result
if (result instanceof Boolean && ((Boolean) result)) {
return expression
.getDelegate()
.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks);
}
// If result is not boolean, treat as false
logger.log(
Level.FINE,
"Expression '"
+ expression.getExpression()
+ "' returned non-boolean result: "
+ result);
} catch (CelEvaluationException e) {
logger.log(
Level.FINE,
"Expression '" + expression.getExpression() + "' evaluation error: " + e.getMessage());
}
}

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

/**
* Convert OpenTelemetry Attributes to a Map that CEL can work with.
*
* @param attributes The OpenTelemetry attributes
* @return A map representation of the attributes
*/
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,94 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.sampler.cel;

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.sdk.trace.samplers.Sampler;
import java.util.ArrayList;
import java.util.List;

/**
* Builder for {@link CelBasedSampler}.
*
* <p>This builder allows configuring CEL expressions with their associated sampling actions. Each
* expression is evaluated in order, and the first matching expression determines the sampling
* decision for a span.
*/
public final class CelBasedSamplerBuilder {
private final CelCompiler celCompiler;
private final List<CelBasedSamplingExpression> expressions = new ArrayList<>();
private final Sampler defaultDelegate;

/**
* Creates a new builder with the specified fallback sampler and CEL compiler.
*
* @param defaultDelegate The fallback sampler to use when no expressions match
* @param celCompiler The CEL compiler for compiling expressions
*/
CelBasedSamplerBuilder(Sampler defaultDelegate, CelCompiler celCompiler) {
this.defaultDelegate = defaultDelegate;
this.celCompiler = celCompiler;
}

/**
* Use the provided sampler when the CEL expression evaluates to true.
*
* @param expression The CEL expression to evaluate
* @param sampler The sampler to use when the expression matches
* @return This builder instance for method chaining
* @throws CelValidationException if the expression cannot be compiled
*/
@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 CEL expression evaluates to true.
*
* @param expression The CEL expression to evaluate
* @return This builder instance for method chaining
* @throws CelValidationException if the expression cannot be compiled
*/
@CanIgnoreReturnValue
public CelBasedSamplerBuilder drop(String expression) throws CelValidationException {
return customize(expression, Sampler.alwaysOff());
}

/**
* Record and sample all spans when the CEL expression evaluates to true.
*
* @param expression The CEL expression to evaluate
* @return This builder instance for method chaining
* @throws CelValidationException if the expression cannot be compiled
*/
@CanIgnoreReturnValue
public CelBasedSamplerBuilder recordAndSample(String expression) throws CelValidationException {
return customize(expression, Sampler.alwaysOn());
}

/**
* Build the sampler based on the configured expressions.
*
* @return a new {@link CelBasedSampler} instance
*/
public CelBasedSampler build() {
return new CelBasedSampler(expressions, defaultDelegate);
}
}
Loading