diff --git a/custom/build.gradle.kts b/custom/build.gradle.kts index a415796a..f133470f 100644 --- a/custom/build.gradle.kts +++ b/custom/build.gradle.kts @@ -29,11 +29,6 @@ dependencies { compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-tooling") compileOnly(libs.bundles.semconv) - implementation(libs.contribConsistentSampling) { - // exclude transitive dependency as it's provided through agent packaging - exclude(group = "io.opentelemetry", module = "opentelemetry-sdk-trace") - exclude(group = "io.opentelemetry", module = "opentelemetry-sdk-extension-autoconfigure-spi") - } implementation(libs.contribSpanStacktrace) { // exclude transitive dependency as it's provided through agent packaging exclude(group = "io.opentelemetry", module = "opentelemetry-sdk") @@ -45,6 +40,9 @@ dependencies { exclude(group = "io.opentelemetry", module = "opentelemetry-sdk-extension-autoconfigure-spi") } + // samplers, included in upstream agent + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator") + testImplementation(libs.contribSpanStacktrace) // needs to be added in order to allow access to AgentListener interface diff --git a/custom/src/main/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProvider.java b/custom/src/main/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProvider.java index 874e119e..9368cf4d 100644 --- a/custom/src/main/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProvider.java +++ b/custom/src/main/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProvider.java @@ -156,9 +156,7 @@ private static void resourceProviders( private static void defaultSampler( Map config, ConfigProperties configProperties) { // enable EDOT default sampler by default if not explicitly disabled - String sampler = - configProperties.getString( - TRACES_SAMPLER, "experimental_composite_parentbased_traceidratio"); + String sampler = configProperties.getString(TRACES_SAMPLER, "elastic"); config.put(TRACES_SAMPLER, sampler); } diff --git a/custom/src/main/java/co/elastic/otel/compositesampling/DynamicCompositeParentBasedTraceIdRatioBasedSampler.java b/custom/src/main/java/co/elastic/otel/compositesampling/DynamicCompositeParentBasedTraceIdRatioBasedSampler.java deleted file mode 100644 index 6135135f..00000000 --- a/custom/src/main/java/co/elastic/otel/compositesampling/DynamicCompositeParentBasedTraceIdRatioBasedSampler.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package co.elastic.otel.compositesampling; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.context.Context; -import io.opentelemetry.contrib.sampler.consistent56.ConsistentSampler; -import io.opentelemetry.sdk.trace.data.LinkData; -import io.opentelemetry.sdk.trace.samplers.Sampler; -import io.opentelemetry.sdk.trace.samplers.SamplingResult; -import java.util.List; - -public enum DynamicCompositeParentBasedTraceIdRatioBasedSampler implements Sampler { - INSTANCE; - - static final double DEFAULT_TRACEIDRATIO_SAMPLE_RATIO = 1.0d; - - private static volatile double latestRatio; - private static volatile Sampler delegate = newSampler(DEFAULT_TRACEIDRATIO_SAMPLE_RATIO); - - public static void setRatio(double ratio) { - delegate = newSampler(ratio); - } - - @Override - public SamplingResult shouldSample( - Context parentContext, - String traceId, - String name, - SpanKind spanKind, - Attributes attributes, - List parentLinks) { - return delegate.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); - } - - @Override - public String getDescription() { - return INSTANCE.getDescription(); - } - - private static Sampler newSampler(double ratio) { - latestRatio = ratio; - return ConsistentSampler.parentBased(ConsistentSampler.probabilityBased(ratio)); - } - - public String toString() { - return "DynamicCompositeParentBasedTraceIdRatioBasedSampler(ratio=" - + latestRatio - + ", " - + delegate.toString() - + ")"; - } -} diff --git a/custom/src/main/java/co/elastic/otel/config/ConfigLoggingAgentListener.java b/custom/src/main/java/co/elastic/otel/config/ConfigLoggingAgentListener.java index a047869d..91d55cec 100644 --- a/custom/src/main/java/co/elastic/otel/config/ConfigLoggingAgentListener.java +++ b/custom/src/main/java/co/elastic/otel/config/ConfigLoggingAgentListener.java @@ -18,7 +18,7 @@ */ package co.elastic.otel.config; -import co.elastic.otel.compositesampling.DynamicCompositeParentBasedTraceIdRatioBasedSampler; +import co.elastic.otel.sampling.ElasticSampler; import com.google.auto.service.AutoService; import io.opentelemetry.javaagent.extension.AgentListener; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; @@ -49,7 +49,7 @@ public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetr logger.info(autoConfiguredOpenTelemetrySdk.toString()); } if (autoConfiguredOpenTelemetrySdk.getOpenTelemetrySdk().getSdkTracerProvider().getSampler() - instanceof DynamicCompositeParentBasedTraceIdRatioBasedSampler) { + instanceof ElasticSampler) { enableDynamicSamplingRate = true; } } diff --git a/custom/src/main/java/co/elastic/otel/dynamicconfig/CentralConfig.java b/custom/src/main/java/co/elastic/otel/dynamicconfig/CentralConfig.java index 1607bfb6..f8e0eb75 100644 --- a/custom/src/main/java/co/elastic/otel/dynamicconfig/CentralConfig.java +++ b/custom/src/main/java/co/elastic/otel/dynamicconfig/CentralConfig.java @@ -18,10 +18,10 @@ */ package co.elastic.otel.dynamicconfig; -import co.elastic.otel.compositesampling.DynamicCompositeParentBasedTraceIdRatioBasedSampler; import co.elastic.otel.config.ConfigLoggingAgentListener; import co.elastic.otel.dynamicconfig.internal.OpampManager; import co.elastic.otel.logging.AgentLog; +import co.elastic.otel.sampling.ElasticSampler; import io.opentelemetry.contrib.inferredspans.InferredSpans; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; @@ -29,7 +29,9 @@ import java.text.MessageFormat; import java.time.Duration; import java.time.format.DateTimeParseException; +import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; @@ -151,7 +153,9 @@ public static class Configs { new LoggingLevel(), new SamplingRate(), new InferSpans(), - new PollingInterval()) + new PollingInterval(), + new HttpExcludeUrls(), + new HttpExcludeUserAgents()) .collect(Collectors.toMap(ConfigOption::getConfigName, option -> option)); } @@ -199,9 +203,9 @@ public abstract static class ConfigOption { protected final String configName; protected final String defaultConfigStringValue; - protected ConfigOption(String configName1, String defaultConfigStringValue1) { - configName = configName1; - defaultConfigStringValue = defaultConfigStringValue1; + protected ConfigOption(String configName, String defaultConfigValue) { + this.configName = configName; + this.defaultConfigStringValue = defaultConfigValue; } public String getConfigName() { @@ -331,11 +335,13 @@ public static final class SamplingRate extends ConfigOption { void update(String configurationValue, OpampManager opampManager) throws IllegalArgumentException { if (!ConfigLoggingAgentListener.getEnableDynamicSamplingRate()) { + // TODO: why do we see this log message on startup ? logger.warning("ignoring \"sampling_rate\" because non-default sampler in use"); return; } - DynamicCompositeParentBasedTraceIdRatioBasedSampler.setRatio( - Double.parseDouble(configurationValue)); + ElasticSampler.INSTANCE.toBuilder() + .withProbability(Double.parseDouble(configurationValue)) + .buildAndSetGlobal(); } } @@ -372,4 +378,34 @@ void update(String configurationValue, OpampManager opampManager) } } } + + public static final class HttpExcludeUrls extends ConfigOption { + HttpExcludeUrls() { + super("http_ignore_urls", ""); + } + + @Override + void update(String configurationValue, OpampManager opampManager) + throws IllegalArgumentException { + + List patterns = Arrays.asList(configurationValue.split(",")); + ElasticSampler.INSTANCE.toBuilder().withIgnoredUrlPatterns(patterns).buildAndSetGlobal(); + } + } + + public static final class HttpExcludeUserAgents extends ConfigOption { + HttpExcludeUserAgents() { + super("http_ignore_user_agents", ""); + } + + @Override + void update(String configurationValue, OpampManager opampManager) + throws IllegalArgumentException { + + List patterns = Arrays.asList(configurationValue.split(",")); + ElasticSampler.INSTANCE.toBuilder() + .withIgnoredUserAgentPatterns(patterns) + .buildAndSetGlobal(); + } + } } diff --git a/custom/src/main/java/co/elastic/otel/dynamicconfig/internal/OpampManager.java b/custom/src/main/java/co/elastic/otel/dynamicconfig/internal/OpampManager.java index abed7d05..f7348f21 100644 --- a/custom/src/main/java/co/elastic/otel/dynamicconfig/internal/OpampManager.java +++ b/custom/src/main/java/co/elastic/otel/dynamicconfig/internal/OpampManager.java @@ -165,17 +165,17 @@ private Map parseCentralConfiguration(ByteString centralConfig) @Override public void onConnect(OpampClient client) { - logger.log(Level.INFO, "onConnect({0})", client); + logger.log(Level.FINE, "onConnect({0})", client); } @Override public void onConnectFailed(OpampClient client, @Nullable Throwable throwable) { - logger.log(Level.INFO, "onConnect({0}, {1})", new Object[] {client, throwable}); + logger.log(Level.FINE, "onConnect({0}, {1})", new Object[] {client, throwable}); } @Override public void onErrorResponse(OpampClient client, @Nonnull ServerErrorResponse errorResponse) { - logger.log(Level.INFO, "onErrorResponse({0}, {1})", new Object[] {client, errorResponse}); + logger.log(Level.FINE, "onErrorResponse({0}, {1})", new Object[] {client, errorResponse}); } } diff --git a/custom/src/main/java/co/elastic/otel/sampling/ElasticSampler.java b/custom/src/main/java/co/elastic/otel/sampling/ElasticSampler.java new file mode 100644 index 00000000..abfc5f9e --- /dev/null +++ b/custom/src/main/java/co/elastic/otel/sampling/ElasticSampler.java @@ -0,0 +1,211 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel.sampling; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.ComposableRuleBasedSamplerBuilder; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.ComposableSampler; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.CompositeSampler; +import io.opentelemetry.sdk.extension.incubator.trace.samplers.SamplingPredicate; +import io.opentelemetry.sdk.internal.IncludeExcludePredicate; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.UserAgentAttributes; +import io.opentelemetry.semconv.incubating.UserAgentIncubatingAttributes; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class ElasticSampler implements Sampler { + + private static final Logger logger = Logger.getLogger(ElasticSampler.class.getName()); + + public static ElasticSampler INSTANCE = new ElasticSampler(); + private static final Builder builder = new Builder(); + private static volatile Sampler delegate = builder.build(); + + static final double DEFAULT_SAMPLE_RATIO = 1.0d; + + private ElasticSampler() {} + + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + + return delegate.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); + } + + public Builder toBuilder() { + return builder; + } + + @Override + public String getDescription() { + return toString(); + } + + public String toString() { + return delegate.getDescription(); + } + + public static class Builder { + + private List ignoredUrlPatterns; + private List ignoredUserAgentPatterns; + private double ratio; + + // package-private for testing + Builder() { + this.ignoredUrlPatterns = Collections.emptyList(); + this.ignoredUserAgentPatterns = Collections.emptyList(); + this.ratio = DEFAULT_SAMPLE_RATIO; + } + + public Builder withProbability(double ratio) { + this.ratio = ratio; + return this; + } + + public Builder withIgnoredUrlPatterns(List patterns) { + this.ignoredUrlPatterns = patterns; + return this; + } + + public Builder withIgnoredUserAgentPatterns(List patterns) { + this.ignoredUserAgentPatterns = patterns; + return this; + } + + // package-private for testing + Sampler build() { + int rulesCount = 0; + ComposableRuleBasedSamplerBuilder ruleBuilder = ComposableSampler.ruleBasedBuilder(); + + if (!ignoredUrlPatterns.isEmpty()) { + Arrays.asList( + UrlAttributes.URL_PATH, // stable + UrlAttributes.URL_FULL, // stable + AttributeKey.stringKey("http.url") // legacy (deprecated) + ) + .forEach( + key -> + ruleBuilder.add( + valueMatching(key, ignoredUrlPatterns), ComposableSampler.alwaysOff())); + rulesCount++; + } + + if (!ignoredUserAgentPatterns.isEmpty()) { + Arrays.asList( + UserAgentAttributes.USER_AGENT_ORIGINAL, // stable + UserAgentIncubatingAttributes.USER_AGENT_NAME, // incubating + AttributeKey.stringKey("http.user_agent") // legacy (deprecated) + ) + .forEach( + key -> + ruleBuilder.add( + valueMatching(key, ignoredUserAgentPatterns), + ComposableSampler.alwaysOff())); + rulesCount++; + } + + if (rulesCount == 0) { + // no rules, just return the probability sampler directly + return CompositeSampler.wrap(ComposableSampler.probability(ratio)); + } + + // probability sampler applied last without any attribute filtering + ruleBuilder.add(any(), ComposableSampler.probability(ratio)); + return CompositeSampler.wrap(ruleBuilder.build()); + } + + public ElasticSampler buildAndSetGlobal() { + Sampler sampler = build(); + logger.fine("set global sampler to " + sampler.getDescription()); + delegate = sampler; + return INSTANCE; + } + + private static SamplingPredicate any() { + return (parentContext, traceId, name, spanKind, attributes, parentLinks) -> true; + } + + private static SamplingPredicate valueMatching( + AttributeKey attributeKey, List patterns) { + Predicate predicate = IncludeExcludePredicate.createPatternMatching(patterns, null); + return new ValueMatchingSamplingPredicate(attributeKey, predicate); + } + + private static class ValueMatchingSamplingPredicate implements SamplingPredicate { + private final AttributeKey attributeKey; + private final Predicate predicate; + + public ValueMatchingSamplingPredicate( + AttributeKey attributeKey, Predicate predicate) { + this.attributeKey = attributeKey; + this.predicate = predicate; + } + + @Override + public boolean matches( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + String value = attributes.get(attributeKey); + if (value == null) { + return false; + } + boolean result = predicate.test(value); + if (logger.isLoggable(Level.FINE)) { + // note: matching on a key means that the sampling intent will be applied, + logger.log( + Level.FINE, + "matching on '" + attributeKey + "' with value '" + value + "' result: " + result); + } + + return result; + } + + @Override + public String toString() { + return "ValueMatchingSamplingPredicate{" + + "attributeKey=" + + attributeKey + + ", predicate=" + + predicate + + '}'; + } + } + } +} diff --git a/custom/src/main/java/co/elastic/otel/compositesampling/CompositeParentBasedTraceIdRatioBasedSamplerProvider.java b/custom/src/main/java/co/elastic/otel/sampling/ElasticSamplerProvider.java similarity index 62% rename from custom/src/main/java/co/elastic/otel/compositesampling/CompositeParentBasedTraceIdRatioBasedSamplerProvider.java rename to custom/src/main/java/co/elastic/otel/sampling/ElasticSamplerProvider.java index 21704f5c..c4fa6046 100644 --- a/custom/src/main/java/co/elastic/otel/compositesampling/CompositeParentBasedTraceIdRatioBasedSamplerProvider.java +++ b/custom/src/main/java/co/elastic/otel/sampling/ElasticSamplerProvider.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.otel.compositesampling; +package co.elastic.otel.sampling; import com.google.auto.service.AutoService; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; @@ -24,20 +24,26 @@ import io.opentelemetry.sdk.trace.samplers.Sampler; @AutoService(ConfigurableSamplerProvider.class) -public class CompositeParentBasedTraceIdRatioBasedSamplerProvider - implements ConfigurableSamplerProvider { +public class ElasticSamplerProvider implements ConfigurableSamplerProvider { + + private static final String ELASTIC_OTEL_IGNORE_URLS = + "elastic.otel.experimental.http.ignore.urls"; + private static final String ELASTIC_OTEL_IGNORE_USER_AGENTS = + "elastic.otel.experimental.http.ignore.user-agents"; @Override public Sampler createSampler(ConfigProperties config) { - DynamicCompositeParentBasedTraceIdRatioBasedSampler.setRatio( - config.getDouble( - "otel.traces.sampler.arg", - DynamicCompositeParentBasedTraceIdRatioBasedSampler.DEFAULT_TRACEIDRATIO_SAMPLE_RATIO)); - return DynamicCompositeParentBasedTraceIdRatioBasedSampler.INSTANCE; + double ratio = config.getDouble("otel.traces.sampler.arg", ElasticSampler.DEFAULT_SAMPLE_RATIO); + + return ElasticSampler.INSTANCE.toBuilder() + .withProbability(ratio) + .withIgnoredUrlPatterns(config.getList(ELASTIC_OTEL_IGNORE_URLS)) + .withIgnoredUserAgentPatterns(config.getList(ELASTIC_OTEL_IGNORE_USER_AGENTS)) + .buildAndSetGlobal(); } @Override public String getName() { - return "experimental_composite_parentbased_traceidratio"; + return "elastic"; } } diff --git a/custom/src/test/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProviderTest.java b/custom/src/test/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProviderTest.java index 42aff53e..4ba82d2a 100644 --- a/custom/src/test/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProviderTest.java +++ b/custom/src/test/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProviderTest.java @@ -55,7 +55,7 @@ void defaultConfiguration() { assertThat(config) .describedAs("edot default sampler when not set by user") - .containsEntry("otel.traces.sampler", "experimental_composite_parentbased_traceidratio"); + .containsEntry("otel.traces.sampler", "elastic"); } @Test diff --git a/custom/src/test/java/co/elastic/otel/sampling/ElasticSamplerTest.java b/custom/src/test/java/co/elastic/otel/sampling/ElasticSamplerTest.java new file mode 100644 index 00000000..f6c500a5 --- /dev/null +++ b/custom/src/test/java/co/elastic/otel/sampling/ElasticSamplerTest.java @@ -0,0 +1,139 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel.sampling; + +import static io.opentelemetry.sdk.trace.samplers.SamplingDecision.DROP; +import static io.opentelemetry.sdk.trace.samplers.SamplingDecision.RECORD_AND_SAMPLE; +import static io.opentelemetry.semconv.UrlAttributes.URL_PATH; +import static io.opentelemetry.semconv.UserAgentAttributes.USER_AGENT_ORIGINAL; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.UserAgentIncubatingAttributes; +import java.util.Arrays; +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 ElasticSamplerTest { + + // those tests rely on the implementation, but it's the simplest way to verify the effective + // sampler configuration we get as multiple samplers are composed. + + @Test + void defaultProbability() { + Sampler sampler = new ElasticSampler.Builder().build(); + assertThat(sampler.getDescription()) + .isEqualTo("ComposableTraceIdRatioBasedSampler{threshold=0, ratio=1.0}"); + } + + @Test + void highProbability() { + Sampler sampler = new ElasticSampler.Builder().withProbability(0.99999999).build(); + assertThat(sampler.getDescription()) + .isEqualTo( + "ComposableTraceIdRatioBasedSampler{threshold=0000002af31dc8, ratio=0.99999999}"); + } + + @Test + void halfProbability() { + Sampler sampler = new ElasticSampler.Builder().withProbability(0.5).build(); + assertThat(sampler.getDescription()) + .isEqualTo("ComposableTraceIdRatioBasedSampler{threshold=8, ratio=0.5}"); + } + + @Test + void offProbability() { + Sampler sampler = new ElasticSampler.Builder().withProbability(0.0).build(); + assertThat(sampler.getDescription()) + .isEqualTo("ComposableTraceIdRatioBasedSampler{threshold=max, ratio=0.0}"); + } + + @ParameterizedTest + @MethodSource("urlPathKeys") + void ignoreUrlPath(AttributeKey key) { + Sampler sampler = + new ElasticSampler.Builder() + .withIgnoredUrlPatterns(Collections.singletonList("/health/*")) + .build(); + checkSampling(sampler, Attributes.empty(), RECORD_AND_SAMPLE); + checkSampling(sampler, Attributes.of(key, "/health/"), DROP); + checkSampling(sampler, Attributes.of(key, "/health/test"), DROP); + checkSampling(sampler, Attributes.of(key, "/healthcheck"), RECORD_AND_SAMPLE); + } + + public static Stream urlPathKeys() { + return Stream.of( + Arguments.of(URL_PATH), + Arguments.of(UrlAttributes.URL_FULL), + Arguments.of(AttributeKey.stringKey("http.url"))); + } + + @ParameterizedTest + @MethodSource("userAgentKeys") + void ignoreUserAgent(AttributeKey key) { + Sampler sampler = + new ElasticSampler.Builder() + .withIgnoredUserAgentPatterns(Arrays.asList("curl*", "*Curly")) + .build(); + checkSampling(sampler, Attributes.empty(), RECORD_AND_SAMPLE); + checkSampling(sampler, Attributes.of(key, "curl"), DROP); + checkSampling(sampler, Attributes.of(key, "HappyCurly"), DROP); + checkSampling(sampler, Attributes.of(key, "CURL"), RECORD_AND_SAMPLE); + } + + public static Stream userAgentKeys() { + return Stream.of( + Arguments.of(USER_AGENT_ORIGINAL), + Arguments.of(UserAgentIncubatingAttributes.USER_AGENT_NAME), + Arguments.of(AttributeKey.stringKey("http.user_agent"))); + } + + private static void checkSampling( + Sampler sampler, Attributes attributes, SamplingDecision expectedDecision) { + SamplingResult samplingResult = + sampler.shouldSample( + Context.root(), + TraceId.getInvalid(), + "name", + null, + attributes, + Collections.emptyList()); + assertThat(samplingResult.getDecision()) + .describedAs("sampling decision for attributes " + attributes) + .isEqualTo(expectedDecision); + } + + @Test + void singleGlobalBuilderInstance() { + ElasticSampler.Builder first = ElasticSampler.INSTANCE.toBuilder(); + ElasticSampler.Builder second = ElasticSampler.INSTANCE.toBuilder(); + assertThat(first).isSameAs(second); + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b37bfa9d..a5c66682 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,6 @@ opentelemetryInstrumentationAlphaBom = { group = "io.opentelemetry.instrumentati opentelemetryProto = { group = "io.opentelemetry.proto", name = "opentelemetry-proto", version.ref = "opentelemetryProto" } -contribConsistentSampling = { group = "io.opentelemetry.contrib", name = "opentelemetry-consistent-sampling", version.ref = "opentelemetryContribAlpha" } contribResources = { group = "io.opentelemetry.contrib", name = "opentelemetry-resource-providers", version.ref = "opentelemetryContribAlpha" } contribSpanStacktrace = { group = "io.opentelemetry.contrib", name = "opentelemetry-span-stacktrace", version.ref = "opentelemetryContribAlpha" } contribInferredSpans = { group = "io.opentelemetry.contrib", name = "opentelemetry-inferred-spans", version.ref = "opentelemetryContribAlpha" }