Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .github/component_owners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ components:
baggage-processor:
- mikegoldsmith
- zeitlinger
cel-sampler:
- dol
- trask
- jack-berg
- breedx-splk
cloudfoundry-resources:
- KarstenSchnitter
compressors:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ feature or via instrumentation, this project is hopefully for you.
| alpha | [AWS X-Ray Propagator](./aws-xray-propagator/README.md) |
| alpha | [Baggage Processors](./baggage-processor/README.md) |
| alpha | [zstd Compressor](./compressors/compressor-zstd/README.md) |
| alpha | [CEL-Based Sampler](./cel-sampler/README.md) |
| alpha | [Consistent Sampling](./consistent-sampling/README.md) |
| alpha | [Disk Buffering](./disk-buffering/README.md) |
| alpha | [GCP Authentication Extension](./gcp-auth-extension/README.md) |
Expand Down
82 changes: 82 additions & 0 deletions cel-sampler/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# 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
* [Jack Berg](https://github.com/jack-berg), New Relic
* [Jason Plumb](https://github.com/breedx-splk), Splunk
* [Trask Stalnaker](https://github.com/trask), Microsoft

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,161 @@
/*
* 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.ArrayList;
import java.util.Collections;
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 =
Collections.unmodifiableList(
new ArrayList<>(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 (Boolean.TRUE.equals(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