-
Notifications
You must be signed in to change notification settings - Fork 168
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
Merged
Merged
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
dol marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| Learn more about component owners in [component_owners.yml](../.github/component_owners.yml). | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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") | ||
| } |
157 changes: 157 additions & 0 deletions
157
cel-sampler/src/main/java/io/opentelemetry/contrib/sampler/cel/CelBasedSampler.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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()); | ||
| } | ||
| }); | ||
dol marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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)) { | ||
dol marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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(); | ||
| } | ||
| } | ||
94 changes: 94 additions & 0 deletions
94
cel-sampler/src/main/java/io/opentelemetry/contrib/sampler/cel/CelBasedSamplerBuilder.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.