diff --git a/samplers/README.md b/samplers/README.md index 4be98779b..5540437a5 100644 --- a/samplers/README.md +++ b/samplers/README.md @@ -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:` * 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: @@ -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:` +* 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 diff --git a/samplers/build.gradle.kts b/samplers/build.gradle.kts index 6061e84ab..fcf465394 100644 --- a/samplers/build.gradle.kts +++ b/samplers/build.gradle.kts @@ -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") diff --git a/samplers/src/main/java/io/opentelemetry/contrib/sampler/CelBasedSampler.java b/samplers/src/main/java/io/opentelemetry/contrib/sampler/CelBasedSampler.java new file mode 100644 index 000000000..e17b5153b --- /dev/null +++ b/samplers/src/main/java/io/opentelemetry/contrib/sampler/CelBasedSampler.java @@ -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. + * + *

Matching is performed by {@link Pattern}. + * + *

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. + * + *

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 + * + *

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 expressions; + private final Sampler fallback; + + public CelBasedSampler(List 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 parentLinks) { + + // Prepare the evaluation context with span data + Map 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 convertAttributesToMap(Attributes attributes) { + Map 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(); + } +} diff --git a/samplers/src/main/java/io/opentelemetry/contrib/sampler/CelBasedSamplerBuilder.java b/samplers/src/main/java/io/opentelemetry/contrib/sampler/CelBasedSamplerBuilder.java new file mode 100644 index 000000000..4ef46432d --- /dev/null +++ b/samplers/src/main/java/io/opentelemetry/contrib/sampler/CelBasedSamplerBuilder.java @@ -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 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); + } +} diff --git a/samplers/src/main/java/io/opentelemetry/contrib/sampler/CelBasedSamplingExpression.java b/samplers/src/main/java/io/opentelemetry/contrib/sampler/CelBasedSamplingExpression.java new file mode 100644 index 000000000..5272f7e43 --- /dev/null +++ b/samplers/src/main/java/io/opentelemetry/contrib/sampler/CelBasedSamplingExpression.java @@ -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); + } +} diff --git a/samplers/src/main/java/io/opentelemetry/contrib/sampler/RuleBasedRoutingSampler.java b/samplers/src/main/java/io/opentelemetry/contrib/sampler/RuleBasedRoutingSampler.java index a9f3d1795..0d098a105 100644 --- a/samplers/src/main/java/io/opentelemetry/contrib/sampler/RuleBasedRoutingSampler.java +++ b/samplers/src/main/java/io/opentelemetry/contrib/sampler/RuleBasedRoutingSampler.java @@ -17,10 +17,10 @@ import java.util.List; /** - * This sampler accepts a list of {@link SamplingRule}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. + * This sampler accepts a list of {@link RuleBasedRoutingSamplingRule}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. * *

Matching is performed by {@link java.util.regex.Pattern}. * @@ -37,11 +37,12 @@ public final class RuleBasedRoutingSampler implements Sampler { // inlined incubating attribute to prevent direct dependency on incubating semconv private static final AttributeKey THREAD_NAME = AttributeKey.stringKey("thread.name"); - private final List rules; + private final List rules; private final SpanKind kind; private final Sampler fallback; - RuleBasedRoutingSampler(List rules, SpanKind kind, Sampler fallback) { + RuleBasedRoutingSampler( + List rules, SpanKind kind, Sampler fallback) { this.kind = requireNonNull(kind); this.fallback = requireNonNull(fallback); this.rules = requireNonNull(rules); @@ -64,18 +65,18 @@ public SamplingResult shouldSample( if (kind != spanKind) { return fallback.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); } - for (SamplingRule samplingRule : rules) { + for (RuleBasedRoutingSamplingRule ruleBasedRoutingSamplingRule : rules) { String attributeValue; - if (samplingRule.attributeKey.getKey().equals(THREAD_NAME.getKey())) { + if (ruleBasedRoutingSamplingRule.attributeKey.getKey().equals(THREAD_NAME.getKey())) { attributeValue = Thread.currentThread().getName(); } else { - attributeValue = attributes.get(samplingRule.attributeKey); + attributeValue = attributes.get(ruleBasedRoutingSamplingRule.attributeKey); } if (attributeValue == null) { continue; } - if (samplingRule.pattern.matcher(attributeValue).find()) { - return samplingRule.delegate.shouldSample( + if (ruleBasedRoutingSamplingRule.pattern.matcher(attributeValue).find()) { + return ruleBasedRoutingSamplingRule.delegate.shouldSample( parentContext, traceId, name, spanKind, attributes, parentLinks); } } diff --git a/samplers/src/main/java/io/opentelemetry/contrib/sampler/RuleBasedRoutingSamplerBuilder.java b/samplers/src/main/java/io/opentelemetry/contrib/sampler/RuleBasedRoutingSamplerBuilder.java index febdf690d..6f467984d 100644 --- a/samplers/src/main/java/io/opentelemetry/contrib/sampler/RuleBasedRoutingSamplerBuilder.java +++ b/samplers/src/main/java/io/opentelemetry/contrib/sampler/RuleBasedRoutingSamplerBuilder.java @@ -15,7 +15,7 @@ import java.util.List; public final class RuleBasedRoutingSamplerBuilder { - private final List rules = new ArrayList<>(); + private final List rules = new ArrayList<>(); private final SpanKind kind; private final Sampler defaultDelegate; @@ -41,7 +41,7 @@ public RuleBasedRoutingSamplerBuilder drop(AttributeKey attributeKey, St public RuleBasedRoutingSamplerBuilder customize( AttributeKey attributeKey, String pattern, Sampler sampler) { rules.add( - new SamplingRule( + new RuleBasedRoutingSamplingRule( requireNonNull(attributeKey, "attributeKey must not be null"), requireNonNull(pattern, "pattern must not be null"), requireNonNull(sampler, "sampler must not be null"))); diff --git a/samplers/src/main/java/io/opentelemetry/contrib/sampler/SamplingRule.java b/samplers/src/main/java/io/opentelemetry/contrib/sampler/RuleBasedRoutingSamplingRule.java similarity index 76% rename from samplers/src/main/java/io/opentelemetry/contrib/sampler/SamplingRule.java rename to samplers/src/main/java/io/opentelemetry/contrib/sampler/RuleBasedRoutingSamplingRule.java index d98656c83..07e6a0e66 100644 --- a/samplers/src/main/java/io/opentelemetry/contrib/sampler/SamplingRule.java +++ b/samplers/src/main/java/io/opentelemetry/contrib/sampler/RuleBasedRoutingSamplingRule.java @@ -11,12 +11,13 @@ import java.util.regex.Pattern; import javax.annotation.Nullable; -class SamplingRule { +class RuleBasedRoutingSamplingRule { final AttributeKey attributeKey; final Sampler delegate; final Pattern pattern; - SamplingRule(AttributeKey attributeKey, String pattern, Sampler delegate) { + RuleBasedRoutingSamplingRule( + AttributeKey attributeKey, String pattern, Sampler delegate) { this.attributeKey = attributeKey; this.pattern = Pattern.compile(pattern); this.delegate = delegate; @@ -24,7 +25,7 @@ class SamplingRule { @Override public String toString() { - return "SamplingRule{" + return "RuleBasedRoutingSamplingRule{" + "attributeKey=" + attributeKey + ", delegate=" @@ -39,10 +40,10 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof SamplingRule)) { + if (!(o instanceof RuleBasedRoutingSamplingRule)) { return false; } - SamplingRule that = (SamplingRule) o; + RuleBasedRoutingSamplingRule that = (RuleBasedRoutingSamplingRule) o; return attributeKey.equals(that.attributeKey) && pattern.equals(that.pattern); } diff --git a/samplers/src/main/java/io/opentelemetry/contrib/sampler/internal/CelBasedSamplerComponentProvider.java b/samplers/src/main/java/io/opentelemetry/contrib/sampler/internal/CelBasedSamplerComponentProvider.java new file mode 100644 index 000000000..49b9423ef --- /dev/null +++ b/samplers/src/main/java/io/opentelemetry/contrib/sampler/internal/CelBasedSamplerComponentProvider.java @@ -0,0 +1,91 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.internal; + +import dev.cel.common.CelValidationException; +import io.opentelemetry.api.incubator.config.DeclarativeConfigException; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.contrib.sampler.CelBasedSampler; +import io.opentelemetry.contrib.sampler.CelBasedSamplerBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.util.List; + +/** + * Declarative configuration SPI implementation for {@link CelBasedSampler}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public class CelBasedSamplerComponentProvider implements ComponentProvider { + + private static final String ACTION_RECORD_AND_SAMPLE = "RECORD_AND_SAMPLE"; + private static final String ACTION_DROP = "DROP"; + + @Override + public Class getType() { + return Sampler.class; + } + + @Override + public String getName() { + return "cel_based"; + } + + @Override + public Sampler create(DeclarativeConfigProperties config) { + DeclarativeConfigProperties fallbackModel = config.getStructured("fallback_sampler"); + if (fallbackModel == null) { + throw new DeclarativeConfigException( + "cel_based sampler .fallback_sampler is required but is null"); + } + Sampler fallbackSampler; + try { + fallbackSampler = DeclarativeConfiguration.createSampler(fallbackModel); + } catch (DeclarativeConfigException e) { + throw new DeclarativeConfigException( + "cel_based sampler failed to create .fallback_sampler sampler", e); + } + + List expressions = config.getStructuredList("expressions"); + if (expressions == null || expressions.isEmpty()) { + throw new DeclarativeConfigException("cel_based sampler .expressions is required"); + } + + CelBasedSamplerBuilder builder = CelBasedSampler.builder(fallbackSampler); + + for (DeclarativeConfigProperties expressionConfig : expressions) { + String expression = expressionConfig.getString("expression"); + if (expression == null) { + throw new DeclarativeConfigException( + "cel_based sampler .expressions[].expression is required"); + } + + String action = expressionConfig.getString("action"); + if (action == null) { + throw new DeclarativeConfigException("cel_based sampler .expressions[].action is required"); + } + + try { + if (action.equals(ACTION_RECORD_AND_SAMPLE)) { + builder.recordAndSample(expression); + } else if (action.equals(ACTION_DROP)) { + builder.drop(expression); + } else { + throw new DeclarativeConfigException( + "cel_based sampler .expressions[].action must be " + + ACTION_RECORD_AND_SAMPLE + + " or " + + ACTION_DROP); + } + } catch (CelValidationException e) { + throw new DeclarativeConfigException("Failed to compile CEL expression: " + expression, e); + } + } + return builder.build(); + } +} diff --git a/samplers/src/main/java/io/opentelemetry/contrib/sampler/internal/RuleBasedRoutingSamplerComponentProvider.java b/samplers/src/main/java/io/opentelemetry/contrib/sampler/internal/RuleBasedRoutingSamplerComponentProvider.java index 9e7fb4971..8b37e9410 100644 --- a/samplers/src/main/java/io/opentelemetry/contrib/sampler/internal/RuleBasedRoutingSamplerComponentProvider.java +++ b/samplers/src/main/java/io/opentelemetry/contrib/sampler/internal/RuleBasedRoutingSamplerComponentProvider.java @@ -42,14 +42,14 @@ public Sampler create(DeclarativeConfigProperties config) { DeclarativeConfigProperties fallbackModel = config.getStructured("fallback_sampler"); if (fallbackModel == null) { throw new DeclarativeConfigException( - "rule_based_routing sampler .fallback is required but is null"); + "rule_based_routing sampler .fallback_sampler is required but is null"); } Sampler fallbackSampler; try { fallbackSampler = DeclarativeConfiguration.createSampler(fallbackModel); } catch (DeclarativeConfigException e) { throw new DeclarativeConfigException( - "rule_Based_routing sampler failed to create .fallback sampler", e); + "rule_Based_routing sampler failed to create .fallback_sampler sampler", e); } String spanKindString = config.getString("span_kind", "SERVER"); diff --git a/samplers/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider b/samplers/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider index 32c554481..38368b629 100644 --- a/samplers/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider +++ b/samplers/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider @@ -1 +1,2 @@ io.opentelemetry.contrib.sampler.internal.RuleBasedRoutingSamplerComponentProvider +io.opentelemetry.contrib.sampler.internal.CelBasedSamplerComponentProvider diff --git a/samplers/src/test/java/internal/CelBasedSamplerComponentProviderTest.java b/samplers/src/test/java/internal/CelBasedSamplerComponentProviderTest.java new file mode 100644 index 000000000..a0b584997 --- /dev/null +++ b/samplers/src/test/java/internal/CelBasedSamplerComponentProviderTest.java @@ -0,0 +1,246 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package internal; + +import static io.opentelemetry.sdk.trace.samplers.SamplingResult.recordAndSample; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import dev.cel.common.CelValidationException; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.config.DeclarativeConfigException; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.contrib.sampler.CelBasedSampler; +import io.opentelemetry.contrib.sampler.CelBasedSamplerBuilder; +import io.opentelemetry.contrib.sampler.internal.CelBasedSamplerComponentProvider; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration; +import io.opentelemetry.sdk.trace.IdGenerator; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import io.opentelemetry.semconv.HttpAttributes; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class CelBasedSamplerComponentProviderTest { + + private static final CelBasedSamplerComponentProvider PROVIDER = + new CelBasedSamplerComponentProvider(); + + @Test + void endToEnd() { + String yaml = + "file_format: 0.4\n" + + "tracer_provider:\n" + + " sampler:\n" + + " parent_based:\n" + + " root:\n" + + " cel_based:\n" + + " fallback_sampler:\n" + + " always_on:\n" + + " expressions:\n" + + " - action: DROP\n" + + " expression: '\"example.com\" in attribute[\"http.response.header.host\"] && attribute[\"http.response.status_code\"] == 200'\n" + + " - action: DROP\n" + + " expression: spanKind == \"SERVER\" && attribute[\"url.path\"].matches(\"/actuator.*\")\n" + + " - action: RECORD_AND_SAMPLE\n" + + " expression: spanKind == \"SERVER\" && attribute[\"url.path\"].matches(\"/actuator.*\")\n"; + OpenTelemetrySdk openTelemetrySdk = + DeclarativeConfiguration.parseAndCreate( + new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8))); + Sampler sampler = openTelemetrySdk.getSdkTracerProvider().getSampler(); + assertThat(sampler.toString()) + .isEqualTo( + "ParentBased{" + + "root:CelBasedSampler{" + + "fallback=AlwaysOnSampler, " + + "expressions=[" + + "CelBasedSamplingExpression{" + + "expression='\"example.com\" in attribute[\"http.response.header.host\"] && attribute[\"http.response.status_code\"] == 200', " + + "delegate=AlwaysOffSampler" + + "}, " + + "CelBasedSamplingExpression{" + + "expression='spanKind == \"SERVER\" && attribute[\"url.path\"].matches(\"/actuator.*\")', " + + "delegate=AlwaysOffSampler" + + "}, " + + "CelBasedSamplingExpression{" + + "expression='spanKind == \"SERVER\" && attribute[\"url.path\"].matches(\"/actuator.*\")', " + + "delegate=AlwaysOnSampler" + + "}" + + "]}," + + "remoteParentSampled:AlwaysOnSampler," + + "remoteParentNotSampled:AlwaysOffSampler," + + "localParentSampled:AlwaysOnSampler," + + "localParentNotSampled:AlwaysOffSampler" + + "}"); + + // SERVER span to other path should be recorded and sampled + assertThat( + sampler.shouldSample( + Context.root(), + IdGenerator.random().generateTraceId(), + "GET /v1/users", + SpanKind.SERVER, + Attributes.builder() + .put(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200) + .put( + HttpAttributes.HTTP_RESPONSE_HEADER.getAttributeKey("host"), + new String[] {"example.com"}) + .build(), + Collections.emptyList())) + .isEqualTo(SamplingResult.drop()); + + // SERVER span to /actuator.* path should be dropped. This is the first expression that matches. + assertThat( + sampler.shouldSample( + Context.root(), + IdGenerator.random().generateTraceId(), + "GET /actuator/health", + SpanKind.SERVER, + Attributes.builder().put("url.path", "/actuator/health").build(), + Collections.emptyList())) + .isEqualTo(SamplingResult.drop()); + + // SERVER span to other path should be recorded and sampled + assertThat( + sampler.shouldSample( + Context.root(), + IdGenerator.random().generateTraceId(), + "GET /v1/users", + SpanKind.SERVER, + Attributes.builder().put("url.path", "/v1/users").build(), + Collections.emptyList())) + .isEqualTo(recordAndSample()); + } + + static Sampler dropSampler(Sampler fallback, String... expressions) { + CelBasedSamplerBuilder builder = CelBasedSampler.builder(fallback); + for (String expression : expressions) { + try { + builder.drop(expression); + } catch (CelValidationException e) { + // Delegate to the provider to handle the exception + } + } + return builder.build(); + } + + static Sampler recordAndSampleSampler(Sampler fallback, String... expressions) { + CelBasedSamplerBuilder builder = CelBasedSampler.builder(fallback); + for (String expression : expressions) { + try { + builder.recordAndSample(expression); + } catch (CelValidationException e) { + // Delegate to the provider to handle the exception + } + } + return builder.build(); + } + + @ParameterizedTest + @MethodSource("createValidArgs") + void create_Valid(String yaml, CelBasedSampler expectedSampler) { + DeclarativeConfigProperties configProperties = + DeclarativeConfiguration.toConfigProperties( + new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8))); + + Sampler sampler = PROVIDER.create(configProperties); + assertThat(sampler.toString()).isEqualTo(expectedSampler.toString()); + } + + static Stream createValidArgs() { + return Stream.of( + Arguments.of( + "fallback_sampler:\n" + + " always_on:\n" + + "expressions:\n" + + " - action: DROP\n" + + " expression: 'spanKind == \"CLIENT\"'\n", + dropSampler(Sampler.alwaysOn(), "spanKind == \"CLIENT\"")), + Arguments.of( + "fallback_sampler:\n" + + " always_off:\n" + + "expressions:\n" + + " - action: RECORD_AND_SAMPLE\n" + + " expression: 'attribute[\"url.path\"] == \"/v1/user\"'\n", + recordAndSampleSampler(Sampler.alwaysOff(), "attribute[\"url.path\"] == \"/v1/user\"")), + Arguments.of( + "fallback_sampler:\n" + + " always_off:\n" + + "expressions:\n" + + " - action: DROP\n" + + " expression: 'attribute[\"http.request.method\"] == \"GET\"'\n" + + " - action: DROP\n" + + " expression: 'attribute[\"url.path\"] == \"/foo/bar\"'\n", + dropSampler( + Sampler.alwaysOff(), + "attribute[\"http.request.method\"] == \"GET\"", + "attribute[\"url.path\"] == \"/foo/bar\""))); + } + + @ParameterizedTest + @MethodSource("createInvalidArgs") + void create_Invalid(String yaml, String expectedErrorMessage) { + DeclarativeConfigProperties configProperties = + DeclarativeConfiguration.toConfigProperties( + new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8))); + + assertThatThrownBy(() -> PROVIDER.create(configProperties)) + .isInstanceOf(DeclarativeConfigException.class) + .hasMessage(expectedErrorMessage); + } + + static Stream createInvalidArgs() { + return Stream.of( + Arguments.of( + "expressions:\n" + " - action: DROP\n" + " expression: 'spanKind == \"CLIENT\"'\n", + "cel_based sampler .fallback_sampler is required but is null"), + Arguments.of( + "fallback_sampler:\n" + + " foo:\n" + + "expressions:\n" + + " - action: DROP\n" + + " expression: 'spanKind == \"CLIENT\"'\n", + "cel_based sampler failed to create .fallback_sampler sampler"), + Arguments.of( + "fallback_sampler:\n" + " always_on: {}\n", + "cel_based sampler .expressions is required"), + Arguments.of( + "fallback_sampler:\n" + " always_on: {}\n" + "expressions: []\n", + "cel_based sampler .expressions is required"), + Arguments.of( + "fallback_sampler:\n" + " always_on: {}\n" + "expressions:\n" + " - action: DROP\n", + "cel_based sampler .expressions[].expression is required"), + Arguments.of( + "fallback_sampler:\n" + + " always_on: {}\n" + + "expressions:\n" + + " - expression: 'spanKind == \"CLIENT\"'\n", + "cel_based sampler .expressions[].action is required"), + Arguments.of( + "fallback_sampler:\n" + + " always_on: {}\n" + + "expressions:\n" + + " - action: INVALID\n" + + " expression: 'spanKind == \"CLIENT\"'\n", + "cel_based sampler .expressions[].action must be RECORD_AND_SAMPLE or DROP"), + Arguments.of( + "fallback_sampler:\n" + + " always_on: {}\n" + + "expressions:\n" + + " - action: DROP\n" + + " expression: 'invalid cel expression!'\n", + "Failed to compile CEL expression: invalid cel expression!")); + } +} diff --git a/samplers/src/test/java/internal/RuleBasedRoutingSamplerComponentProviderTest.java b/samplers/src/test/java/internal/RuleBasedRoutingSamplerComponentProviderTest.java index 02611c9d5..c0fce5f41 100644 --- a/samplers/src/test/java/internal/RuleBasedRoutingSamplerComponentProviderTest.java +++ b/samplers/src/test/java/internal/RuleBasedRoutingSamplerComponentProviderTest.java @@ -60,7 +60,7 @@ void endToEnd() { "ParentBased{" + "root:RuleBasedRoutingSampler{" + "rules=[" - + "SamplingRule{attributeKey=url.path, delegate=AlwaysOffSampler, pattern=/actuator.*}" + + "RuleBasedRoutingSamplingRule{attributeKey=url.path, delegate=AlwaysOffSampler, pattern=/actuator.*}" + "], " + "kind=SERVER, " + "fallback=AlwaysOnSampler" @@ -165,7 +165,7 @@ static Stream createInvalidArgs() { + "rules:\n" + " - attribute: url.path\n" + " pattern: path\n", - "rule_based_routing sampler .fallback is required but is null"), + "rule_based_routing sampler .fallback_sampler is required but is null"), Arguments.of( "fallback_sampler:\n" + " foo:\n" @@ -173,7 +173,7 @@ static Stream createInvalidArgs() { + "rules:\n" + " - attribute: url.path\n" + " pattern: path\n", - "rule_Based_routing sampler failed to create .fallback sampler"), + "rule_Based_routing sampler failed to create .fallback_sampler sampler"), Arguments.of( "fallback_sampler:\n" + " always_on:\n" diff --git a/samplers/src/test/java/io/opentelemetry/contrib/sampler/CelBasedSamplerTest.java b/samplers/src/test/java/io/opentelemetry/contrib/sampler/CelBasedSamplerTest.java new file mode 100644 index 000000000..f06555307 --- /dev/null +++ b/samplers/src/test/java/io/opentelemetry/contrib/sampler/CelBasedSamplerTest.java @@ -0,0 +1,275 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler; + +import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_HEADER; +import static io.opentelemetry.semconv.UrlAttributes.URL_FULL; +import static io.opentelemetry.semconv.UrlAttributes.URL_PATH; +import static io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes.THREAD_NAME; +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import dev.cel.common.CelValidationException; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.IdGenerator; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CelBasedSamplerTest { + private static final String SPAN_NAME = "MySpanName"; + private static final SpanKind SPAN_KIND = SpanKind.SERVER; + private final IdGenerator idsGenerator = IdGenerator.random(); + private final String traceId = idsGenerator.generateTraceId(); + private final String parentSpanId = idsGenerator.generateSpanId(); + private final SpanContext sampledSpanContext = + SpanContext.create(traceId, parentSpanId, TraceFlags.getSampled(), TraceState.getDefault()); + private final Context parentContext = Context.root().with(Span.wrap(sampledSpanContext)); + + private final List expressions = new ArrayList<>(); + + @Mock(strictness = Mock.Strictness.LENIENT) + private Sampler delegate; + + @BeforeEach + public void setup() throws CelValidationException { + when(delegate.shouldSample(any(), any(), any(), any(), any(), any())) + .thenReturn(SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE)); + + expressions.add( + new CelBasedSamplingExpression( + CelBasedSampler.celCompiler + .compile( + "spanKind == 'SERVER' && attribute[\"" + + URL_FULL + + "\"].matches(\"/actuator\")") + .getAst(), + Sampler.alwaysOff())); + expressions.add( + new CelBasedSamplingExpression( + CelBasedSampler.celCompiler + .compile( + "spanKind == 'SERVER' && attribute[\"" + + URL_FULL + + "\"].matches(\".*/healthcheck\")") + .getAst(), + Sampler.alwaysOff())); + } + + @Test + public void testThatThrowsOnNullParameter() { + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> new CelBasedSampler(expressions, null)); + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> new CelBasedSampler(null, delegate)); + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> CelBasedSampler.builder(null)); + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> CelBasedSampler.builder(delegate).drop(null)); + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> CelBasedSampler.builder(delegate).recordAndSample(null)); + } + + @Test + public void testThatDelegatesIfNoExpressionGiven() { + CelBasedSampler sampler = CelBasedSampler.builder(delegate).build(); + + // no http.url attribute + Attributes attributes = Attributes.empty(); + sampler.shouldSample(parentContext, traceId, SPAN_NAME, SPAN_KIND, attributes, emptyList()); + verify(delegate) + .shouldSample(parentContext, traceId, SPAN_NAME, SPAN_KIND, attributes, emptyList()); + + clearInvocations(delegate); + + // with http.url attribute + attributes = Attributes.of(URL_FULL, "https://example.com"); + sampler.shouldSample(parentContext, traceId, SPAN_NAME, SPAN_KIND, attributes, emptyList()); + verify(delegate) + .shouldSample(parentContext, traceId, SPAN_NAME, SPAN_KIND, attributes, emptyList()); + } + + @Test + public void testDropOnExactMatch() throws CelValidationException { + CelBasedSampler sampler = addRules(CelBasedSampler.builder(delegate)).build(); + assertThat(shouldSample(sampler, "https://example.com/healthcheck").getDecision()) + .isEqualTo(SamplingDecision.DROP); + } + + @Test + public void testDelegateOnDifferentKind() throws CelValidationException { + CelBasedSampler sampler = + addRules(CelBasedSampler.builder(delegate), SpanKind.CLIENT.name()).build(); + assertThat(shouldSample(sampler, "https://example.com/healthcheck").getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + verify(delegate).shouldSample(any(), any(), any(), any(), any(), any()); + } + + @Test + public void testDelegateOnNoMatch() throws CelValidationException { + CelBasedSampler sampler = addRules(CelBasedSampler.builder(delegate)).build(); + assertThat(shouldSample(sampler, "https://example.com/customers").getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + verify(delegate).shouldSample(any(), any(), any(), any(), any(), any()); + } + + @Test + public void testDelegateOnMalformedUrl() throws CelValidationException { + CelBasedSampler sampler = addRules(CelBasedSampler.builder(delegate)).build(); + assertThat(shouldSample(sampler, "abracadabra").getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + verify(delegate).shouldSample(any(), any(), any(), any(), any(), any()); + + clearInvocations(delegate); + + assertThat(shouldSample(sampler, "healthcheck").getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + verify(delegate).shouldSample(any(), any(), any(), any(), any(), any()); + } + + @Test + public void testVerifiesAllGivenAttributes() throws CelValidationException { + CelBasedSampler sampler = addRules(CelBasedSampler.builder(delegate)).build(); + Attributes attributes = Attributes.of(URL_PATH, "/actuator/info"); + assertThat( + sampler + .shouldSample(parentContext, traceId, SPAN_NAME, SPAN_KIND, attributes, emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + } + + @Test + void customSampler() throws CelValidationException { + Attributes attributes = Attributes.of(URL_PATH, "/test"); + CelBasedSampler testSampler = + CelBasedSampler.builder(delegate) + .customize( + "attribute[\"" + URL_PATH + "\"].matches(\".*test\")", new AlternatingSampler()) + .build(); + assertThat( + testSampler + .shouldSample(parentContext, traceId, SPAN_NAME, SPAN_KIND, attributes, emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + assertThat( + testSampler + .shouldSample(parentContext, traceId, SPAN_NAME, SPAN_KIND, attributes, emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + } + + @Test + void testThreadNameSampler() throws CelValidationException { + expressions.add( + new CelBasedSamplingExpression( + CelBasedSampler.celCompiler + .compile( + "spanKind == 'SERVER' && attribute[\"" + + THREAD_NAME + + "\"].matches(\"Test.*\")") + .getAst(), + Sampler.alwaysOff())); + Attributes attributes = Attributes.of(THREAD_NAME, "Test worker"); + CelBasedSampler sampler = new CelBasedSampler(expressions, delegate); + SamplingResult samplingResult = + sampler.shouldSample(parentContext, traceId, SPAN_NAME, SPAN_KIND, attributes, emptyList()); + assertThat(samplingResult.getDecision()).isEqualTo(SamplingDecision.DROP); + } + + @Test + void testComplexAttributeSampler() throws CelValidationException { + expressions.add( + new CelBasedSamplingExpression( + CelBasedSampler.celCompiler + .compile( + "\"example.com\" in attribute[\"" + + HTTP_RESPONSE_HEADER.getAttributeKey("host") + + "\"]") + .getAst(), + Sampler.alwaysOff())); + Attributes attributes = + Attributes.of( + HTTP_RESPONSE_HEADER.getAttributeKey("host"), + Arrays.asList("example.com", "example.org")); + CelBasedSampler sampler = new CelBasedSampler(expressions, delegate); + SamplingResult samplingResult = + sampler.shouldSample(parentContext, traceId, SPAN_NAME, SPAN_KIND, attributes, emptyList()); + assertThat(samplingResult.getDecision()).isEqualTo(SamplingDecision.DROP); + } + + private SamplingResult shouldSample(Sampler sampler, String url) { + Attributes attributes = Attributes.of(URL_FULL, url); + return sampler.shouldSample( + parentContext, traceId, SPAN_NAME, SPAN_KIND, attributes, emptyList()); + } + + private static CelBasedSamplerBuilder addRules(CelBasedSamplerBuilder builder, String kind) + throws CelValidationException { + return builder + .drop( + "attribute[\"" + + URL_FULL + + "\"].matches(\".*/healthcheck\") && spanKind == '" + + kind + + "'") + .drop( + "attribute[\"" + URL_PATH + "\"].matches(\"/actuator\") && spanKind == '" + kind + "'"); + } + + private static CelBasedSamplerBuilder addRules(CelBasedSamplerBuilder builder) + throws CelValidationException { + return addRules(builder, SPAN_KIND.name()); + } + + /** Silly sampler that alternates decisions for testing. */ + private static class AlternatingSampler implements Sampler { + private final AtomicBoolean switcher = new AtomicBoolean(); + + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return switcher.getAndSet(!switcher.get()) + ? SamplingResult.recordAndSample() + : SamplingResult.drop(); + } + + @Override + public String getDescription() { + return "weird switching sampler for testing"; + } + } +} diff --git a/samplers/src/test/java/io/opentelemetry/contrib/sampler/CelBasedSamplingExpressionTest.java b/samplers/src/test/java/io/opentelemetry/contrib/sampler/CelBasedSamplingExpressionTest.java new file mode 100644 index 000000000..943429ab1 --- /dev/null +++ b/samplers/src/test/java/io/opentelemetry/contrib/sampler/CelBasedSamplingExpressionTest.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelValidationException; +import dev.cel.compiler.CelCompilerFactory; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import org.junit.jupiter.api.Test; + +class CelBasedSamplingExpressionTest { + + static final String expression = "1 == 1"; + + private static CelBasedSamplingExpression createCelBasedSamplingExpression( + String expression, Sampler sampler) throws CelValidationException { + CelAbstractSyntaxTree ast = + CelCompilerFactory.standardCelCompilerBuilder().build().compile(expression).getAst(); + return new CelBasedSamplingExpression(ast, sampler); + } + + private static CelBasedSamplingExpression createCelBasedSamplingExpression(String expression) + throws CelValidationException { + return createCelBasedSamplingExpression(expression, Sampler.alwaysOn()); + } + + private static CelBasedSamplingExpression createCelBasedSamplingExpression() + throws CelValidationException { + return createCelBasedSamplingExpression(expression); + } + + @Test + public void testThatThrowsOnNullParameter() throws CelValidationException { + CelAbstractSyntaxTree ast = + CelCompilerFactory.standardCelCompilerBuilder().build().compile(expression).getAst(); + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> new CelBasedSamplingExpression(ast, null)); + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> new CelBasedSamplingExpression(null, Sampler.alwaysOn())); + } + + @Test + void testToString() throws CelValidationException { + CelBasedSamplingExpression celExpression = createCelBasedSamplingExpression(); + String expected = + "CelBasedSamplingExpression{expression='" + expression + "', delegate=AlwaysOnSampler}"; + assertEquals(expected, celExpression.toString()); + } + + @Test + void testEquals() throws CelValidationException { + CelBasedSamplingExpression celExpression1 = createCelBasedSamplingExpression(); + + assertEquals(celExpression1, celExpression1); + assertFalse(celExpression1.equals(null)); + + CelBasedSamplingExpression celExpression2 = createCelBasedSamplingExpression(); + + assertEquals(celExpression1, celExpression2); + + assertNotEquals(celExpression1, createCelBasedSamplingExpression("2 == 2")); + assertNotEquals( + celExpression1, createCelBasedSamplingExpression(expression, Sampler.alwaysOff())); + } + + @Test + void testHashCode() throws CelValidationException { + CelBasedSamplingExpression celExpression1 = createCelBasedSamplingExpression(); + int expectedHashCode1 = celExpression1.hashCode(); + int expectedHashCode2 = celExpression1.hashCode(); + + assertEquals(expectedHashCode1, expectedHashCode2); + + CelBasedSamplingExpression celExpression2 = createCelBasedSamplingExpression(); + + assertEquals(expectedHashCode1, celExpression2.hashCode()); + } +} diff --git a/samplers/src/test/java/io/opentelemetry/contrib/sampler/RuleBasedRoutingSamplerTest.java b/samplers/src/test/java/io/opentelemetry/contrib/sampler/RuleBasedRoutingSamplerTest.java index 05a4301e6..fdbb48fa5 100644 --- a/samplers/src/test/java/io/opentelemetry/contrib/sampler/RuleBasedRoutingSamplerTest.java +++ b/samplers/src/test/java/io/opentelemetry/contrib/sampler/RuleBasedRoutingSamplerTest.java @@ -48,7 +48,7 @@ class RuleBasedRoutingSamplerTest { SpanContext.create(traceId, parentSpanId, TraceFlags.getSampled(), TraceState.getDefault()); private final Context parentContext = Context.root().with(Span.wrap(sampledSpanContext)); - private final List patterns = new ArrayList<>(); + private final List patterns = new ArrayList<>(); @Mock(strictness = Mock.Strictness.LENIENT) private Sampler delegate; @@ -58,8 +58,8 @@ public void setup() { when(delegate.shouldSample(any(), any(), any(), any(), any(), any())) .thenReturn(SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE)); - patterns.add(new SamplingRule(URL_FULL, ".*/healthcheck", Sampler.alwaysOff())); - patterns.add(new SamplingRule(URL_PATH, "/actuator", Sampler.alwaysOff())); + patterns.add(new RuleBasedRoutingSamplingRule(URL_FULL, ".*/healthcheck", Sampler.alwaysOff())); + patterns.add(new RuleBasedRoutingSamplingRule(URL_PATH, "/actuator", Sampler.alwaysOff())); } @Test @@ -190,7 +190,7 @@ void customSampler() { @Test void testThreadNameSampler() { - patterns.add(new SamplingRule(THREAD_NAME, "Test.*", Sampler.alwaysOff())); + patterns.add(new RuleBasedRoutingSamplingRule(THREAD_NAME, "Test.*", Sampler.alwaysOff())); Attributes attributes = Attributes.of(THREAD_NAME, "Test worker"); RuleBasedRoutingSampler sampler = new RuleBasedRoutingSampler(patterns, SPAN_KIND, delegate); SamplingResult samplingResult =