diff --git a/.github/component_owners.yml b/.github/component_owners.yml index 371e17dbd..e395c374a 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -41,6 +41,10 @@ components: - sfriberg jmx-metrics: - breedx-splk + jmx-scraper: + - breedx-splk + - robsunday + - sylvainjuge maven-extension: - cyrille-leclerc - kenfinnigan @@ -61,6 +65,7 @@ components: - jeanbisutti samplers: - trask + - jack-berg static-instrumenter: - anosek-an kafka-exporter: diff --git a/aws-xray-propagator/build.gradle.kts b/aws-xray-propagator/build.gradle.kts index ed72d4ff0..7384b462b 100644 --- a/aws-xray-propagator/build.gradle.kts +++ b/aws-xray-propagator/build.gradle.kts @@ -13,5 +13,7 @@ dependencies { testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") testImplementation("io.opentelemetry:opentelemetry-sdk-trace") testImplementation("io.opentelemetry:opentelemetry-sdk-testing") + + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator") testImplementation("uk.org.webcompere:system-stubs-jupiter:2.0.3") } diff --git a/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/AwsXrayLambdaPropagator.java b/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/AwsXrayLambdaPropagator.java index f09f8163a..a6b6a2ab4 100644 --- a/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/AwsXrayLambdaPropagator.java +++ b/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/AwsXrayLambdaPropagator.java @@ -79,6 +79,11 @@ public Context extract(Context context, @Nullable C carrier, TextMapGetter Context extract(Context context, @Nullable C carrier, TextMapGetter Context getContextFromHeader( Context context, @Nullable C carrier, TextMapGetter getter) { String traceHeader = getter.get(carrier, TRACE_HEADER_KEY); @@ -290,7 +295,8 @@ private static String parseShortTraceId(String xrayTraceId) { int secondDelimiter = xrayTraceId.indexOf(TRACE_ID_DELIMITER, firstDelimiter + 2); if (firstDelimiter != TRACE_ID_DELIMITER_INDEX_1 || secondDelimiter == -1 - || secondDelimiter > TRACE_ID_DELIMITER_INDEX_2) { + || secondDelimiter > TRACE_ID_DELIMITER_INDEX_2 + || xrayTraceId.length() < secondDelimiter + 25) { return TraceId.getInvalid(); } diff --git a/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/AwsConfigurablePropagator.java b/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsConfigurablePropagator.java similarity index 84% rename from aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/AwsConfigurablePropagator.java rename to aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsConfigurablePropagator.java index 7027f464e..1a4b871a2 100644 --- a/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/AwsConfigurablePropagator.java +++ b/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsConfigurablePropagator.java @@ -3,9 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.contrib.awsxray.propagator; +package io.opentelemetry.contrib.awsxray.propagator.internal; import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider; diff --git a/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsXrayComponentProvider.java b/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsXrayComponentProvider.java new file mode 100644 index 000000000..fdec190d0 --- /dev/null +++ b/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsXrayComponentProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.awsxray.propagator.internal; + +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; +import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties; + +public class AwsXrayComponentProvider implements ComponentProvider { + @Override + public Class getType() { + return TextMapPropagator.class; + } + + @Override + public String getName() { + return "xray"; + } + + @Override + public TextMapPropagator create(StructuredConfigProperties config) { + return AwsXrayPropagator.getInstance(); + } +} diff --git a/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsXrayLambdaComponentProvider.java b/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsXrayLambdaComponentProvider.java new file mode 100644 index 000000000..86550a22e --- /dev/null +++ b/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsXrayLambdaComponentProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.awsxray.propagator.internal; + +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.contrib.awsxray.propagator.AwsXrayLambdaPropagator; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; +import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties; + +public class AwsXrayLambdaComponentProvider implements ComponentProvider { + @Override + public Class getType() { + return TextMapPropagator.class; + } + + @Override + public String getName() { + return "xray-lambda"; + } + + @Override + public TextMapPropagator create(StructuredConfigProperties config) { + return AwsXrayLambdaPropagator.getInstance(); + } +} diff --git a/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/AwsXrayLambdaConfigurablePropagator.java b/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsXrayLambdaConfigurablePropagator.java similarity index 84% rename from aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/AwsXrayLambdaConfigurablePropagator.java rename to aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsXrayLambdaConfigurablePropagator.java index 57e030b4a..548288256 100644 --- a/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/AwsXrayLambdaConfigurablePropagator.java +++ b/aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsXrayLambdaConfigurablePropagator.java @@ -3,9 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.contrib.awsxray.propagator; +package io.opentelemetry.contrib.awsxray.propagator.internal; import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.contrib.awsxray.propagator.AwsXrayLambdaPropagator; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider; diff --git a/aws-xray-propagator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider b/aws-xray-propagator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider index 95ace8d1c..cebbbbbef 100644 --- a/aws-xray-propagator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider +++ b/aws-xray-propagator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider @@ -1 +1 @@ -io.opentelemetry.contrib.awsxray.propagator.AwsConfigurablePropagator +io.opentelemetry.contrib.awsxray.propagator.internal.AwsConfigurablePropagator diff --git a/aws-xray-propagator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider b/aws-xray-propagator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider new file mode 100644 index 000000000..f62656e7b --- /dev/null +++ b/aws-xray-propagator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider @@ -0,0 +1,2 @@ +io.opentelemetry.contrib.awsxray.propagator.internal.AwsXrayComponentProvider +io.opentelemetry.contrib.awsxray.propagator.internal.AwsXrayLambdaComponentProvider diff --git a/aws-xray-propagator/src/test/java/io/opentelemetry/contrib/awsxray/propagator/AwsXrayPropagatorTest.java b/aws-xray-propagator/src/test/java/io/opentelemetry/contrib/awsxray/propagator/AwsXrayPropagatorTest.java index 2637e78e5..e9cc564ac 100644 --- a/aws-xray-propagator/src/test/java/io/opentelemetry/contrib/awsxray/propagator/AwsXrayPropagatorTest.java +++ b/aws-xray-propagator/src/test/java/io/opentelemetry/contrib/awsxray/propagator/AwsXrayPropagatorTest.java @@ -297,7 +297,7 @@ void extract_InvalidTraceId() { } @Test - void extract_InvalidTraceId_Size() { + void extract_InvalidTraceId_Size_TooBig() { Map invalidHeaders = new LinkedHashMap<>(); invalidHeaders.put( TRACE_HEADER_KEY, @@ -306,6 +306,16 @@ void extract_InvalidTraceId_Size() { verifyInvalidBehavior(invalidHeaders); } + @Test + void extract_InvalidTraceId_Size_TooShort() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, + "Root=1-64fbd5a9-2202432c9dfed25ae1e6996;Parent=53995c3f42cd8ad8;Sampled=0"); + + verifyInvalidBehavior(invalidHeaders); + } + @Test void extract_InvalidSpanId() { Map invalidHeaders = new LinkedHashMap<>(); diff --git a/aws-xray-propagator/src/test/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsComponentProviderTest.java b/aws-xray-propagator/src/test/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsComponentProviderTest.java new file mode 100644 index 000000000..6a1920be3 --- /dev/null +++ b/aws-xray-propagator/src/test/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsComponentProviderTest.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.awsxray.propagator.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.contrib.awsxray.propagator.AwsXrayLambdaPropagator; +import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.extension.incubator.fileconfig.FileConfiguration; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class AwsComponentProviderTest { + + @Test + void endToEnd() { + String yaml = "file_format: 0.1\n" + "propagator:\n" + " composite: [xray, xray-lambda]\n"; + + OpenTelemetrySdk openTelemetrySdk = + FileConfiguration.parseAndCreate( + new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8))); + TextMapPropagator expectedPropagator = + TextMapPropagator.composite( + AwsXrayPropagator.getInstance(), AwsXrayLambdaPropagator.getInstance()); + assertThat(openTelemetrySdk.getPropagators().getTextMapPropagator().toString()) + .isEqualTo(expectedPropagator.toString()); + } +} diff --git a/aws-xray/src/main/java/io/opentelemetry/contrib/awsxray/AwsXrayRemoteSampler.java b/aws-xray/src/main/java/io/opentelemetry/contrib/awsxray/AwsXrayRemoteSampler.java index 4ab156b4b..9b5a2e7e6 100644 --- a/aws-xray/src/main/java/io/opentelemetry/contrib/awsxray/AwsXrayRemoteSampler.java +++ b/aws-xray/src/main/java/io/opentelemetry/contrib/awsxray/AwsXrayRemoteSampler.java @@ -28,6 +28,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.logging.Level; @@ -40,7 +41,6 @@ public final class AwsXrayRemoteSampler implements Sampler, Closeable { static final long DEFAULT_TARGET_INTERVAL_NANOS = TimeUnit.SECONDS.toNanos(10); - private static final Random RANDOM = new Random(); private static final Logger logger = Logger.getLogger(AwsXrayRemoteSampler.class.getName()); private final Resource resource; @@ -97,7 +97,7 @@ public static AwsXrayRemoteSamplerBuilder newBuilder(Resource resource) { this.pollingIntervalNanos = pollingIntervalNanos; // Add ~1% of jitter - jitterNanos = RANDOM.longs(0, pollingIntervalNanos / 100).iterator(); + jitterNanos = ThreadLocalRandom.current().longs(0, pollingIntervalNanos / 100).iterator(); // Execute first update right away on the executor thread. executor.execute(this::getAndUpdateSampler); diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 0256d8181..9b377d0ba 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -15,7 +15,7 @@ dependencies { implementation("com.diffplug.spotless:spotless-plugin-gradle:6.25.0") implementation("net.ltgt.gradle:gradle-errorprone-plugin:4.0.1") implementation("net.ltgt.gradle:gradle-nullaway-plugin:2.0.0") - implementation("com.gradle.enterprise:com.gradle.enterprise.gradle.plugin:3.18") + implementation("com.gradle.enterprise:com.gradle.enterprise.gradle.plugin:3.18.1") } spotless { diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ComposableSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ComposableSampler.java new file mode 100644 index 000000000..85891f47d --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ComposableSampler.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; + +/** An interface for components to be used by composite consistent probability samplers. */ +public interface ComposableSampler { + + /** + * Returns the SamplingIntent that is used for the sampling decision. The SamplingIntent includes + * the threshold value which will be used for the sampling decision. + * + *

NOTE: Keep in mind, that in any case the returned threshold value must not depend directly + * or indirectly on the random value. In particular this means that the parent sampled flag must + * not be used for the calculation of the threshold as the sampled flag depends itself on the + * random value. + */ + SamplingIntent getSamplingIntent( + Context parentContext, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks); + + /** Return the string providing a description of the implementation. */ + String getDescription(); +} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSampler.java index ba4a0869e..2594ef88d 100644 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSampler.java +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSampler.java @@ -5,6 +5,13 @@ package io.opentelemetry.contrib.sampler.consistent56; +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; import javax.annotation.concurrent.Immutable; @Immutable @@ -19,8 +26,14 @@ static ConsistentAlwaysOffSampler getInstance() { } @Override - protected long getThreshold(long parentThreshold, boolean isRoot) { - return ConsistentSamplingUtil.getMaxThreshold(); + public SamplingIntent getSamplingIntent( + Context parentContext, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + + return () -> getInvalidThreshold(); } @Override diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSampler.java index bae1c4b27..620261aad 100644 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSampler.java +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSampler.java @@ -7,6 +7,11 @@ import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMinThreshold; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; import javax.annotation.concurrent.Immutable; @Immutable @@ -21,8 +26,14 @@ static ConsistentAlwaysOnSampler getInstance() { } @Override - protected long getThreshold(long parentThreshold, boolean isRoot) { - return getMinThreshold(); + public SamplingIntent getSamplingIntent( + Context parentContext, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + + return () -> getMinThreshold(); } @Override diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOf.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOf.java new file mode 100644 index 000000000..56add2dfc --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOf.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold; +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.isValidThreshold; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * A consistent sampler that queries all its delegate samplers for their sampling threshold, and + * uses the minimum threshold value received. + */ +@Immutable +final class ConsistentAnyOf extends ConsistentSampler { + + private final ComposableSampler[] delegates; + + private final String description; + + /** + * Constructs a new consistent AnyOf sampler using the provided delegate samplers. + * + * @param delegates the delegate samplers + */ + ConsistentAnyOf(@Nullable ComposableSampler... delegates) { + if (delegates == null || delegates.length == 0) { + throw new IllegalArgumentException( + "At least one delegate must be specified for ConsistentAnyOf"); + } + + this.delegates = delegates; + + this.description = + Stream.of(delegates) + .map(Object::toString) + .collect(Collectors.joining(",", "ConsistentAnyOf{", "}")); + } + + @Override + public SamplingIntent getSamplingIntent( + Context parentContext, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + + SamplingIntent[] intents = new SamplingIntent[delegates.length]; + int k = 0; + long minimumThreshold = getInvalidThreshold(); + for (ComposableSampler delegate : delegates) { + SamplingIntent delegateIntent = + delegate.getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks); + long delegateThreshold = delegateIntent.getThreshold(); + if (isValidThreshold(delegateThreshold)) { + if (isValidThreshold(minimumThreshold)) { + minimumThreshold = Math.min(delegateThreshold, minimumThreshold); + } else { + minimumThreshold = delegateThreshold; + } + } + intents[k++] = delegateIntent; + } + + long resultingThreshold = minimumThreshold; + + return new SamplingIntent() { + @Override + public long getThreshold() { + return resultingThreshold; + } + + @Override + public Attributes getAttributes() { + AttributesBuilder builder = Attributes.builder(); + for (SamplingIntent intent : intents) { + builder = builder.putAll(intent.getAttributes()); + } + return builder.build(); + } + + @Override + public TraceState updateTraceState(TraceState previousState) { + for (SamplingIntent intent : intents) { + previousState = intent.updateTraceState(previousState); + } + return previousState; + } + }; + } + + @Override + public String getDescription() { + return description; + } +} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedAndSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedAndSampler.java deleted file mode 100644 index 40df6c895..000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedAndSampler.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent56; - -import static java.util.Objects.requireNonNull; - -import javax.annotation.concurrent.Immutable; - -/** - * A consistent sampler composed of two consistent samplers. - * - *

This sampler samples if both samplers would sample. - */ -@Immutable -final class ConsistentComposedAndSampler extends ConsistentSampler { - - private final ConsistentSampler sampler1; - private final ConsistentSampler sampler2; - private final String description; - - ConsistentComposedAndSampler(ConsistentSampler sampler1, ConsistentSampler sampler2) { - this.sampler1 = requireNonNull(sampler1); - this.sampler2 = requireNonNull(sampler2); - this.description = - "ConsistentComposedAndSampler{" - + "sampler1=" - + sampler1.getDescription() - + ",sampler2=" - + sampler2.getDescription() - + '}'; - } - - @Override - protected long getThreshold(long parentThreshold, boolean isRoot) { - long threshold1 = sampler1.getThreshold(parentThreshold, isRoot); - long threshold2 = sampler2.getThreshold(parentThreshold, isRoot); - if (ConsistentSamplingUtil.isValidThreshold(threshold1) - && ConsistentSamplingUtil.isValidThreshold(threshold2)) { - return Math.max(threshold1, threshold2); - } else { - return ConsistentSamplingUtil.getInvalidThreshold(); - } - } - - @Override - public String getDescription() { - return description; - } -} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedOrSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedOrSampler.java deleted file mode 100644 index b701b5622..000000000 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedOrSampler.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.sampler.consistent56; - -import static java.util.Objects.requireNonNull; - -import javax.annotation.concurrent.Immutable; - -/** - * A consistent sampler composed of two consistent samplers. - * - *

This sampler samples if any of the two samplers would sample. - */ -@Immutable -final class ConsistentComposedOrSampler extends ConsistentSampler { - - private final ConsistentSampler sampler1; - private final ConsistentSampler sampler2; - private final String description; - - ConsistentComposedOrSampler(ConsistentSampler sampler1, ConsistentSampler sampler2) { - this.sampler1 = requireNonNull(sampler1); - this.sampler2 = requireNonNull(sampler2); - this.description = - "ConsistentComposedOrSampler{" - + "sampler1=" - + sampler1.getDescription() - + ",sampler2=" - + sampler2.getDescription() - + '}'; - } - - @Override - protected long getThreshold(long parentThreshold, boolean isRoot) { - long threshold1 = sampler1.getThreshold(parentThreshold, isRoot); - long threshold2 = sampler2.getThreshold(parentThreshold, isRoot); - if (ConsistentSamplingUtil.isValidThreshold(threshold1)) { - if (ConsistentSamplingUtil.isValidThreshold(threshold2)) { - return Math.min(threshold1, threshold2); - } - return threshold1; - } else { - if (ConsistentSamplingUtil.isValidThreshold(threshold2)) { - return threshold2; - } - return ConsistentSamplingUtil.getInvalidThreshold(); - } - } - - @Override - public String getDescription() { - return description; - } -} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSampler.java index d2e2fc426..f2e92651c 100644 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSampler.java +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentFixedThresholdSampler.java @@ -7,6 +7,14 @@ import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.calculateSamplingProbability; import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.checkThreshold; +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold; +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMaxThreshold; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; public class ConsistentFixedThresholdSampler extends ConsistentSampler { @@ -18,7 +26,7 @@ protected ConsistentFixedThresholdSampler(long threshold) { this.threshold = threshold; String thresholdString; - if (threshold == ConsistentSamplingUtil.getMaxThreshold()) { + if (threshold == getMaxThreshold()) { thresholdString = "max"; } else { thresholdString = @@ -41,7 +49,18 @@ public String getDescription() { } @Override - protected long getThreshold(long parentThreshold, boolean isRoot) { - return threshold; + public SamplingIntent getSamplingIntent( + Context parentContext, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + + return () -> { + if (threshold == getMaxThreshold()) { + return getInvalidThreshold(); + } + return threshold; + }; } } diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentParentBasedSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentParentBasedSampler.java index bb3f3836a..01e42ddcf 100644 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentParentBasedSampler.java +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentParentBasedSampler.java @@ -8,6 +8,14 @@ import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold; import static java.util.Objects.requireNonNull; +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.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; import javax.annotation.concurrent.Immutable; /** @@ -17,7 +25,7 @@ @Immutable final class ConsistentParentBasedSampler extends ConsistentSampler { - private final ConsistentSampler rootSampler; + private final ComposableSampler rootSampler; private final String description; @@ -27,19 +35,40 @@ final class ConsistentParentBasedSampler extends ConsistentSampler { * * @param rootSampler the root sampler */ - ConsistentParentBasedSampler(ConsistentSampler rootSampler) { + ConsistentParentBasedSampler(ComposableSampler rootSampler) { this.rootSampler = requireNonNull(rootSampler); this.description = "ConsistentParentBasedSampler{rootSampler=" + rootSampler.getDescription() + '}'; } @Override - protected long getThreshold(long parentThreshold, boolean isRoot) { + public SamplingIntent getSamplingIntent( + Context parentContext, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + + Span parentSpan = Span.fromContext(parentContext); + SpanContext parentSpanContext = parentSpan.getSpanContext(); + boolean isRoot = !parentSpanContext.isValid(); + if (isRoot) { - return rootSampler.getThreshold(getInvalidThreshold(), isRoot); + return rootSampler.getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks); + } + + TraceState parentTraceState = parentSpanContext.getTraceState(); + String otelTraceStateString = parentTraceState.get(OtelTraceState.TRACE_STATE_KEY); + OtelTraceState otelTraceState = OtelTraceState.parse(otelTraceStateString); + + long parentThreshold; + if (otelTraceState.hasValidThreshold()) { + parentThreshold = otelTraceState.getThreshold(); } else { - return parentThreshold; + parentThreshold = getInvalidThreshold(); } + + return () -> parentThreshold; } @Override diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSampler.java index d7622effa..d28161485 100644 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSampler.java +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSampler.java @@ -5,11 +5,19 @@ package io.opentelemetry.contrib.sampler.consistent56; +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.calculateSamplingProbability; import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.calculateThreshold; -import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMinThreshold; +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold; +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.isValidThreshold; import static java.util.Objects.requireNonNull; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.function.LongSupplier; import javax.annotation.concurrent.Immutable; @@ -66,6 +74,24 @@ *

  • {@code decayFactor} corresponds to {@code b(n)} *
  • {@code adaptationTimeSeconds} corresponds to {@code -1 / ln(1 - a)} * + * + *

    + * + *

    The sampler also keeps track of the average sampling probability delivered by the delegate + * sampler, using exponential smoothing. Given the sequence of the observed probabilities {@code + * P(k)}, the exponentially smoothed values {@code S(k)} are calculated according to the following + * formula: + * + *

    {@code S(0) = 1} + * + *

    {@code S(n) = alpha * P(n) + (1 - alpha) * S(n-1)}, for {@code n > 0} + * + *

    where {@code alpha} is the smoothing factor ({@code 0 < alpha < 1}). + * + *

    The smoothing factor is chosen heuristically to be approximately proportional to the expected + * maximum volume of spans sampled within the adaptation time window, i.e. + * + *

    {@code 1 / (adaptationTimeSeconds * targetSpansPerSecondLimit)} */ final class ConsistentRateLimitingSampler extends ConsistentSampler { @@ -75,12 +101,18 @@ final class ConsistentRateLimitingSampler extends ConsistentSampler { private static final class State { private final double effectiveWindowCount; private final double effectiveWindowNanos; + private final double effectiveDelegateProbability; private final long lastNanoTime; - public State(double effectiveWindowCount, double effectiveWindowNanos, long lastNanoTime) { + public State( + double effectiveWindowCount, + double effectiveWindowNanos, + long lastNanoTime, + double effectiveDelegateProbability) { this.effectiveWindowCount = effectiveWindowCount; this.effectiveWindowNanos = effectiveWindowNanos; this.lastNanoTime = lastNanoTime; + this.effectiveDelegateProbability = effectiveDelegateProbability; } } @@ -88,7 +120,9 @@ public State(double effectiveWindowCount, double effectiveWindowNanos, long last private final LongSupplier nanoTimeSupplier; private final double inverseAdaptationTimeNanos; private final double targetSpansPerNanosecondLimit; + private final double probabilitySmoothingFactor; private final AtomicReference state; + private final ComposableSampler delegate; /** * Constructor. @@ -99,10 +133,13 @@ public State(double effectiveWindowCount, double effectiveWindowNanos, long last * @param nanoTimeSupplier a supplier for the current nano time */ ConsistentRateLimitingSampler( + ComposableSampler delegate, double targetSpansPerSecondLimit, double adaptationTimeSeconds, LongSupplier nanoTimeSupplier) { + this.delegate = requireNonNull(delegate); + if (targetSpansPerSecondLimit < 0.0) { throw new IllegalArgumentException("Limit for sampled spans per second must be nonnegative!"); } @@ -120,36 +157,114 @@ public State(double effectiveWindowCount, double effectiveWindowNanos, long last this.inverseAdaptationTimeNanos = NANOS_IN_SECONDS / adaptationTimeSeconds; this.targetSpansPerNanosecondLimit = NANOS_IN_SECONDS * targetSpansPerSecondLimit; - this.state = new AtomicReference<>(new State(0, 0, nanoTimeSupplier.getAsLong())); + this.probabilitySmoothingFactor = + determineProbabilitySmoothingFactor(targetSpansPerSecondLimit, adaptationTimeSeconds); + + this.state = new AtomicReference<>(new State(0, 0, nanoTimeSupplier.getAsLong(), 1.0)); + } + + private static double determineProbabilitySmoothingFactor( + double targetSpansPerSecondLimit, double adaptationTimeSeconds) { + // The probability smoothing factor alpha will be the weight for the newly observed + // probability P, while (1-alpha) will be the weight for the cumulative average probability + // observed so far (newC = P * alpha + oldC * (1 - alpha)). Any smoothing factor + // alpha from the interval (0.0, 1.0) is mathematically acceptable. + // However, we'd like the weight associated with the newly observed data point to be inversely + // proportional to the adaptation time (larger adaptation time will allow longer time for the + // cumulative probability to stabilize) and inversely proportional to the order of magnitude of + // the data points arriving within a given time unit (because with a lot of data points we can + // afford to give a smaller weight to each single one). We do not know the true rate of Spans + // coming in to get sampled, but we optimistically assume that the user knows what they are + // doing and that the targetSpansPerSecondLimit will be of similar order of magnitude. + + // First approximation of the probability smoothing factor alpha. + double t = 1.0 / (targetSpansPerSecondLimit * adaptationTimeSeconds); + + // We expect that t is a small number, but we have to make sure that alpha is smaller than 1. + // Therefore we apply a "bending" transformation which almost preserves small values, but makes + // sure that the result is within the expected interval. + return t / (1.0 + t); } - private State updateState(State oldState, long currentNanoTime) { - if (currentNanoTime <= oldState.lastNanoTime) { + private State updateState(State oldState, long currentNanoTime, double delegateProbability) { + double currentAverageProbability = + oldState.effectiveDelegateProbability * (1.0 - probabilitySmoothingFactor) + + delegateProbability * probabilitySmoothingFactor; + + long nanoTimeDelta = currentNanoTime - oldState.lastNanoTime; + if (nanoTimeDelta <= 0.0) { + // Low clock resolution or clock jumping backwards. + // Assume time delta equal to zero. return new State( - oldState.effectiveWindowCount + 1, oldState.effectiveWindowNanos, oldState.lastNanoTime); + oldState.effectiveWindowCount + 1, + oldState.effectiveWindowNanos, + oldState.lastNanoTime, + currentAverageProbability); } - long nanoTimeDelta = currentNanoTime - oldState.lastNanoTime; + double decayFactor = Math.exp(-nanoTimeDelta * inverseAdaptationTimeNanos); double currentEffectiveWindowCount = oldState.effectiveWindowCount * decayFactor + 1; double currentEffectiveWindowNanos = oldState.effectiveWindowNanos * decayFactor + nanoTimeDelta; - return new State(currentEffectiveWindowCount, currentEffectiveWindowNanos, currentNanoTime); + + return new State( + currentEffectiveWindowCount, + currentEffectiveWindowNanos, + currentNanoTime, + currentAverageProbability); } @Override - protected long getThreshold(long parentThreshold, boolean isRoot) { - long currentNanoTime = nanoTimeSupplier.getAsLong(); - State currentState = state.updateAndGet(s -> updateState(s, currentNanoTime)); + public SamplingIntent getSamplingIntent( + Context parentContext, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + double suggestedProbability; + long suggestedThreshold; + + SamplingIntent delegateIntent = + delegate.getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks); + long delegateThreshold = delegateIntent.getThreshold(); + + if (isValidThreshold(delegateThreshold)) { + double delegateProbability = calculateSamplingProbability(delegateThreshold); + long currentNanoTime = nanoTimeSupplier.getAsLong(); + State currentState = + state.updateAndGet(s -> updateState(s, currentNanoTime, delegateProbability)); - double samplingProbability = - (currentState.effectiveWindowNanos * targetSpansPerNanosecondLimit) - / currentState.effectiveWindowCount; + double targetMaxProbability = + (currentState.effectiveWindowNanos * targetSpansPerNanosecondLimit) + / currentState.effectiveWindowCount; - if (samplingProbability >= 1.) { - return getMinThreshold(); + if (currentState.effectiveDelegateProbability > targetMaxProbability) { + suggestedProbability = + targetMaxProbability / currentState.effectiveDelegateProbability * delegateProbability; + } else { + suggestedProbability = delegateProbability; + } + suggestedThreshold = calculateThreshold(suggestedProbability); } else { - return calculateThreshold(samplingProbability); + suggestedThreshold = getInvalidThreshold(); } + + return new SamplingIntent() { + @Override + public long getThreshold() { + return suggestedThreshold; + } + + @Override + public Attributes getAttributes() { + return delegateIntent.getAttributes(); + } + + @Override + public TraceState updateTraceState(TraceState previousState) { + return delegateIntent.updateTraceState(previousState); + } + }; } @Override diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSampler.java new file mode 100644 index 000000000..90e4cb026 --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSampler.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * A consistent sampler that uses Span categorization and uses a different delegate sampler for each + * category. Categorization of Spans is aided by Predicates, which can be combined with + * ComposableSamplers into PredicatedSamplers. + */ +@Immutable +final class ConsistentRuleBasedSampler extends ConsistentSampler { + + @Nullable private final SpanKind spanKindToMatch; + private final PredicatedSampler[] samplers; + + private final String description; + + ConsistentRuleBasedSampler( + @Nullable SpanKind spanKindToMatch, @Nullable PredicatedSampler... samplers) { + this.spanKindToMatch = spanKindToMatch; + this.samplers = (samplers != null) ? samplers : new PredicatedSampler[0]; + + this.description = + Stream.of(samplers) + .map((s) -> s.getSampler().getDescription()) + .collect(Collectors.joining(",", "ConsistentRuleBasedSampler{", "}")); + } + + @Override + public SamplingIntent getSamplingIntent( + Context parentContext, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + + if (spanKindToMatch == null || spanKindToMatch == spanKind) { + for (PredicatedSampler delegate : samplers) { + if (delegate + .getPredicate() + .spanMatches(parentContext, name, spanKind, attributes, parentLinks)) { + return delegate + .getSampler() + .getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks); + } + } + } + + return () -> getInvalidThreshold(); + } + + @Override + public String getDescription() { + return description; + } +} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSampler.java index 618728b5c..5592b3215 100644 --- a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSampler.java +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSampler.java @@ -6,7 +6,6 @@ package io.opentelemetry.contrib.sampler.consistent56; import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidRandomValue; -import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold; import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.isValidThreshold; import io.opentelemetry.api.common.Attributes; @@ -21,9 +20,11 @@ import io.opentelemetry.sdk.trace.samplers.SamplingResult; import java.util.List; import java.util.function.LongSupplier; +import javax.annotation.Nullable; /** Abstract base class for consistent samplers. */ -public abstract class ConsistentSampler implements Sampler { +@SuppressWarnings("InconsistentOverloads") +public abstract class ConsistentSampler implements Sampler, ComposableSampler { /** * Returns a {@link ConsistentSampler} that samples all spans. @@ -60,10 +61,23 @@ public static ConsistentSampler probabilityBased(double samplingProbability) { * * @param rootSampler the root sampler */ - public static ConsistentSampler parentBased(ConsistentSampler rootSampler) { + public static ConsistentSampler parentBased(ComposableSampler rootSampler) { return new ConsistentParentBasedSampler(rootSampler); } + /** + * Constructs a new consistent rule based sampler using the given sequence of Predicates and + * delegate Samplers. + * + * @param spanKindToMatch the SpanKind for which the Sampler applies, null value indicates all + * SpanKinds + * @param samplers the PredicatedSamplers to evaluate and query + */ + public static ConsistentRuleBasedSampler ruleBased( + @Nullable SpanKind spanKindToMatch, PredicatedSampler... samplers) { + return new ConsistentRuleBasedSampler(spanKindToMatch, samplers); + } + /** * Returns a new {@link ConsistentSampler} that attempts to adjust the sampling probability * dynamically to meet the target span rate. @@ -72,9 +86,26 @@ public static ConsistentSampler parentBased(ConsistentSampler rootSampler) { * @param adaptationTimeSeconds the typical time to adapt to a new load (time constant used for * exponential smoothing) */ - public static ConsistentSampler rateLimited( + static ConsistentSampler rateLimited( double targetSpansPerSecondLimit, double adaptationTimeSeconds) { - return rateLimited(targetSpansPerSecondLimit, adaptationTimeSeconds, System::nanoTime); + return rateLimited(alwaysOn(), targetSpansPerSecondLimit, adaptationTimeSeconds); + } + + /** + * Returns a new {@link ConsistentSampler} that honors the delegate sampling decision as long as + * it seems to meet the target span rate. In case the delegate sampling rate seems to exceed the + * target, the sampler attempts to decrease the effective sampling probability dynamically to meet + * the target span rate. + * + * @param delegate the delegate sampler + * @param targetSpansPerSecondLimit the desired spans per second limit + * @param adaptationTimeSeconds the typical time to adapt to a new load (time constant used for + * exponential smoothing) + */ + public static ConsistentSampler rateLimited( + ComposableSampler delegate, double targetSpansPerSecondLimit, double adaptationTimeSeconds) { + return rateLimited( + delegate, targetSpansPerSecondLimit, adaptationTimeSeconds, System::nanoTime); } /** @@ -90,52 +121,46 @@ static ConsistentSampler rateLimited( double targetSpansPerSecondLimit, double adaptationTimeSeconds, LongSupplier nanoTimeSupplier) { - return new ConsistentRateLimitingSampler( - targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier); + return rateLimited( + alwaysOn(), targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier); } /** - * Returns a {@link ConsistentSampler} that samples a span if both this and the other given - * consistent sampler would sample the span. - * - *

    If the other consistent sampler is the same as this, this consistent sampler will be - * returned. - * - *

    The returned sampler takes care of setting the trace state correctly, which would not happen - * if the {@link #shouldSample(Context, String, String, SpanKind, Attributes, List)} method was - * called for each sampler individually. Also, the combined sampler is more efficient than - * evaluating the two samplers individually and combining both results afterwards. + * Returns a new {@link ConsistentSampler} that honors the delegate sampling decision as long as + * it seems to meet the target span rate. In case the delegate sampling rate seems to exceed the + * target, the sampler attempts to decrease the effective sampling probability dynamically to meet + * the target span rate. * - * @param otherConsistentSampler the other consistent sampler - * @return the composed consistent sampler + * @param delegate the delegate sampler + * @param targetSpansPerSecondLimit the desired spans per second limit + * @param adaptationTimeSeconds the typical time to adapt to a new load (time constant used for + * exponential smoothing) + * @param nanoTimeSupplier a supplier for the current nano time */ - public ConsistentSampler and(ConsistentSampler otherConsistentSampler) { - if (otherConsistentSampler == this) { - return this; - } - return new ConsistentComposedAndSampler(this, otherConsistentSampler); + static ConsistentSampler rateLimited( + ComposableSampler delegate, + double targetSpansPerSecondLimit, + double adaptationTimeSeconds, + LongSupplier nanoTimeSupplier) { + return new ConsistentRateLimitingSampler( + delegate, targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier); } /** - * Returns a {@link ConsistentSampler} that samples a span if this or the other given consistent - * sampler would sample the span. - * - *

    If the other consistent sampler is the same as this, this consistent sampler will be - * returned. + * Returns a {@link ConsistentSampler} that queries its delegate Samplers for their sampling + * threshold before determining what threshold to use. The intention is to make a positive + * sampling decision if any of the delegates would make a positive decision. * *

    The returned sampler takes care of setting the trace state correctly, which would not happen * if the {@link #shouldSample(Context, String, String, SpanKind, Attributes, List)} method was * called for each sampler individually. Also, the combined sampler is more efficient than - * evaluating the two samplers individually and combining both results afterwards. + * evaluating the samplers individually and combining the results afterwards. * - * @param otherConsistentSampler the other consistent sampler - * @return the composed consistent sampler + * @param delegates the delegate samplers, at least one delegate must be specified + * @return the ConsistentAnyOf sampler */ - public ConsistentSampler or(ConsistentSampler otherConsistentSampler) { - if (otherConsistentSampler == this) { - return this; - } - return new ConsistentComposedOrSampler(this, otherConsistentSampler); + public static ConsistentSampler anyOf(ComposableSampler... delegates) { + return new ConsistentAnyOf(delegates); } @Override @@ -146,55 +171,35 @@ public final SamplingResult shouldSample( SpanKind spanKind, Attributes attributes, List parentLinks) { - Span parentSpan = Span.fromContext(parentContext); SpanContext parentSpanContext = parentSpan.getSpanContext(); - boolean isRoot = !parentSpanContext.isValid(); - boolean isParentSampled = parentSpanContext.isSampled(); TraceState parentTraceState = parentSpanContext.getTraceState(); String otelTraceStateString = parentTraceState.get(OtelTraceState.TRACE_STATE_KEY); OtelTraceState otelTraceState = OtelTraceState.parse(otelTraceStateString); - long randomValue; - if (otelTraceState.hasValidRandomValue()) { - randomValue = otelTraceState.getRandomValue(); - } else { - randomValue = OtelTraceState.parseHex(traceId, 18, 14, getInvalidRandomValue()); - } - - long parentThreshold; - if (otelTraceState.hasValidThreshold()) { - long threshold = otelTraceState.getThreshold(); - if ((randomValue >= threshold) == isParentSampled) { // test invariant - parentThreshold = threshold; - } else { - parentThreshold = getInvalidThreshold(); - } - } else { - parentThreshold = getInvalidThreshold(); - } - - // determine new threshold that is used for the sampling decision - long threshold = getThreshold(parentThreshold, isRoot); + SamplingIntent intent = + getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks); + long threshold = intent.getThreshold(); // determine sampling decision boolean isSampled; if (isValidThreshold(threshold)) { - isSampled = (randomValue >= threshold); - if (isSampled) { - otelTraceState.setThreshold(threshold); - } else { - otelTraceState.invalidateThreshold(); - } + long randomness = getRandomness(otelTraceState, traceId); + isSampled = threshold <= randomness; + } else { // DROP + isSampled = false; + } + + SamplingDecision samplingDecision; + if (isSampled) { + samplingDecision = SamplingDecision.RECORD_AND_SAMPLE; + otelTraceState.setThreshold(threshold); } else { - isSampled = isParentSampled; + samplingDecision = SamplingDecision.DROP; otelTraceState.invalidateThreshold(); } - SamplingDecision samplingDecision = - isSampled ? SamplingDecision.RECORD_AND_SAMPLE : SamplingDecision.DROP; - String newOtTraceState = otelTraceState.serialize(); return new SamplingResult() { @@ -206,31 +211,23 @@ public SamplingDecision getDecision() { @Override public Attributes getAttributes() { - return Attributes.empty(); + return intent.getAttributes(); } @Override public TraceState getUpdatedTraceState(TraceState parentTraceState) { - return parentTraceState.toBuilder() + return intent.updateTraceState(parentTraceState).toBuilder() .put(OtelTraceState.TRACE_STATE_KEY, newOtTraceState) .build(); } }; } - /** - * Returns the threshold that is used for the sampling decision. - * - *

    NOTE: In future, further information like span attributes could be also added as arguments - * such that the sampling probability could be made dependent on those extra arguments. However, - * in any case the returned threshold value must not depend directly or indirectly on the random - * value. In particular this means that the parent sampled flag must not be used for the - * calculation of the threshold as the sampled flag depends itself on the random value. - * - * @param parentThreshold is the threshold (if known) that was used for a consistent sampling - * decision by the parent - * @param isRoot is true for the root span - * @return the threshold to be used for the sampling decision - */ - protected abstract long getThreshold(long parentThreshold, boolean isRoot); + private static long getRandomness(OtelTraceState otelTraceState, String traceId) { + if (otelTraceState.hasValidRandomValue()) { + return otelTraceState.getRandomValue(); + } else { + return OtelTraceState.parseHex(traceId, 18, 14, getInvalidRandomValue()); + } + } } diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/Predicate.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/Predicate.java new file mode 100644 index 000000000..56ce59c46 --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/Predicate.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +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.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; + +/** Interface for logical expression that can be matched against Spans to be sampled */ +@FunctionalInterface +public interface Predicate { + + /* + * Return true if the Span context described by the provided arguments matches the predicate + */ + boolean spanMatches( + Context parentContext, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks); + + /* + * Return a Predicate that will match ROOT Spans only + */ + static Predicate isRootSpan() { + return (parentContext, name, spanKind, attributes, parentLinks) -> { + Span parentSpan = Span.fromContext(parentContext); + SpanContext parentSpanContext = parentSpan.getSpanContext(); + return !parentSpanContext.isValid(); + }; + } + + /* + * Return a Predicate that matches all Spans + */ + static Predicate anySpan() { + return (parentContext, name, spanKind, attributes, parentLinks) -> true; + } + + /* + * Return a Predicate that represents logical AND of the argument predicates + */ + static Predicate and(Predicate p1, Predicate p2) { + return (parentContext, name, spanKind, attributes, parentLinks) -> + p1.spanMatches(parentContext, name, spanKind, attributes, parentLinks) + && p2.spanMatches(parentContext, name, spanKind, attributes, parentLinks); + } + + /* + * Return a Predicate that represents logical negation of the argument predicate + */ + static Predicate not(Predicate p) { + return (parentContext, name, spanKind, attributes, parentLinks) -> + !p.spanMatches(parentContext, name, spanKind, attributes, parentLinks); + } + + /* + * Return a Predicate that represents logical OR of the argument predicates + */ + static Predicate or(Predicate p1, Predicate p2) { + return (parentContext, name, spanKind, attributes, parentLinks) -> + p1.spanMatches(parentContext, name, spanKind, attributes, parentLinks) + || p2.spanMatches(parentContext, name, spanKind, attributes, parentLinks); + } +} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/PredicatedSampler.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/PredicatedSampler.java new file mode 100644 index 000000000..dabec5b74 --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/PredicatedSampler.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import static java.util.Objects.requireNonNull; + +/** A class for holding a pair (Predicate, ComposableSampler) */ +public final class PredicatedSampler { + + public static PredicatedSampler onMatch(Predicate predicate, ComposableSampler sampler) { + return new PredicatedSampler(predicate, sampler); + } + + private final Predicate predicate; + private final ComposableSampler sampler; + + private PredicatedSampler(Predicate predicate, ComposableSampler sampler) { + this.predicate = requireNonNull(predicate); + this.sampler = requireNonNull(sampler); + } + + public Predicate getPredicate() { + return predicate; + } + + public ComposableSampler getSampler() { + return sampler; + } +} diff --git a/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/SamplingIntent.java b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/SamplingIntent.java new file mode 100644 index 000000000..07906ad3b --- /dev/null +++ b/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/SamplingIntent.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.TraceState; + +/** Interface for declaring sampling intent by Composable Samplers. */ +@SuppressWarnings("CanIgnoreReturnValueSuggester") +public interface SamplingIntent { + + /** + * Returns the suggested rejection threshold value. The returned value must be either from the + * interval [0, 2^56) or be equal to ConsistentSamplingUtil.getInvalidThreshold(). + * + * @return a threshold value + */ + long getThreshold(); + + /** + * Returns a set of Attributes to be added to the Span in case of positive sampling decision. + * + * @return Attributes + */ + default Attributes getAttributes() { + return Attributes.empty(); + } + + /** + * Given an input Tracestate and sampling Decision provide a Tracestate to be associated with the + * Span. + * + * @param parentState the TraceState of the parent Span + * @return a TraceState + */ + default TraceState updateTraceState(TraceState parentState) { + return parentState; + } +} diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/CoinFlipSampler.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/CoinFlipSampler.java new file mode 100644 index 000000000..a3999e954 --- /dev/null +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/CoinFlipSampler.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; +import java.util.SplittableRandom; +import javax.annotation.concurrent.Immutable; + +/** + * A consistent sampler that delegates the decision randomly, with a predefined probability, to one + * of its two delegates. Used by unit tests. + */ +@Immutable +final class CoinFlipSampler extends ConsistentSampler { + + private static final SplittableRandom random = new SplittableRandom(0x160a50a2073e17e6L); + + private final ComposableSampler samplerA; + private final ComposableSampler samplerB; + private final double probability; + private final String description; + + /** + * Constructs a new consistent CoinFlipSampler using the given two delegates with equal + * probability. + * + * @param samplerA the first delegate sampler + * @param samplerB the second delegate sampler + */ + CoinFlipSampler(ComposableSampler samplerA, ComposableSampler samplerB) { + this(samplerA, samplerB, 0.5); + } + + /** + * Constructs a new consistent CoinFlipSampler using the given two delegates, and the probability + * to use the first one. + * + * @param probability the probability to use the first sampler + * @param samplerA the first delegate sampler + * @param samplerB the second delegate sampler + */ + CoinFlipSampler(ComposableSampler samplerA, ComposableSampler samplerB, double probability) { + this.samplerA = requireNonNull(samplerA); + this.samplerB = requireNonNull(samplerB); + this.probability = probability; + this.description = + "CoinFlipSampler{p=" + + (float) probability + + ",samplerA=" + + samplerA.getDescription() + + ',' + + "samplerB=" + + samplerB.getDescription() + + '}'; + } + + @Override + public SamplingIntent getSamplingIntent( + Context parentContext, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + + if (random.nextDouble() < probability) { + return samplerA.getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks); + } else { + return samplerB.getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks); + } + } + + @Override + public String getDescription() { + return description; + } +} diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSamplerTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSamplerTest.java index 04d266ac2..9b5fc050b 100644 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSamplerTest.java +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOffSamplerTest.java @@ -6,7 +6,6 @@ package io.opentelemetry.contrib.sampler.consistent56; import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold; -import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMaxThreshold; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; @@ -21,15 +20,10 @@ void testDescription() { @Test void testThreshold() { - assertThat(ConsistentSampler.alwaysOff().getThreshold(getInvalidThreshold(), false)) - .isEqualTo(getMaxThreshold()); - assertThat(ConsistentSampler.alwaysOff().getThreshold(getInvalidThreshold(), true)) - .isEqualTo(getMaxThreshold()); - assertThat(ConsistentSampler.alwaysOff().getThreshold(getMaxThreshold(), false)) - .isEqualTo(getMaxThreshold()); - assertThat(ConsistentSampler.alwaysOff().getThreshold(getMaxThreshold(), true)) - .isEqualTo(getMaxThreshold()); - assertThat(ConsistentSampler.alwaysOff().getThreshold(0, false)).isEqualTo(getMaxThreshold()); - assertThat(ConsistentSampler.alwaysOff().getThreshold(0, true)).isEqualTo(getMaxThreshold()); + assertThat( + ConsistentSampler.alwaysOff() + .getSamplingIntent(null, "span_name", null, null, null) + .getThreshold()) + .isEqualTo(getInvalidThreshold()); } } diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSamplerTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSamplerTest.java index 6df53066b..3a6b8531b 100644 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSamplerTest.java +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAlwaysOnSamplerTest.java @@ -5,7 +5,6 @@ package io.opentelemetry.contrib.sampler.consistent56; -import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold; import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getMinThreshold; import static org.assertj.core.api.Assertions.assertThat; @@ -21,15 +20,10 @@ void testDescription() { @Test void testThreshold() { - assertThat(ConsistentSampler.alwaysOn().getThreshold(getInvalidThreshold(), false)) + assertThat( + ConsistentSampler.alwaysOn() + .getSamplingIntent(null, "span_name", null, null, null) + .getThreshold()) .isEqualTo(getMinThreshold()); - assertThat(ConsistentSampler.alwaysOn().getThreshold(getInvalidThreshold(), true)) - .isEqualTo(getMinThreshold()); - assertThat(ConsistentSampler.alwaysOn().getThreshold(getMinThreshold(), false)) - .isEqualTo(getMinThreshold()); - assertThat(ConsistentSampler.alwaysOn().getThreshold(getMinThreshold(), true)) - .isEqualTo(getMinThreshold()); - assertThat(ConsistentSampler.alwaysOn().getThreshold(0, false)).isEqualTo(getMinThreshold()); - assertThat(ConsistentSampler.alwaysOn().getThreshold(0, true)).isEqualTo(getMinThreshold()); } } diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOfTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOfTest.java new file mode 100644 index 000000000..720a675e6 --- /dev/null +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOfTest.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import org.junit.jupiter.api.Test; + +class ConsistentAnyOfTest { + + @Test + void testMinimumThreshold() { + ComposableSampler delegate1 = new ConsistentFixedThresholdSampler(0x80000000000000L); + ComposableSampler delegate2 = new ConsistentFixedThresholdSampler(0x30000000000000L); + ComposableSampler delegate3 = new ConsistentFixedThresholdSampler(0xa0000000000000L); + ComposableSampler sampler = ConsistentSampler.anyOf(delegate1, delegate2, delegate3); + SamplingIntent intent = sampler.getSamplingIntent(null, "span_name", null, null, null); + assertThat(intent.getThreshold()).isEqualTo(0x30000000000000L); + } + + @Test + void testAlwaysDrop() { + ComposableSampler delegate1 = ConsistentSampler.alwaysOff(); + ComposableSampler sampler = ConsistentSampler.anyOf(delegate1); + SamplingIntent intent = sampler.getSamplingIntent(null, "span_name", null, null, null); + assertThat(intent.getThreshold()).isEqualTo(getInvalidThreshold()); + } + + @Test + void testSpanAttributesAdded() { + AttributeKey key1 = AttributeKey.stringKey("tag1"); + AttributeKey key2 = AttributeKey.stringKey("tag2"); + AttributeKey key3 = AttributeKey.stringKey("tag3"); + ComposableSampler delegate1 = + new MarkingSampler(new ConsistentFixedThresholdSampler(0x30000000000000L), key1, "a"); + ComposableSampler delegate2 = + new MarkingSampler(new ConsistentFixedThresholdSampler(0x50000000000000L), key2, "b"); + ComposableSampler delegate3 = new MarkingSampler(ConsistentSampler.alwaysOff(), key3, "c"); + ComposableSampler sampler = ConsistentSampler.anyOf(delegate1, delegate2, delegate3); + SamplingIntent intent = sampler.getSamplingIntent(null, "span_name", null, null, null); + assertThat(intent.getAttributes().get(key1)).isEqualTo("a"); + assertThat(intent.getAttributes().get(key2)).isEqualTo("b"); + assertThat(intent.getAttributes().get(key3)).isEqualTo("c"); + assertThat(intent.getThreshold()).isEqualTo(0x30000000000000L); + } + + @Test + void testSpanAttributeOverride() { + AttributeKey key1 = AttributeKey.stringKey("shared"); + ComposableSampler delegate1 = + new MarkingSampler(new ConsistentFixedThresholdSampler(0x30000000000000L), key1, "a"); + ComposableSampler delegate2 = + new MarkingSampler(new ConsistentFixedThresholdSampler(0x50000000000000L), key1, "b"); + ComposableSampler sampler = ConsistentSampler.anyOf(delegate1, delegate2); + SamplingIntent intent = sampler.getSamplingIntent(null, "span_name", null, null, null); + assertThat(intent.getAttributes().get(key1)).isEqualTo("b"); + } +} diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSamplerTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSamplerTest.java index 17daaaf6b..79bf064a0 100644 --- a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSamplerTest.java +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRateLimitingSamplerTest.java @@ -8,6 +8,7 @@ import static io.opentelemetry.contrib.sampler.consistent56.TestUtil.generateRandomTraceId; 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.SpanKind; import io.opentelemetry.context.Context; @@ -28,6 +29,7 @@ class ConsistentRateLimitingSamplerTest { private long[] nanoTime; private LongSupplier nanoTimeSupplier; + private LongSupplier lowResolutionTimeSupplier; private Context parentContext; private String name; private SpanKind spanKind; @@ -39,6 +41,7 @@ class ConsistentRateLimitingSamplerTest { void init() { nanoTime = new long[] {0L}; nanoTimeSupplier = () -> nanoTime[0]; + lowResolutionTimeSupplier = () -> (nanoTime[0] / 1000000) * 1000000; // 1ms resolution parentContext = Context.root(); name = "name"; spanKind = SpanKind.SERVER; @@ -61,9 +64,52 @@ void testConstantRate() { double targetSpansPerSecondLimit = 1000; double adaptationTimeSeconds = 5; + ComposableSampler delegate = + new CoinFlipSampler(ConsistentSampler.alwaysOff(), ConsistentSampler.probabilityBased(0.8)); ConsistentSampler sampler = ConsistentSampler.rateLimited( - targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier); + delegate, targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier); + + long nanosBetweenSpans = TimeUnit.MICROSECONDS.toNanos(100); + int numSpans = 1000000; + + List spanSampledNanos = new ArrayList<>(); + + for (int i = 0; i < numSpans; ++i) { + advanceTime(nanosBetweenSpans); + SamplingResult samplingResult = + sampler.shouldSample( + parentContext, + generateRandomTraceId(random), + name, + spanKind, + attributes, + parentLinks); + if (SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult.getDecision())) { + spanSampledNanos.add(getCurrentTimeNanos()); + } + } + + long numSampledSpansInLast5Seconds = + spanSampledNanos.stream() + .filter(x -> x > TimeUnit.SECONDS.toNanos(95) && x <= TimeUnit.SECONDS.toNanos(100)) + .count(); + + assertThat(numSampledSpansInLast5Seconds / 5.) + .isCloseTo(targetSpansPerSecondLimit, Percentage.withPercentage(5)); + } + + @Test + void testConstantRateLowResolution() { + + double targetSpansPerSecondLimit = 1000; + double adaptationTimeSeconds = 5; + + ComposableSampler delegate = + new CoinFlipSampler(ConsistentSampler.alwaysOff(), ConsistentSampler.probabilityBased(0.8)); + ConsistentSampler sampler = + ConsistentSampler.rateLimited( + delegate, targetSpansPerSecondLimit, adaptationTimeSeconds, lowResolutionTimeSupplier); long nanosBetweenSpans = TimeUnit.MICROSECONDS.toNanos(100); int numSpans = 1000000; @@ -228,6 +274,89 @@ void testRateDecrease() { .isCloseTo(targetSpansPerSecondLimit, Percentage.withPercentage(5)); } + /** + * Generate a random number representing time elapsed between two simulated (root) spans. + * + * @param averageSpanRatePerSecond number of simulated spans for each simulated second + * @return the time in nanos to be used by the simulator + */ + private long randomInterval(long averageSpanRatePerSecond) { + // For simulating real traffic, for example as coming from the Internet. + // Assuming Poisson distribution of incoming requests, averageNumberOfSpanPerSecond + // is the lambda parameter of the distribution. Consequently, the time between requests + // has Exponential distribution with the same lambda parameter. + double uniform = random.nextDouble(); + double intervalInSeconds = -Math.log(uniform) / averageSpanRatePerSecond; + return (long) (intervalInSeconds * 1e9); + } + + @Test + void testProportionalBehavior() { + // Based on example discussed at https://github.com/open-telemetry/oteps/pull/250 + // Assume that there are 2 categories A and B of spans. + // Assume there are 10,000 spans/s and 50% belong to A and 50% belong to B. + // Now we want to sample A with a probability of 60% and B with a probability of 40%. + // That means we would sample 30,000 spans/s from A and 20,000 spans/s from B. + // + // However, if we do not want to sample more than 1000 spans/s overall, our expectation is + // that the ratio of the sampled A and B spans will still remain 3:2. + + double targetSpansPerSecondLimit = 1000; + double adaptationTimeSeconds = 5; + AttributeKey key = AttributeKey.stringKey("category"); + + ComposableSampler delegate = + new CoinFlipSampler( + new MarkingSampler(ConsistentSampler.probabilityBased(0.6), key, "A"), + new MarkingSampler(ConsistentSampler.probabilityBased(0.4), key, "B")); + + ConsistentSampler sampler = + ConsistentSampler.rateLimited( + delegate, targetSpansPerSecondLimit, adaptationTimeSeconds, nanoTimeSupplier); + + long averageRequestRatePerSecond = 10000; + int numSpans = 1000000; + + List spanSampledNanos = new ArrayList<>(); + int catAsampledCount = 0; + int catBsampledCount = 0; + + for (int i = 0; i < numSpans; ++i) { + advanceTime(randomInterval(averageRequestRatePerSecond)); + SamplingResult samplingResult = + sampler.shouldSample( + parentContext, + generateRandomTraceId(random), + name, + spanKind, + attributes, + parentLinks); + if (SamplingDecision.RECORD_AND_SAMPLE.equals(samplingResult.getDecision())) { + spanSampledNanos.add(getCurrentTimeNanos()); + + // ConsistentRateLimiting sampler is expected to provide proportional sampling + // at all times, no need to skip the warm-up phase + String category = samplingResult.getAttributes().get(key); + if ("A".equals(category)) { + catAsampledCount++; + } else if ("B".equals(category)) { + catBsampledCount++; + } + } + } + + double expectedRatio = 0.6 / 0.4; + assertThat(catAsampledCount / (double) catBsampledCount) + .isCloseTo(expectedRatio, Percentage.withPercentage(2)); + + long timeNow = nanoTime[0]; + long numSampledSpansInLast5Seconds = + spanSampledNanos.stream().filter(x -> x > timeNow - 5000000000L && x <= timeNow).count(); + + assertThat(numSampledSpansInLast5Seconds / 5.) + .isCloseTo(targetSpansPerSecondLimit, Percentage.withPercentage(5)); + } + @Test void testDescription() { diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSamplerTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSamplerTest.java new file mode 100644 index 000000000..bf79de0c5 --- /dev/null +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSamplerTest.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import org.junit.jupiter.api.Test; + +class ConsistentRuleBasedSamplerTest { + + @Test + void testEmptySet() { + ComposableSampler sampler = ConsistentSampler.ruleBased(SpanKind.SERVER); + SamplingIntent intent = + sampler.getSamplingIntent(null, "span_name", SpanKind.SERVER, null, null); + assertThat(intent.getThreshold()).isEqualTo(getInvalidThreshold()); + } + + private static Predicate matchSpanName(String nameToMatch) { + return (parentContext, name, spanKind, attributes, parentLinks) -> { + return nameToMatch.equals(name); + }; + } + + @Test + void testChoice() { + // Testing the correct choice by checking both the returned threshold and the marking attribute + + AttributeKey key1 = AttributeKey.stringKey("tag1"); + AttributeKey key2 = AttributeKey.stringKey("tag2"); + AttributeKey key3 = AttributeKey.stringKey("tag3"); + + ComposableSampler delegate1 = + new MarkingSampler(new ConsistentFixedThresholdSampler(0x80000000000000L), key1, "a"); + ComposableSampler delegate2 = + new MarkingSampler(new ConsistentFixedThresholdSampler(0x50000000000000L), key2, "b"); + ComposableSampler delegate3 = + new MarkingSampler(new ConsistentFixedThresholdSampler(0x30000000000000L), key3, "c"); + + ComposableSampler sampler = + ConsistentSampler.ruleBased( + null, + PredicatedSampler.onMatch(matchSpanName("A"), delegate1), + PredicatedSampler.onMatch(matchSpanName("B"), delegate2), + PredicatedSampler.onMatch(matchSpanName("C"), delegate3)); + + SamplingIntent intent; + + intent = sampler.getSamplingIntent(null, "A", SpanKind.CLIENT, null, null); + assertThat(intent.getThreshold()).isEqualTo(0x80000000000000L); + assertThat(intent.getAttributes().get(key1)).isEqualTo("a"); + assertThat(intent.getAttributes().get(key2)).isEqualTo(null); + assertThat(intent.getAttributes().get(key3)).isEqualTo(null); + + intent = sampler.getSamplingIntent(null, "B", SpanKind.PRODUCER, null, null); + assertThat(intent.getThreshold()).isEqualTo(0x50000000000000L); + assertThat(intent.getAttributes().get(key1)).isEqualTo(null); + assertThat(intent.getAttributes().get(key2)).isEqualTo("b"); + assertThat(intent.getAttributes().get(key3)).isEqualTo(null); + + intent = sampler.getSamplingIntent(null, "C", SpanKind.SERVER, null, null); + assertThat(intent.getThreshold()).isEqualTo(0x30000000000000L); + assertThat(intent.getAttributes().get(key1)).isEqualTo(null); + assertThat(intent.getAttributes().get(key2)).isEqualTo(null); + assertThat(intent.getAttributes().get(key3)).isEqualTo("c"); + + intent = sampler.getSamplingIntent(null, "D", null, null, null); + assertThat(intent.getThreshold()).isEqualTo(getInvalidThreshold()); + assertThat(intent.getAttributes().get(key1)).isEqualTo(null); + assertThat(intent.getAttributes().get(key2)).isEqualTo(null); + assertThat(intent.getAttributes().get(key3)).isEqualTo(null); + } + + @Test + void testSpanKindMatch() { + ComposableSampler sampler = + ConsistentSampler.ruleBased( + SpanKind.CLIENT, + PredicatedSampler.onMatch(Predicate.anySpan(), ConsistentSampler.alwaysOn())); + + SamplingIntent intent; + + intent = sampler.getSamplingIntent(null, "span name", SpanKind.CONSUMER, null, null); + assertThat(intent.getThreshold()).isEqualTo(getInvalidThreshold()); + + intent = sampler.getSamplingIntent(null, "span name", SpanKind.CLIENT, null, null); + assertThat(intent.getThreshold()).isEqualTo(0); + } +} diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/MarkingSampler.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/MarkingSampler.java new file mode 100644 index 000000000..687cd532a --- /dev/null +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/MarkingSampler.java @@ -0,0 +1,91 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; +import javax.annotation.concurrent.Immutable; + +/** + * A Composable that creates the same sampling intent as the delegate, but it additionally sets a + * Span attribute according to the provided attribute key and value. This is used by unit tests, but + * could be also offered as a general utility. + */ +@Immutable +final class MarkingSampler implements ComposableSampler { + + private final ComposableSampler delegate; + private final AttributeKey attributeKey; + private final String attributeValue; + + private final String description; + + /** + * Constructs a new MarkingSampler + * + * @param delegate the delegate sampler + * @param attributeKey Span attribute key + * @param attributeValue Span attribute value + */ + MarkingSampler( + ComposableSampler delegate, AttributeKey attributeKey, String attributeValue) { + this.delegate = requireNonNull(delegate); + this.attributeKey = requireNonNull(attributeKey); + this.attributeValue = requireNonNull(attributeValue); + this.description = + "MarkingSampler{delegate=" + + delegate.getDescription() + + ",key=" + + attributeKey + + ",value=" + + attributeValue + + '}'; + } + + @Override + public SamplingIntent getSamplingIntent( + Context parentContext, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + + SamplingIntent delegateIntent = + delegate.getSamplingIntent(parentContext, name, spanKind, attributes, parentLinks); + + return new SamplingIntent() { + @Override + public long getThreshold() { + return delegateIntent.getThreshold(); + } + + @Override + public Attributes getAttributes() { + AttributesBuilder builder = delegateIntent.getAttributes().toBuilder(); + builder = builder.put(attributeKey, attributeValue); + return builder.build(); + } + + @Override + public TraceState updateTraceState(TraceState previousState) { + return delegateIntent.updateTraceState(previousState); + } + }; + } + + @Override + public String getDescription() { + return description; + } +} diff --git a/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/UseCaseTest.java b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/UseCaseTest.java new file mode 100644 index 000000000..e164a66fb --- /dev/null +++ b/consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/UseCaseTest.java @@ -0,0 +1,128 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.consistent56; + +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSampler.alwaysOff; +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSampler.alwaysOn; +import static io.opentelemetry.contrib.sampler.consistent56.ConsistentSamplingUtil.getInvalidThreshold; +import static io.opentelemetry.contrib.sampler.consistent56.Predicate.anySpan; +import static io.opentelemetry.contrib.sampler.consistent56.Predicate.isRootSpan; +import static io.opentelemetry.contrib.sampler.consistent56.PredicatedSampler.onMatch; +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.SpanKind; +import org.junit.jupiter.api.Test; + +/** + * Testing a "real life" sampler configuration, as provided as an example in + * https://github.com/open-telemetry/oteps/pull/250. The example uses many different composite + * samplers combining them together to demonstrate the expressiveness and flexibility of the + * proposed specification. + */ +class UseCaseTest { + private static final long[] nanoTime = new long[] {0L}; + + private static final long nanoTime() { + return nanoTime[0]; + } + + private static void advanceTime(long nanosIncrement) { + nanoTime[0] += nanosIncrement; + } + + // + // S = ConsistentRateLimiting( + // ConsistentAnyOf( + // ConsistentParentBased( + // ConsistentRuleBased(ROOT, { + // (http.target == /healthcheck) => ConsistentAlwaysOff, + // (http.target == /checkout) => ConsistentAlwaysOn, + // true => ConsistentFixedThreshold(0.25) + // }), + // ConsistentRuleBased(CLIENT, { + // (http.url == /foo) => ConsistentAlwaysOn + // } + // ), + // 1000.0 + // ) + // + private static final AttributeKey httpTarget = AttributeKey.stringKey("http.target"); + private static final AttributeKey httpUrl = AttributeKey.stringKey("http.url"); + + private static ConsistentSampler buildSampler() { + Predicate healthCheck = + Predicate.and( + isRootSpan(), + (parentContext, name, spanKind, attributes, parentLinks) -> { + return "/healthCheck".equals(attributes.get(httpTarget)); + }); + Predicate checkout = + Predicate.and( + isRootSpan(), + (parentContext, name, spanKind, attributes, parentLinks) -> { + return "/checkout".equals(attributes.get(httpTarget)); + }); + ComposableSampler s1 = + ConsistentSampler.parentBased( + ConsistentSampler.ruleBased( + null, + onMatch(healthCheck, alwaysOff()), + onMatch(checkout, alwaysOn()), + onMatch(anySpan(), ConsistentSampler.probabilityBased(0.25)))); + Predicate foo = + (parentContext, name, spanKind, attributes, parentLinks) -> { + return "/foo".equals(attributes.get(httpUrl)); + }; + + ComposableSampler s2 = ConsistentSampler.ruleBased(SpanKind.CLIENT, onMatch(foo, alwaysOn())); + ComposableSampler s3 = ConsistentSampler.anyOf(s1, s2); + return ConsistentSampler.rateLimited(s3, 1000.0, 5, UseCaseTest::nanoTime); + } + + @Test + void testDropHealthcheck() { + ConsistentSampler s = buildSampler(); + Attributes attributes = createAttributes(httpTarget, "/healthCheck"); + SamplingIntent intent = s.getSamplingIntent(null, "A", SpanKind.SERVER, attributes, null); + assertThat(intent.getThreshold()).isEqualTo(getInvalidThreshold()); + } + + @Test + void testSampleCheckout() { + ConsistentSampler s = buildSampler(); + advanceTime(1000000); + Attributes attributes = createAttributes(httpTarget, "/checkout"); + SamplingIntent intent = s.getSamplingIntent(null, "B", SpanKind.SERVER, attributes, null); + assertThat(intent.getThreshold()).isEqualTo(0L); + advanceTime(1000); // rate limiting should kick in + intent = s.getSamplingIntent(null, "B", SpanKind.SERVER, attributes, null); + assertThat(intent.getThreshold()).isGreaterThan(0L); + } + + @Test + void testSampleClient() { + ConsistentSampler s = buildSampler(); + advanceTime(1000000); + Attributes attributes = createAttributes(httpUrl, "/foo"); + SamplingIntent intent = s.getSamplingIntent(null, "C", SpanKind.CLIENT, attributes, null); + assertThat(intent.getThreshold()).isEqualTo(0L); + } + + @Test + void testOtherRoot() { + ConsistentSampler s = buildSampler(); + advanceTime(1000000); + Attributes attributes = Attributes.empty(); + SamplingIntent intent = s.getSamplingIntent(null, "D", SpanKind.SERVER, attributes, null); + assertThat(intent.getThreshold()).isEqualTo(0xc0000000000000L); + } + + private static Attributes createAttributes(AttributeKey key, String value) { + return Attributes.builder().put(key, value).build(); + } +} diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts index 39ad8462b..c8e9fc865 100644 --- a/dependencyManagement/build.gradle.kts +++ b/dependencyManagement/build.gradle.kts @@ -7,12 +7,12 @@ data class DependencySet(val group: String, val version: String, val modules: Li val dependencyVersions = hashMapOf() rootProject.extra["versions"] = dependencyVersions -val otelInstrumentationVersion = "2.7.0-alpha" +val otelInstrumentationVersion = "2.8.0-alpha" val DEPENDENCY_BOMS = listOf( "com.fasterxml.jackson:jackson-bom:2.17.2", "com.google.guava:guava-bom:33.3.0-jre", - "com.linecorp.armeria:armeria-bom:1.30.0", + "com.linecorp.armeria:armeria-bom:1.30.1", "org.junit:junit-bom:5.11.0", "io.grpc:grpc-bom:1.66.0", "io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha:${otelInstrumentationVersion}", @@ -21,7 +21,7 @@ val DEPENDENCY_BOMS = listOf( val autoServiceVersion = "1.1.1" val autoValueVersion = "1.11.0" -val errorProneVersion = "2.31.0" +val errorProneVersion = "2.32.0" val prometheusVersion = "0.16.0" val mockitoVersion = "4.11.0" val slf4jVersion = "2.0.16" @@ -55,7 +55,7 @@ val DEPENDENCIES = listOf( "com.google.code.findbugs:annotations:3.0.1u2", "com.google.code.findbugs:jsr305:3.0.2", "com.squareup.okhttp3:okhttp:4.12.0", - "com.uber.nullaway:nullaway:0.11.2", + "com.uber.nullaway:nullaway:0.11.3", "org.assertj:assertj-core:3.26.3", "org.awaitility:awaitility:4.2.2", "org.bouncycastle:bcpkix-jdk15on:1.70", diff --git a/disk-buffering/build.gradle.kts b/disk-buffering/build.gradle.kts index 1b9a36e8c..041d2e913 100644 --- a/disk-buffering/build.gradle.kts +++ b/disk-buffering/build.gradle.kts @@ -7,7 +7,7 @@ plugins { id("com.github.johnrengelman.shadow") id("me.champeau.jmh") version "0.7.2" id("ru.vyarus.animalsniffer") version "1.7.1" - id("com.squareup.wire") version "5.0.0" + id("com.squareup.wire") version "5.1.0" } description = "Exporter implementations that store signals on disk" diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/mapping/logs/LogRecordDataMapper.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/mapping/logs/LogRecordDataMapper.java index deeba2cfb..06ff85847 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/mapping/logs/LogRecordDataMapper.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/mapping/logs/LogRecordDataMapper.java @@ -7,8 +7,10 @@ import static io.opentelemetry.contrib.disk.buffering.internal.serialization.mapping.spans.SpanDataMapper.flagsFromInt; import static io.opentelemetry.contrib.disk.buffering.internal.utils.ProtobufTools.toUnsignedInt; +import static java.util.stream.Collectors.toList; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.TraceState; @@ -19,9 +21,9 @@ import io.opentelemetry.proto.logs.v1.LogRecord; import io.opentelemetry.proto.logs.v1.SeverityNumber; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.logs.data.Body; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.resources.Resource; +import java.util.stream.Collectors; public final class LogRecordDataMapper { @@ -42,8 +44,8 @@ public LogRecord mapToProto(LogRecordData source) { if (source.getSeverityText() != null) { logRecord.severity_text(source.getSeverityText()); } - if (source.getBody() != null) { - logRecord.body(bodyToAnyValue(source.getBody())); + if (source.getBodyValue() != null) { + logRecord.body(bodyToAnyValue(source.getBodyValue())); } byte flags = source.getSpanContext().getTraceFlags().asByte(); @@ -73,7 +75,7 @@ public LogRecordData mapToSdk( logRecordData.setSeverity(severityNumberToSdk(source.severity_number)); logRecordData.setSeverityText(source.severity_text); if (source.body != null) { - logRecordData.setBody(anyValueToBody(source.body)); + logRecordData.setBodyValue(anyValueToBody(source.body)); } addExtrasToSdkItemBuilder(source, logRecordData, resource, scopeInfo); @@ -99,7 +101,7 @@ private static void addExtrasToSdkItemBuilder( target.setInstrumentationScopeInfo(scopeInfo); } - private static AnyValue bodyToAnyValue(Body body) { + private static AnyValue bodyToAnyValue(Value body) { return new AnyValue.Builder().string_value(body.asString()).build(); } @@ -107,12 +109,30 @@ private static SeverityNumber severityToProto(Severity severity) { return SeverityNumber.fromValue(severity.getSeverityNumber()); } - private static Body anyValueToBody(AnyValue source) { + private static Value anyValueToBody(AnyValue source) { if (source.string_value != null) { - return Body.string(source.string_value); - } else { - return Body.empty(); + return Value.of(source.string_value); + } else if (source.int_value != null) { + return Value.of(source.int_value); + } else if (source.double_value != null) { + return Value.of(source.double_value); + } else if (source.bool_value != null) { + return Value.of(source.bool_value); + } else if (source.bytes_value != null) { + return Value.of(source.bytes_value.toByteArray()); + } else if (source.kvlist_value != null) { + return Value.of( + source.kvlist_value.values.stream() + .collect( + Collectors.toMap( + keyValue -> keyValue.key, keyValue -> anyValueToBody(keyValue.value)))); + } else if (source.array_value != null) { + return Value.of( + source.array_value.values.stream() + .map(LogRecordDataMapper::anyValueToBody) + .collect(toList())); } + throw new IllegalArgumentException("Unrecognized AnyValue type"); } private static Severity severityNumberToSdk(SeverityNumber source) { diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/mapping/logs/models/LogRecordDataImpl.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/mapping/logs/models/LogRecordDataImpl.java index de130e3d1..9ff0f9410 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/mapping/logs/models/LogRecordDataImpl.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/mapping/logs/models/LogRecordDataImpl.java @@ -6,13 +6,15 @@ package io.opentelemetry.contrib.disk.buffering.internal.serialization.mapping.logs.models; import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.logs.data.Body; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.resources.Resource; +import javax.annotation.Nullable; @AutoValue public abstract class LogRecordDataImpl implements LogRecordData { @@ -21,6 +23,18 @@ public static Builder builder() { return new AutoValue_LogRecordDataImpl.Builder(); } + @Deprecated + public io.opentelemetry.sdk.logs.data.Body getBody() { + Value valueBody = getBodyValue(); + return valueBody == null + ? io.opentelemetry.sdk.logs.data.Body.empty() + : io.opentelemetry.sdk.logs.data.Body.string(valueBody.asString()); + } + + @Override + @Nullable + public abstract Value getBodyValue(); + @AutoValue.Builder public abstract static class Builder { public abstract Builder setResource(Resource value); @@ -37,7 +51,18 @@ public abstract static class Builder { public abstract Builder setSeverityText(String value); - public abstract Builder setBody(Body value); + @Deprecated + @CanIgnoreReturnValue + public Builder setBody(io.opentelemetry.sdk.logs.data.Body body) { + if (body.getType() == io.opentelemetry.sdk.logs.data.Body.Type.STRING) { + setBodyValue(Value.of(body.asString())); + } else if (body.getType() == io.opentelemetry.sdk.logs.data.Body.Type.EMPTY) { + setBodyValue(null); + } + return this; + } + + public abstract Builder setBodyValue(@Nullable Value value); public abstract Builder setAttributes(Attributes value); diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/mapping/logs/LogRecordDataMapperTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/mapping/logs/LogRecordDataMapperTest.java index 4e98a8c69..3eb588b45 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/mapping/logs/LogRecordDataMapperTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/mapping/logs/LogRecordDataMapperTest.java @@ -7,12 +7,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.contrib.disk.buffering.internal.serialization.mapping.logs.models.LogRecordDataImpl; import io.opentelemetry.contrib.disk.buffering.testutils.TestData; import io.opentelemetry.proto.logs.v1.LogRecord; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.logs.data.Body; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.resources.Resource; import org.junit.jupiter.api.Test; @@ -25,7 +25,7 @@ class LogRecordDataMapperTest { .setSpanContext(TestData.SPAN_CONTEXT) .setInstrumentationScopeInfo(TestData.INSTRUMENTATION_SCOPE_INFO_FULL) .setAttributes(TestData.ATTRIBUTES) - .setBody(Body.string("Log body")) + .setBodyValue(Value.of("Log body")) .setSeverity(Severity.DEBUG) .setSeverityText("Log severity text") .setTimestampEpochNanos(100L) diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/mapping/logs/ProtoLogsDataMapperTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/mapping/logs/ProtoLogsDataMapperTest.java index 48a563300..45c3f6e5e 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/mapping/logs/ProtoLogsDataMapperTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/mapping/logs/ProtoLogsDataMapperTest.java @@ -8,6 +8,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.contrib.disk.buffering.internal.serialization.mapping.logs.models.LogRecordDataImpl; import io.opentelemetry.contrib.disk.buffering.testutils.TestData; @@ -15,7 +16,6 @@ import io.opentelemetry.proto.logs.v1.LogsData; import io.opentelemetry.proto.logs.v1.ResourceLogs; import io.opentelemetry.proto.logs.v1.ScopeLogs; -import io.opentelemetry.sdk.logs.data.Body; import io.opentelemetry.sdk.logs.data.LogRecordData; import java.util.Arrays; import java.util.Collection; @@ -31,7 +31,7 @@ class ProtoLogsDataMapperTest { .setSpanContext(TestData.SPAN_CONTEXT) .setInstrumentationScopeInfo(TestData.INSTRUMENTATION_SCOPE_INFO_FULL) .setAttributes(TestData.ATTRIBUTES) - .setBody(Body.string("Log body")) + .setBodyValue(Value.of("Log body")) .setSeverity(Severity.DEBUG) .setSeverityText("Log severity text") .setTimestampEpochNanos(100L) @@ -45,7 +45,7 @@ class ProtoLogsDataMapperTest { .setSpanContext(TestData.SPAN_CONTEXT) .setInstrumentationScopeInfo(TestData.INSTRUMENTATION_SCOPE_INFO_FULL) .setAttributes(TestData.ATTRIBUTES) - .setBody(Body.string("Other log body")) + .setBodyValue(Value.of("Other log body")) .setSeverity(Severity.DEBUG) .setSeverityText("Log severity text") .setTimestampEpochNanos(100L) @@ -59,7 +59,7 @@ class ProtoLogsDataMapperTest { .setSpanContext(TestData.SPAN_CONTEXT) .setInstrumentationScopeInfo(TestData.INSTRUMENTATION_SCOPE_INFO_WITHOUT_VERSION) .setAttributes(TestData.ATTRIBUTES) - .setBody(Body.string("Same resource other scope log")) + .setBodyValue(Value.of("Same resource other scope log")) .setSeverity(Severity.DEBUG) .setSeverityText("Log severity text") .setTimestampEpochNanos(100L) @@ -73,7 +73,7 @@ class ProtoLogsDataMapperTest { .setSpanContext(TestData.SPAN_CONTEXT) .setInstrumentationScopeInfo(TestData.INSTRUMENTATION_SCOPE_INFO_WITHOUT_VERSION) .setAttributes(TestData.ATTRIBUTES) - .setBody(Body.string("Different resource log")) + .setBodyValue(Value.of("Different resource log")) .setSeverity(Severity.DEBUG) .setSeverityText("Log severity text") .setTimestampEpochNanos(100L) diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/serializers/LogRecordDataSerializerTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/serializers/LogRecordDataSerializerTest.java index c5d8cea77..1b52bb219 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/serializers/LogRecordDataSerializerTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/serializers/LogRecordDataSerializerTest.java @@ -6,12 +6,12 @@ package io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; import io.opentelemetry.contrib.disk.buffering.internal.serialization.mapping.logs.models.LogRecordDataImpl; import io.opentelemetry.contrib.disk.buffering.testutils.BaseSignalSerializerTest; import io.opentelemetry.contrib.disk.buffering.testutils.TestData; -import io.opentelemetry.sdk.logs.data.Body; import io.opentelemetry.sdk.logs.data.LogRecordData; import org.junit.jupiter.api.Test; @@ -22,7 +22,7 @@ class LogRecordDataSerializerTest extends BaseSignalSerializerTest labelFuncs private final Closure instrument private final GroovyMetricEnvironment metricEnvironment + private final boolean aggregateAcrossMBeans /** * An InstrumentHelper provides the ability to easily create and update {@link io.opentelemetry.api.metrics.Instrument} @@ -63,8 +64,9 @@ class InstrumentHelper { * (e.g. new OtelHelper().&doubleValueRecorder) * @param metricenvironment - The {@link GroovyMetricEnvironment} used to register callbacks onto the SDK meter for * batch callbacks used to handle {@link CompositeData} + * @param aggregateAcrossMBeans - Whether to aggregate multiple MBeans together before recording. */ - InstrumentHelper(MBeanHelper mBeanHelper, String instrumentName, String description, String unit, Map> labelFuncs, Map>> MBeanAttributes, Closure instrument, GroovyMetricEnvironment metricEnvironment) { + InstrumentHelper(MBeanHelper mBeanHelper, String instrumentName, String description, String unit, Map> labelFuncs, Map>> MBeanAttributes, Closure instrument, GroovyMetricEnvironment metricEnvironment, boolean aggregateAcrossMBeans) { this.mBeanHelper = mBeanHelper this.instrumentName = instrumentName this.description = description @@ -73,6 +75,7 @@ class InstrumentHelper { this.mBeanAttributes = MBeanAttributes this.instrument = instrument this.metricEnvironment = metricEnvironment + this.aggregateAcrossMBeans = aggregateAcrossMBeans } void update() { @@ -181,19 +184,39 @@ class InstrumentHelper { return labels } + private static String getAggregationKey(String instrumentName, Map labels) { + def labelsKey = labels.sort().collect { key, value -> "${key}:${value}" }.join(";") + return "${instrumentName}/${labelsKey}" + } + // Create a closure for simple attributes that will retrieve mbean information on // callback to ensure that metrics are collected on request private Closure prepareUpdateClosure(List mbeans, attributes) { return { result -> + def aggregations = [:] as Map + boolean requireAggregation = aggregateAcrossMBeans && mbeans.size() > 1 && instrumentIsValue(instrument) [mbeans, attributes].combinations().each { pair -> def (mbean, attribute) = pair def value = MBeanHelper.getBeanAttribute(mbean, attribute) if (value != null) { def labels = getLabels(mbean, labelFuncs, mBeanAttributes[attribute]) - logger.fine("Recording ${instrumentName} - ${instrument.method} w/ ${value} - ${labels}") - recordDataPoint(instrument, result, value, GroovyMetricEnvironment.mapToAttributes(labels)) + if (requireAggregation) { + def key = getAggregationKey(instrumentName, labels) + if (aggregations[key] == null) { + aggregations[key] = new Aggregation(labels) + } + logger.fine("Aggregating ${mbean.name()} ${instrumentName} - ${instrument.method} w/ ${value} - ${labels}") + aggregations[key].add(value) + } else { + logger.fine("Recording ${mbean.name()} ${instrumentName} - ${instrument.method} w/ ${value} - ${labels}") + recordDataPoint(instrument, result, value, GroovyMetricEnvironment.mapToAttributes(labels)) + } } } + aggregations.each { entry -> + logger.fine("Recording ${instrumentName} - ${instrument.method} - w/ ${entry.value.value} - ${entry.value.labels}") + recordDataPoint(instrument, result, entry.value.value, GroovyMetricEnvironment.mapToAttributes(entry.value.labels)) + } } } @@ -252,6 +275,14 @@ class InstrumentHelper { ].contains(inst.method) } + @PackageScope + static boolean instrumentIsValue(inst) { + return [ + "doubleValueCallback", + "longValueCallback" + ].contains(inst.method) + } + @PackageScope static boolean instrumentIsCounter(inst) { return [ @@ -261,4 +292,18 @@ class InstrumentHelper { "longUpDownCounter" ].contains(inst.method) } + + static class Aggregation { + private final Map labels + private def value + + Aggregation(Map labels) { + this.labels = labels + this.value = 0.0 + } + + void add(value) { + this.value += value + } + } } diff --git a/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/JmxConfig.java b/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/JmxConfig.java index 9b9ce0a0a..1f1a6abec 100644 --- a/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/JmxConfig.java +++ b/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/JmxConfig.java @@ -31,6 +31,7 @@ class JmxConfig { static final String JMX_PASSWORD = PREFIX + "jmx.password"; static final String JMX_REMOTE_PROFILE = PREFIX + "jmx.remote.profile"; static final String JMX_REALM = PREFIX + "jmx.realm"; + static final String JMX_AGGREGATE_ACROSS_MBEANS = PREFIX + "jmx.aggregate.across.mbeans"; // These properties need to be copied into System Properties if provided via the property // file so that they are available to the JMX Connection builder @@ -77,6 +78,8 @@ class JmxConfig { final boolean registrySsl; final Properties properties; + final boolean aggregateAcrossMBeans; + JmxConfig(final Properties props) { properties = new Properties(); // putAll() instead of using constructor defaults @@ -112,6 +115,8 @@ class JmxConfig { realm = properties.getProperty(JMX_REALM); registrySsl = Boolean.valueOf(properties.getProperty(REGISTRY_SSL)); + aggregateAcrossMBeans = + Boolean.parseBoolean(properties.getProperty(JMX_AGGREGATE_ACROSS_MBEANS)); // For the list of System Properties, if they have been set in the properties file // they need to be set in Java System Properties. diff --git a/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/OtelHelper.groovy b/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/OtelHelper.groovy index 49f071d6a..e26fdb94e 100644 --- a/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/OtelHelper.groovy +++ b/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/OtelHelper.groovy @@ -22,10 +22,12 @@ class OtelHelper { private final JmxClient jmxClient private final GroovyMetricEnvironment groovyMetricEnvironment + private final boolean aggregateAcrossMBeans - OtelHelper(JmxClient jmxClient, GroovyMetricEnvironment groovyMetricEnvironment) { + OtelHelper(JmxClient jmxClient, GroovyMetricEnvironment groovyMetricEnvironment, boolean aggregateAcrossMBeans) { this.jmxClient = jmxClient this.groovyMetricEnvironment = groovyMetricEnvironment + this.aggregateAcrossMBeans = aggregateAcrossMBeans } /** @@ -99,7 +101,7 @@ class OtelHelper { * attribute value(s). The parameters map to the InstrumentHelper constructor. */ InstrumentHelper instrument(MBeanHelper mBeanHelper, String instrumentName, String description, String unit, Map labelFuncs, Map> attributes, Closure otelInstrument) { - def instrumentHelper = new InstrumentHelper(mBeanHelper, instrumentName, description, unit, labelFuncs, attributes, otelInstrument, groovyMetricEnvironment) + def instrumentHelper = new InstrumentHelper(mBeanHelper, instrumentName, description, unit, labelFuncs, attributes, otelInstrument, groovyMetricEnvironment, aggregateAcrossMBeans) instrumentHelper.update() return instrumentHelper } diff --git a/jmx-metrics/src/main/resources/target-systems/tomcat.groovy b/jmx-metrics/src/main/resources/target-systems/tomcat.groovy index ab3f54113..8c692befc 100644 --- a/jmx-metrics/src/main/resources/target-systems/tomcat.groovy +++ b/jmx-metrics/src/main/resources/target-systems/tomcat.groovy @@ -15,10 +15,10 @@ */ -def beantomcatmanager = otel.mbean("Catalina:type=Manager,host=localhost,context=*") -otel.instrument(beantomcatmanager, "tomcat.sessions", "The number of active sessions.", "sessions", "activeSessions", otel.&doubleValueCallback) +def beantomcatmanager = otel.mbeans("Catalina:type=Manager,host=localhost,context=*") +otel.instrument(beantomcatmanager, "tomcat.sessions", "The number of active sessions.", "sessions", "activeSessions", otel.&longValueCallback) -def beantomcatrequestProcessor = otel.mbean("Catalina:type=GlobalRequestProcessor,name=*") +def beantomcatrequestProcessor = otel.mbeans("Catalina:type=GlobalRequestProcessor,name=*") otel.instrument(beantomcatrequestProcessor, "tomcat.errors", "The number of errors encountered.", "errors", ["proto_handler" : { mbean -> mbean.name().getKeyProperty("name") }], "errorCount", otel.&longCounterCallback) @@ -37,15 +37,15 @@ otel.instrument(beantomcatrequestProcessor, "tomcat.traffic", ["bytesReceived":["direction" : {"received"}], "bytesSent": ["direction" : {"sent"}]], otel.&longCounterCallback) -def beantomcatconnectors = otel.mbean("Catalina:type=ThreadPool,name=*") +def beantomcatconnectors = otel.mbeans("Catalina:type=ThreadPool,name=*") otel.instrument(beantomcatconnectors, "tomcat.threads", "The number of threads", "threads", ["proto_handler" : { mbean -> mbean.name().getKeyProperty("name") }], ["currentThreadCount":["state":{"idle"}],"currentThreadsBusy":["state":{"busy"}]], otel.&longValueCallback) -def beantomcatnewmanager = otel.mbean("Tomcat:type=Manager,host=localhost,context=*") -otel.instrument(beantomcatnewmanager, "tomcat.sessions", "The number of active sessions.", "sessions", "activeSessions", otel.&doubleValueCallback) +def beantomcatnewmanager = otel.mbeans("Tomcat:type=Manager,host=localhost,context=*") +otel.instrument(beantomcatnewmanager, "tomcat.sessions", "The number of active sessions.", "sessions", "activeSessions", otel.&longValueCallback) -def beantomcatnewrequestProcessor = otel.mbean("Tomcat:type=GlobalRequestProcessor,name=*") +def beantomcatnewrequestProcessor = otel.mbeans("Tomcat:type=GlobalRequestProcessor,name=*") otel.instrument(beantomcatnewrequestProcessor, "tomcat.errors", "The number of errors encountered.", "errors", ["proto_handler" : { mbean -> mbean.name().getKeyProperty("name") }], "errorCount", otel.&longCounterCallback) @@ -64,7 +64,7 @@ otel.instrument(beantomcatnewrequestProcessor, "tomcat.traffic", ["bytesReceived":["direction" : {"received"}], "bytesSent": ["direction" : {"sent"}]], otel.&longCounterCallback) -def beantomcatnewconnectors = otel.mbean("Tomcat:type=ThreadPool,name=*") +def beantomcatnewconnectors = otel.mbeans("Tomcat:type=ThreadPool,name=*") otel.instrument(beantomcatnewconnectors, "tomcat.threads", "The number of threads", "threads", ["proto_handler" : { mbean -> mbean.name().getKeyProperty("name") }], ["currentThreadCount":["state":{"idle"}],"currentThreadsBusy":["state":{"busy"}]], otel.&longValueCallback) diff --git a/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/InstrumenterHelperTest.java b/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/InstrumenterHelperTest.java index e0f9ff220..9ae5889dd 100644 --- a/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/InstrumenterHelperTest.java +++ b/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/InstrumenterHelperTest.java @@ -96,7 +96,7 @@ void setupOtel() { metricReader = InMemoryMetricReader.create(); meterProvider = SdkMeterProvider.builder().registerMetricReader(metricReader).build(); metricEnvironment = new GroovyMetricEnvironment(meterProvider, "otel.test"); - otel = new OtelHelper(jmxClient, metricEnvironment); + otel = new OtelHelper(jmxClient, metricEnvironment, false); } @AfterEach @@ -429,7 +429,36 @@ void doubleValueCallback() throws Exception { } @Test - void doubleValueCallbackMultipleMBeans() throws Exception { + void doubleValueCallbackMBeans() throws Exception { + String instrumentMethod = "doubleValueCallback"; + String thingName = "multiple:type=" + instrumentMethod + ".Thing"; + MBeanHelper mBeanHelper = registerThings(thingName); + + String instrumentName = "multiple." + instrumentMethod + ".gauge"; + String description = "multiple double gauge description"; + + updateWithHelper( + mBeanHelper, + instrumentMethod, + instrumentName, + description, + "Double", + new HashMap<>(), + /* aggregateAcrossMBeans= */ true); + + assertThat(metricReader.collectAllMetrics()) + .satisfiesExactly( + metric -> + assertThat(metric) + .hasName(instrumentName) + .hasDescription(description) + .hasUnit("1") + .hasDoubleGaugeSatisfying( + gauge -> gauge.hasPointsSatisfying(assertDoublePoint()))); + } + + @Test + void doubleValueCallbackListMBeans() throws Exception { String instrumentMethod = "doubleValueCallback"; ArrayList thingNames = new ArrayList<>(); for (int i = 0; i < 4; i++) { @@ -515,6 +544,12 @@ void longValueCallback() throws Exception { gauge -> gauge.hasPointsSatisfying(assertLongPoints()))); } + @SuppressWarnings("unchecked") + private Consumer[] assertDoublePoint() { + return Stream.>of(point -> point.hasValue(123.456 * 4)) + .toArray(Consumer[]::new); + } + @SuppressWarnings("unchecked") private Consumer[] assertDoublePoints() { return Stream.>of( @@ -679,11 +714,29 @@ void updateWithHelper( String instrumentName, String description, String attribute) { - Closure instrument = (Closure) Eval.me("otel", otel, "otel.&" + instrumentMethod); Map> labelFuncs = new HashMap<>(); labelFuncs.put("labelOne", (Closure) Eval.me("{ unused -> 'labelOneValue' }")); labelFuncs.put( "labelTwo", (Closure) Eval.me("{ mbean -> mbean.name().getKeyProperty('thing') }")); + updateWithHelper( + mBeanHelper, + instrumentMethod, + instrumentName, + description, + attribute, + labelFuncs, + /* aggregateAcrossMBeans= */ false); + } + + void updateWithHelper( + MBeanHelper mBeanHelper, + String instrumentMethod, + String instrumentName, + String description, + String attribute, + Map> labelFuncs, + boolean aggregateAcrossMBeans) { + Closure instrument = (Closure) Eval.me("otel", otel, "otel.&" + instrumentMethod); InstrumentHelper instrumentHelper = new InstrumentHelper( mBeanHelper, @@ -693,7 +746,8 @@ void updateWithHelper( labelFuncs, Collections.singletonMap(attribute, null), instrument, - metricEnvironment); + metricEnvironment, + aggregateAcrossMBeans); instrumentHelper.update(); } @@ -714,7 +768,8 @@ void updateWithHelperMultiAttribute( labelFuncs, attributes, instrument, - metricEnvironment); + metricEnvironment, + false); instrumentHelper.update(); } diff --git a/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/JmxConfigTest.java b/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/JmxConfigTest.java index e07eedab6..e49efefe0 100644 --- a/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/JmxConfigTest.java +++ b/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/JmxConfigTest.java @@ -52,6 +52,7 @@ void defaultValues() { assertThat(config.remoteProfile).isNull(); assertThat(config.realm).isNull(); assertThat(config.properties.getProperty("otel.metric.export.interval")).isEqualTo("10000"); + assertThat(config.aggregateAcrossMBeans).isFalse(); } @Test @@ -87,6 +88,7 @@ void specifiedValues() { assertThat(config.password).isEqualTo("myPassword"); assertThat(config.remoteProfile).isEqualTo("myRemoteProfile"); assertThat(config.realm).isEqualTo("myRealm"); + assertThat(config.aggregateAcrossMBeans).isFalse(); } @Test @@ -109,6 +111,7 @@ void propertiesFile() { assertThat(config.password).isEqualTo("myPassw\\ord"); assertThat(config.remoteProfile).isEqualTo("SASL/DIGEST-MD5"); assertThat(config.realm).isEqualTo("myRealm"); + assertThat(config.aggregateAcrossMBeans).isTrue(); // These properties are set from the config file loading into JmxConfig assertThat(System.getProperty("javax.net.ssl.keyStore")).isEqualTo("/my/key/store"); diff --git a/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/OtelHelperAsynchronousMetricTest.java b/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/OtelHelperAsynchronousMetricTest.java index 27c211308..fb88e18d3 100644 --- a/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/OtelHelperAsynchronousMetricTest.java +++ b/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/OtelHelperAsynchronousMetricTest.java @@ -29,7 +29,7 @@ class OtelHelperAsynchronousMetricTest { void setUp() { metricReader = InMemoryMetricReader.create(); meterProvider = SdkMeterProvider.builder().registerMetricReader(metricReader).build(); - otel = new OtelHelper(null, new GroovyMetricEnvironment(meterProvider, "otel.test")); + otel = new OtelHelper(null, new GroovyMetricEnvironment(meterProvider, "otel.test"), false); } @Test diff --git a/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/OtelHelperJmxTest.java b/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/OtelHelperJmxTest.java index 03f6f0251..ce63d7154 100644 --- a/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/OtelHelperJmxTest.java +++ b/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/OtelHelperJmxTest.java @@ -132,7 +132,7 @@ private static JMXServiceURL setupServer(Map env) throws Excepti } private static OtelHelper setupHelper(JmxConfig config) throws Exception { - return new OtelHelper(new JmxClient(config), new GroovyMetricEnvironment(config)); + return new OtelHelper(new JmxClient(config), new GroovyMetricEnvironment(config), false); } private static void verifyClient(Properties props) throws Exception { diff --git a/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/OtelHelperSynchronousMetricTest.java b/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/OtelHelperSynchronousMetricTest.java index 91df7f0d3..43f5f0746 100644 --- a/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/OtelHelperSynchronousMetricTest.java +++ b/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/OtelHelperSynchronousMetricTest.java @@ -33,7 +33,7 @@ class OtelHelperSynchronousMetricTest { void setUp() { metricReader = InMemoryMetricReader.create(); meterProvider = SdkMeterProvider.builder().registerMetricReader(metricReader).build(); - otel = new OtelHelper(null, new GroovyMetricEnvironment(meterProvider, "otel.test")); + otel = new OtelHelper(null, new GroovyMetricEnvironment(meterProvider, "otel.test"), false); } @Test diff --git a/jmx-metrics/src/test/resources/all.properties b/jmx-metrics/src/test/resources/all.properties index 2e0080391..00614c1b7 100644 --- a/jmx-metrics/src/test/resources/all.properties +++ b/jmx-metrics/src/test/resources/all.properties @@ -19,3 +19,4 @@ javax.net.ssl.keyStoreType=JKS javax.net.ssl.trustStore=/my/trust/store javax.net.ssl.trustStorePassword=def456 javax.net.ssl.trustStoreType=JKS +otel.jmx.aggregate.across.mbeans=true diff --git a/jmx-scrapper/README.md b/jmx-scraper/README.md similarity index 92% rename from jmx-scrapper/README.md rename to jmx-scraper/README.md index 9c952d83a..a041414e6 100644 --- a/jmx-scrapper/README.md +++ b/jmx-scraper/README.md @@ -1,4 +1,4 @@ -# JMX Metric Scrapper +# JMX Metric Scraper This utility provides a way to query JMX metrics and export them to an OTLP endpoint. The JMX MBeans and their metrics mapping is defined in YAML. diff --git a/jmx-scrapper/build.gradle.kts b/jmx-scraper/build.gradle.kts similarity index 97% rename from jmx-scrapper/build.gradle.kts rename to jmx-scraper/build.gradle.kts index 2554f0e01..79c1ab687 100644 --- a/jmx-scrapper/build.gradle.kts +++ b/jmx-scraper/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation("io.opentelemetry.instrumentation:opentelemetry-jmx-metrics") + testImplementation("org.junit-pioneer:junit-pioneer") } testing { @@ -34,7 +35,6 @@ testing { } tasks { - shadowJar { mergeServiceFiles() @@ -73,7 +73,6 @@ tasks.register("appJar") { } } - // Don't publish non-shadowed jar (shadowJar is in shadowRuntimeElements) with(components["java"] as AdhocComponentWithVariants) { configurations.forEach { diff --git a/jmx-scrapper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestApp.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestApp.java similarity index 100% rename from jmx-scrapper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestApp.java rename to jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestApp.java diff --git a/jmx-scrapper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppMXBean.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppMXBean.java similarity index 100% rename from jmx-scrapper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppMXBean.java rename to jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppMXBean.java diff --git a/jmx-scrapper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxRemoteClientTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClientTest.java similarity index 100% rename from jmx-scrapper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxRemoteClientTest.java rename to jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClientTest.java diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/ArgumentsParsingException.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/ArgumentsParsingException.java new file mode 100644 index 000000000..afe3460d6 --- /dev/null +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/ArgumentsParsingException.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper; + +public class ArgumentsParsingException extends Exception { + private static final long serialVersionUID = 0L; +} diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java new file mode 100644 index 000000000..835eebd1e --- /dev/null +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java @@ -0,0 +1,155 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper; + +import io.opentelemetry.contrib.jmxscraper.config.ConfigurationException; +import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig; +import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfigFactory; +import io.opentelemetry.contrib.jmxscraper.jmx.JmxClient; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +public class JmxScraper { + private static final Logger logger = Logger.getLogger(JmxScraper.class.getName()); + private static final int EXECUTOR_TERMINATION_TIMEOUT_MS = 5000; + private final ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + private final JmxScraperConfig config; + + /** + * Main method to create and run a {@link JmxScraper} instance. + * + * @param args - must be of the form "-config {jmx_config_path,'-'}" + */ + @SuppressWarnings({"SystemOut", "SystemExitOutsideMain"}) + public static void main(String[] args) { + try { + JmxScraperConfigFactory factory = new JmxScraperConfigFactory(); + JmxScraperConfig config = JmxScraper.createConfigFromArgs(Arrays.asList(args), factory); + + JmxScraper jmxScraper = new JmxScraper(config); + jmxScraper.start(); + + Runtime.getRuntime() + .addShutdownHook( + new Thread() { + @Override + public void run() { + jmxScraper.shutdown(); + } + }); + } catch (ArgumentsParsingException e) { + System.err.println( + "Usage: java -jar " + + "-config "); + System.exit(1); + } catch (ConfigurationException e) { + System.err.println(e.getMessage()); + System.exit(1); + } + } + + /** + * Create {@link JmxScraperConfig} object basing on command line options + * + * @param args application commandline arguments + */ + static JmxScraperConfig createConfigFromArgs(List args, JmxScraperConfigFactory factory) + throws ArgumentsParsingException, ConfigurationException { + if (!args.isEmpty() && (args.size() != 2 || !args.get(0).equalsIgnoreCase("-config"))) { + throw new ArgumentsParsingException(); + } + + Properties loadedProperties = new Properties(); + if (args.size() == 2) { + String path = args.get(1); + if (path.trim().equals("-")) { + loadPropertiesFromStdin(loadedProperties); + } else { + loadPropertiesFromPath(loadedProperties, path); + } + } + + return factory.createConfig(loadedProperties); + } + + private static void loadPropertiesFromStdin(Properties props) throws ConfigurationException { + try (InputStream is = new DataInputStream(System.in)) { + props.load(is); + } catch (IOException e) { + throw new ConfigurationException("Failed to read config properties from stdin", e); + } + } + + private static void loadPropertiesFromPath(Properties props, String path) + throws ConfigurationException { + try (InputStream is = Files.newInputStream(Paths.get(path))) { + props.load(is); + } catch (IOException e) { + throw new ConfigurationException("Failed to read config properties file: '" + path + "'", e); + } + } + + JmxScraper(JmxScraperConfig config) throws ConfigurationException { + this.config = config; + + try { + @SuppressWarnings("unused") // TODO: Temporary + JmxClient jmxClient = new JmxClient(config); + } catch (MalformedURLException e) { + throw new ConfigurationException("Malformed serviceUrl: ", e); + } + } + + @SuppressWarnings("FutureReturnValueIgnored") // TODO: Temporary + private void start() { + exec.scheduleWithFixedDelay( + () -> { + logger.fine("JMX scraping triggered"); + // try { + // runner.run(); + // } catch (Throwable e) { + // logger.log(Level.SEVERE, "Error gathering JMX metrics", e); + // } + }, + 0, + config.getIntervalMilliseconds(), + TimeUnit.MILLISECONDS); + logger.info("JMX scraping started"); + } + + private void shutdown() { + logger.info("Shutting down JmxScraper and exporting final metrics."); + // Prevent new tasks to be submitted + exec.shutdown(); + try { + // Wait a while for existing tasks to terminate + if (!exec.awaitTermination(EXECUTOR_TERMINATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + // Cancel currently executing tasks + exec.shutdownNow(); + // Wait a while for tasks to respond to being cancelled + if (!exec.awaitTermination(EXECUTOR_TERMINATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + logger.warning("Thread pool did not terminate in time: " + exec); + } + } + } catch (InterruptedException e) { + // (Re-)Cancel if current thread also interrupted + exec.shutdownNow(); + // Preserve interrupt status + Thread.currentThread().interrupt(); + } + } +} diff --git a/jmx-scrapper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxRemoteClient.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java similarity index 98% rename from jmx-scrapper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxRemoteClient.java rename to jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java index 9ca719ecb..020269481 100644 --- a/jmx-scrapper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxRemoteClient.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java @@ -1,4 +1,4 @@ -package io.opentelemetry.contrib.jmxscraper; +package io.opentelemetry.contrib.jmxscraper.client; import java.io.IOException; import java.net.MalformedURLException; diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/ConfigurationException.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/ConfigurationException.java new file mode 100644 index 000000000..76c69998a --- /dev/null +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/ConfigurationException.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.config; + +public class ConfigurationException extends Exception { + private static final long serialVersionUID = 0L; + + public ConfigurationException(String message, Throwable cause) { + super(message, cause); + } + + public ConfigurationException(String message) { + super(message); + } +} diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfig.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfig.java new file mode 100644 index 000000000..eb04e13cd --- /dev/null +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfig.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.config; + +import java.util.Collections; +import java.util.Set; + +/** This class keeps application settings */ +public class JmxScraperConfig { + String serviceUrl = ""; + String customJmxScrapingConfigPath = ""; + Set targetSystems = Collections.emptySet(); + int intervalMilliseconds; + String metricsExporterType = ""; + + String otlpExporterEndpoint = ""; + + String username = ""; + String password = ""; + String realm = ""; + String remoteProfile = ""; + boolean registrySsl; + + JmxScraperConfig() {} + + public String getServiceUrl() { + return serviceUrl; + } + + public String getCustomJmxScrapingConfigPath() { + return customJmxScrapingConfigPath; + } + + public Set getTargetSystems() { + return targetSystems; + } + + public int getIntervalMilliseconds() { + return intervalMilliseconds; + } + + public String getMetricsExporterType() { + return metricsExporterType; + } + + public String getOtlpExporterEndpoint() { + return otlpExporterEndpoint; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getRealm() { + return realm; + } + + public String getRemoteProfile() { + return remoteProfile; + } + + public boolean isRegistrySsl() { + return registrySsl; + } +} diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigFactory.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigFactory.java new file mode 100644 index 000000000..12f054585 --- /dev/null +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigFactory.java @@ -0,0 +1,179 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.config; + +import static io.opentelemetry.contrib.jmxscraper.util.StringUtils.isBlank; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.stream.Collectors; + +public class JmxScraperConfigFactory { + private static final String PREFIX = "otel."; + static final String SERVICE_URL = PREFIX + "jmx.service.url"; + static final String CUSTOM_JMX_SCRAPING_CONFIG = PREFIX + "jmx.custom.jmx.scraping.config"; + static final String TARGET_SYSTEM = PREFIX + "jmx.target.system"; + static final String INTERVAL_MILLISECONDS = PREFIX + "jmx.interval.milliseconds"; + static final String METRICS_EXPORTER_TYPE = PREFIX + "metrics.exporter"; + static final String EXPORTER_INTERVAL = PREFIX + "metric.export.interval"; + static final String REGISTRY_SSL = PREFIX + "jmx.remote.registry.ssl"; + + static final String OTLP_ENDPOINT = PREFIX + "exporter.otlp.endpoint"; + + static final String JMX_USERNAME = PREFIX + "jmx.username"; + static final String JMX_PASSWORD = PREFIX + "jmx.password"; + static final String JMX_REMOTE_PROFILE = PREFIX + "jmx.remote.profile"; + static final String JMX_REALM = PREFIX + "jmx.realm"; + + // These properties need to be copied into System Properties if provided via the property + // file so that they are available to the JMX Connection builder + static final List JAVA_SYSTEM_PROPERTIES = + Arrays.asList( + "javax.net.ssl.keyStore", + "javax.net.ssl.keyStorePassword", + "javax.net.ssl.keyStoreType", + "javax.net.ssl.trustStore", + "javax.net.ssl.trustStorePassword", + "javax.net.ssl.trustStoreType"); + + static final List AVAILABLE_TARGET_SYSTEMS = + Arrays.asList( + "activemq", + "cassandra", + "hbase", + "hadoop", + "jetty", + "jvm", + "kafka", + "kafka-consumer", + "kafka-producer", + "solr", + "tomcat", + "wildfly"); + + private Properties properties = new Properties(); + + public JmxScraperConfig createConfig(Properties props) throws ConfigurationException { + properties = new Properties(); + // putAll() instead of using constructor defaults + // to ensure they will be recorded to underlying map + properties.putAll(props); + + // command line takes precedence so replace any that were specified via config file properties + properties.putAll(System.getProperties()); + + JmxScraperConfig config = new JmxScraperConfig(); + + config.serviceUrl = properties.getProperty(SERVICE_URL); + config.customJmxScrapingConfigPath = properties.getProperty(CUSTOM_JMX_SCRAPING_CONFIG); + String targetSystem = + properties.getProperty(TARGET_SYSTEM, "").toLowerCase(Locale.ENGLISH).trim(); + + List targets = + Arrays.asList(isBlank(targetSystem) ? new String[0] : targetSystem.split(",")); + config.targetSystems = targets.stream().map(String::trim).collect(Collectors.toSet()); + + int interval = getProperty(INTERVAL_MILLISECONDS, 0); + config.intervalMilliseconds = (interval == 0 ? 10000 : interval); + getAndSetPropertyIfUndefined(EXPORTER_INTERVAL, config.intervalMilliseconds); + + config.metricsExporterType = getAndSetPropertyIfUndefined(METRICS_EXPORTER_TYPE, "logging"); + config.otlpExporterEndpoint = properties.getProperty(OTLP_ENDPOINT); + + config.username = properties.getProperty(JMX_USERNAME); + config.password = properties.getProperty(JMX_PASSWORD); + + config.remoteProfile = properties.getProperty(JMX_REMOTE_PROFILE); + config.realm = properties.getProperty(JMX_REALM); + + config.registrySsl = Boolean.parseBoolean(properties.getProperty(REGISTRY_SSL)); + + validateConfig(config); + populateJmxSystemProperties(); + + return config; + } + + private void populateJmxSystemProperties() { + // For the list of System Properties, if they have been set in the properties file + // they need to be set in Java System Properties. + JAVA_SYSTEM_PROPERTIES.forEach( + key -> { + // As properties file & command line properties are combined into properties + // at this point, only override if it was not already set via command line + if (System.getProperty(key) != null) { + return; + } + String value = properties.getProperty(key); + if (value != null) { + System.setProperty(key, value); + } + }); + } + + private int getProperty(String key, int defaultValue) throws ConfigurationException { + String propVal = properties.getProperty(key); + if (propVal == null) { + return defaultValue; + } + try { + return Integer.parseInt(propVal); + } catch (NumberFormatException e) { + throw new ConfigurationException("Failed to parse " + key, e); + } + } + + /** + * Similar to getProperty(key, defaultValue) but sets the property to default if not in object. + */ + private String getAndSetPropertyIfUndefined(String key, String defaultValue) { + String propVal = properties.getProperty(key, defaultValue); + if (propVal.equals(defaultValue)) { + properties.setProperty(key, defaultValue); + } + return propVal; + } + + private int getAndSetPropertyIfUndefined(String key, int defaultValue) + throws ConfigurationException { + int propVal = getProperty(key, defaultValue); + if (propVal == defaultValue) { + properties.setProperty(key, String.valueOf(defaultValue)); + } + return propVal; + } + + /** Will determine if parsed config is complete, setting any applicable values and defaults. */ + private static void validateConfig(JmxScraperConfig config) throws ConfigurationException { + if (isBlank(config.serviceUrl)) { + throw new ConfigurationException(SERVICE_URL + " must be specified."); + } + + if (isBlank(config.customJmxScrapingConfigPath) && config.targetSystems.isEmpty()) { + throw new ConfigurationException( + CUSTOM_JMX_SCRAPING_CONFIG + " or " + TARGET_SYSTEM + " must be specified."); + } + + if (!config.targetSystems.isEmpty() + && !AVAILABLE_TARGET_SYSTEMS.containsAll(config.targetSystems)) { + throw new ConfigurationException( + String.format( + "%s must specify targets from %s", config.targetSystems, AVAILABLE_TARGET_SYSTEMS)); + } + + if (isBlank(config.otlpExporterEndpoint) + && (!isBlank(config.metricsExporterType) + && config.metricsExporterType.equalsIgnoreCase("otlp"))) { + throw new ConfigurationException(OTLP_ENDPOINT + " must be specified for otlp format."); + } + + if (config.intervalMilliseconds < 0) { + throw new ConfigurationException(INTERVAL_MILLISECONDS + " must be positive."); + } + } +} diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/ClientCallbackHandler.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/ClientCallbackHandler.java new file mode 100644 index 000000000..2dfa01115 --- /dev/null +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/ClientCallbackHandler.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.jmx; + +import javax.annotation.Nullable; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.RealmCallback; + +public class ClientCallbackHandler implements CallbackHandler { + private final String username; + @Nullable private final char[] password; + private final String realm; + + /** + * Constructor for the {@link ClientCallbackHandler}, a CallbackHandler implementation for + * authenticating with an MBean server. + * + * @param username - authenticating username + * @param password - authenticating password (plaintext) + * @param realm - authenticating realm + */ + public ClientCallbackHandler(String username, String password, String realm) { + this.username = username; + this.password = password != null ? password.toCharArray() : null; + this.realm = realm; + } + + @Override + public void handle(Callback[] callbacks) throws UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof NameCallback) { + ((NameCallback) callback).setName(this.username); + } else if (callback instanceof PasswordCallback) { + ((PasswordCallback) callback).setPassword(this.password); + } else if (callback instanceof RealmCallback) { + ((RealmCallback) callback).setText(this.realm); + } else { + throw new UnsupportedCallbackException(callback); + } + } + } +} diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/JmxClient.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/JmxClient.java new file mode 100644 index 000000000..0c71d9cc9 --- /dev/null +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/JmxClient.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.jmx; + +import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig; +import io.opentelemetry.contrib.jmxscraper.util.StringUtils; +import java.io.IOException; +import java.net.MalformedURLException; +import java.security.Provider; +import java.security.Security; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXServiceURL; + +@SuppressWarnings("unused") // TODO: Temporary +public class JmxClient { + private static final Logger logger = Logger.getLogger(JmxClient.class.getName()); + + private final JMXServiceURL url; + private final String username; + private final String password; + private final String realm; + private final String remoteProfile; + private final boolean registrySsl; + @Nullable private JMXConnector jmxConn; + + public JmxClient(JmxScraperConfig config) throws MalformedURLException { + this.url = new JMXServiceURL(config.getServiceUrl()); + this.username = config.getUsername(); + this.password = config.getPassword(); + this.realm = config.getRealm(); + this.remoteProfile = config.getRemoteProfile(); + this.registrySsl = config.isRegistrySsl(); + } + + @Nullable + public MBeanServerConnection getConnection() { + if (jmxConn != null) { + try { + return jmxConn.getMBeanServerConnection(); + } catch (IOException e) { + // Attempt to connect with authentication below. + } + } + try { + @SuppressWarnings("ModifiedButNotUsed") // TODO: Temporary + Map env = new HashMap<>(); + if (!StringUtils.isBlank(username)) { + env.put(JMXConnector.CREDENTIALS, new String[] {this.username, this.password}); + } + try { + // Not all supported versions of Java contain this Provider + Class klass = Class.forName("com.sun.security.sasl.Provider"); + Provider provider = (Provider) klass.getDeclaredConstructor().newInstance(); + Security.addProvider(provider); + + env.put("jmx.remote.profile", this.remoteProfile); + env.put( + "jmx.remote.sasl.callback.handler", + new ClientCallbackHandler(this.username, this.password, this.realm)); + } catch (ReflectiveOperationException e) { + logger.warning("SASL unsupported in current environment: " + e.getMessage()); + } + + // jmxConn = JmxConnectorHelper.connect(url, env, registrySsl); + // return jmxConn.getMBeanServerConnection(); + return jmxConn == null ? null : jmxConn.getMBeanServerConnection(); // Temporary + + } catch (IOException e) { + logger.log(Level.WARNING, "Could not connect to remote JMX server: ", e); + return null; + } + } + + /** + * Query the MBean server for a given ObjectName. + * + * @param objectName ObjectName to query + * @return the sorted list of applicable ObjectName instances found by server + */ + public List query(ObjectName objectName) { + MBeanServerConnection mBeanServerConnection = getConnection(); + if (mBeanServerConnection == null) { + return Collections.emptyList(); + } + + try { + List objectNames = + new ArrayList<>(mBeanServerConnection.queryNames(objectName, null)); + Collections.sort(objectNames); + return Collections.unmodifiableList(objectNames); + } catch (IOException e) { + logger.log(Level.WARNING, "Could not query remote JMX server: ", e); + return Collections.emptyList(); + } + } +} diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/util/StringUtils.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/util/StringUtils.java new file mode 100644 index 000000000..aa24e1cea --- /dev/null +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/util/StringUtils.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.util; + +import javax.annotation.Nullable; + +public final class StringUtils { + private StringUtils() {} + + /** + * Determines if a String is null or without non-whitespace chars. + * + * @param s - {@link String} to evaluate + * @return - if s is null or without non-whitespace chars. + */ + public static boolean isBlank(@Nullable String s) { + return (s == null) || s.trim().isEmpty(); + } +} diff --git a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java new file mode 100644 index 000000000..86b83fc27 --- /dev/null +++ b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfigFactory; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +class JmxScraperTest { + @Test + void shouldThrowExceptionWhenInvalidCommandLineArgsProvided() { + // Given + List emptyArgs = Collections.singletonList("-inexistingOption"); + JmxScraperConfigFactory configFactoryMock = mock(JmxScraperConfigFactory.class); + + // When and Then + assertThatThrownBy(() -> JmxScraper.createConfigFromArgs(emptyArgs, configFactoryMock)) + .isInstanceOf(ArgumentsParsingException.class); + } + + @Test + void shouldThrowExceptionWhenTooManyCommandLineArgsProvided() { + // Given + List emptyArgs = Arrays.asList("-config", "path", "-inexistingOption"); + JmxScraperConfigFactory configFactoryMock = mock(JmxScraperConfigFactory.class); + + // When and Then + assertThatThrownBy(() -> JmxScraper.createConfigFromArgs(emptyArgs, configFactoryMock)) + .isInstanceOf(ArgumentsParsingException.class); + } +} diff --git a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigFactoryTest.java b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigFactoryTest.java new file mode 100644 index 000000000..28a2680d7 --- /dev/null +++ b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigFactoryTest.java @@ -0,0 +1,364 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Properties; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.ClearSystemProperty; +import org.junitpioneer.jupiter.SetSystemProperty; + +class JmxScraperConfigFactoryTest { + private static Properties validProperties; + + @BeforeAll + static void setUp() { + validProperties = new Properties(); + validProperties.setProperty( + JmxScraperConfigFactory.SERVICE_URL, + "jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); + validProperties.setProperty(JmxScraperConfigFactory.CUSTOM_JMX_SCRAPING_CONFIG, ""); + validProperties.setProperty(JmxScraperConfigFactory.TARGET_SYSTEM, "tomcat, activemq"); + validProperties.setProperty(JmxScraperConfigFactory.METRICS_EXPORTER_TYPE, "otel"); + validProperties.setProperty(JmxScraperConfigFactory.INTERVAL_MILLISECONDS, "1410"); + validProperties.setProperty(JmxScraperConfigFactory.REGISTRY_SSL, "true"); + validProperties.setProperty(JmxScraperConfigFactory.OTLP_ENDPOINT, "http://localhost:4317"); + validProperties.setProperty(JmxScraperConfigFactory.JMX_USERNAME, "some-user"); + validProperties.setProperty(JmxScraperConfigFactory.JMX_PASSWORD, "some-password"); + validProperties.setProperty(JmxScraperConfigFactory.JMX_REMOTE_PROFILE, "some-profile"); + validProperties.setProperty(JmxScraperConfigFactory.JMX_REALM, "some-realm"); + } + + @Test + void shouldCreateMinimalValidConfiguration() throws ConfigurationException { + // Given + JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); + Properties properties = new Properties(); + properties.setProperty( + JmxScraperConfigFactory.SERVICE_URL, + "jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); + properties.setProperty(JmxScraperConfigFactory.CUSTOM_JMX_SCRAPING_CONFIG, "/file.properties"); + + // When + JmxScraperConfig config = configFactory.createConfig(properties); + + // Then + assertThat(config.serviceUrl).isEqualTo("jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); + assertThat(config.customJmxScrapingConfigPath).isEqualTo("/file.properties"); + assertThat(config.targetSystems).isEmpty(); + assertThat(config.intervalMilliseconds).isEqualTo(10000); + assertThat(config.metricsExporterType).isEqualTo("logging"); + assertThat(config.otlpExporterEndpoint).isNull(); + assertThat(config.username).isNull(); + assertThat(config.password).isNull(); + assertThat(config.remoteProfile).isNull(); + assertThat(config.realm).isNull(); + } + + @Test + @ClearSystemProperty(key = "javax.net.ssl.keyStore") + @ClearSystemProperty(key = "javax.net.ssl.keyStorePassword") + @ClearSystemProperty(key = "javax.net.ssl.keyStoreType") + @ClearSystemProperty(key = "javax.net.ssl.trustStore") + @ClearSystemProperty(key = "javax.net.ssl.trustStorePassword") + @ClearSystemProperty(key = "javax.net.ssl.trustStoreType") + void shouldUseValuesFromProperties() throws ConfigurationException { + // Given + JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); + // Properties to be propagated to system, properties + Properties properties = (Properties) validProperties.clone(); + properties.setProperty("javax.net.ssl.keyStore", "/my/key/store"); + properties.setProperty("javax.net.ssl.keyStorePassword", "abc123"); + properties.setProperty("javax.net.ssl.keyStoreType", "JKS"); + properties.setProperty("javax.net.ssl.trustStore", "/my/trust/store"); + properties.setProperty("javax.net.ssl.trustStorePassword", "def456"); + properties.setProperty("javax.net.ssl.trustStoreType", "JKS"); + + // When + JmxScraperConfig config = configFactory.createConfig(properties); + + // Then + assertThat(config.serviceUrl).isEqualTo("jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); + assertThat(config.customJmxScrapingConfigPath).isEqualTo(""); + assertThat(config.targetSystems).containsOnly("tomcat", "activemq"); + assertThat(config.intervalMilliseconds).isEqualTo(1410); + assertThat(config.metricsExporterType).isEqualTo("otel"); + assertThat(config.otlpExporterEndpoint).isEqualTo("http://localhost:4317"); + assertThat(config.username).isEqualTo("some-user"); + assertThat(config.password).isEqualTo("some-password"); + assertThat(config.remoteProfile).isEqualTo("some-profile"); + assertThat(config.realm).isEqualTo("some-realm"); + assertThat(config.registrySsl).isTrue(); + + // These properties are set from the config file loading into JmxConfig + assertThat(System.getProperty("javax.net.ssl.keyStore")).isEqualTo("/my/key/store"); + assertThat(System.getProperty("javax.net.ssl.keyStorePassword")).isEqualTo("abc123"); + assertThat(System.getProperty("javax.net.ssl.keyStoreType")).isEqualTo("JKS"); + assertThat(System.getProperty("javax.net.ssl.trustStore")).isEqualTo("/my/trust/store"); + assertThat(System.getProperty("javax.net.ssl.trustStorePassword")).isEqualTo("def456"); + assertThat(System.getProperty("javax.net.ssl.trustStoreType")).isEqualTo("JKS"); + } + + @Test + @SetSystemProperty(key = "otel.jmx.service.url", value = "originalServiceUrl") + @SetSystemProperty(key = "javax.net.ssl.keyStorePassword", value = "originalPassword") + void shouldRetainPredefinedSystemProperties() throws ConfigurationException { + // Given + JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); + // Properties to be propagated to system, properties + Properties properties = (Properties) validProperties.clone(); + properties.setProperty("javax.net.ssl.keyStorePassword", "abc123"); + + // When + configFactory.createConfig(properties); + + // Then + assertThat(System.getProperty("otel.jmx.service.url")).isEqualTo("originalServiceUrl"); + assertThat(System.getProperty("javax.net.ssl.keyStorePassword")).isEqualTo("originalPassword"); + } + + @Test + void shouldFailValidation_missingServiceUrl() { + // Given + JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); + Properties properties = (Properties) validProperties.clone(); + properties.remove(JmxScraperConfigFactory.SERVICE_URL); + + // When and Then + assertThatThrownBy(() -> configFactory.createConfig(properties)) + .isInstanceOf(ConfigurationException.class) + .hasMessage("otel.jmx.service.url must be specified."); + } + + @Test + void shouldFailValidation_missingConfigPathAndTargetSystem() { + // Given + JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); + Properties properties = (Properties) validProperties.clone(); + properties.remove(JmxScraperConfigFactory.CUSTOM_JMX_SCRAPING_CONFIG); + properties.remove(JmxScraperConfigFactory.TARGET_SYSTEM); + + // When and Then + assertThatThrownBy(() -> configFactory.createConfig(properties)) + .isInstanceOf(ConfigurationException.class) + .hasMessage( + "otel.jmx.custom.jmx.scraping.config or otel.jmx.target.system must be specified."); + } + + @Test + void shouldFailValidation_invalidTargetSystem() { + // Given + JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); + Properties properties = (Properties) validProperties.clone(); + properties.setProperty(JmxScraperConfigFactory.TARGET_SYSTEM, "hal9000"); + + // When and Then + assertThatThrownBy(() -> configFactory.createConfig(properties)) + .isInstanceOf(ConfigurationException.class) + .hasMessage( + "[hal9000] must specify targets from " + + JmxScraperConfigFactory.AVAILABLE_TARGET_SYSTEMS); + } + + @Test + void shouldFailValidation_missingOtlpEndpoint() { + // Given + JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); + Properties properties = (Properties) validProperties.clone(); + properties.remove(JmxScraperConfigFactory.OTLP_ENDPOINT); + properties.setProperty(JmxScraperConfigFactory.METRICS_EXPORTER_TYPE, "otlp"); + + // When and Then + assertThatThrownBy(() -> configFactory.createConfig(properties)) + .isInstanceOf(ConfigurationException.class) + .hasMessage("otel.exporter.otlp.endpoint must be specified for otlp format."); + } + + @Test + void shouldFailValidation_negativeInterval() { + // Given + JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); + Properties properties = (Properties) validProperties.clone(); + properties.setProperty(JmxScraperConfigFactory.INTERVAL_MILLISECONDS, "-1"); + + // When and Then + assertThatThrownBy(() -> configFactory.createConfig(properties)) + .isInstanceOf(ConfigurationException.class) + .hasMessage("otel.jmx.interval.milliseconds must be positive."); + } + + @Test + void shouldFailConfigCreation_invalidInterval() { + // Given + JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); + Properties properties = (Properties) validProperties.clone(); + properties.setProperty(JmxScraperConfigFactory.INTERVAL_MILLISECONDS, "abc"); + + // When and Then + assertThatThrownBy(() -> configFactory.createConfig(properties)) + .isInstanceOf(ConfigurationException.class) + .hasMessage("Failed to parse otel.jmx.interval.milliseconds"); + } + + // @ClearSystemProperty(key = "otel.metric.export.interval") + + // @Test + // @SetSystemProperty(key = "otel.jmx.service.url", value = "myServiceUrl") + // @SetSystemProperty(key = "otel.jmx.groovy.script", value = "myGroovyScript") + // @SetSystemProperty( + // key = "otel.jmx.target.system", + // value = "mytargetsystem,mytargetsystem,myothertargetsystem,myadditionaltargetsystem") + // @SetSystemProperty(key = "otel.jmx.interval.milliseconds", value = "123") + // @SetSystemProperty(key = "otel.metrics.exporter", value = "inmemory") + // @SetSystemProperty(key = "otel.exporter.otlp.endpoint", value = "https://myOtlpEndpoint") + // @SetSystemProperty(key = "otel.exporter.prometheus.host", value = "myPrometheusHost") + // @SetSystemProperty(key = "otel.exporter.prometheus.port", value = "234") + // @SetSystemProperty(key = "otel.jmx.username", value = "myUsername") + // @SetSystemProperty(key = "otel.jmx.password", value = "myPassword") + // @SetSystemProperty(key = "otel.jmx.remote.profile", value = "myRemoteProfile") + // @SetSystemProperty(key = "otel.jmx.realm", value = "myRealm") + // void specifiedValues() { + // JmxConfig config = new JmxConfig(); + // + // assertThat(config.serviceUrl).isEqualTo("myServiceUrl"); + // assertThat(config.groovyScript).isEqualTo("myGroovyScript"); + // assertThat(config.targetSystem) + // + // .isEqualTo("mytargetsystem,mytargetsystem,myothertargetsystem,myadditionaltargetsystem"); + // assertThat(config.targetSystems) + // .containsOnly("mytargetsystem", "myothertargetsystem", "myadditionaltargetsystem"); + // assertThat(config.intervalMilliseconds).isEqualTo(123); + // assertThat(config.metricsExporterType).isEqualTo("inmemory"); + // assertThat(config.otlpExporterEndpoint).isEqualTo("https://myOtlpEndpoint"); + // assertThat(config.prometheusExporterHost).isEqualTo("myPrometheusHost"); + // assertThat(config.prometheusExporterPort).isEqualTo(234); + // assertThat(config.username).isEqualTo("myUsername"); + // assertThat(config.password).isEqualTo("myPassword"); + // assertThat(config.remoteProfile).isEqualTo("myRemoteProfile"); + // assertThat(config.realm).isEqualTo("myRealm"); + // } + // + // @Test + // void propertiesFile() { + // Properties props = new Properties(); + // JmxMetrics.loadPropertiesFromPath( + // props, ClassLoader.getSystemClassLoader().getResource("all.properties").getPath()); + // JmxConfig config = new JmxConfig(props); + // + // + // assertThat(config.serviceUrl).isEqualTo("service:jmx:rmi:///jndi/rmi://myhost:12345/jmxrmi"); + // assertThat(config.groovyScript).isEqualTo("/my/groovy/script"); + // assertThat(config.targetSystem).isEqualTo("jvm,cassandra"); + // assertThat(config.targetSystems).containsOnly("jvm", "cassandra"); + // assertThat(config.intervalMilliseconds).isEqualTo(20000); + // assertThat(config.metricsExporterType).isEqualTo("otlp"); + // assertThat(config.otlpExporterEndpoint).isEqualTo("https://myotlpendpoint"); + // assertThat(config.prometheusExporterHost).isEqualTo("host123.domain.com"); + // assertThat(config.prometheusExporterPort).isEqualTo(67890); + // assertThat(config.username).isEqualTo("myUser\nname"); + // assertThat(config.password).isEqualTo("myPassw\\ord"); + // assertThat(config.remoteProfile).isEqualTo("SASL/DIGEST-MD5"); + // assertThat(config.realm).isEqualTo("myRealm"); + // + // // These properties are set from the config file loading into JmxConfig + // assertThat(System.getProperty("javax.net.ssl.keyStore")).isEqualTo("/my/key/store"); + // assertThat(System.getProperty("javax.net.ssl.keyStorePassword")).isEqualTo("abc123"); + // assertThat(System.getProperty("javax.net.ssl.keyStoreType")).isEqualTo("JKS"); + // assertThat(System.getProperty("javax.net.ssl.trustStore")).isEqualTo("/my/trust/store"); + // assertThat(System.getProperty("javax.net.ssl.trustStorePassword")).isEqualTo("def456"); + // assertThat(System.getProperty("javax.net.ssl.trustStoreType")).isEqualTo("JKS"); + // } + // + // @Test + // @SetSystemProperty(key = "otel.jmx.service.url", value = "myServiceUrl") + // @SetSystemProperty(key = "javax.net.ssl.keyStorePassword", value = "truth") + // void propertiesFileOverride() { + // Properties props = new Properties(); + // JmxMetrics.loadPropertiesFromPath( + // props, ClassLoader.getSystemClassLoader().getResource("all.properties").getPath()); + // JmxConfig config = new JmxConfig(props); + // + // // This property should retain the system property value, not the config file value + // assertThat(config.serviceUrl).isEqualTo("myServiceUrl"); + // // These properties are set from the config file + // assertThat(config.groovyScript).isEqualTo("/my/groovy/script"); + // assertThat(config.targetSystem).isEqualTo("jvm,cassandra"); + // assertThat(config.targetSystems).containsOnly("jvm", "cassandra"); + // assertThat(config.intervalMilliseconds).isEqualTo(20000); + // assertThat(config.metricsExporterType).isEqualTo("otlp"); + // assertThat(config.otlpExporterEndpoint).isEqualTo("https://myotlpendpoint"); + // assertThat(config.prometheusExporterHost).isEqualTo("host123.domain.com"); + // assertThat(config.prometheusExporterPort).isEqualTo(67890); + // assertThat(config.username).isEqualTo("myUser\nname"); + // assertThat(config.password).isEqualTo("myPassw\\ord"); + // assertThat(config.remoteProfile).isEqualTo("SASL/DIGEST-MD5"); + // assertThat(config.realm).isEqualTo("myRealm"); + // + // // This property should retain the system property value, not the config file value + // assertThat(System.getProperty("javax.net.ssl.keyStorePassword")).isEqualTo("truth"); + // // These properties are set from the config file loading into JmxConfig + // assertThat(System.getProperty("javax.net.ssl.keyStore")).isEqualTo("/my/key/store"); + // assertThat(System.getProperty("javax.net.ssl.keyStoreType")).isEqualTo("JKS"); + // assertThat(System.getProperty("javax.net.ssl.trustStore")).isEqualTo("/my/trust/store"); + // assertThat(System.getProperty("javax.net.ssl.trustStorePassword")).isEqualTo("def456"); + // assertThat(System.getProperty("javax.net.ssl.trustStoreType")).isEqualTo("JKS"); + // } + // + // @Test + // @SetSystemProperty(key = "otel.jmx.interval.milliseconds", value = "abc") + // void invalidInterval() { + // assertThatThrownBy(JmxConfig::new) + // .isInstanceOf(ConfigurationException.class) + // .hasMessage("Failed to parse otel.jmx.interval.milliseconds"); + // } + // + // @Test + // @SetSystemProperty(key = "otel.exporter.prometheus.port", value = "abc") + // void invalidPrometheusPort() { + // assertThatThrownBy(JmxConfig::new) + // .isInstanceOf(ConfigurationException.class) + // .hasMessage("Failed to parse otel.exporter.prometheus.port"); + // } + // + // @Test + // @SetSystemProperty(key = "otel.jmx.service.url", value = "myServiceUrl") + // @SetSystemProperty(key = "otel.jmx.groovy.script", value = "myGroovyScript") + // @SetSystemProperty(key = "otel.jmx.target.system", value = "myTargetSystem") + // void canSupportScriptAndTargetSystem() { + // JmxConfig config = new JmxConfig(); + // + // assertThat(config.serviceUrl).isEqualTo("myServiceUrl"); + // assertThat(config.groovyScript).isEqualTo("myGroovyScript"); + // assertThat(config.targetSystem).isEqualTo("mytargetsystem"); + // assertThat(config.targetSystems).containsOnly("mytargetsystem"); + // } + // + // @Test + // @SetSystemProperty(key = "otel.jmx.service.url", value = "requiredValue") + // @SetSystemProperty(key = "otel.jmx.target.system", value = "jvm,unavailableTargetSystem") + // void invalidTargetSystem() { + // JmxConfig config = new JmxConfig(); + // + // assertThatThrownBy(config::validate) + // .isInstanceOf(ConfigurationException.class) + // .hasMessage( + // "[jvm, unavailabletargetsystem] must specify targets from [activemq, cassandra, + // hbase, hadoop, jetty, jvm, " + // + "kafka, kafka-consumer, kafka-producer, solr, tomcat, wildfly]"); + // } + // + // @Test + // @SetSystemProperty(key = "otel.metric.export.interval", value = "123") + // void otelMetricExportIntervalRespected() { + // JmxConfig config = new JmxConfig(); + // assertThat(config.intervalMilliseconds).isEqualTo(10000); + // assertThat(config.properties.getProperty("otel.metric.export.interval")).isEqualTo("123"); + // } + // +} diff --git a/jmx-scrapper/src/main/java/io/opentelemetry/contrib/jmxscrapper/JmxMetrics.java b/jmx-scrapper/src/main/java/io/opentelemetry/contrib/jmxscrapper/JmxMetrics.java deleted file mode 100644 index b9afbe34f..000000000 --- a/jmx-scrapper/src/main/java/io/opentelemetry/contrib/jmxscrapper/JmxMetrics.java +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.jmxscrapper; - -public class JmxMetrics { - - private JmxMetrics() {} - - public static void main(String[] args) {} -} diff --git a/micrometer-meter-provider/build.gradle.kts b/micrometer-meter-provider/build.gradle.kts index de97949e0..89a023254 100644 --- a/micrometer-meter-provider/build.gradle.kts +++ b/micrometer-meter-provider/build.gradle.kts @@ -20,14 +20,14 @@ dependencies { annotationProcessor("com.google.auto.value:auto-value") compileOnly("com.google.auto.value:auto-value-annotations") - testImplementation("io.micrometer:micrometer-core:1.13.3") + testImplementation("io.micrometer:micrometer-core:1.13.4") } testing { suites { val integrationTest by registering(JvmTestSuite::class) { dependencies { - implementation("io.micrometer:micrometer-registry-prometheus:1.13.3") + implementation("io.micrometer:micrometer-registry-prometheus:1.13.4") } } } diff --git a/processors/src/test/java/io/opentelemetry/contrib/interceptor/InterceptableLogRecordExporterTest.java b/processors/src/test/java/io/opentelemetry/contrib/interceptor/InterceptableLogRecordExporterTest.java index ff336b347..3b81ce277 100644 --- a/processors/src/test/java/io/opentelemetry/contrib/interceptor/InterceptableLogRecordExporterTest.java +++ b/processors/src/test/java/io/opentelemetry/contrib/interceptor/InterceptableLogRecordExporterTest.java @@ -10,18 +10,19 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.logs.Logger; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.contrib.interceptor.common.ComposableInterceptor; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.logs.SdkLoggerProvider; -import io.opentelemetry.sdk.logs.data.Body; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; import java.util.List; +import java.util.Objects; import javax.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -74,7 +75,7 @@ void verifyLogModification() { void verifyLogFiltering() { interceptor.add( item -> { - if (item.getBody().asString().contains("deleted")) { + if (Objects.requireNonNull(item.getBodyValue()).asString().contains("deleted")) { return null; } return item; @@ -87,8 +88,8 @@ void verifyLogFiltering() { List finishedLogRecordItems = memoryLogRecordExporter.getFinishedLogRecordItems(); assertEquals(2, finishedLogRecordItems.size()); - assertEquals("One log", finishedLogRecordItems.get(0).getBody().asString()); - assertEquals("Another log", finishedLogRecordItems.get(1).getBody().asString()); + assertEquals(Value.of("One log"), finishedLogRecordItems.get(0).getBodyValue()); + assertEquals(Value.of("Another log"), finishedLogRecordItems.get(1).getBodyValue()); } private static class ModifiableLogRecordData implements LogRecordData { @@ -136,7 +137,8 @@ public String getSeverityText() { } @Override - public Body getBody() { + @SuppressWarnings("deprecation") // implement deprecated method + public io.opentelemetry.sdk.logs.data.Body getBody() { return delegate.getBody(); } diff --git a/samplers/README.md b/samplers/README.md index 67a8d561a..c21877682 100644 --- a/samplers/README.md +++ b/samplers/README.md @@ -1,7 +1,67 @@ # Samplers +## Declarative configuration + +The following samplers support [declarative configuration](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/configuration#declarative-configuration): + +* `RuleBasedRoutingSampler` + +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. + +NOTE: Not yet available for use with the OTEL java agent, but should be in the near future. Please check back for updates. + +Schema for `rule_based_routing` sampler: + +```yaml +# The fallback sampler to the use if the criteria is not met. +fallback_sampler: + always_on: +# Filter to spans of this span_kind. Must be one of: SERVER, CLIENT, INTERNAL, CONSUMER, PRODUCER. +span_kind: SERVER # only apply to server spans +# List of rules describing spans to drop. Spans are dropped if they match one of the rules. +rules: + # The action to take when the rule is matches. Must be of: DROP, RECORD_AND_SAMPLE. + - action: DROP + # The span attribute to match against. + attribute: url.path + # The pattern to compare the span attribute to. + pattern: /actuator.* +``` + +`rule_based_routing` sampler can be used anywhere a sampler is used in the configuration model. For example, the following YAML demonstrates a typical configuration, setting `rule_based_routing` sampler as the `root` sampler of `parent_based` sampler. In this configuration: + +* The `parent_based` sampler samples based on the sampling status of the parent. +* Or, if there is no parent, delegates to the `rule_based_routing` sampler. +* The `rule_based_routing` sampler drops spans where `kind=SERVER` and `url.full matches /actuator.*`, else it samples and records. + +```yaml +// ... the rest of the configuration file is omitted for brevity +// For more examples see: https://github.com/open-telemetry/opentelemetry-configuration/blob/main/README.md#starter-templates +tracer_provider: + sampler: + parent_based: + # Configure the parent_based sampler's root sampler to be rule_based_routing sampler. + root: + rule_based_routing: + # Fallback to the always_on sampler if the criteria is not met. + fallback_sampler: + always_on: + # Only apply to SERVER spans. + span_kind: SERVER + rules: + # Drop spans where url.path matches the regex /actuator.* (i.e. spring boot actuator endpoints). + - action: DROP + attribute: url.path + pattern: /actuator.* +``` + ## Component owners +- [Jack Berg](https://github.com/jack-berg), New Relic - [Trask Stalnaker](https://github.com/trask), Microsoft Learn more about component owners in [component_owners.yml](../.github/component_owners.yml). diff --git a/samplers/build.gradle.kts b/samplers/build.gradle.kts index ffe2631d3..47451c79c 100644 --- a/samplers/build.gradle.kts +++ b/samplers/build.gradle.kts @@ -8,8 +8,13 @@ otelJava.moduleName.set("io.opentelemetry.contrib.sampler") dependencies { api("io.opentelemetry:opentelemetry-sdk") + + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator") + implementation("io.opentelemetry.semconv:opentelemetry-semconv") + testImplementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating") testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") - api("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator") } 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 new file mode 100644 index 000000000..9bdc0564d --- /dev/null +++ b/samplers/src/main/java/io/opentelemetry/contrib/sampler/internal/RuleBasedRoutingSamplerComponentProvider.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.internal; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.contrib.sampler.RuleBasedRoutingSampler; +import io.opentelemetry.contrib.sampler.RuleBasedRoutingSamplerBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; +import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties; +import io.opentelemetry.sdk.extension.incubator.fileconfig.FileConfiguration; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.util.List; + +/** + * Declarative configuration SPI implementation for {@link RuleBasedRoutingSampler}. + * + *

    This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public class RuleBasedRoutingSamplerComponentProvider 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 "rule_based_routing"; + } + + @Override + public Sampler create(StructuredConfigProperties config) { + StructuredConfigProperties fallbackModel = config.getStructured("fallback_sampler"); + if (fallbackModel == null) { + throw new ConfigurationException( + "rule_based_routing sampler .fallback is required but is null"); + } + Sampler fallbackSampler; + try { + fallbackSampler = FileConfiguration.createSampler(fallbackModel); + } catch (ConfigurationException e) { + throw new ConfigurationException( + "rule_Based_routing sampler failed to create .fallback sampler", e); + } + + String spanKindString = config.getString("span_kind", "SERVER"); + SpanKind spanKind; + try { + spanKind = SpanKind.valueOf(spanKindString); + } catch (IllegalArgumentException e) { + throw new ConfigurationException( + "rule_based_routing sampler .span_kind is invalid: " + spanKindString, e); + } + + RuleBasedRoutingSamplerBuilder builder = + RuleBasedRoutingSampler.builder(spanKind, fallbackSampler); + + List rules = config.getStructuredList("rules"); + if (rules == null || rules.isEmpty()) { + throw new ConfigurationException("rule_based_routing sampler .rules is required"); + } + + for (StructuredConfigProperties rule : rules) { + String attribute = rule.getString("attribute"); + if (attribute == null) { + throw new ConfigurationException( + "rule_based_routing sampler .rules[].attribute is required"); + } + AttributeKey attributeKey = AttributeKey.stringKey(attribute); + String pattern = rule.getString("pattern"); + if (pattern == null) { + throw new ConfigurationException("rule_based_routing sampler .rules[].pattern is required"); + } + String action = rule.getString("action"); + if (action == null) { + throw new ConfigurationException("rule_based_routing sampler .rules[].action is required"); + } + if (action.equals(ACTION_RECORD_AND_SAMPLE)) { + builder.recordAndSample(attributeKey, pattern); + } else if (action.equals(ACTION_DROP)) { + builder.drop(attributeKey, pattern); + } else { + throw new ConfigurationException( + "rule_based_routing sampler .rules[].action is must be " + + ACTION_RECORD_AND_SAMPLE + + " or " + + ACTION_DROP); + } + } + + return builder.build(); + } +} 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 new file mode 100644 index 000000000..32c554481 --- /dev/null +++ b/samplers/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider @@ -0,0 +1 @@ +io.opentelemetry.contrib.sampler.internal.RuleBasedRoutingSamplerComponentProvider diff --git a/samplers/src/test/java/internal/RuleBasedRoutingSamplerComponentProviderTest.java b/samplers/src/test/java/internal/RuleBasedRoutingSamplerComponentProviderTest.java new file mode 100644 index 000000000..e5807452b --- /dev/null +++ b/samplers/src/test/java/internal/RuleBasedRoutingSamplerComponentProviderTest.java @@ -0,0 +1,223 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +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.contrib.sampler.RuleBasedRoutingSampler; +import io.opentelemetry.contrib.sampler.internal.RuleBasedRoutingSamplerComponentProvider; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties; +import io.opentelemetry.sdk.extension.incubator.fileconfig.FileConfiguration; +import io.opentelemetry.sdk.trace.IdGenerator; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +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 RuleBasedRoutingSamplerComponentProviderTest { + + private static final RuleBasedRoutingSamplerComponentProvider PROVIDER = + new RuleBasedRoutingSamplerComponentProvider(); + + @Test + void endToEnd() { + String yaml = + "file_format: 0.1\n" + + "tracer_provider:\n" + + " sampler:\n" + + " parent_based:\n" + + " root:\n" + + " rule_based_routing:\n" + + " fallback_sampler:\n" + + " always_on:\n" + + " span_kind: SERVER\n" + + " rules:\n" + + " - attribute: url.path\n" + + " pattern: /actuator.*\n" + + " action: DROP\n"; + OpenTelemetrySdk openTelemetrySdk = + FileConfiguration.parseAndCreate( + new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8))); + Sampler sampler = openTelemetrySdk.getSdkTracerProvider().getSampler(); + assertThat(sampler.toString()) + .isEqualTo( + "ParentBased{" + + "root:RuleBasedRoutingSampler{" + + "rules=[" + + "SamplingRule{attributeKey=url.path, delegate=AlwaysOffSampler, pattern=/actuator.*}" + + "], " + + "kind=SERVER, " + + "fallback=AlwaysOnSampler" + + "}," + + "remoteParentSampled:AlwaysOnSampler," + + "remoteParentNotSampled:AlwaysOffSampler," + + "localParentSampled:AlwaysOnSampler," + + "localParentNotSampled:AlwaysOffSampler" + + "}"); + + // SERVER span to /actuator.* path should be dropped + 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 /actuator/health", + SpanKind.SERVER, + Attributes.builder().put("url.path", "/v1/users").build(), + Collections.emptyList())) + .isEqualTo(SamplingResult.recordAndSample()); + } + + @ParameterizedTest + @MethodSource("createValidArgs") + void create_Valid(String yaml, RuleBasedRoutingSampler expectedSampler) { + StructuredConfigProperties configProperties = + FileConfiguration.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" + + "span_kind: SERVER\n" + + "rules:\n" + + " - attribute: url.path\n" + + " pattern: path\n" + + " action: DROP\n", + RuleBasedRoutingSampler.builder(SpanKind.SERVER, Sampler.alwaysOn()) + .drop(AttributeKey.stringKey("url.path"), "path") + .build()), + Arguments.of( + "fallback_sampler:\n" + + " always_off:\n" + + "span_kind: SERVER\n" + + "rules:\n" + + " - attribute: url.path\n" + + " pattern: path\n" + + " action: RECORD_AND_SAMPLE\n", + RuleBasedRoutingSampler.builder(SpanKind.SERVER, Sampler.alwaysOff()) + .recordAndSample(AttributeKey.stringKey("url.path"), "path") + .build()), + Arguments.of( + "fallback_sampler:\n" + + " always_off:\n" + + "span_kind: CLIENT\n" + + "rules:\n" + + " - attribute: http.request.method\n" + + " pattern: GET\n" + + " action: DROP\n" + + " - attribute: url.path\n" + + " pattern: /foo/bar\n" + + " action: DROP\n", + RuleBasedRoutingSampler.builder(SpanKind.CLIENT, Sampler.alwaysOff()) + .drop(AttributeKey.stringKey("http.request.method"), "GET") + .drop(AttributeKey.stringKey("url.path"), "/foo/bar") + .build())); + } + + @ParameterizedTest + @MethodSource("createInvalidArgs") + void create_Invalid(String yaml, String expectedErrorMessage) { + StructuredConfigProperties configProperties = + FileConfiguration.toConfigProperties( + new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8))); + + assertThatThrownBy(() -> PROVIDER.create(configProperties)) + .isInstanceOf(ConfigurationException.class) + .hasMessage(expectedErrorMessage); + } + + static Stream createInvalidArgs() { + return Stream.of( + Arguments.of( + "span_kind: SERVER\n" + + "rules:\n" + + " - attribute: url.path\n" + + " pattern: path\n", + "rule_based_routing sampler .fallback is required but is null"), + Arguments.of( + "fallback_sampler:\n" + + " foo:\n" + + "span_kind: foo\n" + + "rules:\n" + + " - attribute: url.path\n" + + " pattern: path\n", + "rule_Based_routing sampler failed to create .fallback sampler"), + Arguments.of( + "fallback_sampler:\n" + + " always_on:\n" + + "span_kind: foo\n" + + "rules:\n" + + " - attribute: url.path\n" + + " pattern: path\n", + "rule_based_routing sampler .span_kind is invalid: foo"), + Arguments.of( + "fallback_sampler:\n" + " always_on:\n" + "span_kind: SERVER\n", + "rule_based_routing sampler .rules is required"), + Arguments.of( + "fallback_sampler:\n" + " always_on:\n" + "span_kind: SERVER\n" + "rules: []\n", + "rule_based_routing sampler .rules is required"), + Arguments.of( + "fallback_sampler:\n" + + " always_on:\n" + + "span_kind: SERVER\n" + + "rules:\n" + + " - attribute: url.path\n", + "rule_based_routing sampler .rules[].pattern is required"), + Arguments.of( + "fallback_sampler:\n" + + " always_on:\n" + + "span_kind: SERVER\n" + + "rules:\n" + + " - pattern: path\n", + "rule_based_routing sampler .rules[].attribute is required"), + Arguments.of( + "fallback_sampler:\n" + + " always_on:\n" + + "span_kind: SERVER\n" + + "rules:\n" + + " - attribute: url.path\n" + + " pattern: path\n", + "rule_based_routing sampler .rules[].action is required"), + Arguments.of( + "fallback_sampler:\n" + + " always_on:\n" + + "span_kind: SERVER\n" + + "rules:\n" + + " - attribute: url.path\n" + + " pattern: path\n" + + " action: foo\n", + "rule_based_routing sampler .rules[].action is must be RECORD_AND_SAMPLE or DROP")); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e6cddf9c8..0e32492b7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,7 @@ pluginManagement { plugins { id("com.github.johnrengelman.shadow") version "8.1.1" - id("com.gradle.develocity") version "3.18" + id("com.gradle.develocity") version "3.18.1" id("io.github.gradle-nexus.publish-plugin") version "2.0.0" } } @@ -71,7 +71,7 @@ include(":example") include(":jfr-events") include(":jfr-connection") include(":jmx-metrics") -include(":jmx-scrapper") +include(":jmx-scraper") include(":maven-extension") include(":micrometer-meter-provider") include(":noop-api") diff --git a/span-stacktrace/README.md b/span-stacktrace/README.md index 2cffbcb38..fcd9f6554 100644 --- a/span-stacktrace/README.md +++ b/span-stacktrace/README.md @@ -20,13 +20,17 @@ section below to configure it. ### Manual SDK setup Here is an example registration of `StackTraceSpanProcessor` to capture stack trace for all -the spans that have a duration >= 1000 ns. The spans that have an `ignorespan` string attribute +the spans that have a duration >= 1 ms. The spans that have an `ignorespan` string attribute will be ignored. ```java InMemorySpanExporter spansExporter = InMemorySpanExporter.create(); SpanProcessor exportProcessor = SimpleSpanProcessor.create(spansExporter); +Map configMap = new HashMap<>(); +configMap.put("otel.java.experimental.span-stacktrace.min.duration", "1ms"); +ConfigProperties config = DefaultConfigProperties.createFromMap(configMap); + Predicate filterPredicate = readableSpan -> { if(readableSpan.getAttribute(AttributeKey.stringKey("ignorespan")) != null){ return false; @@ -34,12 +38,20 @@ Predicate filterPredicate = readableSpan -> { return true; }; SdkTracerProvider tracerProvider = SdkTracerProvider.builder() - .addSpanProcessor(new StackTraceSpanProcessor(exportProcessor, 1000, filterPredicate)) + .addSpanProcessor(new StackTraceSpanProcessor(exportProcessor, config, filterPredicate)) .build(); OpenTelemetrySdk sdk = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build(); ``` +### Configuration + +The `otel.java.experimental.span-stacktrace.min.duration` configuration option (defaults to 5ms) allows configuring +the minimal duration for which spans should have a stacktrace captured. + +Setting `otel.java.experimental.span-stacktrace.min.duration` to zero will include all spans, and using a negative +value will disable the feature. + ## Component owners - [Jack Shirazi](https://github.com/jackshirazi), Elastic diff --git a/span-stacktrace/build.gradle.kts b/span-stacktrace/build.gradle.kts index 80e861635..50901b6e4 100644 --- a/span-stacktrace/build.gradle.kts +++ b/span-stacktrace/build.gradle.kts @@ -10,5 +10,10 @@ dependencies { api("io.opentelemetry:opentelemetry-sdk") testImplementation("io.opentelemetry:opentelemetry-sdk-testing") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") + testImplementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating") } diff --git a/span-stacktrace/src/main/java/io/opentelemetry/contrib/stacktrace/StackTraceSpanProcessor.java b/span-stacktrace/src/main/java/io/opentelemetry/contrib/stacktrace/StackTraceSpanProcessor.java index 4b9f99ad4..1714cc917 100644 --- a/span-stacktrace/src/main/java/io/opentelemetry/contrib/stacktrace/StackTraceSpanProcessor.java +++ b/span-stacktrace/src/main/java/io/opentelemetry/contrib/stacktrace/StackTraceSpanProcessor.java @@ -8,16 +8,22 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.contrib.stacktrace.internal.AbstractSimpleChainingSpanProcessor; import io.opentelemetry.contrib.stacktrace.internal.MutableSpan; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.SpanProcessor; import java.io.PrintWriter; import java.io.StringWriter; +import java.time.Duration; import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; public class StackTraceSpanProcessor extends AbstractSimpleChainingSpanProcessor { + private static final String CONFIG_MIN_DURATION = + "otel.java.experimental.span-stacktrace.min.duration"; + private static final Duration CONFIG_MIN_DURATION_DEFAULT = Duration.ofMillis(5); + // inlined incubating attribute to prevent direct dependency on incubating semconv private static final AttributeKey SPAN_STACKTRACE = AttributeKey.stringKey("code.stacktrace"); @@ -38,10 +44,27 @@ public StackTraceSpanProcessor( super(next); this.minSpanDurationNanos = minSpanDurationNanos; this.filterPredicate = filterPredicate; - logger.log( - Level.FINE, - "Stack traces will be added to spans with a minimum duration of {0} nanos", - minSpanDurationNanos); + if (minSpanDurationNanos < 0) { + logger.log(Level.FINE, "Stack traces capture is disabled"); + } else { + logger.log( + Level.FINE, + "Stack traces will be added to spans with a minimum duration of {0} nanos", + minSpanDurationNanos); + } + } + + /** + * @param next next span processor to invoke + * @param config configuration + * @param filterPredicate extra filter function to exclude spans if needed + */ + public StackTraceSpanProcessor( + SpanProcessor next, ConfigProperties config, Predicate filterPredicate) { + this( + next, + config.getDuration(CONFIG_MIN_DURATION, CONFIG_MIN_DURATION_DEFAULT).toNanos(), + filterPredicate); } @Override @@ -56,7 +79,7 @@ protected boolean requiresEnd() { @Override protected ReadableSpan doOnEnd(ReadableSpan span) { - if (span.getLatencyNanos() < minSpanDurationNanos) { + if (minSpanDurationNanos < 0 || span.getLatencyNanos() < minSpanDurationNanos) { return span; } if (span.getAttribute(SPAN_STACKTRACE) != null) { diff --git a/span-stacktrace/src/test/java/io/opentelemetry/contrib/stacktrace/StackTraceSpanProcessorTest.java b/span-stacktrace/src/test/java/io/opentelemetry/contrib/stacktrace/StackTraceSpanProcessorTest.java index 87cf2093b..0ddffec9e 100644 --- a/span-stacktrace/src/test/java/io/opentelemetry/contrib/stacktrace/StackTraceSpanProcessorTest.java +++ b/span-stacktrace/src/test/java/io/opentelemetry/contrib/stacktrace/StackTraceSpanProcessorTest.java @@ -13,58 +13,71 @@ import io.opentelemetry.context.Scope; import io.opentelemetry.contrib.stacktrace.internal.TestUtils; import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.SpanProcessor; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; +import java.time.Duration; import java.time.Instant; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class StackTraceSpanProcessorTest { - private InMemorySpanExporter spansExporter; - private SpanProcessor exportProcessor; - - @BeforeEach - public void setup() { - spansExporter = InMemorySpanExporter.create(); - exportProcessor = SimpleSpanProcessor.create(spansExporter); + private static long msToNs(int ms) { + return Duration.ofMillis(ms).toNanos(); } @Test void durationAndFiltering() { + // on duration threshold + checkSpanWithStackTrace(span -> true, "1ms", msToNs(1)); // over duration threshold - testSpan(span -> true, 11, 1); + checkSpanWithStackTrace(span -> true, "1ms", msToNs(2)); // under duration threshold - testSpan(span -> true, 9, 0); + checkSpanWithoutStackTrace(span -> true, "2ms", msToNs(1)); // filtering out span - testSpan(span -> false, 20, 0); + checkSpanWithoutStackTrace(span -> false, "1ms", 20); + } + + @Test + void defaultConfig() { + long expectedDefault = msToNs(5); + checkSpanWithStackTrace(span -> true, null, expectedDefault); + checkSpanWithStackTrace(span -> true, null, expectedDefault + 1); + checkSpanWithoutStackTrace(span -> true, null, expectedDefault - 1); + } + + @Test + void disabledConfig() { + checkSpanWithoutStackTrace(span -> true, "-1", 5); } @Test void spanWithExistingStackTrace() { - testSpan( + checkSpan( span -> true, - 20, - 1, + "1ms", + Duration.ofMillis(1).toNanos(), sb -> sb.setAttribute(CodeIncubatingAttributes.CODE_STACKTRACE, "hello"), stacktrace -> assertThat(stacktrace).isEqualTo("hello")); } - private void testSpan( - Predicate filterPredicate, long spanDurationNanos, int expectedSpansCount) { - testSpan( + private static void checkSpanWithStackTrace( + Predicate filterPredicate, String configString, long spanDurationNanos) { + checkSpan( filterPredicate, + configString, spanDurationNanos, - expectedSpansCount, Function.identity(), (stackTrace) -> assertThat(stackTrace) @@ -72,14 +85,35 @@ private void testSpan( .contains(StackTraceSpanProcessorTest.class.getCanonicalName())); } - private void testSpan( + private static void checkSpanWithoutStackTrace( + Predicate filterPredicate, String configString, long spanDurationNanos) { + checkSpan( + filterPredicate, + configString, + spanDurationNanos, + Function.identity(), + (stackTrace) -> assertThat(stackTrace).describedAs("no stack trace expected").isNull()); + } + + private static void checkSpan( Predicate filterPredicate, + String configString, long spanDurationNanos, - int expectedSpansCount, Function customizeSpanBuilder, Consumer stackTraceCheck) { + + // they must be re-created as they are shutdown when the span processor is closed + InMemorySpanExporter spansExporter = InMemorySpanExporter.create(); + SpanProcessor exportProcessor = SimpleSpanProcessor.create(spansExporter); + + Map configMap = new HashMap<>(); + if (configString != null) { + configMap.put("otel.java.experimental.span-stacktrace.min.duration", configString); + } + try (SpanProcessor processor = - new StackTraceSpanProcessor(exportProcessor, 10, filterPredicate)) { + new StackTraceSpanProcessor( + exportProcessor, DefaultConfigProperties.createFromMap(configMap), filterPredicate)) { OpenTelemetrySdk sdk = TestUtils.sdkWith(processor); Tracer tracer = sdk.getTracer("test"); @@ -96,14 +130,12 @@ private void testSpan( } List finishedSpans = spansExporter.getFinishedSpanItems(); - assertThat(finishedSpans).hasSize(expectedSpansCount); + assertThat(finishedSpans).hasSize(1); - if (!finishedSpans.isEmpty()) { - String stackTrace = - finishedSpans.get(0).getAttributes().get(CodeIncubatingAttributes.CODE_STACKTRACE); + String stackTrace = + finishedSpans.get(0).getAttributes().get(CodeIncubatingAttributes.CODE_STACKTRACE); - stackTraceCheck.accept(stackTrace); - } + stackTraceCheck.accept(stackTrace); } } }