From 0f60e222937d12ee409706da484d806f5c78609f Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Thu, 5 Sep 2024 10:58:10 +0200 Subject: [PATCH 01/39] module bootstrap --- jmx-scrapper/README.md | 6 ++++++ jmx-scrapper/build.gradle.kts | 12 ++++++++++++ .../contrib/jmxscrapper/JmxMetrics.java | 10 ++++++++++ settings.gradle.kts | 1 + 4 files changed, 29 insertions(+) create mode 100644 jmx-scrapper/README.md create mode 100644 jmx-scrapper/build.gradle.kts create mode 100644 jmx-scrapper/src/main/java/io/opentelemetry/contrib/jmxscrapper/JmxMetrics.java diff --git a/jmx-scrapper/README.md b/jmx-scrapper/README.md new file mode 100644 index 000000000..9c952d83a --- /dev/null +++ b/jmx-scrapper/README.md @@ -0,0 +1,6 @@ +# JMX Metric Scrapper + +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. + +This is currently experimental, but the end goal is to provide an alternative to the [jmx-metrics](../jmx-metrics/README.md) utility. diff --git a/jmx-scrapper/build.gradle.kts b/jmx-scrapper/build.gradle.kts new file mode 100644 index 000000000..7b2bdef9a --- /dev/null +++ b/jmx-scrapper/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + application + id("com.github.johnrengelman.shadow") + + id("otel.java-conventions") + id("otel.publish-conventions") +} + +description = "JMX metrics scrapper" +otelJava.moduleName.set("io.opentelemetry.contrib.jmxscrapper") + +application.mainClass.set("io.opentelemetry.contrib.jmxscrapper.JmxMetrics") 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 new file mode 100644 index 000000000..29f423811 --- /dev/null +++ b/jmx-scrapper/src/main/java/io/opentelemetry/contrib/jmxscrapper/JmxMetrics.java @@ -0,0 +1,10 @@ +package io.opentelemetry.contrib.jmxscrapper; + +public class JmxMetrics { + + private JmxMetrics() { + } + + public static void main(String[] args) { + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 917288732..4505ae5e8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -70,6 +70,7 @@ include(":example") include(":jfr-events") include(":jfr-connection") include(":jmx-metrics") +include(":jmx-scrapper") include(":maven-extension") include(":micrometer-meter-provider") include(":noop-api") From 720633c39330e16cd372b8ab07d356657b6b70ca Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:14:35 +0200 Subject: [PATCH 02/39] add dependencies + test setup --- jmx-scrapper/build.gradle.kts | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/jmx-scrapper/build.gradle.kts b/jmx-scrapper/build.gradle.kts index 7b2bdef9a..061859b11 100644 --- a/jmx-scrapper/build.gradle.kts +++ b/jmx-scrapper/build.gradle.kts @@ -10,3 +10,56 @@ description = "JMX metrics scrapper" otelJava.moduleName.set("io.opentelemetry.contrib.jmxscrapper") application.mainClass.set("io.opentelemetry.contrib.jmxscrapper.JmxMetrics") + +dependencies { + implementation("io.opentelemetry:opentelemetry-api") + implementation("io.opentelemetry:opentelemetry-sdk") + implementation("io.opentelemetry:opentelemetry-sdk-metrics") + implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + implementation("io.opentelemetry:opentelemetry-sdk-testing") + + implementation("io.opentelemetry.instrumentation:opentelemetry-jmx-metrics") + +} + + +tasks { + shadowJar { + mergeServiceFiles() + + manifest { + attributes["Implementation-Version"] = project.version + } + // This should always be standalone, so remove "-all" to prevent unnecessary artifact. + archiveClassifier.set("") + } + + jar { + archiveClassifier.set("noshadow") + } + + withType().configureEach { + dependsOn(shadowJar) + systemProperty("shadow.jar.path", shadowJar.get().archiveFile.get().asFile.absolutePath) + systemProperty("gradle.project.version", "${project.version}") + } + + // Because we reconfigure publishing to only include the shadow jar, the Gradle metadata is not correct. + // Since we are fully bundled and have no dependencies, Gradle metadata wouldn't provide any advantage over + // the POM anyways so in practice we shouldn't be losing anything. + withType().configureEach { + enabled = false + } +} + +// Don't publish non-shadowed jar (shadowJar is in shadowRuntimeElements) +with(components["java"] as AdhocComponentWithVariants) { + configurations.forEach { + withVariantsFromConfiguration(configurations["apiElements"]) { + skip() + } + withVariantsFromConfiguration(configurations["runtimeElements"]) { + skip() + } + } +} From 07027a2386e40abe42bcfac51ad3c9df1cd00ecf Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:17:21 +0200 Subject: [PATCH 03/39] spotless --- jmx-scrapper/build.gradle.kts | 2 -- .../opentelemetry/contrib/jmxscrapper/JmxMetrics.java | 11 +++++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/jmx-scrapper/build.gradle.kts b/jmx-scrapper/build.gradle.kts index 061859b11..5bc45843b 100644 --- a/jmx-scrapper/build.gradle.kts +++ b/jmx-scrapper/build.gradle.kts @@ -19,10 +19,8 @@ dependencies { implementation("io.opentelemetry:opentelemetry-sdk-testing") implementation("io.opentelemetry.instrumentation:opentelemetry-jmx-metrics") - } - tasks { shadowJar { mergeServiceFiles() 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 index 29f423811..b9afbe34f 100644 --- a/jmx-scrapper/src/main/java/io/opentelemetry/contrib/jmxscrapper/JmxMetrics.java +++ b/jmx-scrapper/src/main/java/io/opentelemetry/contrib/jmxscrapper/JmxMetrics.java @@ -1,10 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.contrib.jmxscrapper; public class JmxMetrics { - private JmxMetrics() { - } + private JmxMetrics() {} - public static void main(String[] args) { - } + public static void main(String[] args) {} } From 5a82aaceedfbf9710a4a91fa5942a02765f55082 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Fri, 6 Sep 2024 11:46:12 +0200 Subject: [PATCH 04/39] resolve dependencies on maven local repo --- jmx-metrics/build.gradle.kts | 1 + settings.gradle.kts | 1 + 2 files changed, 2 insertions(+) diff --git a/jmx-metrics/build.gradle.kts b/jmx-metrics/build.gradle.kts index b35d98de7..c6aafe9ed 100644 --- a/jmx-metrics/build.gradle.kts +++ b/jmx-metrics/build.gradle.kts @@ -13,6 +13,7 @@ application.mainClass.set("io.opentelemetry.contrib.jmxmetrics.JmxMetrics") repositories { mavenCentral() + mavenLocal() maven { setUrl("https://repo.terracotta.org/maven2") content { diff --git a/settings.gradle.kts b/settings.gradle.kts index 4505ae5e8..e6cddf9c8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,6 +13,7 @@ plugins { dependencyResolutionManagement { repositories { mavenCentral() + mavenLocal() } } From d623abea7686bcfd5ce711a8edc7175573db3dad Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:40:17 +0200 Subject: [PATCH 05/39] @robsunday PR with upstream updates --- .github/component_owners.yml | 5 + aws-xray-propagator/build.gradle.kts | 2 + .../propagator/AwsXrayLambdaPropagator.java | 5 + .../awsxray/propagator/AwsXrayPropagator.java | 8 +- .../AwsConfigurablePropagator.java | 3 +- .../internal/AwsXrayComponentProvider.java | 28 ++ .../AwsXrayLambdaComponentProvider.java | 28 ++ .../AwsXrayLambdaConfigurablePropagator.java | 3 +- ...nfigure.spi.ConfigurablePropagatorProvider | 2 +- ...toconfigure.spi.internal.ComponentProvider | 2 + .../propagator/AwsXrayPropagatorTest.java | 12 +- .../internal/AwsComponentProviderTest.java | 34 ++ .../contrib/awsxray/AwsXrayRemoteSampler.java | 4 +- buildSrc/build.gradle.kts | 2 +- .../consistent56/ComposableSampler.java | 35 ++ .../ConsistentAlwaysOffSampler.java | 17 +- .../ConsistentAlwaysOnSampler.java | 15 +- .../sampler/consistent56/ConsistentAnyOf.java | 109 ++++++ .../ConsistentComposedAndSampler.java | 52 --- .../ConsistentComposedOrSampler.java | 57 --- .../ConsistentFixedThresholdSampler.java | 25 +- .../ConsistentParentBasedSampler.java | 39 +- .../ConsistentRateLimitingSampler.java | 149 ++++++- .../ConsistentRuleBasedSampler.java | 71 ++++ .../consistent56/ConsistentSampler.java | 173 ++++----- .../sampler/consistent56/Predicate.java | 73 ++++ .../consistent56/PredicatedSampler.java | 32 ++ .../sampler/consistent56/SamplingIntent.java | 42 ++ .../sampler/consistent56/CoinFlipSampler.java | 85 ++++ .../ConsistentAlwaysOffSamplerTest.java | 16 +- .../ConsistentAlwaysOnSamplerTest.java | 14 +- .../consistent56/ConsistentAnyOfTest.java | 63 +++ .../ConsistentRateLimitingSamplerTest.java | 131 ++++++- .../ConsistentRuleBasedSamplerTest.java | 95 +++++ .../sampler/consistent56/MarkingSampler.java | 91 +++++ .../sampler/consistent56/UseCaseTest.java | 128 ++++++ dependencyManagement/build.gradle.kts | 8 +- disk-buffering/build.gradle.kts | 2 +- .../mapping/logs/LogRecordDataMapper.java | 38 +- .../logs/models/LogRecordDataImpl.java | 29 +- .../mapping/logs/LogRecordDataMapperTest.java | 4 +- .../mapping/logs/ProtoLogsDataMapperTest.java | 10 +- .../LogRecordDataSerializerTest.java | 6 +- .../storage/files/ReadableFileTest.java | 8 +- gcp-resources/README.md | 34 +- gradle/wrapper/gradle-wrapper.properties | 4 +- jmx-metrics/README.md | 4 + .../contrib/jmxmetrics/GroovyRunner.java | 3 +- .../jmxmetrics/InstrumentHelper.groovy | 51 ++- .../contrib/jmxmetrics/JmxConfig.java | 5 + .../contrib/jmxmetrics/OtelHelper.groovy | 6 +- .../resources/target-systems/tomcat.groovy | 16 +- .../jmxmetrics/InstrumenterHelperTest.java | 65 +++- .../contrib/jmxmetrics/JmxConfigTest.java | 3 + .../OtelHelperAsynchronousMetricTest.java | 2 +- .../contrib/jmxmetrics/OtelHelperJmxTest.java | 2 +- .../OtelHelperSynchronousMetricTest.java | 2 +- jmx-metrics/src/test/resources/all.properties | 1 + {jmx-scrapper => jmx-scraper}/README.md | 2 +- .../build.gradle.kts | 8 +- .../jmxscraper/ArgumentsParsingException.java | 10 + .../contrib/jmxscraper/JmxScraper.java | 155 ++++++++ .../config/ConfigurationException.java | 18 + .../jmxscraper/config/JmxScraperConfig.java | 72 ++++ .../config/JmxScraperConfigFactory.java | 179 +++++++++ .../jmxscraper/jmx/ClientCallbackHandler.java | 49 +++ .../contrib/jmxscraper/jmx/JmxClient.java | 109 ++++++ .../contrib/jmxscraper/util/StringUtils.java | 22 ++ .../contrib/jmxscraper/JmxScraperTest.java | 39 ++ .../config/JmxScraperConfigFactoryTest.java | 364 ++++++++++++++++++ .../contrib/jmxscrapper/JmxMetrics.java | 13 - micrometer-meter-provider/build.gradle.kts | 4 +- .../InterceptableLogRecordExporterTest.java | 12 +- samplers/README.md | 60 +++ samplers/build.gradle.kts | 7 +- ...eBasedRoutingSamplerComponentProvider.java | 102 +++++ ...toconfigure.spi.internal.ComponentProvider | 1 + ...edRoutingSamplerComponentProviderTest.java | 223 +++++++++++ settings.gradle.kts | 4 +- span-stacktrace/README.md | 16 +- span-stacktrace/build.gradle.kts | 5 + .../stacktrace/StackTraceSpanProcessor.java | 33 +- .../StackTraceSpanProcessorTest.java | 86 +++-- 83 files changed, 3158 insertions(+), 388 deletions(-) rename aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/{ => internal}/AwsConfigurablePropagator.java (84%) create mode 100644 aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsXrayComponentProvider.java create mode 100644 aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsXrayLambdaComponentProvider.java rename aws-xray-propagator/src/main/java/io/opentelemetry/contrib/awsxray/propagator/{ => internal}/AwsXrayLambdaConfigurablePropagator.java (84%) create mode 100644 aws-xray-propagator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider create mode 100644 aws-xray-propagator/src/test/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsComponentProviderTest.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ComposableSampler.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOf.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedAndSampler.java delete mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentComposedOrSampler.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSampler.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/Predicate.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/PredicatedSampler.java create mode 100644 consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/SamplingIntent.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/CoinFlipSampler.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentAnyOfTest.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentRuleBasedSamplerTest.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/MarkingSampler.java create mode 100644 consistent-sampling/src/test/java/io/opentelemetry/contrib/sampler/consistent56/UseCaseTest.java rename {jmx-scrapper => jmx-scraper}/README.md (92%) rename {jmx-scrapper => jmx-scraper}/build.gradle.kts (88%) create mode 100644 jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/ArgumentsParsingException.java create mode 100644 jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java create mode 100644 jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/ConfigurationException.java create mode 100644 jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfig.java create mode 100644 jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigFactory.java create mode 100644 jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/ClientCallbackHandler.java create mode 100644 jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/JmxClient.java create mode 100644 jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/util/StringUtils.java create mode 100644 jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java create mode 100644 jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigFactoryTest.java delete mode 100644 jmx-scrapper/src/main/java/io/opentelemetry/contrib/jmxscrapper/JmxMetrics.java create mode 100644 samplers/src/main/java/io/opentelemetry/contrib/sampler/internal/RuleBasedRoutingSamplerComponentProvider.java create mode 100644 samplers/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider create mode 100644 samplers/src/test/java/internal/RuleBasedRoutingSamplerComponentProviderTest.java 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 88% rename from jmx-scrapper/build.gradle.kts rename to jmx-scraper/build.gradle.kts index 5bc45843b..87c2129d8 100644 --- a/jmx-scrapper/build.gradle.kts +++ b/jmx-scraper/build.gradle.kts @@ -6,10 +6,10 @@ plugins { id("otel.publish-conventions") } -description = "JMX metrics scrapper" -otelJava.moduleName.set("io.opentelemetry.contrib.jmxscrapper") +description = "JMX metrics scraper" +otelJava.moduleName.set("io.opentelemetry.contrib.jmxscraper") -application.mainClass.set("io.opentelemetry.contrib.jmxscrapper.JmxMetrics") +application.mainClass.set("io.opentelemetry.contrib.jmxscraper.JmxScraper") dependencies { implementation("io.opentelemetry:opentelemetry-api") @@ -19,6 +19,8 @@ dependencies { implementation("io.opentelemetry:opentelemetry-sdk-testing") implementation("io.opentelemetry.instrumentation:opentelemetry-jmx-metrics") + + testImplementation("org.junit-pioneer:junit-pioneer") } tasks { 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-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); } } } From f7b5a31538aa01c9c8058c3a0495b2c35c807461 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:43:36 +0200 Subject: [PATCH 06/39] @SylvainJuge PR commit b84a73e9cb2d91fa233541bb6c03535818f01df3 Merge: 352202f 7df9862 Author: SylvainJuge <763082+SylvainJuge@users.noreply.github.com> Date: Tue Sep 17 11:13:11 2024 +0200 Merge pull request #2 from SylvainJuge/jmx-scraper-it basic JMX client implementation + tests commit 7df9862348e20d98b9efdf74ed0a79fd2a786bf2 Author: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue Sep 17 11:11:50 2024 +0200 fix bad merge again commit e4a8f83e66fb36f74c7f853d8e7142edcf417194 Author: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue Sep 17 11:10:26 2024 +0200 fix bad merge again commit dad197b551206b7e65971f953a13cefa494a3c42 Author: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue Sep 17 11:09:18 2024 +0200 fix bad merge commit f8a73d7866577a8895f68021b0c0734f9350beda Author: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue Sep 17 11:08:42 2024 +0200 spotless commit eab01e9220982b92df8aefc0726330d7341b7c31 Author: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue Sep 17 11:01:47 2024 +0200 spotless commit f6667dd88a12994cdf3dc1e6e30bf58b64b7055f Merge: 5b3ef9d 352202f Author: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue Sep 17 10:55:34 2024 +0200 Merge branch 'jmx-scrapper' of github.com:SylvainJuge/opentelemetry-java-contrib into jmx-scraper-it commit 5b3ef9d5fd0ab364679db77b273d3fc288681391 Author: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue Sep 17 10:51:11 2024 +0200 add TODOs commit 352202f1b0fe03b35a815e0cf183b468cbbcaf45 Merge: 5a82aac 40a2591 Author: SylvainJuge <763082+SylvainJuge@users.noreply.github.com> Date: Tue Sep 17 10:50:50 2024 +0200 Merge pull request #1 from robsunday/jmx-scrapper Initial commit of JmxScraper code. commit 40a259154057c9301edac2b91e911d23a7fa75f4 Author: jack-berg <34418638+jack-berg@users.noreply.github.com> Date: Mon Sep 16 16:05:50 2024 -0500 Add declarative config support for RuleBasedRoutingSampler (#1440) commit 6d39e9336b92379abae9bf025d598184b9933c73 Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Mon Sep 16 10:40:16 2024 -0700 Update dependency com.uber.nullaway:nullaway to v0.11.3 (#1457) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit eb5dc6ffa1012a83b1d4276755174ba3452bcc08 Author: Bruno Baptista Date: Mon Sep 16 18:26:53 2024 +0100 Fix native mode error cause by static init of random (#862) commit 5ffa34889898e43a3a4955ea48c52366bcf4763c Author: jack-berg <34418638+jack-berg@users.noreply.github.com> Date: Mon Sep 16 12:23:41 2024 -0500 Add declarative config support for aws xray propagators (#1442) commit 151b74446d65fd483972fbffe6720cc9fd164918 Author: SylvainJuge <763082+SylvainJuge@users.noreply.github.com> Date: Mon Sep 16 19:23:19 2024 +0200 add span stacktrace config option (#1414) Co-authored-by: jackshirazi commit 9ee5fb1ed54ff9b739fef7042e3a383e721e9dec Author: Yury Bubnov Date: Mon Sep 16 10:18:51 2024 -0700 Issue-1034 Short XRay Trace (#1036) commit 11a2e1a4dca2c329cf2449950bbac5b875858299 Author: Jeffrey Chien Date: Mon Sep 16 13:18:10 2024 -0400 Fix Tomcat metric definitions to aggregate multiple MBeans. (#1366) commit 9fa44beddc52446b89f17eda9e3697a8a21d49bb Author: Pranav Sharma Date: Mon Sep 16 11:22:32 2024 -0400 Fix incorrect cloud.platform value for GCF (#1454) commit 11f2111d088ea824a844ac818c66e827c5f0fa79 Author: Peter Findeisen Date: Mon Sep 16 08:21:46 2024 -0700 Composite Samplers prototype (#1443) Co-authored-by: Otmar Ertl commit b3386debb1434dd7bb888625f6509bc9e9a1d635 Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Mon Sep 16 08:20:44 2024 -0700 Update plugin com.squareup.wire to v5.1.0 (#1452) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit 27b631ddf56da907a5d56591246288ac2f542e56 Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Mon Sep 16 08:20:21 2024 -0700 Update errorProneVersion to v2.32.0 (#1453) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit 791df4b0b85ea267bdda8fe49e9009ac07cdb6e4 Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Mon Sep 16 09:50:03 2024 -0500 Update dependency io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha to v2.8.0-alpha (#1456) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jack Berg commit 19357e6885b5c6596d8e59bad02fa597b1fbf090 Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Tue Sep 10 15:20:46 2024 -0700 Update dependency com.gradle.enterprise:com.gradle.enterprise.gradle.plugin to v3.18.1 (#1450) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit 460470b7e6f8799825ea9276e69be35bc48f2faa Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Tue Sep 10 15:20:23 2024 -0700 Update plugin com.gradle.develocity to v3.18.1 (#1451) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit b45cdab4d422d76b6e40979a7c07791853efc78f Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Tue Sep 10 12:32:56 2024 +0300 Update dependency com.linecorp.armeria:armeria-bom to v1.30.1 (#1449) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit d36106ea2b0840309d9830bec06be78224118835 Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Tue Sep 10 09:11:26 2024 +0300 Update micrometer packages to v1.13.4 (#1448) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit fb25c798d666f756d86715c6423cbb0db35783e1 Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Mon Sep 9 15:44:53 2024 +0300 Update dependency gradle to v8.10.1 (#1447) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit f847561e1c6763f0d9f5a648c7d6e73442b1183b Author: robsunday Date: Tue Sep 17 09:22:21 2024 +0200 Code review changes: Argument parsing moved to the main class. Argument validation now throw exception. More tests added for config factory. Created unit tests for JmxScraper class. commit 03788ffb69200e9ec066ff98f1dd722f5ed53ea7 Author: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Mon Sep 16 17:26:12 2024 +0200 add TODO for testing server SSL support commit a3fbeb51a3afe62377a32536167ba6a25dd38263 Author: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Mon Sep 16 17:08:16 2024 +0200 add TODO to enable server-side SSL commit 161959506da44a63e879996994ae276da4b2823b Author: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Mon Sep 16 16:59:17 2024 +0200 wip commit 164679f431fe423259077538ebb75d3bf5aa45ef Author: robsunday Date: Wed Sep 11 12:34:12 2024 +0200 Cleanup of config. Refactoring of JmxScraperConfigFactory. Added first unit tests. commit 165fcb8a42c244f67548a57e8f42332da9f8d2ae Author: robsunday Date: Tue Sep 10 12:36:29 2024 +0200 Initial commit of JmxScraper code. Application compiles and runs as standalone and can read config file. --- jmx-scraper/build.gradle.kts | 21 ++ .../contrib/jmxscraper/TestApp.java | 60 +++++ .../contrib/jmxscraper/TestAppMXBean.java | 14 + .../client/JmxRemoteClientTest.java | 240 ++++++++++++++++++ .../jmxscraper/client/JmxRemoteClient.java | 151 +++++++++++ 5 files changed, 486 insertions(+) create mode 100644 jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestApp.java create mode 100644 jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppMXBean.java create mode 100644 jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClientTest.java create mode 100644 jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java diff --git a/jmx-scraper/build.gradle.kts b/jmx-scraper/build.gradle.kts index 87c2129d8..d3c7e4b58 100644 --- a/jmx-scraper/build.gradle.kts +++ b/jmx-scraper/build.gradle.kts @@ -23,6 +23,17 @@ dependencies { testImplementation("org.junit-pioneer:junit-pioneer") } +testing { + suites { + val integrationTest by registering(JvmTestSuite::class) { + dependencies { + implementation("org.testcontainers:junit-jupiter") + implementation("org.slf4j:slf4j-simple") + } + } + } +} + tasks { shadowJar { mergeServiceFiles() @@ -40,7 +51,9 @@ tasks { withType().configureEach { dependsOn(shadowJar) + dependsOn(named("appJar")) systemProperty("shadow.jar.path", shadowJar.get().archiveFile.get().asFile.absolutePath) + systemProperty("app.jar.path", named("appJar").get().archiveFile.get().asFile.absolutePath) systemProperty("gradle.project.version", "${project.version}") } @@ -52,6 +65,14 @@ tasks { } } +tasks.register("appJar") { + from(sourceSets.get("integrationTest").output) + archiveClassifier.set("app") + manifest { + attributes["Main-Class"] = "io.opentelemetry.contrib.jmxscraper.TestApp" + } +} + // Don't publish non-shadowed jar (shadowJar is in shadowRuntimeElements) with(components["java"] as AdhocComponentWithVariants) { configurations.forEach { diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestApp.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestApp.java new file mode 100644 index 000000000..1316ca036 --- /dev/null +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestApp.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper; + +import java.lang.management.ManagementFactory; +import javax.management.MBeanServer; +import javax.management.ObjectName; + +@SuppressWarnings("all") +public class TestApp implements TestAppMXBean { + + public static final String APP_STARTED_MSG = "app started"; + public static final String OBJECT_NAME = "io.opentelemetry.test:name=TestApp"; + + private volatile boolean running; + + public static void main(String[] args) { + TestApp app = TestApp.start(); + while (app.isRunning()) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private TestApp() {} + + static TestApp start() { + TestApp app = new TestApp(); + MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); + try { + ObjectName objectName = new ObjectName(OBJECT_NAME); + mbs.registerMBean(app, objectName); + } catch (Exception e) { + throw new RuntimeException(e); + } + app.running = true; + System.out.println(APP_STARTED_MSG); + return app; + } + + @Override + public int getIntValue() { + return 42; + } + + @Override + public void stopApp() { + running = false; + } + + boolean isRunning() { + return running; + } +} diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppMXBean.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppMXBean.java new file mode 100644 index 000000000..11ea69905 --- /dev/null +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppMXBean.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper; + +@SuppressWarnings("unused") +public interface TestAppMXBean { + + int getIntValue(); + + void stopApp(); +} diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClientTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClientTest.java new file mode 100644 index 000000000..291fc9ac5 --- /dev/null +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClientTest.java @@ -0,0 +1,240 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.contrib.jmxscraper.TestApp; +import java.io.Closeable; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.management.ObjectName; +import javax.management.remote.JMXConnector; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.shaded.com.google.errorprone.annotations.CanIgnoreReturnValue; +import org.testcontainers.utility.MountableFile; + +public class JmxRemoteClientTest { + + private static final Logger logger = LoggerFactory.getLogger(JmxRemoteClientTest.class); + + private static Network network; + + private static final List toClose = new ArrayList<>(); + + @BeforeAll + static void beforeAll() { + network = Network.newNetwork(); + toClose.add(network); + } + + @AfterAll + static void afterAll() { + for (AutoCloseable item : toClose) { + try { + item.close(); + } catch (Exception e) { + logger.warn("Error closing " + item, e); + } + } + } + + @Test + void noAuth() { + try (AppContainer app = new AppContainer().withJmxPort(9990).start()) { + testConnector(() -> JmxRemoteClient.createNew(app.getHost(), app.getPort()).connect()); + } + } + + @Test + void loginPwdAuth() { + String login = "user"; + String pwd = "t0p!Secret"; + try (AppContainer app = new AppContainer().withJmxPort(9999).withUserAuth(login, pwd).start()) { + testConnector( + () -> + JmxRemoteClient.createNew(app.getHost(), app.getPort()) + .userCredentials(login, pwd) + .connect()); + } + } + + @Test + void serverSSL() { + // TODO: test with SSL enabled as RMI registry seems to work differently with SSL + + // create keypair (public,private) + // create server keystore with private key + // configure server keystore + // + // create client truststore with public key + // can we configure to use a custom truststore ??? + // connect to server + } + + private static void testConnector(ConnectorSupplier connectorSupplier) { + try (JMXConnector connector = connectorSupplier.get()) { + assertThat(connector.getMBeanServerConnection()) + .isNotNull() + .satisfies( + connection -> { + try { + ObjectName name = new ObjectName(TestApp.OBJECT_NAME); + Object value = connection.getAttribute(name, "IntValue"); + assertThat(value).isEqualTo(42); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private interface ConnectorSupplier { + JMXConnector get() throws IOException; + } + + private static class AppContainer implements Closeable { + + private final GenericContainer appContainer; + private final Map properties; + private int port; + private String login; + private String pwd; + + private AppContainer() { + this.properties = new HashMap<>(); + + properties.put("com.sun.management.jmxremote.ssl", "false"); // TODO : + + // SSL registry : com.sun.management.jmxremote.registry.ssl + // client side ssl auth: com.sun.management.jmxremote.ssl.need.client.auth + + String appJar = System.getProperty("app.jar.path"); + assertThat(Paths.get(appJar)).isNotEmptyFile().isReadable(); + + this.appContainer = + new GenericContainer<>("openjdk:8u272-jre-slim") + .withCopyFileToContainer(MountableFile.forHostPath(appJar), "/app.jar") + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withNetwork(network) + .waitingFor( + Wait.forLogMessage(TestApp.APP_STARTED_MSG + "\\n", 1) + .withStartupTimeout(Duration.ofSeconds(5))) + .withCommand("java", "-jar", "/app.jar"); + } + + @CanIgnoreReturnValue + public AppContainer withJmxPort(int port) { + this.port = port; + properties.put("com.sun.management.jmxremote.port", Integer.toString(port)); + appContainer.withExposedPorts(port); + return this; + } + + @CanIgnoreReturnValue + public AppContainer withUserAuth(String login, String pwd) { + this.login = login; + this.pwd = pwd; + return this; + } + + @CanIgnoreReturnValue + AppContainer start() { + if (pwd == null) { + properties.put("com.sun.management.jmxremote.authenticate", "false"); + } else { + properties.put("com.sun.management.jmxremote.authenticate", "true"); + + Path pwdFile = createPwdFile(login, pwd); + appContainer.withCopyFileToContainer(MountableFile.forHostPath(pwdFile), "/jmx.password"); + properties.put("com.sun.management.jmxremote.password.file", "/jmx.password"); + + Path accessFile = createAccessFile(login); + appContainer.withCopyFileToContainer(MountableFile.forHostPath(accessFile), "/jmx.access"); + properties.put("com.sun.management.jmxremote.access.file", "/jmx.access"); + } + + String confArgs = + properties.entrySet().stream() + .map( + e -> { + String s = "-D" + e.getKey(); + if (!e.getValue().isEmpty()) { + s += "=" + e.getValue(); + } + return s; + }) + .collect(Collectors.joining(" ")); + + appContainer.withEnv("JAVA_TOOL_OPTIONS", confArgs).start(); + + logger.info("Test application JMX port mapped to {}:{}", getHost(), getPort()); + + toClose.add(this); + return this; + } + + int getPort() { + return appContainer.getMappedPort(port); + } + + String getHost() { + return appContainer.getHost(); + } + + @Override + public void close() { + if (appContainer.isRunning()) { + appContainer.stop(); + } + } + + private static Path createPwdFile(String login, String pwd) { + try { + Path path = Files.createTempFile("test", ".pwd"); + writeLine(path, String.format("%s %s", login, pwd)); + return path; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static Path createAccessFile(String login) { + try { + Path path = Files.createTempFile("test", ".pwd"); + writeLine(path, String.format("%s %s", login, "readwrite")); + return path; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static void writeLine(Path path, String line) throws IOException { + line = line + "\n"; + Files.write(path, line.getBytes(StandardCharsets.UTF_8)); + } + } +} diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java new file mode 100644 index 000000000..de48ebe6e --- /dev/null +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java @@ -0,0 +1,151 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.client; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.io.IOException; +import java.net.MalformedURLException; +import java.security.Provider; +import java.security.Security; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; +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 JmxRemoteClient { + + private static final Logger logger = Logger.getLogger(JmxRemoteClient.class.getName()); + + private final String host; + private final int port; + @Nullable + private String userName; + @Nullable + private String password; + @Nullable + private String profile; + @Nullable + private String realm; + private boolean sslRegistry; + + private JmxRemoteClient(@Nonnull String host, int port) { + this.host = host; + this.port = port; + } + + public static JmxRemoteClient createNew(String host, int port) { + return new JmxRemoteClient(host, port); + } + + @CanIgnoreReturnValue + public JmxRemoteClient userCredentials(String userName, String password) { + this.userName = userName; + this.password = password; + return this; + } + + @CanIgnoreReturnValue + public JmxRemoteClient withRemoteProfile(String profile) { + this.profile = profile; + return this; + } + + @CanIgnoreReturnValue + public JmxRemoteClient withRealm(String realm) { + this.realm = realm; + return this; + } + + @CanIgnoreReturnValue + public JmxRemoteClient withSslRegistry() { + this.sslRegistry = true; + return this; + } + + public JMXConnector connect() throws IOException { + Map env = new HashMap<>(); + if (userName != null && password != null) { + env.put(JMXConnector.CREDENTIALS, new String[] {userName, password}); + } + + if (profile != null) { + env.put("jmx.remote.profile", profile); + } + + 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.sasl.callback.handler", + (CallbackHandler) + callbacks -> { + for (Callback callback : callbacks) { + if (callback instanceof NameCallback) { + ((NameCallback) callback).setName(userName); + } else if (callback instanceof PasswordCallback) { + char[] pwd = password == null ? null : password.toCharArray(); + ((PasswordCallback) callback).setPassword(pwd); + } else if (callback instanceof RealmCallback) { + ((RealmCallback) callback).setText(realm); + } else { + throw new UnsupportedCallbackException(callback); + } + } + }); + } catch (ReflectiveOperationException e) { + logger.log(Level.WARNING, "SASL unsupported in current environment: " + e.getMessage(), e); + } + + JMXServiceURL url = buildUrl(host, port); + try { + if (sslRegistry) { + return doConnectSslRegistry(url, env); + } else { + return doConnect(url, env); + } + } catch (IOException e) { + throw new IOException("Unable to connect to " + url.getHost() + ":" + url.getPort(), e); + } + } + + @SuppressWarnings("BanJNDI") + private static JMXConnector doConnect(JMXServiceURL url, Map env) + throws IOException { + return JMXConnectorFactory.connect(url, env); + } + + public JMXConnector doConnectSslRegistry(JMXServiceURL url, Map env) { + throw new IllegalStateException("TODO"); + } + + private static JMXServiceURL buildUrl(String host, int port) { + StringBuilder sb = new StringBuilder("service:jmx:rmi:///jndi/rmi://"); + if (host != null) { + sb.append(host); + } + sb.append(":").append(port).append("/jmxrmi"); + + try { + return new JMXServiceURL(sb.toString()); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("invalid url", e); + } + } +} From 48b095d8661aea9f38698f0a64b81db4bc8f5437 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:48:56 +0200 Subject: [PATCH 07/39] spotless --- .../contrib/jmxscraper/client/JmxRemoteClient.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java index de48ebe6e..449b05ba4 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java @@ -32,14 +32,10 @@ public class JmxRemoteClient { private final String host; private final int port; - @Nullable - private String userName; - @Nullable - private String password; - @Nullable - private String profile; - @Nullable - private String realm; + @Nullable private String userName; + @Nullable private String password; + @Nullable private String profile; + @Nullable private String realm; private boolean sslRegistry; private JmxRemoteClient(@Nonnull String host, int port) { From e4f47691ac9d07e3fde79e80dce84a4138bfc70e Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:44:40 +0200 Subject: [PATCH 08/39] prepare for tomcat e2e test --- jmx-scraper/build.gradle.kts | 4 + .../TargetSystemIntegrationTest.java | 160 ++++++++++++++++++ .../target_systems/TomcatIntegrationTest.java | 41 +++++ 3 files changed, 205 insertions(+) create mode 100644 jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java create mode 100644 jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java diff --git a/jmx-scraper/build.gradle.kts b/jmx-scraper/build.gradle.kts index d3c7e4b58..498ffc0b9 100644 --- a/jmx-scraper/build.gradle.kts +++ b/jmx-scraper/build.gradle.kts @@ -29,6 +29,10 @@ testing { dependencies { implementation("org.testcontainers:junit-jupiter") implementation("org.slf4j:slf4j-simple") + implementation("com.linecorp.armeria:armeria-grpc") + implementation("com.linecorp.armeria:armeria-junit5") + implementation("com.linecorp.armeria:armeria-grpc") + implementation("io.opentelemetry.proto:opentelemetry-proto:0.20.0-alpha") } } } diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java new file mode 100644 index 000000000..a142ab46e --- /dev/null +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java @@ -0,0 +1,160 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.target_systems; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.server.grpc.GrpcService; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.contrib.jmxscraper.client.JmxRemoteClient; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse; +import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.LinkedBlockingDeque; +import javax.management.remote.JMXConnector; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.Testcontainers; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; + +public abstract class TargetSystemIntegrationTest { + + private static final Logger logger = LoggerFactory.getLogger(TargetSystemIntegrationTest.class); + + /** + * Create target system container + * + * @param jmxPort JMX port target JVM should listen to + * @return target system container + */ + protected abstract GenericContainer createTargetContainer(int jmxPort); + + // assert on received metrics + + private static Network network; + private static OtlpGrpcServer otlpServer; + private GenericContainer target; + private GenericContainer scraper; + + // private static final String OTLP_HOST = "host.testcontainers.internal"; + private static final int JMX_PORT = 9999; + + @BeforeAll + static void beforeAll() { + network = Network.newNetwork(); + otlpServer = new OtlpGrpcServer(); + otlpServer.start(); + Testcontainers.exposeHostPorts(otlpServer.httpPort()); + // String otlpEndpoint = "http://" + OTLP_HOST + ":" + otlpServer.httpPort(); + } + + @AfterAll + static void afterAll() { + network.close(); + try { + otlpServer.stop().get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + @AfterEach + void afterEach() { + if (target != null && target.isRunning()) { + target.stop(); + } + if (scraper != null && scraper.isRunning()) { + scraper.stop(); + } + if (otlpServer != null) { + otlpServer.reset(); + } + } + + @Test + void endToEndTest() { + + target = + createTargetContainer(JMX_PORT) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withNetwork(network) + .withExposedPorts(JMX_PORT) + .withNetworkAliases("target_system"); + target.start(); + + logger.info( + "Target system started, JMX port: {} mapped to {}:{}", + JMX_PORT, + target.getHost(), + target.getMappedPort(JMX_PORT)); + + scraper = createScraperContainer(); + + // TODO: start scraper container + // scraper.start(); + + // TODO : wait for metrics to be sent and add assertions on what is being captured + // for now we just test that we can connect to remote JMX using our client. + try (JMXConnector connector = + JmxRemoteClient.createNew(target.getHost(), target.getMappedPort(JMX_PORT)).connect()) { + assertThat(connector.getMBeanServerConnection()).isNotNull(); + } catch (IOException e) { + throw new RuntimeException(e); + } + // TODO: replace with real assertions + assertThat(otlpServer.getMetrics()).isEmpty(); + } + + private static GenericContainer createScraperContainer() { + return null; + } + + private static class OtlpGrpcServer extends ServerExtension { + + private final BlockingQueue metricRequests = + new LinkedBlockingDeque<>(); + + List getMetrics() { + return new ArrayList<>(metricRequests); + } + + void reset() { + metricRequests.clear(); + } + + @Override + protected void configure(ServerBuilder sb) { + sb.service( + GrpcService.builder() + .addService( + new MetricsServiceGrpc.MetricsServiceImplBase() { + @Override + public void export( + ExportMetricsServiceRequest request, + StreamObserver responseObserver) { + metricRequests.add(request); + responseObserver.onNext(ExportMetricsServiceResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + }) + .build()); + sb.http(0); + } + } +} diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java new file mode 100644 index 000000000..69e1bba27 --- /dev/null +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.target_systems; + +import java.time.Duration; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.ImageFromDockerfile; + +public class TomcatIntegrationTest extends TargetSystemIntegrationTest { + + @Override + protected GenericContainer createTargetContainer(int jmxPort) { + return new GenericContainer<>( + new ImageFromDockerfile() + .withDockerfileFromBuilder( + builder -> + builder + .from("tomcat:9.0") + .run("rm", "-fr", "/usr/local/tomcat/webapps/ROOT") + .add( + "https://tomcat.apache.org/tomcat-9.0-doc/appdev/sample/sample.war", + "/usr/local/tomcat/webapps/ROOT.war") + .build())) + .withEnv("LOCAL_JMX", "no") + .withEnv( + "CATALINA_OPTS", + "-Dcom.sun.management.jmxremote.local.only=false" + + " -Dcom.sun.management.jmxremote.authenticate=false" + + " -Dcom.sun.management.jmxremote.ssl=false" + + " -Dcom.sun.management.jmxremote.port=" + + jmxPort + + " -Dcom.sun.management.jmxremote.rmi.port=" + + jmxPort) + .withStartupTimeout(Duration.ofMinutes(2)) + .waitingFor(Wait.forListeningPort()); + } +} From 71e6258c949aa66cfa777d8fd22fab90b6199be8 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue, 17 Sep 2024 22:31:55 +0200 Subject: [PATCH 09/39] start wiring things together --- dependencyManagement/build.gradle.kts | 2 +- .../target_systems/JvmIntegrationTest.java | 4 + .../TargetSystemIntegrationTest.java | 80 +++++++++++++++---- .../target_systems/TomcatIntegrationTest.java | 5 ++ .../contrib/jmxscraper/JmxScraper.java | 63 ++++++++------- .../jmxscraper/client/JmxRemoteClient.java | 29 +++---- 6 files changed, 126 insertions(+), 57 deletions(-) create mode 100644 jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/JvmIntegrationTest.java diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts index c8e9fc865..9baba0736 100644 --- a/dependencyManagement/build.gradle.kts +++ b/dependencyManagement/build.gradle.kts @@ -7,7 +7,7 @@ data class DependencySet(val group: String, val version: String, val modules: Li val dependencyVersions = hashMapOf() rootProject.extra["versions"] = dependencyVersions -val otelInstrumentationVersion = "2.8.0-alpha" +val otelInstrumentationVersion = "2.9.0-alpha-SNAPSHOT" val DEPENDENCY_BOMS = listOf( "com.fasterxml.jackson:jackson-bom:2.17.2", diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/JvmIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/JvmIntegrationTest.java new file mode 100644 index 000000000..3d5285aa8 --- /dev/null +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/JvmIntegrationTest.java @@ -0,0 +1,4 @@ +package io.opentelemetry.contrib.jmxscraper.target_systems; + +public class JvmIntegrationTest { +} diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java index a142ab46e..119acb103 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java @@ -16,8 +16,11 @@ import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse; import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc; import java.io.IOException; +import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.LinkedBlockingDeque; @@ -32,10 +35,13 @@ import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.MountableFile; public abstract class TargetSystemIntegrationTest { private static final Logger logger = LoggerFactory.getLogger(TargetSystemIntegrationTest.class); + private static String otlpEndpoint; /** * Create target system container @@ -45,14 +51,14 @@ public abstract class TargetSystemIntegrationTest { */ protected abstract GenericContainer createTargetContainer(int jmxPort); - // assert on received metrics + protected abstract String getTargetSystem(); private static Network network; private static OtlpGrpcServer otlpServer; private GenericContainer target; private GenericContainer scraper; - // private static final String OTLP_HOST = "host.testcontainers.internal"; + private static final String OTLP_HOST = "host.testcontainers.internal"; private static final int JMX_PORT = 9999; @BeforeAll @@ -61,7 +67,7 @@ static void beforeAll() { otlpServer = new OtlpGrpcServer(); otlpServer.start(); Testcontainers.exposeHostPorts(otlpServer.httpPort()); - // String otlpEndpoint = "http://" + OTLP_HOST + ":" + otlpServer.httpPort(); + otlpEndpoint = "http://" + OTLP_HOST + ":" + otlpServer.httpPort(); } @AfterAll @@ -98,21 +104,21 @@ void endToEndTest() { .withNetworkAliases("target_system"); target.start(); + String targetHost = target.getHost(); + Integer targetPort = target.getMappedPort(JMX_PORT); logger.info( - "Target system started, JMX port: {} mapped to {}:{}", - JMX_PORT, - target.getHost(), - target.getMappedPort(JMX_PORT)); + "Target system started, JMX port: {} mapped to {}:{}", JMX_PORT, targetHost, targetPort); - scraper = createScraperContainer(); + scraper = + createScraperContainer(otlpEndpoint, getTargetSystem(), null, "target_system", JMX_PORT); + logger.info( + "starting scraper with JVM arguments : {}", String.join(" ", scraper.getCommandParts())); - // TODO: start scraper container - // scraper.start(); + scraper.start(); // TODO : wait for metrics to be sent and add assertions on what is being captured // for now we just test that we can connect to remote JMX using our client. - try (JMXConnector connector = - JmxRemoteClient.createNew(target.getHost(), target.getMappedPort(JMX_PORT)).connect()) { + try (JMXConnector connector = JmxRemoteClient.createNew(targetHost, targetPort).connect()) { assertThat(connector.getMBeanServerConnection()).isNotNull(); } catch (IOException e) { throw new RuntimeException(e); @@ -121,8 +127,54 @@ void endToEndTest() { assertThat(otlpServer.getMetrics()).isEmpty(); } - private static GenericContainer createScraperContainer() { - return null; + protected GenericContainer createScraperContainer( + String otlpEndpoint, + String targetSystem, + String customYaml, + String targetHost, + int targetPort) { + + String scraperJarPath = System.getProperty("shadow.jar.path"); + assertThat(scraperJarPath).isNotNull(); + + // TODO: adding a way to provide 'host:port' syntax would make this easier for common use + String url = + String.format( + Locale.getDefault(), + "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi", + targetHost, + targetPort); + + // for now only configure through JVM args + List arguments = + new ArrayList<>( + Arrays.asList( + "java", + "-Dotel.exporter.otlp.endpoint=" + otlpEndpoint, + "-Dotel.jmx.target.system=" + targetSystem, + "-Dotel.jmx.interval.milliseconds=1000", + "-Dotel.jmx.service.url=" + url, + "-jar", + "/scraper.jar")); + + GenericContainer scraper = + new GenericContainer<>("openjdk:8u272-jre-slim") + .withNetwork(network) + .withCopyFileToContainer(MountableFile.forHostPath(scraperJarPath), "/scraper.jar") + .withLogConsumer(new Slf4jLogConsumer(logger)) + .waitingFor( + Wait.forLogMessage(".*JMX scraping started.*", 1) + .withStartupTimeout(Duration.ofSeconds(10))); + + if (customYaml != null) { + arguments.add("-Dotel.jmx.config=/custom.yaml"); + scraper.withCopyFileToContainer( + MountableFile.forClasspathResource(customYaml), "/custom.yaml"); + } + + scraper.withCommand(arguments.toArray(new String[0])); + + return scraper; } private static class OtlpGrpcServer extends ServerExtension { diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java index 69e1bba27..dc1f9c0f9 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java @@ -12,6 +12,11 @@ public class TomcatIntegrationTest extends TargetSystemIntegrationTest { + @Override + protected String getTargetSystem() { + return "tomcat"; + } + @Override protected GenericContainer createTargetContainer(int jmxPort) { return new GenericContainer<>( 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 index 835eebd1e..627346a7a 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java @@ -5,29 +5,38 @@ package io.opentelemetry.contrib.jmxscraper; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.contrib.jmxscraper.client.JmxRemoteClient; 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 io.opentelemetry.instrumentation.jmx.engine.JmxMetricInsight; +import io.opentelemetry.instrumentation.jmx.engine.MetricConfiguration; 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.Collections; 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; +import javax.annotation.Nullable; +import javax.management.MBeanServerConnection; +import javax.management.remote.JMXConnector; 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; + private final JmxRemoteClient client; + private final JmxMetricInsight service; + @Nullable private MBeanServerConnection connection; /** * Main method to create and run a {@link JmxScraper} instance. @@ -43,14 +52,7 @@ public static void main(String[] args) { JmxScraper jmxScraper = new JmxScraper(config); jmxScraper.start(); - Runtime.getRuntime() - .addShutdownHook( - new Thread() { - @Override - public void run() { - jmxScraper.shutdown(); - } - }); + Runtime.getRuntime().addShutdownHook(new Thread(jmxScraper::shutdown)); } catch (ArgumentsParsingException e) { System.err.println( "Usage: java -jar " @@ -106,31 +108,36 @@ private static void loadPropertiesFromPath(Properties props, String path) 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); + String serviceUrl = config.getServiceUrl(); + if (serviceUrl == null) { + throw new ConfigurationException("missing service URL"); + } + int interval = config.getIntervalMilliseconds(); + if (interval < 0) { + throw new ConfigurationException("interval must be positive"); } + this.client = JmxRemoteClient.createNew(serviceUrl); + this.service = JmxMetricInsight.createService(GlobalOpenTelemetry.get(), interval); } - @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); + try { + JMXConnector connector = client.connect(); + connection = connector.getMBeanServerConnection(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + service.startRemote(getMetricConfig(config), () -> Collections.singletonList(connection)); logger.info("JMX scraping started"); } + @SuppressWarnings("unused") + private static MetricConfiguration getMetricConfig(JmxScraperConfig config) { + MetricConfiguration metricConfig = new MetricConfiguration(); + + return metricConfig; + } + private void shutdown() { logger.info("Shutting down JmxScraper and exporting final metrics."); // Prevent new tasks to be submitted diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java index 449b05ba4..f6fea12af 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java @@ -11,10 +11,10 @@ import java.security.Provider; import java.security.Security; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; @@ -30,21 +30,23 @@ public class JmxRemoteClient { private static final Logger logger = Logger.getLogger(JmxRemoteClient.class.getName()); - private final String host; - private final int port; + private final JMXServiceURL url; @Nullable private String userName; @Nullable private String password; @Nullable private String profile; @Nullable private String realm; private boolean sslRegistry; - private JmxRemoteClient(@Nonnull String host, int port) { - this.host = host; - this.port = port; + private JmxRemoteClient(JMXServiceURL url) { + this.url = url; } public static JmxRemoteClient createNew(String host, int port) { - return new JmxRemoteClient(host, port); + return new JmxRemoteClient(buildUrl(host, port)); + } + + public static JmxRemoteClient createNew(String url) { + return new JmxRemoteClient(buildUrl(url)); } @CanIgnoreReturnValue @@ -109,7 +111,6 @@ public JMXConnector connect() throws IOException { logger.log(Level.WARNING, "SASL unsupported in current environment: " + e.getMessage(), e); } - JMXServiceURL url = buildUrl(host, port); try { if (sslRegistry) { return doConnectSslRegistry(url, env); @@ -132,14 +133,14 @@ public JMXConnector doConnectSslRegistry(JMXServiceURL url, Map } private static JMXServiceURL buildUrl(String host, int port) { - StringBuilder sb = new StringBuilder("service:jmx:rmi:///jndi/rmi://"); - if (host != null) { - sb.append(host); - } - sb.append(":").append(port).append("/jmxrmi"); + return buildUrl( + String.format( + Locale.getDefault(), "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi", host, port)); + } + private static JMXServiceURL buildUrl(String url) { try { - return new JMXServiceURL(sb.toString()); + return new JMXServiceURL(url); } catch (MalformedURLException e) { throw new IllegalArgumentException("invalid url", e); } From 586f7800630994d2305217a7cf4b15a984fd78df Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:35:18 +0200 Subject: [PATCH 10/39] remove stack trace when sasl not supported --- .../contrib/jmxscraper/client/JmxRemoteClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java index f6fea12af..9ff594f54 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java @@ -108,7 +108,7 @@ public JMXConnector connect() throws IOException { } }); } catch (ReflectiveOperationException e) { - logger.log(Level.WARNING, "SASL unsupported in current environment: " + e.getMessage(), e); + logger.log(Level.WARNING, "SASL unsupported in current environment: " + e.getMessage()); } try { From 966aee6b852f7bef50fb57d54e9db84000651cfc Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:38:21 +0200 Subject: [PATCH 11/39] enhance comments & test cmd log msg --- .../jmxscraper/target_systems/TargetSystemIntegrationTest.java | 2 +- .../contrib/jmxscraper/client/JmxRemoteClient.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java index 119acb103..895775905 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java @@ -112,7 +112,7 @@ void endToEndTest() { scraper = createScraperContainer(otlpEndpoint, getTargetSystem(), null, "target_system", JMX_PORT); logger.info( - "starting scraper with JVM arguments : {}", String.join(" ", scraper.getCommandParts())); + "starting scraper with command: {}", String.join(" ", scraper.getCommandParts())); scraper.start(); diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java index 9ff594f54..483f63b9d 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java @@ -86,6 +86,7 @@ public JMXConnector connect() throws IOException { try { // Not all supported versions of Java contain this Provider + // Also it might not be accessible due to java.security.sasl module not accessible Class klass = Class.forName("com.sun.security.sasl.Provider"); Provider provider = (Provider) klass.getDeclaredConstructor().newInstance(); Security.addProvider(provider); From 350f8a99f20a225d453acfd38cd9b586c56271f9 Mon Sep 17 00:00:00 2001 From: robsunday Date: Wed, 18 Sep 2024 12:23:14 +0200 Subject: [PATCH 12/39] More tests --- .../config/JmxScraperConfigFactory.java | 2 +- .../contrib/jmxscraper/JmxScraperTest.java | 46 +++++++++++++++++-- .../config/JmxScraperConfigFactoryTest.java | 32 ++++++++++++- .../src/test/resources/validConfig.properties | 20 ++++++++ 4 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 jmx-scraper/src/test/resources/validConfig.properties 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 index 12f054585..6d4baf378 100644 --- 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 @@ -16,7 +16,7 @@ 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 CUSTOM_JMX_SCRAPING_CONFIG = PREFIX + "jmx.custom.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"; 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 index 86b83fc27..48dcb57a4 100644 --- a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java +++ b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java @@ -5,10 +5,15 @@ package io.opentelemetry.contrib.jmxscraper; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; +import io.opentelemetry.contrib.jmxscraper.config.ConfigurationException; +import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig; import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfigFactory; +import java.io.IOException; +import java.io.InputStream; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -18,7 +23,7 @@ class JmxScraperTest { @Test void shouldThrowExceptionWhenInvalidCommandLineArgsProvided() { // Given - List emptyArgs = Collections.singletonList("-inexistingOption"); + List emptyArgs = Collections.singletonList("-nonExistentOption"); JmxScraperConfigFactory configFactoryMock = mock(JmxScraperConfigFactory.class); // When and Then @@ -29,11 +34,46 @@ void shouldThrowExceptionWhenInvalidCommandLineArgsProvided() { @Test void shouldThrowExceptionWhenTooManyCommandLineArgsProvided() { // Given - List emptyArgs = Arrays.asList("-config", "path", "-inexistingOption"); + List args = Arrays.asList("-config", "path", "-nonExistentOption"); JmxScraperConfigFactory configFactoryMock = mock(JmxScraperConfigFactory.class); // When and Then - assertThatThrownBy(() -> JmxScraper.createConfigFromArgs(emptyArgs, configFactoryMock)) + assertThatThrownBy(() -> JmxScraper.createConfigFromArgs(args, configFactoryMock)) .isInstanceOf(ArgumentsParsingException.class); } + + @Test + void shouldCreateConfig_propertiesLoadedFromFile() + throws ConfigurationException, ArgumentsParsingException { + // Given + String filePath = ClassLoader.getSystemClassLoader().getResource("validConfig.properties").getPath(); + List args = Arrays.asList("-config", filePath); + JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); + + // When + JmxScraperConfig config = JmxScraper.createConfigFromArgs(args, configFactory); + + // Then + assertThat(config).isNotNull(); + } + + @Test + void shouldCreateConfig_propertiesLoadedFromStdIn() + throws ConfigurationException, ArgumentsParsingException, IOException { + InputStream originalIn = System.in; + try(InputStream stream = ClassLoader.getSystemClassLoader().getResourceAsStream("validConfig.properties")) { + // Given + System.setIn(stream); + List args = Arrays.asList("-config", "-"); + JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); + + // When + JmxScraperConfig config = JmxScraper.createConfigFromArgs(args, configFactory); + + // Then + assertThat(config).isNotNull(); + } finally { + System.setIn(originalIn); + } + } } 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 index 28a2680d7..2fb83fa39 100644 --- 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 @@ -148,7 +148,7 @@ void shouldFailValidation_missingConfigPathAndTargetSystem() { assertThatThrownBy(() -> configFactory.createConfig(properties)) .isInstanceOf(ConfigurationException.class) .hasMessage( - "otel.jmx.custom.jmx.scraping.config or otel.jmx.target.system must be specified."); + "otel.jmx.custom.scraping.config or otel.jmx.target.system must be specified."); } @Test @@ -180,6 +180,36 @@ void shouldFailValidation_missingOtlpEndpoint() { .hasMessage("otel.exporter.otlp.endpoint must be specified for otlp format."); } + @Test + void shouldPassValidation_noMetricsExporterType() throws ConfigurationException { + // Given + JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); + Properties properties = (Properties) validProperties.clone(); + properties.remove(JmxScraperConfigFactory.OTLP_ENDPOINT); + properties.remove(JmxScraperConfigFactory.METRICS_EXPORTER_TYPE); + + // When + JmxScraperConfig config = configFactory.createConfig(properties); + + // Then + assertThat(config).isNotNull(); + } + + @Test + void shouldPassValidation_nonOtlpMetricsExporterType() throws ConfigurationException { + // Given + JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); + Properties properties = (Properties) validProperties.clone(); + properties.remove(JmxScraperConfigFactory.OTLP_ENDPOINT); + properties.setProperty(JmxScraperConfigFactory.METRICS_EXPORTER_TYPE, "logging"); + + // When + JmxScraperConfig config = configFactory.createConfig(properties); + + // Then + assertThat(config).isNotNull(); + } + @Test void shouldFailValidation_negativeInterval() { // Given diff --git a/jmx-scraper/src/test/resources/validConfig.properties b/jmx-scraper/src/test/resources/validConfig.properties new file mode 100644 index 000000000..c4c7ac092 --- /dev/null +++ b/jmx-scraper/src/test/resources/validConfig.properties @@ -0,0 +1,20 @@ +otel.jmx.service.url=service:jmx:rmi:///jndi/rmi://myhost:12345/jmxrmi +otel.jmx.custom.scraping.config=/my/scraping-config.yaml +otel.jmx.target.system=jvm,cassandra +otel.jmx.interval.milliseconds=20000 +otel.metrics.exporter=otlp +otel.metric.export.interval=1000 +otel.exporter.otlp.endpoint=https://myotlpendpoint +otel.jmx.username=myUser\n\ + name +otel.jmx.password=myPassw\\ord +otel.jmx.remote.profile=SASL/DIG\EST-MD5 +otel.jmx.realm=myRealm +otel.resource.attributes=one=two,three=four +javax.net.ssl.keyStore=/my/key/store +javax.net.ssl.keyStorePassword=abc123 +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 From 91976c06da0b4a10452760284ee5a3a1ecdb805d Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 18 Sep 2024 13:49:45 +0200 Subject: [PATCH 13/39] refactor to use test containers --- .../jmxscraper/JmxScraperContainer.java | 108 ++++++++++++ .../contrib/jmxscraper/TestAppContainer.java | 139 +++++++++++++++ .../client/JmxRemoteClientTest.java | 166 ++---------------- .../target_systems/JvmIntegrationTest.java | 22 ++- .../TargetSystemIntegrationTest.java | 66 +------ .../target_systems/TomcatIntegrationTest.java | 11 +- 6 files changed, 291 insertions(+), 221 deletions(-) create mode 100644 jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java create mode 100644 jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java new file mode 100644 index 000000000..0be5ba9ca --- /dev/null +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.MountableFile; + +/** Test container that allows to execute {@link JmxScraper} in an isolated container */ +public class JmxScraperContainer extends GenericContainer { + + private final String endpoint; + private final Set targetSystems; + private String serviceUrl; + private int intervalMillis; + private final Set customYaml; + + public JmxScraperContainer(String otlpEndpoint) { + super("openjdk:8u272-jre-slim"); + + String scraperJarPath = System.getProperty("shadow.jar.path"); + assertThat(scraperJarPath).isNotNull(); + + this.withCopyFileToContainer(MountableFile.forHostPath(scraperJarPath), "/scraper.jar") + .waitingFor( + Wait.forLogMessage(".*JMX scraping started.*", 1) + .withStartupTimeout(Duration.ofSeconds(10))); + + this.endpoint = otlpEndpoint; + this.targetSystems = new HashSet<>(); + this.customYaml = new HashSet<>(); + this.intervalMillis = 1000; + } + + public JmxScraperContainer withTargetSystem(String targetSystem) { + targetSystems.add(targetSystem); + return this; + } + + public JmxScraperContainer withIntervalMillis(int intervalMillis) { + this.intervalMillis = intervalMillis; + return this; + } + + public JmxScraperContainer withService(String host, int port) { + // TODO: adding a way to provide 'host:port' syntax would make this easier for end users + this.serviceUrl = + String.format( + Locale.getDefault(), "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi", host, port); + return this; + } + + public JmxScraperContainer withCustomYaml(String yamlPath) { + this.customYaml.add(yamlPath); + return this; + } + + @Override + public void start() { + // for now only configure through JVM args + List arguments = new ArrayList<>(); + arguments.add("java"); + arguments.add("-Dotel.exporter.otlp.endpoint=" + endpoint); + + if (!targetSystems.isEmpty()) { + arguments.add("-Dotel.jmx.target.system=" + String.join(",", targetSystems)); + } + + if (serviceUrl == null) { + throw new IllegalStateException("Missing service URL"); + } + arguments.add("-Dotel.jmx.service.url=" + serviceUrl); + arguments.add("-Dotel.jmx.interval.milliseconds=" + intervalMillis); + + if (!customYaml.isEmpty()) { + int i = 0; + StringBuilder sb = new StringBuilder("-Dotel.jmx.config="); + for (String yaml : customYaml) { + String containerPath = "/custom_" + i + ".yaml"; + this.withCopyFileToContainer(MountableFile.forClasspathResource(yaml), containerPath); + if (i > 0) { + sb.append(","); + } + sb.append(containerPath); + i++; + } + arguments.add(sb.toString()); + } + + arguments.add("-jar"); + arguments.add("/scraper.jar"); + + this.withCommand(arguments.toArray(new String[0])); + + super.start(); + } +} diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java new file mode 100644 index 000000000..a5dff3006 --- /dev/null +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java @@ -0,0 +1,139 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.shaded.com.google.errorprone.annotations.CanIgnoreReturnValue; +import org.testcontainers.utility.MountableFile; + +/** Test container that allows to execute {@link TestApp} in an isolated container */ +public class TestAppContainer extends GenericContainer { + + private static final Logger logger = LoggerFactory.getLogger(TestAppContainer.class); + + private final Map properties; + private int port; + private String login; + private String pwd; + + public TestAppContainer() { + super("openjdk:8u272-jre-slim"); + + this.properties = new HashMap<>(); + + String appJar = System.getProperty("app.jar.path"); + assertThat(Paths.get(appJar)).isNotEmptyFile().isReadable(); + + this.withCopyFileToContainer(MountableFile.forHostPath(appJar), "/app.jar") + .waitingFor( + Wait.forLogMessage(TestApp.APP_STARTED_MSG + "\\n", 1) + .withStartupTimeout(Duration.ofSeconds(5))) + .withCommand("java", "-jar", "/app.jar"); + } + + @CanIgnoreReturnValue + public TestAppContainer withJmxPort(int port) { + this.port = port; + properties.put("com.sun.management.jmxremote.port", Integer.toString(port)); + return this.withExposedPorts(port); + } + + @CanIgnoreReturnValue + public TestAppContainer withUserAuth(String login, String pwd) { + this.login = login; + this.pwd = pwd; + return this; + } + + public int getPort() { + return getMappedPort(port); + } + + @Override + protected void doStart() { + super.doStart(); + } + + @Override + public void start() { + + // TODO: add support for ssl + properties.put("com.sun.management.jmxremote.ssl", "false"); + + if (pwd == null) { + properties.put("com.sun.management.jmxremote.authenticate", "false"); + } else { + properties.put("com.sun.management.jmxremote.authenticate", "true"); + + Path pwdFile = createPwdFile(login, pwd); + this.withCopyFileToContainer(MountableFile.forHostPath(pwdFile), "/jmx.password"); + properties.put("com.sun.management.jmxremote.password.file", "/jmx.password"); + + Path accessFile = createAccessFile(login); + this.withCopyFileToContainer(MountableFile.forHostPath(accessFile), "/jmx.access"); + properties.put("com.sun.management.jmxremote.access.file", "/jmx.access"); + } + + String confArgs = + properties.entrySet().stream() + .map( + e -> { + String s = "-D" + e.getKey(); + if (!e.getValue().isEmpty()) { + s += "=" + e.getValue(); + } + return s; + }) + .collect(Collectors.joining(" ")); + + this.withEnv("JAVA_TOOL_OPTIONS", confArgs); + + logger.info("Test application JAVA_TOOL_OPTIONS = " + confArgs); + + super.start(); + + logger.info("Test application JMX port mapped to {}:{}", getHost(), getMappedPort(port)); + } + + private static Path createPwdFile(String login, String pwd) { + try { + Path path = Files.createTempFile("test", ".pwd"); + writeLine(path, String.format("%s %s", login, pwd)); + return path; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static Path createAccessFile(String login) { + try { + Path path = Files.createTempFile("test", ".pwd"); + writeLine(path, String.format("%s %s", login, "readwrite")); + return path; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static void writeLine(Path path, String line) throws IOException { + line = line + "\n"; + Files.write(path, line.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClientTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClientTest.java index 291fc9ac5..ec7b9ba61 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClientTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClientTest.java @@ -8,61 +8,35 @@ import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.contrib.jmxscraper.TestApp; -import java.io.Closeable; +import io.opentelemetry.contrib.jmxscraper.TestAppContainer; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Duration; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; import javax.management.ObjectName; import javax.management.remote.JMXConnector; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.shaded.com.google.errorprone.annotations.CanIgnoreReturnValue; -import org.testcontainers.utility.MountableFile; public class JmxRemoteClientTest { - private static final Logger logger = LoggerFactory.getLogger(JmxRemoteClientTest.class); - private static Network network; - private static final List toClose = new ArrayList<>(); - @BeforeAll static void beforeAll() { network = Network.newNetwork(); - toClose.add(network); } @AfterAll static void afterAll() { - for (AutoCloseable item : toClose) { - try { - item.close(); - } catch (Exception e) { - logger.warn("Error closing " + item, e); - } - } + network.close(); } @Test void noAuth() { - try (AppContainer app = new AppContainer().withJmxPort(9990).start()) { - testConnector(() -> JmxRemoteClient.createNew(app.getHost(), app.getPort()).connect()); + try (TestAppContainer app = new TestAppContainer().withNetwork(network).withJmxPort(9990)) { + app.start(); + testConnector( + () -> JmxRemoteClient.createNew(app.getHost(), app.getMappedPort(9990)).connect()); } } @@ -70,10 +44,12 @@ void noAuth() { void loginPwdAuth() { String login = "user"; String pwd = "t0p!Secret"; - try (AppContainer app = new AppContainer().withJmxPort(9999).withUserAuth(login, pwd).start()) { + try (TestAppContainer app = + new TestAppContainer().withNetwork(network).withJmxPort(9999).withUserAuth(login, pwd)) { + app.start(); testConnector( () -> - JmxRemoteClient.createNew(app.getHost(), app.getPort()) + JmxRemoteClient.createNew(app.getHost(), app.getMappedPort(9999)) .userCredentials(login, pwd) .connect()); } @@ -115,126 +91,4 @@ private static void testConnector(ConnectorSupplier connectorSupplier) { private interface ConnectorSupplier { JMXConnector get() throws IOException; } - - private static class AppContainer implements Closeable { - - private final GenericContainer appContainer; - private final Map properties; - private int port; - private String login; - private String pwd; - - private AppContainer() { - this.properties = new HashMap<>(); - - properties.put("com.sun.management.jmxremote.ssl", "false"); // TODO : - - // SSL registry : com.sun.management.jmxremote.registry.ssl - // client side ssl auth: com.sun.management.jmxremote.ssl.need.client.auth - - String appJar = System.getProperty("app.jar.path"); - assertThat(Paths.get(appJar)).isNotEmptyFile().isReadable(); - - this.appContainer = - new GenericContainer<>("openjdk:8u272-jre-slim") - .withCopyFileToContainer(MountableFile.forHostPath(appJar), "/app.jar") - .withLogConsumer(new Slf4jLogConsumer(logger)) - .withNetwork(network) - .waitingFor( - Wait.forLogMessage(TestApp.APP_STARTED_MSG + "\\n", 1) - .withStartupTimeout(Duration.ofSeconds(5))) - .withCommand("java", "-jar", "/app.jar"); - } - - @CanIgnoreReturnValue - public AppContainer withJmxPort(int port) { - this.port = port; - properties.put("com.sun.management.jmxremote.port", Integer.toString(port)); - appContainer.withExposedPorts(port); - return this; - } - - @CanIgnoreReturnValue - public AppContainer withUserAuth(String login, String pwd) { - this.login = login; - this.pwd = pwd; - return this; - } - - @CanIgnoreReturnValue - AppContainer start() { - if (pwd == null) { - properties.put("com.sun.management.jmxremote.authenticate", "false"); - } else { - properties.put("com.sun.management.jmxremote.authenticate", "true"); - - Path pwdFile = createPwdFile(login, pwd); - appContainer.withCopyFileToContainer(MountableFile.forHostPath(pwdFile), "/jmx.password"); - properties.put("com.sun.management.jmxremote.password.file", "/jmx.password"); - - Path accessFile = createAccessFile(login); - appContainer.withCopyFileToContainer(MountableFile.forHostPath(accessFile), "/jmx.access"); - properties.put("com.sun.management.jmxremote.access.file", "/jmx.access"); - } - - String confArgs = - properties.entrySet().stream() - .map( - e -> { - String s = "-D" + e.getKey(); - if (!e.getValue().isEmpty()) { - s += "=" + e.getValue(); - } - return s; - }) - .collect(Collectors.joining(" ")); - - appContainer.withEnv("JAVA_TOOL_OPTIONS", confArgs).start(); - - logger.info("Test application JMX port mapped to {}:{}", getHost(), getPort()); - - toClose.add(this); - return this; - } - - int getPort() { - return appContainer.getMappedPort(port); - } - - String getHost() { - return appContainer.getHost(); - } - - @Override - public void close() { - if (appContainer.isRunning()) { - appContainer.stop(); - } - } - - private static Path createPwdFile(String login, String pwd) { - try { - Path path = Files.createTempFile("test", ".pwd"); - writeLine(path, String.format("%s %s", login, pwd)); - return path; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static Path createAccessFile(String login) { - try { - Path path = Files.createTempFile("test", ".pwd"); - writeLine(path, String.format("%s %s", login, "readwrite")); - return path; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static void writeLine(Path path, String line) throws IOException { - line = line + "\n"; - Files.write(path, line.getBytes(StandardCharsets.UTF_8)); - } - } } diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/JvmIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/JvmIntegrationTest.java index 3d5285aa8..d1972371f 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/JvmIntegrationTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/JvmIntegrationTest.java @@ -1,4 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.contrib.jmxscraper.target_systems; -public class JvmIntegrationTest { +import io.opentelemetry.contrib.jmxscraper.JmxScraperContainer; +import io.opentelemetry.contrib.jmxscraper.TestAppContainer; +import org.testcontainers.containers.GenericContainer; + +public class JvmIntegrationTest extends TargetSystemIntegrationTest { + + @Override + protected GenericContainer createTargetContainer(int jmxPort) { + // reusing test application for JVM metrics and custom yaml + return new TestAppContainer().withJmxPort(jmxPort); + } + + @Override + protected JmxScraperContainer customizeScraperContainer(JmxScraperContainer scraper) { + return scraper.withTargetSystem("jvm"); + } } diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java index 895775905..a59ade88c 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java @@ -11,16 +11,14 @@ import com.linecorp.armeria.server.grpc.GrpcService; import com.linecorp.armeria.testing.junit5.server.ServerExtension; import io.grpc.stub.StreamObserver; +import io.opentelemetry.contrib.jmxscraper.JmxScraperContainer; import io.opentelemetry.contrib.jmxscraper.client.JmxRemoteClient; import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse; import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc; import java.io.IOException; -import java.time.Duration; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import java.util.Locale; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.LinkedBlockingDeque; @@ -35,8 +33,6 @@ import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.utility.MountableFile; public abstract class TargetSystemIntegrationTest { @@ -51,12 +47,10 @@ public abstract class TargetSystemIntegrationTest { */ protected abstract GenericContainer createTargetContainer(int jmxPort); - protected abstract String getTargetSystem(); - private static Network network; private static OtlpGrpcServer otlpServer; private GenericContainer target; - private GenericContainer scraper; + private JmxScraperContainer scraper; private static final String OTLP_HOST = "host.testcontainers.internal"; private static final int JMX_PORT = 9999; @@ -109,10 +103,10 @@ void endToEndTest() { logger.info( "Target system started, JMX port: {} mapped to {}:{}", JMX_PORT, targetHost, targetPort); - scraper = - createScraperContainer(otlpEndpoint, getTargetSystem(), null, "target_system", JMX_PORT); - logger.info( - "starting scraper with command: {}", String.join(" ", scraper.getCommandParts())); + scraper = new JmxScraperContainer(otlpEndpoint).withService("target_system", JMX_PORT); + + scraper = customizeScraperContainer(scraper); + logger.info("starting scraper with command: {}", String.join(" ", scraper.getCommandParts())); scraper.start(); @@ -127,53 +121,7 @@ void endToEndTest() { assertThat(otlpServer.getMetrics()).isEmpty(); } - protected GenericContainer createScraperContainer( - String otlpEndpoint, - String targetSystem, - String customYaml, - String targetHost, - int targetPort) { - - String scraperJarPath = System.getProperty("shadow.jar.path"); - assertThat(scraperJarPath).isNotNull(); - - // TODO: adding a way to provide 'host:port' syntax would make this easier for common use - String url = - String.format( - Locale.getDefault(), - "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi", - targetHost, - targetPort); - - // for now only configure through JVM args - List arguments = - new ArrayList<>( - Arrays.asList( - "java", - "-Dotel.exporter.otlp.endpoint=" + otlpEndpoint, - "-Dotel.jmx.target.system=" + targetSystem, - "-Dotel.jmx.interval.milliseconds=1000", - "-Dotel.jmx.service.url=" + url, - "-jar", - "/scraper.jar")); - - GenericContainer scraper = - new GenericContainer<>("openjdk:8u272-jre-slim") - .withNetwork(network) - .withCopyFileToContainer(MountableFile.forHostPath(scraperJarPath), "/scraper.jar") - .withLogConsumer(new Slf4jLogConsumer(logger)) - .waitingFor( - Wait.forLogMessage(".*JMX scraping started.*", 1) - .withStartupTimeout(Duration.ofSeconds(10))); - - if (customYaml != null) { - arguments.add("-Dotel.jmx.config=/custom.yaml"); - scraper.withCopyFileToContainer( - MountableFile.forClasspathResource(customYaml), "/custom.yaml"); - } - - scraper.withCommand(arguments.toArray(new String[0])); - + protected JmxScraperContainer customizeScraperContainer(JmxScraperContainer scraper) { return scraper; } diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java index dc1f9c0f9..f6b9870c5 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java @@ -5,6 +5,7 @@ package io.opentelemetry.contrib.jmxscraper.target_systems; +import io.opentelemetry.contrib.jmxscraper.JmxScraperContainer; import java.time.Duration; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; @@ -12,11 +13,6 @@ public class TomcatIntegrationTest extends TargetSystemIntegrationTest { - @Override - protected String getTargetSystem() { - return "tomcat"; - } - @Override protected GenericContainer createTargetContainer(int jmxPort) { return new GenericContainer<>( @@ -43,4 +39,9 @@ protected GenericContainer createTargetContainer(int jmxPort) { .withStartupTimeout(Duration.ofMinutes(2)) .waitingFor(Wait.forListeningPort()); } + + @Override + protected JmxScraperContainer customizeScraperContainer(JmxScraperContainer scraper) { + return scraper.withTargetSystem("tomcat"); + } } From d28fd4257c22d101dc00eec2fabd5e88ab4292ff Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:49:21 +0200 Subject: [PATCH 14/39] make it mergeable without snapshot --- dependencyManagement/build.gradle.kts | 2 +- .../jmxscraper/JmxScraperContainer.java | 5 ++ .../contrib/jmxscraper/TestAppContainer.java | 4 -- .../contrib/jmxscraper/JmxScraper.java | 58 +++++-------------- 4 files changed, 19 insertions(+), 50 deletions(-) diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts index 9baba0736..c8e9fc865 100644 --- a/dependencyManagement/build.gradle.kts +++ b/dependencyManagement/build.gradle.kts @@ -7,7 +7,7 @@ data class DependencySet(val group: String, val version: String, val modules: Li val dependencyVersions = hashMapOf() rootProject.extra["versions"] = dependencyVersions -val otelInstrumentationVersion = "2.9.0-alpha-SNAPSHOT" +val otelInstrumentationVersion = "2.8.0-alpha" val DEPENDENCY_BOMS = listOf( "com.fasterxml.jackson:jackson-bom:2.17.2", diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java index 0be5ba9ca..f1fc04f39 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.Locale; import java.util.Set; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.MountableFile; @@ -43,16 +44,19 @@ public JmxScraperContainer(String otlpEndpoint) { this.intervalMillis = 1000; } + @CanIgnoreReturnValue public JmxScraperContainer withTargetSystem(String targetSystem) { targetSystems.add(targetSystem); return this; } + @CanIgnoreReturnValue public JmxScraperContainer withIntervalMillis(int intervalMillis) { this.intervalMillis = intervalMillis; return this; } + @CanIgnoreReturnValue public JmxScraperContainer withService(String host, int port) { // TODO: adding a way to provide 'host:port' syntax would make this easier for end users this.serviceUrl = @@ -61,6 +65,7 @@ public JmxScraperContainer withService(String host, int port) { return this; } + @CanIgnoreReturnValue public JmxScraperContainer withCustomYaml(String yamlPath) { this.customYaml.add(yamlPath); return this; diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java index a5dff3006..8a3d8d9b0 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java @@ -62,10 +62,6 @@ public TestAppContainer withUserAuth(String login, String pwd) { return this; } - public int getPort() { - return getMappedPort(port); - } - @Override protected void doStart() { super.doStart(); 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 index 627346a7a..22722b111 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java @@ -5,38 +5,29 @@ package io.opentelemetry.contrib.jmxscraper; -import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.contrib.jmxscraper.client.JmxRemoteClient; import io.opentelemetry.contrib.jmxscraper.config.ConfigurationException; import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig; import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfigFactory; -import io.opentelemetry.instrumentation.jmx.engine.JmxMetricInsight; -import io.opentelemetry.instrumentation.jmx.engine.MetricConfiguration; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; -import java.util.Collections; 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; -import javax.annotation.Nullable; import javax.management.MBeanServerConnection; import javax.management.remote.JMXConnector; 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; + private final JmxRemoteClient client; - private final JmxMetricInsight service; - @Nullable private MBeanServerConnection connection; + + // TODO depend on instrumentation 2.9.0 snapshot + // private final JmxMetricInsight service; /** * Main method to create and run a {@link JmxScraper} instance. @@ -52,7 +43,6 @@ public static void main(String[] args) { JmxScraper jmxScraper = new JmxScraper(config); jmxScraper.start(); - Runtime.getRuntime().addShutdownHook(new Thread(jmxScraper::shutdown)); } catch (ArgumentsParsingException e) { System.err.println( "Usage: java -jar " @@ -106,7 +96,6 @@ private static void loadPropertiesFromPath(Properties props, String path) } JmxScraper(JmxScraperConfig config) throws ConfigurationException { - this.config = config; String serviceUrl = config.getServiceUrl(); if (serviceUrl == null) { @@ -117,46 +106,25 @@ private static void loadPropertiesFromPath(Properties props, String path) throw new ConfigurationException("interval must be positive"); } this.client = JmxRemoteClient.createNew(serviceUrl); - this.service = JmxMetricInsight.createService(GlobalOpenTelemetry.get(), interval); + // TODO: depend on instrumentation 2.9.0 snapshot + // this.service = JmxMetricInsight.createService(GlobalOpenTelemetry.get(), interval); } private void start() { + @SuppressWarnings("unused") + MBeanServerConnection connection; try { JMXConnector connector = client.connect(); connection = connector.getMBeanServerConnection(); } catch (IOException e) { throw new IllegalStateException(e); } - service.startRemote(getMetricConfig(config), () -> Collections.singletonList(connection)); - logger.info("JMX scraping started"); - } - - @SuppressWarnings("unused") - private static MetricConfiguration getMetricConfig(JmxScraperConfig config) { - MetricConfiguration metricConfig = new MetricConfiguration(); - return metricConfig; - } + // TODO: depend on instrumentation 2.9.0 snapshot + // MetricConfiguration metricConfig = new MetricConfiguration(); + // TODO create JMX insight config from scraper config + // service.startRemote(metricConfig, () -> Collections.singletonList(connection)); - 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(); - } + logger.info("JMX scraping started"); } } From 9072de06f593e232cb653526c990f46aeb609a27 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:03:34 +0200 Subject: [PATCH 15/39] tidy a few things --- jmx-scraper/build.gradle.kts | 1 - .../jmxscraper/JmxScraperContainer.java | 4 +++- .../contrib/jmxscraper/TestAppContainer.java | 8 ++------ .../TargetSystemIntegrationTest.java | 19 +++++++++++-------- .../contrib/jmxscraper/JmxScraper.java | 7 +++++++ 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/jmx-scraper/build.gradle.kts b/jmx-scraper/build.gradle.kts index 498ffc0b9..1ffe28de6 100644 --- a/jmx-scraper/build.gradle.kts +++ b/jmx-scraper/build.gradle.kts @@ -29,7 +29,6 @@ testing { dependencies { implementation("org.testcontainers:junit-jupiter") implementation("org.slf4j:slf4j-simple") - implementation("com.linecorp.armeria:armeria-grpc") implementation("com.linecorp.armeria:armeria-junit5") implementation("com.linecorp.armeria:armeria-grpc") implementation("io.opentelemetry.proto:opentelemetry-proto:0.20.0-alpha") diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java index f1fc04f39..fbd64c8ce 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java @@ -7,13 +7,13 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.time.Duration; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; -import com.google.errorprone.annotations.CanIgnoreReturnValue; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.MountableFile; @@ -108,6 +108,8 @@ public void start() { this.withCommand(arguments.toArray(new String[0])); + logger().info("Starting scraper with command: " + String.join(" ", arguments)); + super.start(); } } diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java index 8a3d8d9b0..243aa01bb 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java @@ -16,8 +16,6 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.shaded.com.google.errorprone.annotations.CanIgnoreReturnValue; @@ -26,8 +24,6 @@ /** Test container that allows to execute {@link TestApp} in an isolated container */ public class TestAppContainer extends GenericContainer { - private static final Logger logger = LoggerFactory.getLogger(TestAppContainer.class); - private final Map properties; private int port; private String login; @@ -101,11 +97,11 @@ public void start() { this.withEnv("JAVA_TOOL_OPTIONS", confArgs); - logger.info("Test application JAVA_TOOL_OPTIONS = " + confArgs); + logger().info("Test application JAVA_TOOL_OPTIONS = " + confArgs); super.start(); - logger.info("Test application JMX port mapped to {}:{}", getHost(), getMappedPort(port)); + logger().info("Test application JMX port mapped to {}:{}", getHost(), getMappedPort(port)); } private static Path createPwdFile(String login, String pwd) { diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java index a59ade88c..89f90ed57 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java @@ -37,6 +37,7 @@ public abstract class TargetSystemIntegrationTest { private static final Logger logger = LoggerFactory.getLogger(TargetSystemIntegrationTest.class); + public static final String TARGET_SYSTEM_NETWORK_ALIAS = "targetsystem"; private static String otlpEndpoint; /** @@ -95,7 +96,7 @@ void endToEndTest() { .withLogConsumer(new Slf4jLogConsumer(logger)) .withNetwork(network) .withExposedPorts(JMX_PORT) - .withNetworkAliases("target_system"); + .withNetworkAliases(TARGET_SYSTEM_NETWORK_ALIAS); target.start(); String targetHost = target.getHost(); @@ -103,13 +104,6 @@ void endToEndTest() { logger.info( "Target system started, JMX port: {} mapped to {}:{}", JMX_PORT, targetHost, targetPort); - scraper = new JmxScraperContainer(otlpEndpoint).withService("target_system", JMX_PORT); - - scraper = customizeScraperContainer(scraper); - logger.info("starting scraper with command: {}", String.join(" ", scraper.getCommandParts())); - - scraper.start(); - // TODO : wait for metrics to be sent and add assertions on what is being captured // for now we just test that we can connect to remote JMX using our client. try (JMXConnector connector = JmxRemoteClient.createNew(targetHost, targetPort).connect()) { @@ -117,6 +111,15 @@ void endToEndTest() { } catch (IOException e) { throw new RuntimeException(e); } + + scraper = + new JmxScraperContainer(otlpEndpoint) + .withNetwork(network) + .withService(TARGET_SYSTEM_NETWORK_ALIAS, JMX_PORT); + + scraper = customizeScraperContainer(scraper); + scraper.start(); + // TODO: replace with real assertions assertThat(otlpServer.getMetrics()).isEmpty(); } 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 index 22722b111..78588c1b5 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java @@ -126,5 +126,12 @@ private void start() { // service.startRemote(metricConfig, () -> Collections.singletonList(connection)); logger.info("JMX scraping started"); + + // TODO: wait a bit to keep the JVM running, this won't be needed once calling jmx insight + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } } } From c0ac43fed8036d1701d8f3504fb119d399d08103 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:04:21 +0200 Subject: [PATCH 16/39] remove JmxClient --- .../TargetSystemIntegrationTest.java | 2 +- .../jmxscraper/jmx/ClientCallbackHandler.java | 49 -------- .../contrib/jmxscraper/jmx/JmxClient.java | 109 ------------------ 3 files changed, 1 insertion(+), 159 deletions(-) delete mode 100644 jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/ClientCallbackHandler.java delete mode 100644 jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/JmxClient.java diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java index 89f90ed57..71754ea6f 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java @@ -37,7 +37,7 @@ public abstract class TargetSystemIntegrationTest { private static final Logger logger = LoggerFactory.getLogger(TargetSystemIntegrationTest.class); - public static final String TARGET_SYSTEM_NETWORK_ALIAS = "targetsystem"; + private static final String TARGET_SYSTEM_NETWORK_ALIAS = "targetsystem"; private static String otlpEndpoint; /** 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 deleted file mode 100644 index 2dfa01115..000000000 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/ClientCallbackHandler.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 deleted file mode 100644 index 0c71d9cc9..000000000 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/JmxClient.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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(); - } - } -} From 14c0c2d6cf902861de68332aa5e51d71f10c25d4 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:06:55 +0200 Subject: [PATCH 17/39] post-review changes --- .../jmxscraper/JmxScraperContainer.java | 22 ++++--------- .../contrib/jmxscraper/TestAppContainer.java | 5 --- .../contrib/jmxscraper/JmxScraper.java | 32 +++++++++---------- 3 files changed, 23 insertions(+), 36 deletions(-) diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java index fbd64c8ce..f85a5ba17 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java @@ -25,7 +25,7 @@ public class JmxScraperContainer extends GenericContainer { private final Set targetSystems; private String serviceUrl; private int intervalMillis; - private final Set customYaml; + private final Set customYamlFiles; public JmxScraperContainer(String otlpEndpoint) { super("openjdk:8u272-jre-slim"); @@ -40,7 +40,7 @@ public JmxScraperContainer(String otlpEndpoint) { this.endpoint = otlpEndpoint; this.targetSystems = new HashSet<>(); - this.customYaml = new HashSet<>(); + this.customYamlFiles = new HashSet<>(); this.intervalMillis = 1000; } @@ -67,7 +67,7 @@ public JmxScraperContainer withService(String host, int port) { @CanIgnoreReturnValue public JmxScraperContainer withCustomYaml(String yamlPath) { - this.customYaml.add(yamlPath); + this.customYamlFiles.add(yamlPath); return this; } @@ -88,19 +88,11 @@ public void start() { arguments.add("-Dotel.jmx.service.url=" + serviceUrl); arguments.add("-Dotel.jmx.interval.milliseconds=" + intervalMillis); - if (!customYaml.isEmpty()) { - int i = 0; - StringBuilder sb = new StringBuilder("-Dotel.jmx.config="); - for (String yaml : customYaml) { - String containerPath = "/custom_" + i + ".yaml"; - this.withCopyFileToContainer(MountableFile.forClasspathResource(yaml), containerPath); - if (i > 0) { - sb.append(","); - } - sb.append(containerPath); - i++; + if (!customYamlFiles.isEmpty()) { + for (String yaml : customYamlFiles) { + this.withCopyFileToContainer(MountableFile.forClasspathResource(yaml), yaml); } - arguments.add(sb.toString()); + arguments.add("-Dotel.jmx.config=" + String.join(",", customYamlFiles)); } arguments.add("-jar"); diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java index 243aa01bb..a38dd7ace 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java @@ -58,11 +58,6 @@ public TestAppContainer withUserAuth(String login, String pwd) { return this; } - @Override - protected void doStart() { - super.doStart(); - } - @Override public void start() { 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 index 78588c1b5..3d3131a30 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java @@ -36,12 +36,12 @@ public class JmxScraper { */ @SuppressWarnings({"SystemOut", "SystemExitOutsideMain"}) public static void main(String[] args) { + JmxScraperConfig config = null; + JmxScraper jmxScraper = null; try { JmxScraperConfigFactory factory = new JmxScraperConfigFactory(); - JmxScraperConfig config = JmxScraper.createConfigFromArgs(Arrays.asList(args), factory); - - JmxScraper jmxScraper = new JmxScraper(config); - jmxScraper.start(); + config = JmxScraper.createConfigFromArgs(Arrays.asList(args), factory); + jmxScraper = new JmxScraper(config); } catch (ArgumentsParsingException e) { System.err.println( @@ -52,6 +52,13 @@ public static void main(String[] args) { System.err.println(e.getMessage()); System.exit(1); } + + try { + jmxScraper.start(); + } catch (IOException e) { + System.err.println("Unable to connect to " + config.getServiceUrl() + " " + e.getMessage()); + System.exit(2); + } } /** @@ -96,11 +103,7 @@ private static void loadPropertiesFromPath(Properties props, String path) } JmxScraper(JmxScraperConfig config) throws ConfigurationException { - String serviceUrl = config.getServiceUrl(); - if (serviceUrl == null) { - throw new ConfigurationException("missing service URL"); - } int interval = config.getIntervalMilliseconds(); if (interval < 0) { throw new ConfigurationException("interval must be positive"); @@ -110,15 +113,12 @@ private static void loadPropertiesFromPath(Properties props, String path) // this.service = JmxMetricInsight.createService(GlobalOpenTelemetry.get(), interval); } - private void start() { + private void start() throws IOException { + + JMXConnector connector = client.connect(); + @SuppressWarnings("unused") - MBeanServerConnection connection; - try { - JMXConnector connector = client.connect(); - connection = connector.getMBeanServerConnection(); - } catch (IOException e) { - throw new IllegalStateException(e); - } + MBeanServerConnection connection = connector.getMBeanServerConnection(); // TODO: depend on instrumentation 2.9.0 snapshot // MetricConfiguration metricConfig = new MetricConfiguration(); From 1a6533e046cc5e9e9f53a61678d5f482552b2da1 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:39:43 +0200 Subject: [PATCH 18/39] remove warnings --- .../io/opentelemetry/contrib/jmxscraper/JmxScraper.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 index 3d3131a30..9ab051d44 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java @@ -16,6 +16,7 @@ import java.nio.file.Paths; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.Properties; import java.util.logging.Logger; import javax.management.MBeanServerConnection; @@ -36,7 +37,7 @@ public class JmxScraper { */ @SuppressWarnings({"SystemOut", "SystemExitOutsideMain"}) public static void main(String[] args) { - JmxScraperConfig config = null; + JmxScraperConfig config; JmxScraper jmxScraper = null; try { JmxScraperConfigFactory factory = new JmxScraperConfigFactory(); @@ -54,9 +55,9 @@ public static void main(String[] args) { } try { - jmxScraper.start(); + Objects.requireNonNull(jmxScraper).start(); } catch (IOException e) { - System.err.println("Unable to connect to " + config.getServiceUrl() + " " + e.getMessage()); + System.err.println("Unable to connect " + e.getMessage()); System.exit(2); } } From ea46e8d6a749eaf160605866d76fe6a6a733d3e1 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:46:34 +0200 Subject: [PATCH 19/39] reformat again --- .../io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java | 6 ++++-- .../jmxscraper/config/JmxScraperConfigFactoryTest.java | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) 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 index 48dcb57a4..39ba63bdb 100644 --- a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java +++ b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java @@ -46,7 +46,8 @@ void shouldThrowExceptionWhenTooManyCommandLineArgsProvided() { void shouldCreateConfig_propertiesLoadedFromFile() throws ConfigurationException, ArgumentsParsingException { // Given - String filePath = ClassLoader.getSystemClassLoader().getResource("validConfig.properties").getPath(); + String filePath = + ClassLoader.getSystemClassLoader().getResource("validConfig.properties").getPath(); List args = Arrays.asList("-config", filePath); JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); @@ -61,7 +62,8 @@ void shouldCreateConfig_propertiesLoadedFromFile() void shouldCreateConfig_propertiesLoadedFromStdIn() throws ConfigurationException, ArgumentsParsingException, IOException { InputStream originalIn = System.in; - try(InputStream stream = ClassLoader.getSystemClassLoader().getResourceAsStream("validConfig.properties")) { + try (InputStream stream = + ClassLoader.getSystemClassLoader().getResourceAsStream("validConfig.properties")) { // Given System.setIn(stream); List args = Arrays.asList("-config", "-"); 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 index 2fb83fa39..32b50ff21 100644 --- 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 @@ -147,8 +147,7 @@ void shouldFailValidation_missingConfigPathAndTargetSystem() { // When and Then assertThatThrownBy(() -> configFactory.createConfig(properties)) .isInstanceOf(ConfigurationException.class) - .hasMessage( - "otel.jmx.custom.scraping.config or otel.jmx.target.system must be specified."); + .hasMessage("otel.jmx.custom.scraping.config or otel.jmx.target.system must be specified."); } @Test From f8461b2fc59ca24c285270f5752c397839bb54b9 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Thu, 19 Sep 2024 11:04:37 +0200 Subject: [PATCH 20/39] update readme --- jmx-scraper/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/jmx-scraper/README.md b/jmx-scraper/README.md index a041414e6..d6293b787 100644 --- a/jmx-scraper/README.md +++ b/jmx-scraper/README.md @@ -1,6 +1,8 @@ # 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. +The JMX MBeans and their metrics mapping is defined in YAML and is reusing implementation from +[jmx-metrics instrumentation](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/jmx-metrics). -This is currently experimental, but the end goal is to provide an alternative to the [jmx-metrics](../jmx-metrics/README.md) utility. +This is currently a work-in-progress component not ready to be used in production. +The end goal is to provide an alternative to the [JMX Gatherer](../jmx-metrics/README.md) utility. From 71a5749c885ab4d7a75aea55f506c8787de8c63d Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Thu, 19 Sep 2024 11:04:45 +0200 Subject: [PATCH 21/39] disable publication until ready --- jmx-scraper/build.gradle.kts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jmx-scraper/build.gradle.kts b/jmx-scraper/build.gradle.kts index 1ffe28de6..2c86b84e7 100644 --- a/jmx-scraper/build.gradle.kts +++ b/jmx-scraper/build.gradle.kts @@ -3,7 +3,9 @@ plugins { id("com.github.johnrengelman.shadow") id("otel.java-conventions") - id("otel.publish-conventions") + + // publishing disabled until component is ready to be used + // id("otel.publish-conventions") } description = "JMX metrics scraper" From 317fcdec67fc254d9e8631e59e8c2de46e9fabd8 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Thu, 19 Sep 2024 11:04:57 +0200 Subject: [PATCH 22/39] remove test dependency from runtime --- jmx-scraper/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jmx-scraper/build.gradle.kts b/jmx-scraper/build.gradle.kts index 2c86b84e7..d034f1183 100644 --- a/jmx-scraper/build.gradle.kts +++ b/jmx-scraper/build.gradle.kts @@ -18,11 +18,11 @@ dependencies { implementation("io.opentelemetry:opentelemetry-sdk") implementation("io.opentelemetry:opentelemetry-sdk-metrics") implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") - implementation("io.opentelemetry:opentelemetry-sdk-testing") implementation("io.opentelemetry.instrumentation:opentelemetry-jmx-metrics") testImplementation("org.junit-pioneer:junit-pioneer") + testImplementation("io.opentelemetry:opentelemetry-sdk-testing") } testing { From 378af7a4445f61a121be05ea796dc2463439535f Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:20:27 +0200 Subject: [PATCH 23/39] add TODOs to ensure we don't forget about it --- jmx-scraper/build.gradle.kts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jmx-scraper/build.gradle.kts b/jmx-scraper/build.gradle.kts index d034f1183..bab7b7cb7 100644 --- a/jmx-scraper/build.gradle.kts +++ b/jmx-scraper/build.gradle.kts @@ -4,7 +4,7 @@ plugins { id("otel.java-conventions") - // publishing disabled until component is ready to be used + // TODO publishing disabled until component is ready to be used // id("otel.publish-conventions") } @@ -14,6 +14,9 @@ otelJava.moduleName.set("io.opentelemetry.contrib.jmxscraper") application.mainClass.set("io.opentelemetry.contrib.jmxscraper.JmxScraper") dependencies { + // TODO remove snapshot dependency on upstream once 2.9.0 is released + // api(enforcedPlatform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha:2.9.0-SNAPSHOT-alpha",)) + implementation("io.opentelemetry:opentelemetry-api") implementation("io.opentelemetry:opentelemetry-sdk") implementation("io.opentelemetry:opentelemetry-sdk-metrics") From 178fae3d7e7c14b42aabb747221908779b9abd92 Mon Sep 17 00:00:00 2001 From: SylvainJuge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 25 Sep 2024 10:06:56 +0200 Subject: [PATCH 24/39] Apply suggestions from code review Co-authored-by: jason plumb <75337021+breedx-splk@users.noreply.github.com> --- jmx-scraper/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jmx-scraper/README.md b/jmx-scraper/README.md index d6293b787..175dd8c4d 100644 --- a/jmx-scraper/README.md +++ b/jmx-scraper/README.md @@ -1,7 +1,7 @@ # 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 and is reusing implementation from +The JMX MBeans and their metric mappings are defined in YAML and reuse implementation from [jmx-metrics instrumentation](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/jmx-metrics). This is currently a work-in-progress component not ready to be used in production. From c656bf73819c667db7e77ca92695ae7a27f24e24 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 25 Sep 2024 10:31:54 +0200 Subject: [PATCH 25/39] remove maven local repo --- jmx-metrics/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/jmx-metrics/build.gradle.kts b/jmx-metrics/build.gradle.kts index c6aafe9ed..b35d98de7 100644 --- a/jmx-metrics/build.gradle.kts +++ b/jmx-metrics/build.gradle.kts @@ -13,7 +13,6 @@ application.mainClass.set("io.opentelemetry.contrib.jmxmetrics.JmxMetrics") repositories { mavenCentral() - mavenLocal() maven { setUrl("https://repo.terracotta.org/maven2") content { From 2d24b22986c5f31ce10d959074a92c62f365b2d5 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 25 Sep 2024 10:51:24 +0200 Subject: [PATCH 26/39] post-review: arguments parsing --- .../jmxscraper/ArgumentsParsingException.java | 5 ++- .../contrib/jmxscraper/JmxScraper.java | 33 +++++++++++-------- 2 files changed, 23 insertions(+), 15 deletions(-) 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 index afe3460d6..5ebecd74d 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/ArgumentsParsingException.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/ArgumentsParsingException.java @@ -6,5 +6,8 @@ package io.opentelemetry.contrib.jmxscraper; public class ArgumentsParsingException extends Exception { - private static final long serialVersionUID = 0L; + + public ArgumentsParsingException(String msg) { + super(msg); + } } 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 index 9ab051d44..d67f1b7d7 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java @@ -24,6 +24,7 @@ public class JmxScraper { private static final Logger logger = Logger.getLogger(JmxScraper.class.getName()); + private static final String CONFIG_ARG = "-config"; private final JmxRemoteClient client; @@ -40,11 +41,11 @@ public static void main(String[] args) { JmxScraperConfig config; JmxScraper jmxScraper = null; try { - JmxScraperConfigFactory factory = new JmxScraperConfigFactory(); - config = JmxScraper.createConfigFromArgs(Arrays.asList(args), factory); + config = JmxScraper.createConfigFromArgs(Arrays.asList(args)); jmxScraper = new JmxScraper(config); } catch (ArgumentsParsingException e) { + System.err.println("ERROR: " + e.getMessage()); System.err.println( "Usage: java -jar " + "-config "); @@ -67,23 +68,27 @@ public static void main(String[] args) { * * @param args application commandline arguments */ - static JmxScraperConfig createConfigFromArgs(List args, JmxScraperConfigFactory factory) + static JmxScraperConfig createConfigFromArgs(List args) throws ArgumentsParsingException, ConfigurationException { - if (!args.isEmpty() && (args.size() != 2 || !args.get(0).equalsIgnoreCase("-config"))) { - throw new ArgumentsParsingException(); + + if (args.isEmpty()) { + throw new ArgumentsParsingException("no argument provided"); + } + if (args.size() != 2) { + throw new ArgumentsParsingException("exactly two arguments expected, got " + args.size()); + } + if (!args.get(0).equalsIgnoreCase(CONFIG_ARG)) { + throw new ArgumentsParsingException("unexpected first argument must be '" + CONFIG_ARG + "'"); } Properties loadedProperties = new Properties(); - if (args.size() == 2) { - String path = args.get(1); - if (path.trim().equals("-")) { - loadPropertiesFromStdin(loadedProperties); - } else { - loadPropertiesFromPath(loadedProperties, path); - } + String path = args.get(1); + if (path.trim().equals("-")) { + loadPropertiesFromStdin(loadedProperties); + } else { + loadPropertiesFromPath(loadedProperties, path); } - - return factory.createConfig(loadedProperties); + return new JmxScraperConfigFactory().createConfig(loadedProperties); } private static void loadPropertiesFromStdin(Properties props) throws ConfigurationException { From bb74e6ef38bea26fa32beb507b4374d700e5f80c Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 25 Sep 2024 10:51:57 +0200 Subject: [PATCH 27/39] post-review: move factory creation --- .../contrib/jmxscraper/JmxScraperTest.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) 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 index 39ba63bdb..63d9d5a6c 100644 --- a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java +++ b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java @@ -24,10 +24,9 @@ class JmxScraperTest { void shouldThrowExceptionWhenInvalidCommandLineArgsProvided() { // Given List emptyArgs = Collections.singletonList("-nonExistentOption"); - JmxScraperConfigFactory configFactoryMock = mock(JmxScraperConfigFactory.class); // When and Then - assertThatThrownBy(() -> JmxScraper.createConfigFromArgs(emptyArgs, configFactoryMock)) + assertThatThrownBy(() -> JmxScraper.createConfigFromArgs(emptyArgs)) .isInstanceOf(ArgumentsParsingException.class); } @@ -35,10 +34,9 @@ void shouldThrowExceptionWhenInvalidCommandLineArgsProvided() { void shouldThrowExceptionWhenTooManyCommandLineArgsProvided() { // Given List args = Arrays.asList("-config", "path", "-nonExistentOption"); - JmxScraperConfigFactory configFactoryMock = mock(JmxScraperConfigFactory.class); // When and Then - assertThatThrownBy(() -> JmxScraper.createConfigFromArgs(args, configFactoryMock)) + assertThatThrownBy(() -> JmxScraper.createConfigFromArgs(args)) .isInstanceOf(ArgumentsParsingException.class); } @@ -49,10 +47,9 @@ void shouldCreateConfig_propertiesLoadedFromFile() String filePath = ClassLoader.getSystemClassLoader().getResource("validConfig.properties").getPath(); List args = Arrays.asList("-config", filePath); - JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); // When - JmxScraperConfig config = JmxScraper.createConfigFromArgs(args, configFactory); + JmxScraperConfig config = JmxScraper.createConfigFromArgs(args); // Then assertThat(config).isNotNull(); @@ -67,10 +64,9 @@ void shouldCreateConfig_propertiesLoadedFromStdIn() // Given System.setIn(stream); List args = Arrays.asList("-config", "-"); - JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); // When - JmxScraperConfig config = JmxScraper.createConfigFromArgs(args, configFactory); + JmxScraperConfig config = JmxScraper.createConfigFromArgs(args); // Then assertThat(config).isNotNull(); From 1f0fc8480d203d189277ade5c197cb1357016745 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 25 Sep 2024 10:58:29 +0200 Subject: [PATCH 28/39] post-review: properties loading --- .../contrib/jmxscraper/JmxScraper.java | 20 +++++++++++-------- .../config/ConfigurationException.java | 1 - 2 files changed, 12 insertions(+), 9 deletions(-) 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 index d67f1b7d7..b252c72b3 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java @@ -81,28 +81,32 @@ static JmxScraperConfig createConfigFromArgs(List args) throw new ArgumentsParsingException("unexpected first argument must be '" + CONFIG_ARG + "'"); } - Properties loadedProperties = new Properties(); + Properties properties; String path = args.get(1); if (path.trim().equals("-")) { - loadPropertiesFromStdin(loadedProperties); + properties = loadPropertiesFromStdin(); } else { - loadPropertiesFromPath(loadedProperties, path); + properties = loadPropertiesFromPath(path); } - return new JmxScraperConfigFactory().createConfig(loadedProperties); + return new JmxScraperConfigFactory().createConfig(properties); } - private static void loadPropertiesFromStdin(Properties props) throws ConfigurationException { + private static Properties loadPropertiesFromStdin() throws ConfigurationException { + Properties properties = new Properties(); try (InputStream is = new DataInputStream(System.in)) { - props.load(is); + properties.load(is); + return properties; } catch (IOException e) { throw new ConfigurationException("Failed to read config properties from stdin", e); } } - private static void loadPropertiesFromPath(Properties props, String path) + private static Properties loadPropertiesFromPath(String path) throws ConfigurationException { + Properties properties = new Properties(); try (InputStream is = Files.newInputStream(Paths.get(path))) { - props.load(is); + properties.load(is); + return properties; } catch (IOException e) { throw new ConfigurationException("Failed to read config properties file: '" + path + "'", e); } 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 index 76c69998a..d193a281b 100644 --- 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 @@ -6,7 +6,6 @@ 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); From 140ac1708bc2185c0262c64ca3912d6f0e2cce12 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:16:07 +0200 Subject: [PATCH 29/39] restore missing serialization ids --- .../contrib/jmxscraper/ArgumentsParsingException.java | 1 + .../contrib/jmxscraper/config/ConfigurationException.java | 1 + 2 files changed, 2 insertions(+) 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 index 5ebecd74d..f006269cb 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/ArgumentsParsingException.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/ArgumentsParsingException.java @@ -6,6 +6,7 @@ package io.opentelemetry.contrib.jmxscraper; public class ArgumentsParsingException extends Exception { + private static final long serialVersionUID = 0L; public ArgumentsParsingException(String msg) { super(msg); 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 index d193a281b..76c69998a 100644 --- 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 @@ -6,6 +6,7 @@ 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); From 4c61c2222324cb80e9cdb8b76ed7ec634fc044ed Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:16:34 +0200 Subject: [PATCH 30/39] rework JmxScraper creation --- .../contrib/jmxscraper/JmxScraper.java | 26 ++++++------------- .../config/JmxScraperConfigFactory.java | 4 +++ 2 files changed, 12 insertions(+), 18 deletions(-) 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 index b252c72b3..bae4d2855 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java @@ -16,7 +16,6 @@ import java.nio.file.Paths; import java.util.Arrays; import java.util.List; -import java.util.Objects; import java.util.Properties; import java.util.logging.Logger; import javax.management.MBeanServerConnection; @@ -38,11 +37,12 @@ public class JmxScraper { */ @SuppressWarnings({"SystemOut", "SystemExitOutsideMain"}) public static void main(String[] args) { - JmxScraperConfig config; - JmxScraper jmxScraper = null; try { - config = JmxScraper.createConfigFromArgs(Arrays.asList(args)); - jmxScraper = new JmxScraper(config); + JmxScraperConfig config = JmxScraper.createConfigFromArgs(Arrays.asList(args)); + // TODO: depend on instrumentation 2.9.0 snapshot + // service = JmxMetricInsight.createService(GlobalOpenTelemetry.get(), config.getIntervalMilliseconds()); + JmxScraper jmxScraper = new JmxScraper(JmxRemoteClient.createNew(config.getServiceUrl())); + jmxScraper.start(); } catch (ArgumentsParsingException e) { System.err.println("ERROR: " + e.getMessage()); @@ -53,14 +53,11 @@ public static void main(String[] args) { } catch (ConfigurationException e) { System.err.println(e.getMessage()); System.exit(1); - } - - try { - Objects.requireNonNull(jmxScraper).start(); } catch (IOException e) { System.err.println("Unable to connect " + e.getMessage()); System.exit(2); } + } /** @@ -112,15 +109,8 @@ private static Properties loadPropertiesFromPath(String path) } } - JmxScraper(JmxScraperConfig config) throws ConfigurationException { - String serviceUrl = config.getServiceUrl(); - int interval = config.getIntervalMilliseconds(); - if (interval < 0) { - throw new ConfigurationException("interval must be positive"); - } - this.client = JmxRemoteClient.createNew(serviceUrl); - // TODO: depend on instrumentation 2.9.0 snapshot - // this.service = JmxMetricInsight.createService(GlobalOpenTelemetry.get(), interval); + JmxScraper(JmxRemoteClient client) { + this.client = client; } private void start() throws IOException { 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 index 6d4baf378..4ffb61eeb 100644 --- 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 @@ -81,6 +81,10 @@ public JmxScraperConfig createConfig(Properties props) throws ConfigurationExcep int interval = getProperty(INTERVAL_MILLISECONDS, 0); config.intervalMilliseconds = (interval == 0 ? 10000 : interval); getAndSetPropertyIfUndefined(EXPORTER_INTERVAL, config.intervalMilliseconds); + if (config.intervalMilliseconds < 0) { + throw new ConfigurationException( + "interval must be positive, got " + config.intervalMilliseconds); + } config.metricsExporterType = getAndSetPropertyIfUndefined(METRICS_EXPORTER_TYPE, "logging"); config.otlpExporterEndpoint = properties.getProperty(OTLP_ENDPOINT); From 8917452e5185cfc72287c319103dd52f4b1c4b54 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:17:00 +0200 Subject: [PATCH 31/39] cleanup --- .../io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java | 2 -- 1 file changed, 2 deletions(-) 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 index 63d9d5a6c..93d74c819 100644 --- a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java +++ b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java @@ -7,11 +7,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; import io.opentelemetry.contrib.jmxscraper.config.ConfigurationException; import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig; -import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfigFactory; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; From 7a7fed752d56f779d9156f4242ac681fb3707c0b Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:19:49 +0200 Subject: [PATCH 32/39] remove useless config check --- .../contrib/jmxscraper/config/JmxScraperConfigFactory.java | 4 ---- 1 file changed, 4 deletions(-) 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 index 4ffb61eeb..6d4baf378 100644 --- 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 @@ -81,10 +81,6 @@ public JmxScraperConfig createConfig(Properties props) throws ConfigurationExcep int interval = getProperty(INTERVAL_MILLISECONDS, 0); config.intervalMilliseconds = (interval == 0 ? 10000 : interval); getAndSetPropertyIfUndefined(EXPORTER_INTERVAL, config.intervalMilliseconds); - if (config.intervalMilliseconds < 0) { - throw new ConfigurationException( - "interval must be positive, got " + config.intervalMilliseconds); - } config.metricsExporterType = getAndSetPropertyIfUndefined(METRICS_EXPORTER_TYPE, "logging"); config.otlpExporterEndpoint = properties.getProperty(OTLP_ENDPOINT); From ffc05c1a4867467370eb0ba05fbed279517377ea Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:28:17 +0200 Subject: [PATCH 33/39] post-review: JmxRemoteClient > JmxConnectorBuilder --- ...Test.java => JmxConnectorBuilderTest.java} | 12 ++-- .../TargetSystemIntegrationTest.java | 4 +- ...teClient.java => JmxConnectorBuilder.java} | 58 +++++++++++-------- .../contrib/jmxscraper/JmxScraper.java | 9 ++- 4 files changed, 46 insertions(+), 37 deletions(-) rename jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/{client/JmxRemoteClientTest.java => JmxConnectorBuilderTest.java} (84%) rename jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/{client/JmxRemoteClient.java => JmxConnectorBuilder.java} (78%) diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClientTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxConnectorBuilderTest.java similarity index 84% rename from jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClientTest.java rename to jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxConnectorBuilderTest.java index ec7b9ba61..132605242 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClientTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxConnectorBuilderTest.java @@ -3,12 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.contrib.jmxscraper.client; +package io.opentelemetry.contrib.jmxscraper; import static org.assertj.core.api.Assertions.assertThat; -import io.opentelemetry.contrib.jmxscraper.TestApp; -import io.opentelemetry.contrib.jmxscraper.TestAppContainer; import java.io.IOException; import javax.management.ObjectName; import javax.management.remote.JMXConnector; @@ -17,7 +15,7 @@ import org.junit.jupiter.api.Test; import org.testcontainers.containers.Network; -public class JmxRemoteClientTest { +public class JmxConnectorBuilderTest { private static Network network; @@ -36,7 +34,7 @@ void noAuth() { try (TestAppContainer app = new TestAppContainer().withNetwork(network).withJmxPort(9990)) { app.start(); testConnector( - () -> JmxRemoteClient.createNew(app.getHost(), app.getMappedPort(9990)).connect()); + () -> JmxConnectorBuilder.createNew(app.getHost(), app.getMappedPort(9990)).build()); } } @@ -49,9 +47,9 @@ void loginPwdAuth() { app.start(); testConnector( () -> - JmxRemoteClient.createNew(app.getHost(), app.getMappedPort(9999)) + JmxConnectorBuilder.createNew(app.getHost(), app.getMappedPort(9999)) .userCredentials(login, pwd) - .connect()); + .build()); } } diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java index 71754ea6f..eec0aab44 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java @@ -12,7 +12,7 @@ import com.linecorp.armeria.testing.junit5.server.ServerExtension; import io.grpc.stub.StreamObserver; import io.opentelemetry.contrib.jmxscraper.JmxScraperContainer; -import io.opentelemetry.contrib.jmxscraper.client.JmxRemoteClient; +import io.opentelemetry.contrib.jmxscraper.JmxConnectorBuilder; import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse; import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc; @@ -106,7 +106,7 @@ void endToEndTest() { // TODO : wait for metrics to be sent and add assertions on what is being captured // for now we just test that we can connect to remote JMX using our client. - try (JMXConnector connector = JmxRemoteClient.createNew(targetHost, targetPort).connect()) { + try (JMXConnector connector = JmxConnectorBuilder.createNew(targetHost, targetPort).build()) { assertThat(connector.getMBeanServerConnection()).isNotNull(); } catch (IOException e) { throw new RuntimeException(e); diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxConnectorBuilder.java similarity index 78% rename from jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java rename to jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxConnectorBuilder.java index 483f63b9d..dd509aa13 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxConnectorBuilder.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.contrib.jmxscraper.client; +package io.opentelemetry.contrib.jmxscraper; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; @@ -26,9 +26,9 @@ import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.sasl.RealmCallback; -public class JmxRemoteClient { +public class JmxConnectorBuilder { - private static final Logger logger = Logger.getLogger(JmxRemoteClient.class.getName()); + private static final Logger logger = Logger.getLogger(JmxConnectorBuilder.class.getName()); private final JMXServiceURL url; @Nullable private String userName; @@ -37,44 +37,65 @@ public class JmxRemoteClient { @Nullable private String realm; private boolean sslRegistry; - private JmxRemoteClient(JMXServiceURL url) { + private JmxConnectorBuilder(JMXServiceURL url) { this.url = url; } - public static JmxRemoteClient createNew(String host, int port) { - return new JmxRemoteClient(buildUrl(host, port)); + public static JmxConnectorBuilder createNew(String host, int port) { + return new JmxConnectorBuilder(buildUrl(host, port)); } - public static JmxRemoteClient createNew(String url) { - return new JmxRemoteClient(buildUrl(url)); + public static JmxConnectorBuilder createNew(String url) { + return new JmxConnectorBuilder(buildUrl(url)); } @CanIgnoreReturnValue - public JmxRemoteClient userCredentials(String userName, String password) { + public JmxConnectorBuilder userCredentials(String userName, String password) { this.userName = userName; this.password = password; return this; } @CanIgnoreReturnValue - public JmxRemoteClient withRemoteProfile(String profile) { + public JmxConnectorBuilder withRemoteProfile(String profile) { this.profile = profile; return this; } @CanIgnoreReturnValue - public JmxRemoteClient withRealm(String realm) { + public JmxConnectorBuilder withRealm(String realm) { this.realm = realm; return this; } @CanIgnoreReturnValue - public JmxRemoteClient withSslRegistry() { + public JmxConnectorBuilder withSslRegistry() { this.sslRegistry = true; return this; } - public JMXConnector connect() throws IOException { + /** + * Builds JMX connector instance by connecting to the remote JMX endpoint + * + * @return JMX connector + * @throws IOException in case of communication error + */ + public JMXConnector build() throws IOException { + Map env = buildEnv(); + + try { + if (sslRegistry) { + return doConnectSslRegistry(url, env); + } + + return doConnect(url, env); + + } catch (IOException e) { + throw new IOException("Unable to connect to " + url.getHost() + ":" + url.getPort(), e); + } + } + + private Map buildEnv() { Map env = new HashMap<>(); if (userName != null && password != null) { env.put(JMXConnector.CREDENTIALS, new String[] {userName, password}); @@ -111,16 +132,7 @@ public JMXConnector connect() throws IOException { } catch (ReflectiveOperationException e) { logger.log(Level.WARNING, "SASL unsupported in current environment: " + e.getMessage()); } - - try { - if (sslRegistry) { - return doConnectSslRegistry(url, env); - } else { - return doConnect(url, env); - } - } catch (IOException e) { - throw new IOException("Unable to connect to " + url.getHost() + ":" + url.getPort(), e); - } + return env; } @SuppressWarnings("BanJNDI") 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 index bae4d2855..34dbc98ae 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java @@ -5,7 +5,6 @@ package io.opentelemetry.contrib.jmxscraper; -import io.opentelemetry.contrib.jmxscraper.client.JmxRemoteClient; import io.opentelemetry.contrib.jmxscraper.config.ConfigurationException; import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig; import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfigFactory; @@ -25,7 +24,7 @@ public class JmxScraper { private static final Logger logger = Logger.getLogger(JmxScraper.class.getName()); private static final String CONFIG_ARG = "-config"; - private final JmxRemoteClient client; + private final JmxConnectorBuilder client; // TODO depend on instrumentation 2.9.0 snapshot // private final JmxMetricInsight service; @@ -41,7 +40,7 @@ public static void main(String[] args) { JmxScraperConfig config = JmxScraper.createConfigFromArgs(Arrays.asList(args)); // TODO: depend on instrumentation 2.9.0 snapshot // service = JmxMetricInsight.createService(GlobalOpenTelemetry.get(), config.getIntervalMilliseconds()); - JmxScraper jmxScraper = new JmxScraper(JmxRemoteClient.createNew(config.getServiceUrl())); + JmxScraper jmxScraper = new JmxScraper(JmxConnectorBuilder.createNew(config.getServiceUrl())); jmxScraper.start(); } catch (ArgumentsParsingException e) { @@ -109,13 +108,13 @@ private static Properties loadPropertiesFromPath(String path) } } - JmxScraper(JmxRemoteClient client) { + JmxScraper(JmxConnectorBuilder client) { this.client = client; } private void start() throws IOException { - JMXConnector connector = client.connect(); + JMXConnector connector = client.build(); @SuppressWarnings("unused") MBeanServerConnection connection = connector.getMBeanServerConnection(); From 30094f55f6e49523bb6770f7afc29c53a28ac051 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:46:19 +0200 Subject: [PATCH 34/39] post-review de-groovify + static factory --- .../TargetSystemIntegrationTest.java | 2 +- .../contrib/jmxscraper/JmxScraper.java | 23 +- .../jmxscraper/config/JmxScraperConfig.java | 200 ++++++++++++++++-- .../config/JmxScraperConfigFactory.java | 179 ---------------- .../contrib/jmxscraper/JmxScraperTest.java | 11 +- ...oryTest.java => JmxScraperConfigTest.java} | 136 ++++++------ 6 files changed, 267 insertions(+), 284 deletions(-) delete mode 100644 jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigFactory.java rename jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/{JmxScraperConfigFactoryTest.java => JmxScraperConfigTest.java} (73%) diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java index eec0aab44..8fe79fc4d 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java @@ -11,8 +11,8 @@ import com.linecorp.armeria.server.grpc.GrpcService; import com.linecorp.armeria.testing.junit5.server.ServerExtension; import io.grpc.stub.StreamObserver; -import io.opentelemetry.contrib.jmxscraper.JmxScraperContainer; import io.opentelemetry.contrib.jmxscraper.JmxConnectorBuilder; +import io.opentelemetry.contrib.jmxscraper.JmxScraperContainer; import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse; import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc; 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 index 34dbc98ae..5234eae9d 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java @@ -7,7 +7,6 @@ import io.opentelemetry.contrib.jmxscraper.config.ConfigurationException; import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig; -import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfigFactory; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; @@ -37,9 +36,13 @@ public class JmxScraper { @SuppressWarnings({"SystemOut", "SystemExitOutsideMain"}) public static void main(String[] args) { try { - JmxScraperConfig config = JmxScraper.createConfigFromArgs(Arrays.asList(args)); + JmxScraperConfig config = + JmxScraperConfig.fromProperties(parseArgs(Arrays.asList(args)), System.getProperties()); + // propagate effective user-provided configuration to JVM system properties + config.propagateSystemProperties(); // TODO: depend on instrumentation 2.9.0 snapshot - // service = JmxMetricInsight.createService(GlobalOpenTelemetry.get(), config.getIntervalMilliseconds()); + // service = JmxMetricInsight.createService(GlobalOpenTelemetry.get(), + // config.getIntervalMilliseconds()); JmxScraper jmxScraper = new JmxScraper(JmxConnectorBuilder.createNew(config.getServiceUrl())); jmxScraper.start(); @@ -56,15 +59,14 @@ public static void main(String[] args) { System.err.println("Unable to connect " + e.getMessage()); System.exit(2); } - } /** - * Create {@link JmxScraperConfig} object basing on command line options + * Create {@link Properties} from command line options * * @param args application commandline arguments */ - static JmxScraperConfig createConfigFromArgs(List args) + static Properties parseArgs(List args) throws ArgumentsParsingException, ConfigurationException { if (args.isEmpty()) { @@ -77,14 +79,12 @@ static JmxScraperConfig createConfigFromArgs(List args) throw new ArgumentsParsingException("unexpected first argument must be '" + CONFIG_ARG + "'"); } - Properties properties; String path = args.get(1); if (path.trim().equals("-")) { - properties = loadPropertiesFromStdin(); + return loadPropertiesFromStdin(); } else { - properties = loadPropertiesFromPath(path); + return loadPropertiesFromPath(path); } - return new JmxScraperConfigFactory().createConfig(properties); } private static Properties loadPropertiesFromStdin() throws ConfigurationException { @@ -97,8 +97,7 @@ private static Properties loadPropertiesFromStdin() throws ConfigurationExceptio } } - private static Properties loadPropertiesFromPath(String path) - throws ConfigurationException { + private static Properties loadPropertiesFromPath(String path) throws ConfigurationException { Properties properties = new Properties(); try (InputStream is = Files.newInputStream(Paths.get(path))) { properties.load(is); 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 index eb04e13cd..7e727b1bf 100644 --- 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 @@ -5,26 +5,68 @@ package io.opentelemetry.contrib.jmxscraper.config; +import static io.opentelemetry.contrib.jmxscraper.util.StringUtils.isBlank; + +import java.util.Arrays; import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; import java.util.Set; +import java.util.stream.Collectors; /** 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() {} + static final String SERVICE_URL = "otel.jmx.service.url"; + static final String CUSTOM_JMX_SCRAPING_CONFIG = "otel.jmx.custom.scraping.config"; + static final String TARGET_SYSTEM = "otel.jmx.target.system"; + static final String INTERVAL_MILLISECONDS = "otel.jmx.interval.milliseconds"; + static final String METRICS_EXPORTER_TYPE = "otel.metrics.exporter"; + static final String EXPORTER_INTERVAL = "otel.metric.export.interval"; + static final String REGISTRY_SSL = "otel.jmx.remote.registry.ssl"; + + static final String OTLP_ENDPOINT = "otel.exporter.otlp.endpoint"; + + static final String JMX_USERNAME = "otel.jmx.username"; + static final String JMX_PASSWORD = "otel.jmx.password"; + static final String JMX_REMOTE_PROFILE = "otel.jmx.remote.profile"; + static final String JMX_REALM = "otel.jmx.realm"; + + static final List AVAILABLE_TARGET_SYSTEMS = + Arrays.asList( + "activemq", + "cassandra", + "hbase", + "hadoop", + "jetty", + "jvm", + "kafka", + "kafka-consumer", + "kafka-producer", + "solr", + "tomcat", + "wildfly"); + + private String serviceUrl = ""; + private String customJmxScrapingConfigPath = ""; + private Set targetSystems = Collections.emptySet(); + private int intervalMilliseconds; + private String metricsExporterType = ""; + private String otlpExporterEndpoint = ""; + private String username = ""; + private String password = ""; + private String realm = ""; + private String remoteProfile = ""; + private boolean registrySsl; + + /** Combined properties kept for initializing system properties */ + private final Properties properties; + + private JmxScraperConfig(Properties properties) { + this.properties = properties; + } public String getServiceUrl() { return serviceUrl; @@ -69,4 +111,134 @@ public String getRemoteProfile() { public boolean isRegistrySsl() { return registrySsl; } + + /** + * Builds scraper configuration from user and system properties + * + * @param userProperties user-provided configuration + * @param systemProperties system properties through '-Dxxx' JVM arguments + * @return JMX scraper configuration + * @throws ConfigurationException if there is any configuration error + */ + public static JmxScraperConfig fromProperties( + Properties userProperties, Properties systemProperties) throws ConfigurationException { + + Properties properties = new Properties(); + properties.putAll(userProperties); + + // command line takes precedence so replace any that were specified via config file properties + properties.putAll(systemProperties); + + JmxScraperConfig config = new JmxScraperConfig(properties); + + 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(properties, INTERVAL_MILLISECONDS, 0); + config.intervalMilliseconds = (interval == 0 ? 10000 : interval); + // configure SDK metric exporter interval from jmx metric interval + getAndSetPropertyIfUndefined(properties, EXPORTER_INTERVAL, config.intervalMilliseconds); + + config.metricsExporterType = + getAndSetPropertyIfUndefined(properties, 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); + return config; + } + + /** + * Sets system properties from effective configuration, must be called once and early before any + * OTel SDK or SSL/TLS stack initialization. This allows to override JVM system properties using + * user-providded configuration and also to set standard OTel SDK configuration. + */ + public void propagateSystemProperties() { + for (Map.Entry entry : properties.entrySet()) { + + String key = (String) entry.getKey(); + String value = (String) entry.getValue(); + if (key.startsWith("otel.") + || key.startsWith("javax.net.ssl.keyStore") + || key.startsWith("javax.net.ssl.trustStore")) { + System.setProperty(key, value); + } + } + } + + private static int getProperty(Properties properties, 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 static String getAndSetPropertyIfUndefined( + Properties properties, String key, String defaultValue) { + String propVal = properties.getProperty(key, defaultValue); + if (propVal.equals(defaultValue)) { + properties.setProperty(key, defaultValue); + } + return propVal; + } + + private static int getAndSetPropertyIfUndefined( + Properties properties, String key, int defaultValue) throws ConfigurationException { + int propVal = getProperty(properties, 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/config/JmxScraperConfigFactory.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigFactory.java deleted file mode 100644 index 6d4baf378..000000000 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigFactory.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * 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.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/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java index 93d74c819..1dfe2717d 100644 --- a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java +++ b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java @@ -15,6 +15,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Properties; import org.junit.jupiter.api.Test; class JmxScraperTest { @@ -24,7 +25,7 @@ void shouldThrowExceptionWhenInvalidCommandLineArgsProvided() { List emptyArgs = Collections.singletonList("-nonExistentOption"); // When and Then - assertThatThrownBy(() -> JmxScraper.createConfigFromArgs(emptyArgs)) + assertThatThrownBy(() -> JmxScraper.parseArgs(emptyArgs)) .isInstanceOf(ArgumentsParsingException.class); } @@ -34,7 +35,7 @@ void shouldThrowExceptionWhenTooManyCommandLineArgsProvided() { List args = Arrays.asList("-config", "path", "-nonExistentOption"); // When and Then - assertThatThrownBy(() -> JmxScraper.createConfigFromArgs(args)) + assertThatThrownBy(() -> JmxScraper.parseArgs(args)) .isInstanceOf(ArgumentsParsingException.class); } @@ -47,7 +48,8 @@ void shouldCreateConfig_propertiesLoadedFromFile() List args = Arrays.asList("-config", filePath); // When - JmxScraperConfig config = JmxScraper.createConfigFromArgs(args); + JmxScraperConfig config = + JmxScraperConfig.fromProperties(JmxScraper.parseArgs(args), new Properties()); // Then assertThat(config).isNotNull(); @@ -64,7 +66,8 @@ void shouldCreateConfig_propertiesLoadedFromStdIn() List args = Arrays.asList("-config", "-"); // When - JmxScraperConfig config = JmxScraper.createConfigFromArgs(args); + JmxScraperConfig config = + JmxScraperConfig.fromProperties(JmxScraper.parseArgs(args), new Properties()); // Then assertThat(config).isNotNull(); 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/JmxScraperConfigTest.java similarity index 73% rename from jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigFactoryTest.java rename to jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java index 32b50ff21..64a03ae58 100644 --- a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigFactoryTest.java +++ b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java @@ -14,51 +14,49 @@ import org.junitpioneer.jupiter.ClearSystemProperty; import org.junitpioneer.jupiter.SetSystemProperty; -class JmxScraperConfigFactoryTest { +class JmxScraperConfigTest { 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"); + JmxScraperConfig.SERVICE_URL, "jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); + validProperties.setProperty(JmxScraperConfig.CUSTOM_JMX_SCRAPING_CONFIG, ""); + validProperties.setProperty(JmxScraperConfig.TARGET_SYSTEM, "tomcat, activemq"); + validProperties.setProperty(JmxScraperConfig.METRICS_EXPORTER_TYPE, "otel"); + validProperties.setProperty(JmxScraperConfig.INTERVAL_MILLISECONDS, "1410"); + validProperties.setProperty(JmxScraperConfig.REGISTRY_SSL, "true"); + validProperties.setProperty(JmxScraperConfig.OTLP_ENDPOINT, "http://localhost:4317"); + validProperties.setProperty(JmxScraperConfig.JMX_USERNAME, "some-user"); + validProperties.setProperty(JmxScraperConfig.JMX_PASSWORD, "some-password"); + validProperties.setProperty(JmxScraperConfig.JMX_REMOTE_PROFILE, "some-profile"); + validProperties.setProperty(JmxScraperConfig.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"); + JmxScraperConfig.SERVICE_URL, "jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); + properties.setProperty(JmxScraperConfig.CUSTOM_JMX_SCRAPING_CONFIG, "/file.properties"); // When - JmxScraperConfig config = configFactory.createConfig(properties); + JmxScraperConfig config = JmxScraperConfig.fromProperties(properties, new 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(); + assertThat(config.getServiceUrl()) + .isEqualTo("jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); + assertThat(config.getCustomJmxScrapingConfigPath()).isEqualTo("/file.properties"); + assertThat(config.getTargetSystems()).isEmpty(); + assertThat(config.getIntervalMilliseconds()).isEqualTo(10000); + assertThat(config.getMetricsExporterType()).isEqualTo("logging"); + assertThat(config.getOtlpExporterEndpoint()).isNull(); + assertThat(config.getUsername()).isNull(); + assertThat(config.getPassword()).isNull(); + assertThat(config.getRemoteProfile()).isNull(); + assertThat(config.getRealm()).isNull(); } @Test @@ -70,7 +68,6 @@ void shouldCreateMinimalValidConfiguration() throws ConfigurationException { @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"); @@ -81,20 +78,22 @@ void shouldUseValuesFromProperties() throws ConfigurationException { properties.setProperty("javax.net.ssl.trustStoreType", "JKS"); // When - JmxScraperConfig config = configFactory.createConfig(properties); + JmxScraperConfig config = JmxScraperConfig.fromProperties(properties, new Properties()); + config.propagateSystemProperties(); // 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(); + assertThat(config.getServiceUrl()) + .isEqualTo("jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); + assertThat(config.getCustomJmxScrapingConfigPath()).isEqualTo(""); + assertThat(config.getTargetSystems()).containsOnly("tomcat", "activemq"); + assertThat(config.getIntervalMilliseconds()).isEqualTo(1410); + assertThat(config.getMetricsExporterType()).isEqualTo("otel"); + assertThat(config.getOtlpExporterEndpoint()).isEqualTo("http://localhost:4317"); + assertThat(config.getUsername()).isEqualTo("some-user"); + assertThat(config.getPassword()).isEqualTo("some-password"); + assertThat(config.getRemoteProfile()).isEqualTo("some-profile"); + assertThat(config.getRealm()).isEqualTo("some-realm"); + assertThat(config.isRegistrySsl()).isTrue(); // These properties are set from the config file loading into JmxConfig assertThat(System.getProperty("javax.net.ssl.keyStore")).isEqualTo("/my/key/store"); @@ -110,13 +109,12 @@ void shouldUseValuesFromProperties() throws ConfigurationException { @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); + JmxScraperConfig.fromProperties(properties, new Properties()); // Then assertThat(System.getProperty("otel.jmx.service.url")).isEqualTo("originalServiceUrl"); @@ -126,12 +124,11 @@ void shouldRetainPredefinedSystemProperties() throws ConfigurationException { @Test void shouldFailValidation_missingServiceUrl() { // Given - JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); Properties properties = (Properties) validProperties.clone(); - properties.remove(JmxScraperConfigFactory.SERVICE_URL); + properties.remove(JmxScraperConfig.SERVICE_URL); // When and Then - assertThatThrownBy(() -> configFactory.createConfig(properties)) + assertThatThrownBy(() -> JmxScraperConfig.fromProperties(properties, new Properties())) .isInstanceOf(ConfigurationException.class) .hasMessage("otel.jmx.service.url must be specified."); } @@ -139,13 +136,12 @@ void shouldFailValidation_missingServiceUrl() { @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); + properties.remove(JmxScraperConfig.CUSTOM_JMX_SCRAPING_CONFIG); + properties.remove(JmxScraperConfig.TARGET_SYSTEM); // When and Then - assertThatThrownBy(() -> configFactory.createConfig(properties)) + assertThatThrownBy(() -> JmxScraperConfig.fromProperties(properties, new Properties())) .isInstanceOf(ConfigurationException.class) .hasMessage("otel.jmx.custom.scraping.config or otel.jmx.target.system must be specified."); } @@ -153,28 +149,24 @@ void shouldFailValidation_missingConfigPathAndTargetSystem() { @Test void shouldFailValidation_invalidTargetSystem() { // Given - JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); Properties properties = (Properties) validProperties.clone(); - properties.setProperty(JmxScraperConfigFactory.TARGET_SYSTEM, "hal9000"); + properties.setProperty(JmxScraperConfig.TARGET_SYSTEM, "hal9000"); // When and Then - assertThatThrownBy(() -> configFactory.createConfig(properties)) + assertThatThrownBy(() -> JmxScraperConfig.fromProperties(properties, new Properties())) .isInstanceOf(ConfigurationException.class) - .hasMessage( - "[hal9000] must specify targets from " - + JmxScraperConfigFactory.AVAILABLE_TARGET_SYSTEMS); + .hasMessageStartingWith("[hal9000] must specify targets from "); } @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"); + properties.remove(JmxScraperConfig.OTLP_ENDPOINT); + properties.setProperty(JmxScraperConfig.METRICS_EXPORTER_TYPE, "otlp"); // When and Then - assertThatThrownBy(() -> configFactory.createConfig(properties)) + assertThatThrownBy(() -> JmxScraperConfig.fromProperties(properties, new Properties())) .isInstanceOf(ConfigurationException.class) .hasMessage("otel.exporter.otlp.endpoint must be specified for otlp format."); } @@ -182,13 +174,12 @@ void shouldFailValidation_missingOtlpEndpoint() { @Test void shouldPassValidation_noMetricsExporterType() throws ConfigurationException { // Given - JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); Properties properties = (Properties) validProperties.clone(); - properties.remove(JmxScraperConfigFactory.OTLP_ENDPOINT); - properties.remove(JmxScraperConfigFactory.METRICS_EXPORTER_TYPE); + properties.remove(JmxScraperConfig.OTLP_ENDPOINT); + properties.remove(JmxScraperConfig.METRICS_EXPORTER_TYPE); // When - JmxScraperConfig config = configFactory.createConfig(properties); + JmxScraperConfig config = JmxScraperConfig.fromProperties(properties, new Properties()); // Then assertThat(config).isNotNull(); @@ -197,13 +188,12 @@ void shouldPassValidation_noMetricsExporterType() throws ConfigurationException @Test void shouldPassValidation_nonOtlpMetricsExporterType() throws ConfigurationException { // Given - JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); Properties properties = (Properties) validProperties.clone(); - properties.remove(JmxScraperConfigFactory.OTLP_ENDPOINT); - properties.setProperty(JmxScraperConfigFactory.METRICS_EXPORTER_TYPE, "logging"); + properties.remove(JmxScraperConfig.OTLP_ENDPOINT); + properties.setProperty(JmxScraperConfig.METRICS_EXPORTER_TYPE, "logging"); // When - JmxScraperConfig config = configFactory.createConfig(properties); + JmxScraperConfig config = JmxScraperConfig.fromProperties(properties, new Properties()); // Then assertThat(config).isNotNull(); @@ -212,12 +202,11 @@ void shouldPassValidation_nonOtlpMetricsExporterType() throws ConfigurationExcep @Test void shouldFailValidation_negativeInterval() { // Given - JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); Properties properties = (Properties) validProperties.clone(); - properties.setProperty(JmxScraperConfigFactory.INTERVAL_MILLISECONDS, "-1"); + properties.setProperty(JmxScraperConfig.INTERVAL_MILLISECONDS, "-1"); // When and Then - assertThatThrownBy(() -> configFactory.createConfig(properties)) + assertThatThrownBy(() -> JmxScraperConfig.fromProperties(properties, new Properties())) .isInstanceOf(ConfigurationException.class) .hasMessage("otel.jmx.interval.milliseconds must be positive."); } @@ -225,12 +214,11 @@ void shouldFailValidation_negativeInterval() { @Test void shouldFailConfigCreation_invalidInterval() { // Given - JmxScraperConfigFactory configFactory = new JmxScraperConfigFactory(); Properties properties = (Properties) validProperties.clone(); - properties.setProperty(JmxScraperConfigFactory.INTERVAL_MILLISECONDS, "abc"); + properties.setProperty(JmxScraperConfig.INTERVAL_MILLISECONDS, "abc"); // When and Then - assertThatThrownBy(() -> configFactory.createConfig(properties)) + assertThatThrownBy(() -> JmxScraperConfig.fromProperties(properties, new Properties())) .isInstanceOf(ConfigurationException.class) .hasMessage("Failed to parse otel.jmx.interval.milliseconds"); } From 2c16a92360d2c1ff5adabdf06e183066ab141f96 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:03:26 +0200 Subject: [PATCH 35/39] fix tests --- .../jmxscraper/config/JmxScraperConfig.java | 4 +-- .../config/JmxScraperConfigTest.java | 34 ++++++++++++++++--- 2 files changed, 31 insertions(+), 7 deletions(-) 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 index 7e727b1bf..7023763ca 100644 --- 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 @@ -169,8 +169,8 @@ public static JmxScraperConfig fromProperties( public void propagateSystemProperties() { for (Map.Entry entry : properties.entrySet()) { - String key = (String) entry.getKey(); - String value = (String) entry.getValue(); + String key = entry.getKey().toString(); + String value = entry.getValue().toString(); if (key.startsWith("otel.") || key.startsWith("javax.net.ssl.keyStore") || key.startsWith("javax.net.ssl.trustStore")) { diff --git a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java index 64a03ae58..e84859449 100644 --- a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java +++ b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java @@ -9,10 +9,10 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.Properties; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.ClearSystemProperty; -import org.junitpioneer.jupiter.SetSystemProperty; class JmxScraperConfigTest { private static Properties validProperties; @@ -34,6 +34,18 @@ static void setUp() { validProperties.setProperty(JmxScraperConfig.JMX_REALM, "some-realm"); } + @AfterEach + void afterEach() { + // make sure that no test leaked in global system properties + Properties systemProperties = System.getProperties(); + for (Object k : systemProperties.keySet()) { + String key = k.toString(); + if (key.startsWith("otel.") || key.startsWith("javax.net.ssl.")) { + System.clearProperty(key); + } + } + } + @Test void shouldCreateMinimalValidConfiguration() throws ConfigurationException { // Given @@ -77,6 +89,10 @@ void shouldUseValuesFromProperties() throws ConfigurationException { properties.setProperty("javax.net.ssl.trustStorePassword", "def456"); properties.setProperty("javax.net.ssl.trustStoreType", "JKS"); + assertThat(System.getProperty("javax.net.ssl.keyStore")) + .describedAs("keystore config should not be set") + .isNull(); + // When JmxScraperConfig config = JmxScraperConfig.fromProperties(properties, new Properties()); config.propagateSystemProperties(); @@ -105,16 +121,24 @@ void shouldUseValuesFromProperties() throws ConfigurationException { } @Test - @SetSystemProperty(key = "otel.jmx.service.url", value = "originalServiceUrl") - @SetSystemProperty(key = "javax.net.ssl.keyStorePassword", value = "originalPassword") + @ClearSystemProperty(key = "otel.jmx.service.url") + @ClearSystemProperty(key = "javax.net.ssl.keyStorePassword") void shouldRetainPredefinedSystemProperties() throws ConfigurationException { // Given - // Properties to be propagated to system, properties + // user properties to be propagated to system properties Properties properties = (Properties) validProperties.clone(); properties.setProperty("javax.net.ssl.keyStorePassword", "abc123"); + // system properties + Properties systemProperties = new Properties(); + systemProperties.put("otel.jmx.service.url", "originalServiceUrl"); + systemProperties.put("javax.net.ssl.keyStorePassword", "originalPassword"); + // When - JmxScraperConfig.fromProperties(properties, new Properties()); + JmxScraperConfig config = JmxScraperConfig.fromProperties(properties, systemProperties); + // even when effective configuration is propagated to system properties original values are kept + // due to priority of system properties over user-provided ones. + config.propagateSystemProperties(); // Then assertThat(System.getProperty("otel.jmx.service.url")).isEqualTo("originalServiceUrl"); From 8ad40e0a92c572337ed43213731dd59e1247bc82 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:33:58 +0200 Subject: [PATCH 36/39] attempt to fix tests on some jdks --- .../jmxscraper/config/JmxScraperConfigTest.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java index e84859449..c15fe98a1 100644 --- a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java +++ b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java @@ -9,6 +9,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.Properties; +import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -37,13 +38,14 @@ static void setUp() { @AfterEach void afterEach() { // make sure that no test leaked in global system properties - Properties systemProperties = System.getProperties(); - for (Object k : systemProperties.keySet()) { - String key = k.toString(); - if (key.startsWith("otel.") || key.startsWith("javax.net.ssl.")) { - System.clearProperty(key); - } - } + Stream.of(System.getProperties().keySet()) + .map(Object::toString) + .forEach( + key -> { + if (key.startsWith("otel.") || key.startsWith("javax.net.ssl.")) { + System.clearProperty(key); + } + }); } @Test From 7d0be38a2dea0ab796b36782492848652efd17e2 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:55:08 +0200 Subject: [PATCH 37/39] allow empty user config when using system properties --- .../java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 index 5234eae9d..ebed7c780 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java @@ -70,7 +70,9 @@ static Properties parseArgs(List args) throws ArgumentsParsingException, ConfigurationException { if (args.isEmpty()) { - throw new ArgumentsParsingException("no argument provided"); + // empty properties from stdin or external file + // config could still be provided through JVM system properties + return new Properties(); } if (args.size() != 2) { throw new ArgumentsParsingException("exactly two arguments expected, got " + args.size()); From c15f40d1303459c9741f22dec3f0282df42135e4 Mon Sep 17 00:00:00 2001 From: robsunday Date: Thu, 26 Sep 2024 12:36:02 +0200 Subject: [PATCH 38/39] Code review followup changes --- .../target_systems/JvmIntegrationTest.java | 7 + .../TargetSystemIntegrationTest.java | 5 +- .../target_systems/TomcatIntegrationTest.java | 7 + .../jmxscraper/config/JmxScraperConfig.java | 31 +-- .../{util => internal}/StringUtils.java | 6 +- .../config/JmxScraperConfigTest.java | 184 +++++------------- 6 files changed, 87 insertions(+), 153 deletions(-) rename jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/{util => internal}/StringUtils.java (65%) diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/JvmIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/JvmIntegrationTest.java index d1972371f..4c240ee16 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/JvmIntegrationTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/JvmIntegrationTest.java @@ -7,6 +7,8 @@ import io.opentelemetry.contrib.jmxscraper.JmxScraperContainer; import io.opentelemetry.contrib.jmxscraper.TestAppContainer; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; +import java.util.List; import org.testcontainers.containers.GenericContainer; public class JvmIntegrationTest extends TargetSystemIntegrationTest { @@ -21,4 +23,9 @@ protected GenericContainer createTargetContainer(int jmxPort) { protected JmxScraperContainer customizeScraperContainer(JmxScraperContainer scraper) { return scraper.withTargetSystem("jvm"); } + + @Override + protected void verifyMetrics(List metrics) { + // TODO: Verify gathered metrics + } } diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java index 8fe79fc4d..0552aa3bd 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java @@ -120,10 +120,11 @@ void endToEndTest() { scraper = customizeScraperContainer(scraper); scraper.start(); - // TODO: replace with real assertions - assertThat(otlpServer.getMetrics()).isEmpty(); + verifyMetrics(otlpServer.getMetrics()); } + protected abstract void verifyMetrics(List metrics); + protected JmxScraperContainer customizeScraperContainer(JmxScraperContainer scraper) { return scraper; } diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java index f6b9870c5..ccf7e59a7 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java @@ -6,7 +6,9 @@ package io.opentelemetry.contrib.jmxscraper.target_systems; import io.opentelemetry.contrib.jmxscraper.JmxScraperContainer; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; import java.time.Duration; +import java.util.List; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.images.builder.ImageFromDockerfile; @@ -44,4 +46,9 @@ protected GenericContainer createTargetContainer(int jmxPort) { protected JmxScraperContainer customizeScraperContainer(JmxScraperContainer scraper) { return scraper.withTargetSystem("tomcat"); } + + @Override + protected void verifyMetrics(List metrics) { + // TODO: Verify gathered metrics + } } 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 index 7023763ca..ce4ddd080 100644 --- 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 @@ -5,7 +5,7 @@ package io.opentelemetry.contrib.jmxscraper.config; -import static io.opentelemetry.contrib.jmxscraper.util.StringUtils.isBlank; +import static io.opentelemetry.contrib.jmxscraper.internal.StringUtils.isBlank; import java.util.Arrays; import java.util.Collections; @@ -35,19 +35,20 @@ public class JmxScraperConfig { static final String JMX_REALM = "otel.jmx.realm"; static final List AVAILABLE_TARGET_SYSTEMS = - Arrays.asList( - "activemq", - "cassandra", - "hbase", - "hadoop", - "jetty", - "jvm", - "kafka", - "kafka-consumer", - "kafka-producer", - "solr", - "tomcat", - "wildfly"); + Collections.unmodifiableList( + Arrays.asList( + "activemq", + "cassandra", + "hbase", + "hadoop", + "jetty", + "jvm", + "kafka", + "kafka-consumer", + "kafka-producer", + "solr", + "tomcat", + "wildfly")); private String serviceUrl = ""; private String customJmxScrapingConfigPath = ""; @@ -164,7 +165,7 @@ public static JmxScraperConfig fromProperties( /** * Sets system properties from effective configuration, must be called once and early before any * OTel SDK or SSL/TLS stack initialization. This allows to override JVM system properties using - * user-providded configuration and also to set standard OTel SDK configuration. + * user-provided configuration and also to set standard OTel SDK configuration. */ public void propagateSystemProperties() { for (Map.Entry entry : properties.entrySet()) { 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/internal/StringUtils.java similarity index 65% rename from jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/util/StringUtils.java rename to jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/internal/StringUtils.java index aa24e1cea..fa12d24b4 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/util/StringUtils.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/internal/StringUtils.java @@ -3,10 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.contrib.jmxscraper.util; +package io.opentelemetry.contrib.jmxscraper.internal; import javax.annotation.Nullable; +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time.
    This is a utility class implementing miscellaneous String operations. + */ public final class StringUtils { private StringUtils() {} diff --git a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java index c15fe98a1..2e6145baf 100644 --- a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java +++ b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java @@ -5,6 +5,18 @@ package io.opentelemetry.contrib.jmxscraper.config; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.CUSTOM_JMX_SCRAPING_CONFIG; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.INTERVAL_MILLISECONDS; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.JMX_PASSWORD; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.JMX_REALM; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.JMX_REMOTE_PROFILE; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.JMX_USERNAME; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.METRICS_EXPORTER_TYPE; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.OTLP_ENDPOINT; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.REGISTRY_SSL; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.SERVICE_URL; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.TARGET_SYSTEM; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.fromProperties; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -22,17 +34,17 @@ class JmxScraperConfigTest { static void setUp() { validProperties = new Properties(); validProperties.setProperty( - JmxScraperConfig.SERVICE_URL, "jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); - validProperties.setProperty(JmxScraperConfig.CUSTOM_JMX_SCRAPING_CONFIG, ""); - validProperties.setProperty(JmxScraperConfig.TARGET_SYSTEM, "tomcat, activemq"); - validProperties.setProperty(JmxScraperConfig.METRICS_EXPORTER_TYPE, "otel"); - validProperties.setProperty(JmxScraperConfig.INTERVAL_MILLISECONDS, "1410"); - validProperties.setProperty(JmxScraperConfig.REGISTRY_SSL, "true"); - validProperties.setProperty(JmxScraperConfig.OTLP_ENDPOINT, "http://localhost:4317"); - validProperties.setProperty(JmxScraperConfig.JMX_USERNAME, "some-user"); - validProperties.setProperty(JmxScraperConfig.JMX_PASSWORD, "some-password"); - validProperties.setProperty(JmxScraperConfig.JMX_REMOTE_PROFILE, "some-profile"); - validProperties.setProperty(JmxScraperConfig.JMX_REALM, "some-realm"); + SERVICE_URL, "jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); + validProperties.setProperty(CUSTOM_JMX_SCRAPING_CONFIG, ""); + validProperties.setProperty(TARGET_SYSTEM, "tomcat, activemq"); + validProperties.setProperty(METRICS_EXPORTER_TYPE, "otel"); + validProperties.setProperty(INTERVAL_MILLISECONDS, "1410"); + validProperties.setProperty(REGISTRY_SSL, "true"); + validProperties.setProperty(OTLP_ENDPOINT, "http://localhost:4317"); + validProperties.setProperty(JMX_USERNAME, "some-user"); + validProperties.setProperty(JMX_PASSWORD, "some-password"); + validProperties.setProperty(JMX_REMOTE_PROFILE, "some-profile"); + validProperties.setProperty(JMX_REALM, "some-realm"); } @AfterEach @@ -52,12 +64,11 @@ void afterEach() { void shouldCreateMinimalValidConfiguration() throws ConfigurationException { // Given Properties properties = new Properties(); - properties.setProperty( - JmxScraperConfig.SERVICE_URL, "jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); - properties.setProperty(JmxScraperConfig.CUSTOM_JMX_SCRAPING_CONFIG, "/file.properties"); + properties.setProperty(SERVICE_URL, "jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); + properties.setProperty(CUSTOM_JMX_SCRAPING_CONFIG, "/file.properties"); // When - JmxScraperConfig config = JmxScraperConfig.fromProperties(properties, new Properties()); + JmxScraperConfig config = fromProperties(properties, new Properties()); // Then assertThat(config.getServiceUrl()) @@ -96,7 +107,7 @@ void shouldUseValuesFromProperties() throws ConfigurationException { .isNull(); // When - JmxScraperConfig config = JmxScraperConfig.fromProperties(properties, new Properties()); + JmxScraperConfig config = fromProperties(properties, new Properties()); config.propagateSystemProperties(); // Then @@ -137,7 +148,7 @@ void shouldRetainPredefinedSystemProperties() throws ConfigurationException { systemProperties.put("javax.net.ssl.keyStorePassword", "originalPassword"); // When - JmxScraperConfig config = JmxScraperConfig.fromProperties(properties, systemProperties); + JmxScraperConfig config = fromProperties(properties, systemProperties); // even when effective configuration is propagated to system properties original values are kept // due to priority of system properties over user-provided ones. config.propagateSystemProperties(); @@ -151,10 +162,10 @@ void shouldRetainPredefinedSystemProperties() throws ConfigurationException { void shouldFailValidation_missingServiceUrl() { // Given Properties properties = (Properties) validProperties.clone(); - properties.remove(JmxScraperConfig.SERVICE_URL); + properties.remove(SERVICE_URL); // When and Then - assertThatThrownBy(() -> JmxScraperConfig.fromProperties(properties, new Properties())) + assertThatThrownBy(() -> fromProperties(properties, new Properties())) .isInstanceOf(ConfigurationException.class) .hasMessage("otel.jmx.service.url must be specified."); } @@ -163,11 +174,11 @@ void shouldFailValidation_missingServiceUrl() { void shouldFailValidation_missingConfigPathAndTargetSystem() { // Given Properties properties = (Properties) validProperties.clone(); - properties.remove(JmxScraperConfig.CUSTOM_JMX_SCRAPING_CONFIG); - properties.remove(JmxScraperConfig.TARGET_SYSTEM); + properties.remove(CUSTOM_JMX_SCRAPING_CONFIG); + properties.remove(TARGET_SYSTEM); // When and Then - assertThatThrownBy(() -> JmxScraperConfig.fromProperties(properties, new Properties())) + assertThatThrownBy(() -> fromProperties(properties, new Properties())) .isInstanceOf(ConfigurationException.class) .hasMessage("otel.jmx.custom.scraping.config or otel.jmx.target.system must be specified."); } @@ -176,10 +187,10 @@ void shouldFailValidation_missingConfigPathAndTargetSystem() { void shouldFailValidation_invalidTargetSystem() { // Given Properties properties = (Properties) validProperties.clone(); - properties.setProperty(JmxScraperConfig.TARGET_SYSTEM, "hal9000"); + properties.setProperty(TARGET_SYSTEM, "hal9000"); // When and Then - assertThatThrownBy(() -> JmxScraperConfig.fromProperties(properties, new Properties())) + assertThatThrownBy(() -> fromProperties(properties, new Properties())) .isInstanceOf(ConfigurationException.class) .hasMessageStartingWith("[hal9000] must specify targets from "); } @@ -188,11 +199,11 @@ void shouldFailValidation_invalidTargetSystem() { void shouldFailValidation_missingOtlpEndpoint() { // Given Properties properties = (Properties) validProperties.clone(); - properties.remove(JmxScraperConfig.OTLP_ENDPOINT); - properties.setProperty(JmxScraperConfig.METRICS_EXPORTER_TYPE, "otlp"); + properties.remove(OTLP_ENDPOINT); + properties.setProperty(METRICS_EXPORTER_TYPE, "otlp"); // When and Then - assertThatThrownBy(() -> JmxScraperConfig.fromProperties(properties, new Properties())) + assertThatThrownBy(() -> fromProperties(properties, new Properties())) .isInstanceOf(ConfigurationException.class) .hasMessage("otel.exporter.otlp.endpoint must be specified for otlp format."); } @@ -201,11 +212,11 @@ void shouldFailValidation_missingOtlpEndpoint() { void shouldPassValidation_noMetricsExporterType() throws ConfigurationException { // Given Properties properties = (Properties) validProperties.clone(); - properties.remove(JmxScraperConfig.OTLP_ENDPOINT); - properties.remove(JmxScraperConfig.METRICS_EXPORTER_TYPE); + properties.remove(OTLP_ENDPOINT); + properties.remove(METRICS_EXPORTER_TYPE); // When - JmxScraperConfig config = JmxScraperConfig.fromProperties(properties, new Properties()); + JmxScraperConfig config = fromProperties(properties, new Properties()); // Then assertThat(config).isNotNull(); @@ -215,11 +226,11 @@ void shouldPassValidation_noMetricsExporterType() throws ConfigurationException void shouldPassValidation_nonOtlpMetricsExporterType() throws ConfigurationException { // Given Properties properties = (Properties) validProperties.clone(); - properties.remove(JmxScraperConfig.OTLP_ENDPOINT); - properties.setProperty(JmxScraperConfig.METRICS_EXPORTER_TYPE, "logging"); + properties.remove(OTLP_ENDPOINT); + properties.setProperty(METRICS_EXPORTER_TYPE, "logging"); // When - JmxScraperConfig config = JmxScraperConfig.fromProperties(properties, new Properties()); + JmxScraperConfig config = fromProperties(properties, new Properties()); // Then assertThat(config).isNotNull(); @@ -229,10 +240,10 @@ void shouldPassValidation_nonOtlpMetricsExporterType() throws ConfigurationExcep void shouldFailValidation_negativeInterval() { // Given Properties properties = (Properties) validProperties.clone(); - properties.setProperty(JmxScraperConfig.INTERVAL_MILLISECONDS, "-1"); + properties.setProperty(INTERVAL_MILLISECONDS, "-1"); // When and Then - assertThatThrownBy(() -> JmxScraperConfig.fromProperties(properties, new Properties())) + assertThatThrownBy(() -> fromProperties(properties, new Properties())) .isInstanceOf(ConfigurationException.class) .hasMessage("otel.jmx.interval.milliseconds must be positive."); } @@ -241,83 +252,16 @@ void shouldFailValidation_negativeInterval() { void shouldFailConfigCreation_invalidInterval() { // Given Properties properties = (Properties) validProperties.clone(); - properties.setProperty(JmxScraperConfig.INTERVAL_MILLISECONDS, "abc"); + properties.setProperty(INTERVAL_MILLISECONDS, "abc"); // When and Then - assertThatThrownBy(() -> JmxScraperConfig.fromProperties(properties, new Properties())) + assertThatThrownBy(() -> fromProperties(properties, new Properties())) .isInstanceOf(ConfigurationException.class) .hasMessage("Failed to parse otel.jmx.interval.milliseconds"); } - // @ClearSystemProperty(key = "otel.metric.export.interval") + // TODO: Tests below will be reimplemented - // @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") @@ -354,22 +298,6 @@ void shouldFailConfigCreation_invalidInterval() { // } // // @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") @@ -383,20 +311,6 @@ void shouldFailConfigCreation_invalidInterval() { // } // // @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(); From 2c95782292d06eb29c41f266466e3648ca025fc8 Mon Sep 17 00:00:00 2001 From: robsunday Date: Fri, 27 Sep 2024 10:08:42 +0200 Subject: [PATCH 39/39] Code review followup changes - added default value for OTLP endpoint --- .../jmxscraper/config/JmxScraperConfig.java | 13 ++++++---- .../config/JmxScraperConfigTest.java | 26 +++++++++++++++---- 2 files changed, 29 insertions(+), 10 deletions(-) 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 index ce4ddd080..edb7599fd 100644 --- 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 @@ -34,6 +34,8 @@ public class JmxScraperConfig { static final String JMX_REMOTE_PROFILE = "otel.jmx.remote.profile"; static final String JMX_REALM = "otel.jmx.realm"; + static final String OTLP_METRICS_EXPORTER = "otlp"; + static final List AVAILABLE_TARGET_SYSTEMS = Collections.unmodifiableList( Arrays.asList( @@ -148,8 +150,10 @@ public static JmxScraperConfig fromProperties( config.metricsExporterType = getAndSetPropertyIfUndefined(properties, METRICS_EXPORTER_TYPE, "logging"); - config.otlpExporterEndpoint = properties.getProperty(OTLP_ENDPOINT); - + if (OTLP_METRICS_EXPORTER.equalsIgnoreCase(config.metricsExporterType)) { + config.otlpExporterEndpoint = + getAndSetPropertyIfUndefined(properties, OTLP_ENDPOINT, "http://localhost:4318"); + } config.username = properties.getProperty(JMX_USERNAME); config.password = properties.getProperty(JMX_PASSWORD); @@ -232,9 +236,8 @@ private static void validateConfig(JmxScraperConfig config) throws Configuration "%s must specify targets from %s", config.targetSystems, AVAILABLE_TARGET_SYSTEMS)); } - if (isBlank(config.otlpExporterEndpoint) - && (!isBlank(config.metricsExporterType) - && config.metricsExporterType.equalsIgnoreCase("otlp"))) { + if (OTLP_METRICS_EXPORTER.equalsIgnoreCase(config.metricsExporterType) + && isBlank(config.otlpExporterEndpoint)) { throw new ConfigurationException(OTLP_ENDPOINT + " must be specified for otlp format."); } diff --git a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java index 2e6145baf..4764ada6a 100644 --- a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java +++ b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java @@ -37,7 +37,7 @@ static void setUp() { SERVICE_URL, "jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); validProperties.setProperty(CUSTOM_JMX_SCRAPING_CONFIG, ""); validProperties.setProperty(TARGET_SYSTEM, "tomcat, activemq"); - validProperties.setProperty(METRICS_EXPORTER_TYPE, "otel"); + validProperties.setProperty(METRICS_EXPORTER_TYPE, "otlp"); validProperties.setProperty(INTERVAL_MILLISECONDS, "1410"); validProperties.setProperty(REGISTRY_SSL, "true"); validProperties.setProperty(OTLP_ENDPOINT, "http://localhost:4317"); @@ -77,13 +77,29 @@ void shouldCreateMinimalValidConfiguration() throws ConfigurationException { assertThat(config.getTargetSystems()).isEmpty(); assertThat(config.getIntervalMilliseconds()).isEqualTo(10000); assertThat(config.getMetricsExporterType()).isEqualTo("logging"); - assertThat(config.getOtlpExporterEndpoint()).isNull(); + assertThat(config.getOtlpExporterEndpoint()).isBlank(); assertThat(config.getUsername()).isNull(); assertThat(config.getPassword()).isNull(); assertThat(config.getRemoteProfile()).isNull(); assertThat(config.getRealm()).isNull(); } + @Test + void shouldCreateConfig_defaultOtlEndpoint() throws ConfigurationException { + // Given + Properties properties = new Properties(); + properties.setProperty(SERVICE_URL, "jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); + properties.setProperty(CUSTOM_JMX_SCRAPING_CONFIG, "/file.properties"); + properties.setProperty(METRICS_EXPORTER_TYPE, "otlp"); + + // When + JmxScraperConfig config = fromProperties(properties, new Properties()); + + // Then + assertThat(config.getMetricsExporterType()).isEqualTo("otlp"); + assertThat(config.getOtlpExporterEndpoint()).isEqualTo("http://localhost:4318"); + } + @Test @ClearSystemProperty(key = "javax.net.ssl.keyStore") @ClearSystemProperty(key = "javax.net.ssl.keyStorePassword") @@ -116,7 +132,7 @@ void shouldUseValuesFromProperties() throws ConfigurationException { assertThat(config.getCustomJmxScrapingConfigPath()).isEqualTo(""); assertThat(config.getTargetSystems()).containsOnly("tomcat", "activemq"); assertThat(config.getIntervalMilliseconds()).isEqualTo(1410); - assertThat(config.getMetricsExporterType()).isEqualTo("otel"); + assertThat(config.getMetricsExporterType()).isEqualTo("otlp"); assertThat(config.getOtlpExporterEndpoint()).isEqualTo("http://localhost:4317"); assertThat(config.getUsername()).isEqualTo("some-user"); assertThat(config.getPassword()).isEqualTo("some-password"); @@ -196,10 +212,10 @@ void shouldFailValidation_invalidTargetSystem() { } @Test - void shouldFailValidation_missingOtlpEndpoint() { + void shouldFailValidation_blankOtlpEndpointProvided() { // Given Properties properties = (Properties) validProperties.clone(); - properties.remove(OTLP_ENDPOINT); + properties.setProperty(OTLP_ENDPOINT, ""); properties.setProperty(METRICS_EXPORTER_TYPE, "otlp"); // When and Then