-
Notifications
You must be signed in to change notification settings - Fork 166
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
dol
wants to merge
2
commits into
open-telemetry:main
Choose a base branch
from
dol:feature/cel-sampler
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
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
141 changes: 141 additions & 0 deletions
141
samplers/src/main/java/io/opentelemetry/contrib/sampler/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,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(); | ||
} | ||
} |
68 changes: 68 additions & 0 deletions
68
samplers/src/main/java/io/opentelemetry/contrib/sampler/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,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); | ||
} | ||
} |
54 changes: 54 additions & 0 deletions
54
samplers/src/main/java/io/opentelemetry/contrib/sampler/CelBasedSamplingExpression.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,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; | ||
dol marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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); | ||
} | ||
} |
Oops, something went wrong.
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.