From eac111a9c2fb2eea070c1b631c661c81cf12828a Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue, 25 Feb 2025 16:09:41 +0100 Subject: [PATCH 01/20] add jvm yaml + some docs --- instrumentation/jmx-metrics/library/jvm.md | 35 +++++ .../src/main/resources/jmx/rules/jvm.yaml | 125 ++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 instrumentation/jmx-metrics/library/jvm.md create mode 100644 instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml diff --git a/instrumentation/jmx-metrics/library/jvm.md b/instrumentation/jmx-metrics/library/jvm.md new file mode 100644 index 000000000000..bf8e1aa91396 --- /dev/null +++ b/instrumentation/jmx-metrics/library/jvm.md @@ -0,0 +1,35 @@ +# JVM Metrics + +Here is the list of metrics based on MBeans exposed by the JVM and that are defined in [`jvm.yaml`](./resources/jmx/rules/jvm.yaml). + +Those metrics are defined in the [JVM runtime metrics semantic conventions](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/). + +| Metric Name | semconv maturity | Type | Attributes | Description | +|---------------------------------------------------------------------------------------------------------------------------------------|:-----------------|---------------|---------------------------|----------------------------------------------------| +| [jvm.memory.used](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmmemoryused) | stable | UpDownCounter | jvm.memory.pool.name [^1] | Used memory | +| [jvm.memory.committed](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmmemorycommitted) | stable | UpDownCounter | jvm.memory.pool.name [^1] | Committed memory | +| [jvm.memory.limit](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmmemorylimit) | stable | UpDownCounter | jvm.memory.pool.name [^1] | Max obtainable memory | +| [jvm.memory.init](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmmemoryinit) | experimental | UpDownCounter | jvm.memory.pool.name [^1] | Initial memory requested | +| [jvm.memory.used_after_last_gc](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmmemoryused_after_last_gc) | stable | UpDownCounter | jvm.memory.pool.name [^1] | Memory used after latest GC | +| [jvm.thread.count](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmthreadcount) | stable | UpDownCounter | [^2] | Threads count | +| [jvm.class.loaded](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmclassloaded) | stable | Counter | | Classes loaded since JVM start | +| [jvm.class.unloaded](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmclassunloaded) | stable | Counter | | Classes unloaded since JVM start | +| [jvm.class.count](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmclasscount) | stable | UpDownCounter | | Classes currently loaded count | +| [jvm.cpu.count](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmcpucount) | stable | UpDownCounter | | Number of CPUs available | +| [jvm.cpu.recent_utilization](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmcpurecent_utilization) | stable | Gauge | | Recent CPU utilization for process reported by JVM | +| [jvm.system.cpu.load_1m](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmsystemcpuload_1m) | experimental | Gauge | | Average CPU load reported by JVM | +| [jvm.system.cpu.recent_utilization](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmcpurecent_utilization) | experimental | Gauge | | Recent CPU utilization reported by JVM | +| [jvm.buffer.memory.used](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmbuffermemoryused) | experimental | UpDownCounter | jvm.buffer.pool.name | Memory used by buffers | +| [jvm.buffer.memory.limit](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmbuffermemorylimit) | experimental | UpDownCounter | jvm.buffer.pool.name | Maximum memory usage for buffers | +| [jvm.buffer.memory.count](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmbuffermemorycount) | experimental | UpDownCounter | jvm.buffer.pool.name | Buffers count | + +## Limitations and unsupported metrics + +There are a few limitations to the JVM metrics that are captured through the JMX interface with declarative YAML. +Using the [instrumentation/runtime-telemetry](./instrumentation/runtime-telemetry) modules with instrumentation allow to capture metrics without those limitations. + +[^1]: `jvm.memory.type` attribute is not supported yet due to [#13361](https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13361) +[^2]: `jvm.thread.daemon` and `jvm.thread.state` attributes are not supported. + +- [jvm.gc.duration](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmgcduration) metric is not supported as it is only exposed through JMX notifications which are not supported with YAML. +- [jvm.cpu.time](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmcputime) metric is not supported yet due to lack of unit conversion, see [#13369](https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13369) for details. diff --git a/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml b/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml new file mode 100644 index 000000000000..960d1917e214 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml @@ -0,0 +1,125 @@ +--- + +rules: + + - bean: java.lang:type=MemoryPool,name=* + prefix: jvm.memory. + type: updowncounter + unit: By + metricAttribute: + jvm.memory.pool.name: param(name) + # TODO: here the value should be lower-cased https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13361 + # for now attribute is omitted to remain compliant with semconv + # jvm.memory.type: attribute(name) + mapping: + # jvm.memory.used + Usage.used: + metric: used + desc: Measure of memory used. + # jvm.memory.committed + Usage.committed: + metric: committed + desc: Measure of memory committed. + # jvm.memory.limit + Usage.max: + metric: limit + desc: Measure of max obtainable memory. + # jvm.memory.init (experimental) + Usage.init: + metric: init + desc: Measure of initial memory requested. + # jvm.memory.used_after_last_gc + CollectionUsage.used: + metric: used_after_last_gc + desc: Measure of memory used, as measured after the most recent garbage collection event on this pool. + + - bean: java.lang:type=Threading + prefix: jvm.thread. + mapping: + # jvm.thread.count + # limitation: 'jvm.thread.daemon' and 'jvm.thread.state' metric attributes are not provided + ThreadCount: + metric: count + type: updowncounter + unit: "{thread}" + desc: Number of executing platform threads. + + - bean: java.lang:type=ClassLoading + prefix: jvm.class. + type: updowncounter + unit: "{class}" + mapping: + # jvm.class.loaded + TotalLoadedClassCount: + metric: loaded + desc: Number of classes loaded since JVM start. + # jvm.class.unloaded + UnloadedClassCount: + metric: unloaded + desc: Number of classes unloaded since JVM start. + # jvm.class.count + LoadedClassCount: + metric: count + desc: Number of classes currently loaded. + + - bean: java.lang:type=OperatingSystem + prefix: jvm. + mapping: + # jvm.cpu.count + AvailableProcessors: + metric: cpu.count + type: updowncounter + unit: "{cpu}" + desc: Number of processors available to the Java virtual machine. + # jvm.cpu.time + # TODO requires time unit conversion to seconds be semconv compliant, so disabled for now + # https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13369 + # ProcessCpuTime: + # metric: jvm.cpu.time + # type: counter + # unit: ns + # desc: CPU time used by the process as reported by the JVM. + # # TODO: should we filter zero values ? + # jvm.cpu.recent_utilization + ProcessCpuLoad: + metric: cpu.recent_utilization + type: gauge + unit: 1 + desc: Recent CPU utilization for the process as reported by the JVM. + # TODO: should we filter zero values ? + # jvm.system.cpu.load_1m (experimental) + SystemLoadAverage: + metric: system.cpu.load_1m + type: gauge + unit: "{run_queue_item}" + desc: Average CPU load of the whole system for the last minute as reported by the JVM. + # TODO: should we filter zero values ? + # jvm.system.cpu.utilization (experimental) + SystemCpuLoad: + metric: system.cpu.utilization + type: gauge + unit: 1 + desc: Recent CPU utilization for the whole system as reported by the JVM. + # TODO: should we filter zero values ? + + - bean: java.nio:name=*,type=BufferPool + prefix: jvm.buffer. + type: updowncounter + metricAttribute: + jvm.buffer.pool.name: param(name) + mapping: + # jvm.buffer.memory.used (experimental) + MemoryUsed: + metric: memory.used + unit: By + desc: Measure of memory used by buffers. + # jvm.buffer.memory.limit (experimental) + TotalCapacity: + metric: memory.limit + unit: By + desc: Measure of total memory capacity of buffers. + # jvm.buffer.count (experimental) + Count: + metric: count + unit: "{buffer}" + desc: Number of buffers in the pool. From 2e166f2ecf7ef49f2576cc2be7a5b91467547bda Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue, 25 Feb 2025 17:58:10 +0100 Subject: [PATCH 02/20] fix some markdown --- instrumentation/jmx-metrics/library/jvm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/jmx-metrics/library/jvm.md b/instrumentation/jmx-metrics/library/jvm.md index bf8e1aa91396..dfaea1cf8a79 100644 --- a/instrumentation/jmx-metrics/library/jvm.md +++ b/instrumentation/jmx-metrics/library/jvm.md @@ -26,7 +26,7 @@ Those metrics are defined in the [JVM runtime metrics semantic conventions](http ## Limitations and unsupported metrics There are a few limitations to the JVM metrics that are captured through the JMX interface with declarative YAML. -Using the [instrumentation/runtime-telemetry](./instrumentation/runtime-telemetry) modules with instrumentation allow to capture metrics without those limitations. +Using the [instrumentation/runtime-telemetry](./instrumentation/runtime-telemetry) modules with instrumentation allow to capture metrics without those limitations. [^1]: `jvm.memory.type` attribute is not supported yet due to [#13361](https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13361) [^2]: `jvm.thread.daemon` and `jvm.thread.state` attributes are not supported. From 729063ebfe26d5d81216333e7fcb9591387d9e3d Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue, 25 Feb 2025 18:01:26 +0100 Subject: [PATCH 03/20] fix yaml link --- instrumentation/jmx-metrics/library/jvm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/jmx-metrics/library/jvm.md b/instrumentation/jmx-metrics/library/jvm.md index dfaea1cf8a79..a460f08846e0 100644 --- a/instrumentation/jmx-metrics/library/jvm.md +++ b/instrumentation/jmx-metrics/library/jvm.md @@ -1,6 +1,6 @@ # JVM Metrics -Here is the list of metrics based on MBeans exposed by the JVM and that are defined in [`jvm.yaml`](./resources/jmx/rules/jvm.yaml). +Here is the list of metrics based on MBeans exposed by the JVM and that are defined in [`jvm.yaml`](./src/main/resources/jmx/rules/jvm.yaml). Those metrics are defined in the [JVM runtime metrics semantic conventions](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/). From 1825e27a77d996fae539711ba4a78bcc085d75c9 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue, 25 Feb 2025 18:03:06 +0100 Subject: [PATCH 04/20] fix another link --- instrumentation/jmx-metrics/library/jvm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/jmx-metrics/library/jvm.md b/instrumentation/jmx-metrics/library/jvm.md index a460f08846e0..e02dd9bde6a8 100644 --- a/instrumentation/jmx-metrics/library/jvm.md +++ b/instrumentation/jmx-metrics/library/jvm.md @@ -26,7 +26,7 @@ Those metrics are defined in the [JVM runtime metrics semantic conventions](http ## Limitations and unsupported metrics There are a few limitations to the JVM metrics that are captured through the JMX interface with declarative YAML. -Using the [instrumentation/runtime-telemetry](./instrumentation/runtime-telemetry) modules with instrumentation allow to capture metrics without those limitations. +Using the [runtime-telemetry](../../runtime-telemetry) modules with instrumentation allow to capture metrics without those limitations. [^1]: `jvm.memory.type` attribute is not supported yet due to [#13361](https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13361) [^2]: `jvm.thread.daemon` and `jvm.thread.state` attributes are not supported. From aebd0589d4663aa31f98b12c9151fabbf09d3c6d Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Mon, 3 Mar 2025 13:27:30 +0100 Subject: [PATCH 05/20] add test dependencies + path to agent jar --- .../jmx-metrics/library/build.gradle.kts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/instrumentation/jmx-metrics/library/build.gradle.kts b/instrumentation/jmx-metrics/library/build.gradle.kts index 375e5e77d932..86f0cb0515a8 100644 --- a/instrumentation/jmx-metrics/library/build.gradle.kts +++ b/instrumentation/jmx-metrics/library/build.gradle.kts @@ -1,3 +1,5 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + plugins { id("otel.library-instrumentation") } @@ -6,4 +8,28 @@ dependencies { implementation("org.snakeyaml:snakeyaml-engine") testImplementation(project(":testing-common")) + testImplementation("org.testcontainers:testcontainers") + + testImplementation("org.testcontainers:junit-jupiter") + testImplementation("com.linecorp.armeria:armeria-junit5:1.31.3") + testImplementation("com.linecorp.armeria:armeria-junit5:1.31.3") + testImplementation("com.linecorp.armeria:armeria-grpc:1.31.3") + testImplementation("io.opentelemetry.proto:opentelemetry-proto:1.5.0-alpha") +} + +tasks { + test { + // get packaged agent jar for testing + val shadowTask = project(":javaagent").tasks.named("shadowJar").get() + + dependsOn(shadowTask) + + inputs.files(layout.files(shadowTask)) + .withPropertyName("javaagent") + .withNormalizer(ClasspathNormalizer::class) + + doFirst { + jvmArgs("-Dio.opentelemetry.javaagent.path=${shadowTask.archiveFile.get()}") + } + } } From 48ed53e43cf88f98928e5071ba1c8afc0e2b4f45 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Mon, 3 Mar 2025 13:27:56 +0100 Subject: [PATCH 06/20] fix jvm metrics definitions --- .../jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml b/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml index 960d1917e214..22f9a6ea6ccf 100644 --- a/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml +++ b/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml @@ -84,7 +84,7 @@ rules: ProcessCpuLoad: metric: cpu.recent_utilization type: gauge - unit: 1 + unit: '1' desc: Recent CPU utilization for the process as reported by the JVM. # TODO: should we filter zero values ? # jvm.system.cpu.load_1m (experimental) @@ -98,7 +98,7 @@ rules: SystemCpuLoad: metric: system.cpu.utilization type: gauge - unit: 1 + unit: '1' desc: Recent CPU utilization for the whole system as reported by the JVM. # TODO: should we filter zero values ? From ea42a9e3e954f3fa02cdd0f5adf62a9144a7e0ca Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Mon, 3 Mar 2025 13:28:20 +0100 Subject: [PATCH 07/20] add jvm rules to library --- .../src/main/resources/jmx/rules/jvm.yaml | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/jvm.yaml diff --git a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/jvm.yaml b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/jvm.yaml new file mode 100644 index 000000000000..22f9a6ea6ccf --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/jvm.yaml @@ -0,0 +1,125 @@ +--- + +rules: + + - bean: java.lang:type=MemoryPool,name=* + prefix: jvm.memory. + type: updowncounter + unit: By + metricAttribute: + jvm.memory.pool.name: param(name) + # TODO: here the value should be lower-cased https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13361 + # for now attribute is omitted to remain compliant with semconv + # jvm.memory.type: attribute(name) + mapping: + # jvm.memory.used + Usage.used: + metric: used + desc: Measure of memory used. + # jvm.memory.committed + Usage.committed: + metric: committed + desc: Measure of memory committed. + # jvm.memory.limit + Usage.max: + metric: limit + desc: Measure of max obtainable memory. + # jvm.memory.init (experimental) + Usage.init: + metric: init + desc: Measure of initial memory requested. + # jvm.memory.used_after_last_gc + CollectionUsage.used: + metric: used_after_last_gc + desc: Measure of memory used, as measured after the most recent garbage collection event on this pool. + + - bean: java.lang:type=Threading + prefix: jvm.thread. + mapping: + # jvm.thread.count + # limitation: 'jvm.thread.daemon' and 'jvm.thread.state' metric attributes are not provided + ThreadCount: + metric: count + type: updowncounter + unit: "{thread}" + desc: Number of executing platform threads. + + - bean: java.lang:type=ClassLoading + prefix: jvm.class. + type: updowncounter + unit: "{class}" + mapping: + # jvm.class.loaded + TotalLoadedClassCount: + metric: loaded + desc: Number of classes loaded since JVM start. + # jvm.class.unloaded + UnloadedClassCount: + metric: unloaded + desc: Number of classes unloaded since JVM start. + # jvm.class.count + LoadedClassCount: + metric: count + desc: Number of classes currently loaded. + + - bean: java.lang:type=OperatingSystem + prefix: jvm. + mapping: + # jvm.cpu.count + AvailableProcessors: + metric: cpu.count + type: updowncounter + unit: "{cpu}" + desc: Number of processors available to the Java virtual machine. + # jvm.cpu.time + # TODO requires time unit conversion to seconds be semconv compliant, so disabled for now + # https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13369 + # ProcessCpuTime: + # metric: jvm.cpu.time + # type: counter + # unit: ns + # desc: CPU time used by the process as reported by the JVM. + # # TODO: should we filter zero values ? + # jvm.cpu.recent_utilization + ProcessCpuLoad: + metric: cpu.recent_utilization + type: gauge + unit: '1' + desc: Recent CPU utilization for the process as reported by the JVM. + # TODO: should we filter zero values ? + # jvm.system.cpu.load_1m (experimental) + SystemLoadAverage: + metric: system.cpu.load_1m + type: gauge + unit: "{run_queue_item}" + desc: Average CPU load of the whole system for the last minute as reported by the JVM. + # TODO: should we filter zero values ? + # jvm.system.cpu.utilization (experimental) + SystemCpuLoad: + metric: system.cpu.utilization + type: gauge + unit: '1' + desc: Recent CPU utilization for the whole system as reported by the JVM. + # TODO: should we filter zero values ? + + - bean: java.nio:name=*,type=BufferPool + prefix: jvm.buffer. + type: updowncounter + metricAttribute: + jvm.buffer.pool.name: param(name) + mapping: + # jvm.buffer.memory.used (experimental) + MemoryUsed: + metric: memory.used + unit: By + desc: Measure of memory used by buffers. + # jvm.buffer.memory.limit (experimental) + TotalCapacity: + metric: memory.limit + unit: By + desc: Measure of total memory capacity of buffers. + # jvm.buffer.count (experimental) + Count: + metric: count + unit: "{buffer}" + desc: Number of buffers in the pool. From 62a323a2ae22a7838cbf9da2438941e72e2268af Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Mon, 3 Mar 2025 13:29:14 +0100 Subject: [PATCH 08/20] add test infrastructure + jvm metrics tests --- .../jmx/rules/JvmTargetSystemTest.java | 189 +++++++++++ .../jmx/rules/MetricsVerifier.java | 124 ++++++++ .../jmx/rules/TargetSystemTest.java | 297 ++++++++++++++++++ .../jmx/rules/assertions/Assertions.java | 16 + .../rules/assertions/AttributeMatcher.java | 61 ++++ .../assertions/AttributeMatcherGroup.java | 60 ++++ .../rules/assertions/DataPointAttributes.java | 53 ++++ .../jmx/rules/assertions/MetricAssert.java | 266 ++++++++++++++++ 8 files changed, 1066 insertions(+) create mode 100644 instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java create mode 100644 instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/MetricsVerifier.java create mode 100644 instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/TargetSystemTest.java create mode 100644 instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/Assertions.java create mode 100644 instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/AttributeMatcher.java create mode 100644 instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/AttributeMatcherGroup.java create mode 100644 instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/DataPointAttributes.java create mode 100644 instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/MetricAssert.java diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java new file mode 100644 index 000000000000..df7308596bb9 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java @@ -0,0 +1,189 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jmx.rules; + +import static io.opentelemetry.instrumentation.jmx.rules.assertions.DataPointAttributes.attributeWithAnyValue; + +import io.opentelemetry.instrumentation.jmx.rules.assertions.AttributeMatcher; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +public class JvmTargetSystemTest extends TargetSystemTest { + + @ParameterizedTest + @ValueSource( + strings = { + // openj9 image that might have slight differences for JVM metrics + "tomcat:jdk8-adoptopenjdk-openj9", + // basic tomcat image with standard hotspot jdk + "tomcat:9.0" + }) + void testJvmMetrics(String image) { + List yamlFiles = Collections.singletonList("jvm.yaml"); + + yamlFiles.forEach(this::validateYamlSyntax); + + List jvmArgs = new ArrayList<>(); + jvmArgs.add(javaAgentJvmArgument()); + jvmArgs.addAll(javaPropertiesToJvmArgs(otelConfigProperties(yamlFiles))); + + // testing with a basic tomcat image as test application to capture JVM metrics + GenericContainer target = + new GenericContainer<>(image) + .withEnv("CATALINA_OPTS", String.join(" ", jvmArgs)) + .withStartupTimeout(Duration.ofMinutes(2)) + .withExposedPorts(8080) + .waitingFor(Wait.forListeningPorts(8080)); + + copyFilesToTarget(target, yamlFiles); + + startTarget(target); + + AttributeMatcher jvmPoolName = attributeWithAnyValue("jvm.memory.pool.name"); + AttributeMatcher bufferPoolName = attributeWithAnyValue("jvm.buffer.pool.name"); + verifyMetrics( + MetricsVerifier.create() + .add( + "jvm.memory.used", + metric -> + metric + .hasDescription("Measure of memory used.") + .hasUnit("By") + .isUpDownCounter() + .hasDataPointsWithOneAttribute(jvmPoolName)) + .add( + "jvm.memory.committed", + metric -> + metric + .hasDescription("Measure of memory committed.") + .hasUnit("By") + .isUpDownCounter() + .hasDataPointsWithOneAttribute(jvmPoolName)) + .add( + "jvm.memory.limit", + metric -> + metric + .hasDescription("Measure of max obtainable memory.") + .hasUnit("By") + .isUpDownCounter() + .hasDataPointsWithOneAttribute(jvmPoolName)) + .add( + "jvm.memory.init", + metric -> + metric + .hasDescription("Measure of initial memory requested.") + .hasUnit("By") + .isUpDownCounter() + .hasDataPointsWithOneAttribute(jvmPoolName)) + .add( + "jvm.memory.used_after_last_gc", + metric -> + metric + .hasDescription( + "Measure of memory used, as measured after the most recent garbage collection event on this pool.") + .hasUnit("By") + .isUpDownCounter() + .hasDataPointsWithOneAttribute(jvmPoolName)) + .add( + "jvm.thread.count", + metric -> + metric + .hasDescription("Number of executing platform threads.") + .hasUnit("{thread}") + .isUpDownCounter() + .hasDataPointsWithoutAttributes()) + .add( + "jvm.class.loaded", + metric -> + metric + .hasDescription("Number of classes loaded since JVM start.") + .hasUnit("{class}") + .isUpDownCounter() + .hasDataPointsWithoutAttributes()) + .add( + "jvm.class.unloaded", + metric -> + metric + .hasDescription("Number of classes unloaded since JVM start.") + .hasUnit("{class}") + .isUpDownCounter() + .hasDataPointsWithoutAttributes()) + .add( + "jvm.class.count", + metric -> + metric + .hasDescription("Number of classes currently loaded.") + .hasUnit("{class}") + .isUpDownCounter() + .hasDataPointsWithoutAttributes()) + .add( + "jvm.cpu.count", + metric -> + metric + .hasDescription( + "Number of processors available to the Java virtual machine.") + .hasUnit("{cpu}") + .isUpDownCounter() + .hasDataPointsWithoutAttributes()) + .add( + "jvm.cpu.recent_utilization", + metric -> + metric + .hasDescription( + "Recent CPU utilization for the process as reported by the JVM.") + .hasUnit("1") + .isGauge() + .hasDataPointsWithoutAttributes()) + .add( + "jvm.system.cpu.load_1m", + metric -> + metric + .hasDescription( + "Average CPU load of the whole system for the last minute as reported by the JVM.") + .hasUnit("{run_queue_item}") + .isGauge() + .hasDataPointsWithoutAttributes()) + .add( + "jvm.system.cpu.utilization", + metric -> + metric + .hasDescription( + "Recent CPU utilization for the whole system as reported by the JVM.") + .hasUnit("1") + .isGauge() + .hasDataPointsWithoutAttributes()) + .add( + "jvm.buffer.memory.used", + metric -> + metric + .hasDescription("Measure of memory used by buffers.") + .hasUnit("By") + .isUpDownCounter() + .hasDataPointsWithOneAttribute(bufferPoolName)) + .add( + "jvm.buffer.memory.limit", + metric -> + metric + .hasDescription("Measure of total memory capacity of buffers.") + .hasUnit("By") + .isUpDownCounter() + .hasDataPointsWithOneAttribute(bufferPoolName)) + .add( + "jvm.buffer.count", + metric -> + metric + .hasDescription("Number of buffers in the pool.") + .hasUnit("{buffer}") + .isUpDownCounter() + .hasDataPointsWithOneAttribute(bufferPoolName))); + } +} diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/MetricsVerifier.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/MetricsVerifier.java new file mode 100644 index 000000000000..dff77ac7e1a2 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/MetricsVerifier.java @@ -0,0 +1,124 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jmx.rules; + +import static io.opentelemetry.instrumentation.jmx.rules.assertions.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.instrumentation.jmx.rules.assertions.MetricAssert; +import io.opentelemetry.proto.metrics.v1.Metric; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class MetricsVerifier { + + private final Map> assertions = new HashMap<>(); + private boolean strictMode = true; + + private MetricsVerifier() {} + + /** + * Create instance of MetricsVerifier configured to fail verification if any metric was not + * verified because there is no assertion defined for it. This behavior can be changed by calling + * {@link #disableStrictMode()} method. + * + * @return new instance of MetricsVerifier + * @see #disableStrictMode() + */ + public static MetricsVerifier create() { + return new MetricsVerifier(); + } + + /** + * Disable strict checks of metric assertions. It means that all metrics checks added after + * calling this method will not enforce asserting all metric properties and will not detect + * duplicate property assertions. Also, there will be no error reported if any of metrics was + * skipped because no assertion was added for it. + * + * @return this + * @see #verify(List) + * @see #add(String, Consumer) + */ + @CanIgnoreReturnValue + public MetricsVerifier disableStrictMode() { + strictMode = false; + return this; + } + + /** + * Add assertion for given metric + * + * @param metricName name of metric to be verified by provided assertion + * @param assertion an assertion to verify properties of the metric + * @return this + */ + @CanIgnoreReturnValue + public MetricsVerifier add(String metricName, Consumer assertion) { + if (assertions.containsKey(metricName)) { + throw new IllegalArgumentException("Duplicate assertion for metric " + metricName); + } + assertions.put( + metricName, + metric -> { + MetricAssert metricAssert = assertThat(metric); + metricAssert.setStrict(strictMode); + assertion.accept(metricAssert); + metricAssert.strictCheck(); + }); + return this; + } + + /** + * Execute all defined assertions against provided list of metrics. Error is reported if any of + * defined assertions failed. Error is also reported if any of expected metrics was not present in + * the metrics list, unless strict mode is disabled with {@link #disableStrictMode()}. + * + * @param metrics list of metrics to be verified + * @see #add(String, Consumer) + * @see #disableStrictMode() + */ + public void verify(List metrics) { + verifyAllExpectedMetricsWereReceived(metrics); + + Set unverifiedMetrics = new HashSet<>(); + + for (Metric metric : metrics) { + String metricName = metric.getName(); + Consumer assertion = assertions.get(metricName); + + if (assertion != null) { + assertion.accept(metric); + } else { + unverifiedMetrics.add(metricName); + } + } + + if (strictMode && !unverifiedMetrics.isEmpty()) { + fail("Metrics received but not verified because no assertion exists: " + unverifiedMetrics); + } + } + + private void verifyAllExpectedMetricsWereReceived(List metrics) { + Set receivedMetricNames = + metrics.stream().map(Metric::getName).collect(Collectors.toSet()); + Set assertionNames = new HashSet<>(assertions.keySet()); + + assertionNames.removeAll(receivedMetricNames); + if (!assertionNames.isEmpty()) { + fail( + "Metrics expected but not received: " + + assertionNames + + "\nReceived only: " + + receivedMetricNames); + } + } +} diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/TargetSystemTest.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/TargetSystemTest.java new file mode 100644 index 000000000000..eb0bc294bc5a --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/TargetSystemTest.java @@ -0,0 +1,297 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jmx.rules; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; + +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.instrumentation.jmx.yaml.JmxConfig; +import io.opentelemetry.instrumentation.jmx.yaml.JmxRule; +import io.opentelemetry.instrumentation.jmx.yaml.RuleParser; +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 io.opentelemetry.proto.metrics.v1.Metric; +import io.opentelemetry.proto.metrics.v1.ResourceMetrics; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +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; +import org.testcontainers.utility.MountableFile; + +/** Base class for testing YAML metric definitions with a real target system */ +public class TargetSystemTest { + + private static final Logger logger = LoggerFactory.getLogger(TargetSystemTest.class); + private static final Logger targetSystemLogger = LoggerFactory.getLogger("targetSystem"); + + private static final String AGENT_PATH = "/opentelemetry-instrumentation-javaagent.jar"; + + private static final Network network = Network.newNetwork(); + + private static OtlpGrpcServer otlpServer; + private static Path agentPath; + private static String otlpEndpoint; + + private GenericContainer targetSystem; + private Collection> targetDependencies; + + @BeforeAll + static void beforeAll() { + otlpServer = new OtlpGrpcServer(); + otlpServer.start(); + Testcontainers.exposeHostPorts(otlpServer.httpPort()); + otlpEndpoint = "http://host.testcontainers.internal:" + otlpServer.httpPort(); + + String path = System.getProperty("io.opentelemetry.javaagent.path"); + assertThat(path).isNotNull(); + agentPath = Paths.get(path); + assertThat(agentPath).isReadable().isNotEmptyFile(); + } + + @BeforeEach + void beforeEach() { + otlpServer.reset(); + } + + @AfterEach + void afterEAch() { + stop(targetSystem); + targetSystem = null; + + for (GenericContainer targetDependency : targetDependencies) { + stop(targetDependency); + } + targetDependencies = Collections.emptyList(); + } + + private static void stop(@Nullable GenericContainer container) { + if (container != null && container.isRunning()) { + container.stop(); + } + } + + @AfterAll + static void afterAll() { + try { + otlpServer.stop().get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + protected static String javaAgentJvmArgument() { + return "-javaagent:" + AGENT_PATH; + } + + protected static List javaPropertiesToJvmArgs(Map config) { + return config.entrySet().stream() + .map(e -> String.format("-D%s=%s", e.getKey(), e.getValue())) + .collect(Collectors.toList()); + } + + /** + * Generates otel configuration for JMX testing with instrumentation agent + * + * @param yamlFiles JMX metrics definitions in YAML + * @return map of otel configuration properties for JMX testing + */ + protected static Map otelConfigProperties(List yamlFiles) { + Map config = new HashMap<>(); + // only export metrics + config.put("otel.logs.exporter", "none"); + config.put("otel.traces.exporter", "none"); + config.put("otel.metrics.exporter", "otlp"); + // use test grpc endpoint + config.put("otel.exporter.otlp.endpoint", otlpEndpoint); + config.put("otel.exporter.otlp.protocol", "grpc"); + // short export interval for testing + config.put("otel.metric.export.interval", "5s"); + // disable runtime telemetry metrics + config.put("otel.instrumentation.runtime-telemetry.enabled", "false"); + // set yaml config files to test + config.put("otel.jmx.target", "tomcat"); + config.put( + "otel.jmx.config", + yamlFiles.stream() + .map(TargetSystemTest::containerYamlPath) + .collect(Collectors.joining(","))); + return config; + } + + /** + * Starts the target system + * + * @param target target system to start + */ + protected void startTarget(GenericContainer target) { + startTarget(target, Collections.emptyList()); + } + + /** + * Starts the target system with its dependencies first + * + * @param target target system + * @param targetDependencies dependencies of target system + */ + protected void startTarget( + GenericContainer target, Collection> targetDependencies) { + + // If there are any containers that must be started before target then initialize them. + // Then make target depending on them, so it is started after dependencies + this.targetDependencies = targetDependencies; + for (GenericContainer container : targetDependencies) { + container.withNetwork(network); + target.dependsOn(container); + } + + targetSystem = + target.withLogConsumer(new Slf4jLogConsumer(targetSystemLogger)).withNetwork(network); + targetSystem.start(); + } + + protected static void copyFilesToTarget(GenericContainer target, List yamlFiles) { + // copy agent to target system + target.withCopyFileToContainer(MountableFile.forHostPath(agentPath), AGENT_PATH); + + // copy yaml files to target system + for (String file : yamlFiles) { + String resourcePath = yamlResourcePath(file); + String destPath = containerYamlPath(file); + logger.info("copying yaml from resources {} to container {}", resourcePath, destPath); + target.withCopyFileToContainer(MountableFile.forClasspathResource(resourcePath), destPath); + } + } + + private static String yamlResourcePath(String yaml) { + return "jmx/rules/" + yaml; + } + + private static String containerYamlPath(String yaml) { + return "/" + yaml; + } + + /** + * Validates YAML definition by parsing it to check for syntax errors + * + * @param yaml path to YAML resource (in classpath) + */ + protected void validateYamlSyntax(String yaml) { + String path = yamlResourcePath(yaml); + try (InputStream input = TargetSystemTest.class.getClassLoader().getResourceAsStream(path)) { + JmxConfig config; + // try-catch to provide a slightly better error + try { + config = RuleParser.get().loadConfig(input); + } catch (RuntimeException e) { + fail("Failed to parse yaml file " + path, e); + throw e; + } + + // make sure all the rules in that file are valid + for (JmxRule rule : config.getRules()) { + try { + rule.buildMetricDef(); + } catch (Exception e) { + fail("Failed to build metric definition " + rule.getBean(), e); + } + } + } catch (IOException e) { + fail("Failed to read yaml file " + path, e); + } + } + + protected void verifyMetrics(MetricsVerifier metricsVerifier) { + await() + .atMost(Duration.ofSeconds(60)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted( + () -> { + List receivedMetrics = otlpServer.getMetrics(); + assertThat(receivedMetrics).describedAs("No metric received").isNotEmpty(); + + List metrics = + receivedMetrics.stream() + .map(ExportMetricsServiceRequest::getResourceMetricsList) + .flatMap(rm -> rm.stream().map(ResourceMetrics::getScopeMetricsList)) + .flatMap(Collection::stream) + .filter( + // TODO: disabling batch span exporter might help remove unwanted metrics + sm -> sm.getScope().getName().equals("io.opentelemetry.jmx")) + .flatMap(sm -> sm.getMetricsList().stream()) + .collect(Collectors.toList()); + + assertThat(metrics).describedAs("Metrics received but not from JMX").isNotEmpty(); + + metricsVerifier.verify(metrics); + }); + } + + /** Minimal OTLP gRPC backend to capture metrics */ + 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) { + + // verbose but helpful to diagnose what is received + logger.debug("receiving metrics {}", request); + + metricRequests.add(request); + responseObserver.onNext(ExportMetricsServiceResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + }) + .build()); + sb.http(0); + } + } +} diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/Assertions.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/Assertions.java new file mode 100644 index 000000000000..8dae56470a6a --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/Assertions.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jmx.rules.assertions; + +import io.opentelemetry.proto.metrics.v1.Metric; + +/** Dedicated Assertj extension to provide convenient fluent API for metrics testing */ +public class Assertions extends org.assertj.core.api.Assertions { + + public static MetricAssert assertThat(Metric metric) { + return new MetricAssert(metric); + } +} diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/AttributeMatcher.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/AttributeMatcher.java new file mode 100644 index 000000000000..33721e66cc0c --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/AttributeMatcher.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jmx.rules.assertions; + +import javax.annotation.Nullable; + +/** Implements functionality of matching data point attributes. */ +public class AttributeMatcher { + private final String attributeName; + @Nullable private final String attributeValue; + + /** + * Create instance used to match data point attribute with any value. + * + * @param attributeName matched attribute name + */ + AttributeMatcher(String attributeName) { + this(attributeName, null); + } + + /** + * Create instance used to match data point attribute with te same name and with the same value. + * + * @param attributeName attribute name + * @param attributeValue attribute value + */ + AttributeMatcher(String attributeName, @Nullable String attributeValue) { + this.attributeName = attributeName; + this.attributeValue = attributeValue; + } + + /** + * Return name of data point attribute that this AttributeMatcher is supposed to match value with. + * + * @return name of validated attribute + */ + public String getAttributeName() { + return attributeName; + } + + @Override + public String toString() { + return attributeValue == null + ? '{' + attributeName + '}' + : '{' + attributeName + '=' + attributeValue + '}'; + } + + /** + * Verify if this matcher is matching provided attribute value. If this matcher holds null value + * then it is matching any attribute value. + * + * @param value a value to be matched + * @return true if this matcher is matching provided value, false otherwise. + */ + boolean matchesValue(String value) { + return attributeValue == null || attributeValue.equals(value); + } +} diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/AttributeMatcherGroup.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/AttributeMatcherGroup.java new file mode 100644 index 000000000000..90e6a0da77c5 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/AttributeMatcherGroup.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jmx.rules.assertions; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + +/** Group of attribute matchers */ +public class AttributeMatcherGroup { + + // stored as a Map for easy lookup by name + private final Map matchers; + + /** + * Constructor for a set of attribute matchers + * + * @param matchers collection of matchers to build a group from + * @throws IllegalStateException if there is any duplicate key + */ + AttributeMatcherGroup(Collection matchers) { + this.matchers = + matchers.stream().collect(Collectors.toMap(AttributeMatcher::getAttributeName, m -> m)); + } + + /** + * Checks if attributes match this attribute matcher group + * + * @param attributes attributes to check as map + * @return {@literal true} when the attributes match all attributes from this group + */ + public boolean matches(Map attributes) { + if (attributes.size() != matchers.size()) { + return false; + } + + for (Map.Entry entry : attributes.entrySet()) { + AttributeMatcher matcher = matchers.get(entry.getKey()); + if (matcher == null) { + // no matcher for this key: unexpected key + return false; + } + + if (!matcher.matchesValue(entry.getValue())) { + // value does not match: unexpected value + return false; + } + } + + return true; + } + + @Override + public String toString() { + return matchers.values().toString(); + } +} diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/DataPointAttributes.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/DataPointAttributes.java new file mode 100644 index 000000000000..c72099e85eac --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/DataPointAttributes.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jmx.rules.assertions; + +import java.util.Arrays; + +/** + * Utility class implementing convenience static methods to construct data point attribute matchers + * and sets of matchers. + */ +public class DataPointAttributes { + private DataPointAttributes() {} + + /** + * Create instance of matcher that should be used to check if data point attribute with given name + * has value identical to the one provided as a parameter (exact match). + * + * @param name name of the data point attribute to check + * @param value expected value of checked data point attribute + * @return instance of matcher + */ + public static AttributeMatcher attribute(String name, String value) { + return new AttributeMatcher(name, value); + } + + /** + * Create instance of matcher that should be used to check if data point attribute with given name + * exists. Any value of the attribute is considered as matching (any value match). + * + * @param name name of the data point attribute to check + * @return instance of matcher + */ + public static AttributeMatcher attributeWithAnyValue(String name) { + return new AttributeMatcher(name); + } + + /** + * Creates a group of attribute matchers that should be used to verify data point attributes. + * + * @param attributes list of matchers to create group. It must contain matchers with unique names. + * @return group of attribute matchers + * @throws IllegalArgumentException if provided list contains two or more matchers with the same + * attribute name + * @see MetricAssert#hasDataPointsWithAttributes(AttributeMatcherGroup...) for detailed + * description off the algorithm used for matching + */ + public static AttributeMatcherGroup attributeGroup(AttributeMatcher... attributes) { + return new AttributeMatcherGroup(Arrays.asList(attributes)); + } +} diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/MetricAssert.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/MetricAssert.java new file mode 100644 index 000000000000..2ead92ae7668 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/MetricAssert.java @@ -0,0 +1,266 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jmx.rules.assertions; + +import static io.opentelemetry.instrumentation.jmx.rules.assertions.DataPointAttributes.attributeGroup; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.metrics.v1.Metric; +import io.opentelemetry.proto.metrics.v1.NumberDataPoint; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.internal.Integers; +import org.assertj.core.internal.Iterables; +import org.assertj.core.internal.Objects; + +public class MetricAssert extends AbstractAssert { + + private static final Objects objects = Objects.instance(); + private static final Iterables iterables = Iterables.instance(); + private static final Integers integers = Integers.instance(); + + private boolean strict; + + private boolean descriptionChecked; + private boolean unitChecked; + private boolean typeChecked; + private boolean dataPointAttributesChecked; + + MetricAssert(Metric actual) { + super(actual, MetricAssert.class); + } + + public void setStrict(boolean strict) { + this.strict = strict; + } + + public void strictCheck() { + strictCheck("description", /* expectedCheckStatus= */ true, descriptionChecked); + strictCheck("unit", /* expectedCheckStatus= */ true, unitChecked); + strictCheck("type", /* expectedCheckStatus= */ true, typeChecked); + strictCheck( + "data point attributes", /* expectedCheckStatus= */ true, dataPointAttributesChecked); + } + + private void strictCheck( + String metricProperty, boolean expectedCheckStatus, boolean actualCheckStatus) { + if (!strict) { + return; + } + String failMsgPrefix = expectedCheckStatus ? "Missing" : "Duplicate"; + info.description( + "%s assertion on %s for metric '%s'", failMsgPrefix, metricProperty, actual.getName()); + objects.assertEqual(info, actualCheckStatus, expectedCheckStatus); + } + + /** + * Verifies metric description + * + * @param description expected description + * @return this + */ + @CanIgnoreReturnValue + public MetricAssert hasDescription(String description) { + isNotNull(); + + info.description("unexpected description for metric '%s'", actual.getName()); + objects.assertEqual(info, actual.getDescription(), description); + strictCheck("description", /* expectedCheckStatus= */ false, descriptionChecked); + descriptionChecked = true; + return this; + } + + /** + * Verifies metric unit + * + * @param unit expected unit + * @return this + */ + @CanIgnoreReturnValue + public MetricAssert hasUnit(String unit) { + isNotNull(); + + info.description("unexpected unit for metric '%s'", actual.getName()); + objects.assertEqual(info, actual.getUnit(), unit); + strictCheck("unit", /* expectedCheckStatus= */ false, unitChecked); + unitChecked = true; + return this; + } + + /** + * Verifies the metric is a gauge + * + * @return this + */ + @CanIgnoreReturnValue + public MetricAssert isGauge() { + isNotNull(); + + info.description("gauge expected for metric '%s'", actual.getName()); + objects.assertEqual(info, actual.hasGauge(), true); + strictCheck("type", /* expectedCheckStatus= */ false, typeChecked); + typeChecked = true; + return this; + } + + @CanIgnoreReturnValue + private MetricAssert hasSum(boolean monotonic) { + isNotNull(); + + info.description("sum expected for metric '%s'", actual.getName()); + objects.assertEqual(info, actual.hasSum(), true); + + String sumType = monotonic ? "monotonic" : "non-monotonic"; + info.description("sum for metric '%s' is expected to be %s", actual.getName(), sumType); + objects.assertEqual(info, actual.getSum().getIsMonotonic(), monotonic); + return this; + } + + /** + * Verifies the metric is a counter + * + * @return this + */ + @CanIgnoreReturnValue + public MetricAssert isCounter() { + // counters have a monotonic sum as their value can't decrease + hasSum(true); + strictCheck("type", /* expectedCheckStatus= */ false, typeChecked); + typeChecked = true; + return this; + } + + /** + * Verifies the metric is an up-down counter + * + * @return this + */ + @CanIgnoreReturnValue + public MetricAssert isUpDownCounter() { + // up down counters are non-monotonic as their value can increase & decrease + hasSum(false); + strictCheck("type", /* expectedCheckStatus= */ false, typeChecked); + typeChecked = true; + return this; + } + + /** + * Verifies that there is no attribute in any of data points. + * + * @return this + */ + @CanIgnoreReturnValue + public MetricAssert hasDataPointsWithoutAttributes() { + isNotNull(); + + return checkDataPoints( + dataPoints -> { + dataPointsCommonCheck(dataPoints); + + // all data points must not have any attribute + for (NumberDataPoint dataPoint : dataPoints) { + info.description( + "no attribute expected on data point for metric '%s'", actual.getName()); + iterables.assertEmpty(info, dataPoint.getAttributesList()); + } + }); + } + + @CanIgnoreReturnValue + private MetricAssert checkDataPoints(Consumer> listConsumer) { + // in practice usually one set of data points is provided but the + // protobuf does not enforce that, so we have to ensure checking at least one + int count = 0; + if (actual.hasGauge()) { + count++; + listConsumer.accept(actual.getGauge().getDataPointsList()); + } + if (actual.hasSum()) { + count++; + listConsumer.accept(actual.getSum().getDataPointsList()); + } + info.description("at least one set of data points expected for metric '%s'", actual.getName()); + integers.assertGreaterThan(info, count, 0); + + strictCheck( + "data point attributes", /* expectedCheckStatus= */ false, dataPointAttributesChecked); + dataPointAttributesChecked = true; + return this; + } + + private void dataPointsCommonCheck(List dataPoints) { + info.description("unable to retrieve data points from metric '%s'", actual.getName()); + objects.assertNotNull(info, dataPoints); + + // at least one data point must be reported + info.description("at least one data point expected for metric '%s'", actual.getName()); + iterables.assertNotEmpty(info, dataPoints); + } + + /** + * Verifies that all metric data points have the same expected one attribute + * + * @param expectedAttribute attribute matcher to validate data points attributes + * @return this + */ + @CanIgnoreReturnValue + public final MetricAssert hasDataPointsWithOneAttribute(AttributeMatcher expectedAttribute) { + return hasDataPointsWithAttributes(attributeGroup(expectedAttribute)); + } + + /** + * Verifies that every data point attributes is matched exactly by one of the matcher groups + * provided. Also, each matcher group must match at least one data point attributes set. Data + * point attributes are matched by matcher group if each attribute is matched by one matcher and + * each matcher matches one attribute. In other words: number of attributes is the same as number + * of matchers and there is 1:1 matching between them. + * + * @param matcherGroups array of attribute matcher groups + * @return this + */ + @CanIgnoreReturnValue + public final MetricAssert hasDataPointsWithAttributes(AttributeMatcherGroup... matcherGroups) { + return checkDataPoints( + dataPoints -> { + dataPointsCommonCheck(dataPoints); + + boolean[] matchedSets = new boolean[matcherGroups.length]; + + // validate each datapoint attributes match exactly one of the provided attributes sets + for (NumberDataPoint dataPoint : dataPoints) { + Map dataPointAttributes = + dataPoint.getAttributesList().stream() + .collect( + Collectors.toMap(KeyValue::getKey, kv -> kv.getValue().getStringValue())); + int matchCount = 0; + for (int i = 0; i < matcherGroups.length; i++) { + if (matcherGroups[i].matches(dataPointAttributes)) { + matchedSets[i] = true; + matchCount++; + } + } + + info.description( + "data point attributes '%s' for metric '%s' must match exactly one of the attribute sets '%s'.\nActual data points: %s", + dataPointAttributes, actual.getName(), Arrays.asList(matcherGroups), dataPoints); + integers.assertEqual(info, matchCount, 1); + } + + // check that all attribute sets matched at least once + for (int i = 0; i < matchedSets.length; i++) { + info.description( + "no data point matched attribute set '%s' for metric '%s'", + matcherGroups[i], actual.getName()); + objects.assertEqual(info, matchedSets[i], true); + } + }); + } +} From 887dee1275e66bd190e11173f3bde1b80287c3bd Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:01:51 +0100 Subject: [PATCH 09/20] work around checkstyle rules --- .../instrumentation/jmx/rules/MetricsVerifier.java | 2 +- .../instrumentation/jmx/rules/TargetSystemTest.java | 2 +- .../jmx/rules/assertions/{Assertions.java => JmxAssertj.java} | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) rename instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/{Assertions.java => JmxAssertj.java} (64%) diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/MetricsVerifier.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/MetricsVerifier.java index dff77ac7e1a2..b89659806d65 100644 --- a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/MetricsVerifier.java +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/MetricsVerifier.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.jmx.rules; -import static io.opentelemetry.instrumentation.jmx.rules.assertions.Assertions.assertThat; +import static io.opentelemetry.instrumentation.jmx.rules.assertions.JmxAssertj.assertThat; import static org.assertj.core.api.Assertions.fail; import com.google.errorprone.annotations.CanIgnoreReturnValue; diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/TargetSystemTest.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/TargetSystemTest.java index eb0bc294bc5a..bc4856e7b14f 100644 --- a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/TargetSystemTest.java +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/TargetSystemTest.java @@ -85,7 +85,7 @@ void beforeEach() { } @AfterEach - void afterEAch() { + void afterEach() { stop(targetSystem); targetSystem = null; diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/Assertions.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/JmxAssertj.java similarity index 64% rename from instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/Assertions.java rename to instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/JmxAssertj.java index 8dae56470a6a..45a2d0cd6588 100644 --- a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/Assertions.java +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/JmxAssertj.java @@ -8,7 +8,9 @@ import io.opentelemetry.proto.metrics.v1.Metric; /** Dedicated Assertj extension to provide convenient fluent API for metrics testing */ -public class Assertions extends org.assertj.core.api.Assertions { +// TODO: we should contribute this back to sdk-testing +// This has been intentionally not named `*Assertions` to prevent checkstyle rule to be triggered +public class JmxAssertj extends org.assertj.core.api.Assertions { public static MetricAssert assertThat(Metric metric) { return new MetricAssert(metric); From 65e33ffa9c377c5f45b8aa40f259a066bd5418d6 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue, 4 Mar 2025 10:57:48 +0100 Subject: [PATCH 10/20] add missing jvm.memory.type attribute --- .../src/main/resources/jmx/rules/jvm.yaml | 4 +--- .../jmx/rules/JvmTargetSystemTest.java | 23 ++++++++++++++----- .../jmx/rules/TargetSystemTest.java | 6 +++-- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml b/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml index 22f9a6ea6ccf..d9c6a9e63f08 100644 --- a/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml +++ b/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml @@ -8,9 +8,7 @@ rules: unit: By metricAttribute: jvm.memory.pool.name: param(name) - # TODO: here the value should be lower-cased https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13361 - # for now attribute is omitted to remain compliant with semconv - # jvm.memory.type: attribute(name) + jvm.memory.type: lowercase(beanattr(Type)) mapping: # jvm.memory.used Usage.used: diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java index df7308596bb9..27c3a27a120f 100644 --- a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java @@ -5,9 +5,12 @@ package io.opentelemetry.instrumentation.jmx.rules; +import static io.opentelemetry.instrumentation.jmx.rules.assertions.DataPointAttributes.attribute; +import static io.opentelemetry.instrumentation.jmx.rules.assertions.DataPointAttributes.attributeGroup; import static io.opentelemetry.instrumentation.jmx.rules.assertions.DataPointAttributes.attributeWithAnyValue; import io.opentelemetry.instrumentation.jmx.rules.assertions.AttributeMatcher; +import io.opentelemetry.instrumentation.jmx.rules.assertions.AttributeMatcherGroup; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; @@ -48,7 +51,14 @@ void testJvmMetrics(String image) { startTarget(target); - AttributeMatcher jvmPoolName = attributeWithAnyValue("jvm.memory.pool.name"); + AttributeMatcher poolNameAttribute = attributeWithAnyValue("jvm.memory.pool.name"); + + AttributeMatcherGroup heapPoolAttributes = + attributeGroup(attribute("jvm.memory.type", "heap"), poolNameAttribute); + + AttributeMatcherGroup nonHeapPoolAttributes = + attributeGroup(attribute("jvm.memory.type", "non_heap"), poolNameAttribute); + AttributeMatcher bufferPoolName = attributeWithAnyValue("jvm.buffer.pool.name"); verifyMetrics( MetricsVerifier.create() @@ -59,7 +69,7 @@ void testJvmMetrics(String image) { .hasDescription("Measure of memory used.") .hasUnit("By") .isUpDownCounter() - .hasDataPointsWithOneAttribute(jvmPoolName)) + .hasDataPointsWithAttributes(heapPoolAttributes, nonHeapPoolAttributes)) .add( "jvm.memory.committed", metric -> @@ -67,7 +77,7 @@ void testJvmMetrics(String image) { .hasDescription("Measure of memory committed.") .hasUnit("By") .isUpDownCounter() - .hasDataPointsWithOneAttribute(jvmPoolName)) + .hasDataPointsWithAttributes(heapPoolAttributes, nonHeapPoolAttributes)) .add( "jvm.memory.limit", metric -> @@ -75,7 +85,7 @@ void testJvmMetrics(String image) { .hasDescription("Measure of max obtainable memory.") .hasUnit("By") .isUpDownCounter() - .hasDataPointsWithOneAttribute(jvmPoolName)) + .hasDataPointsWithAttributes(heapPoolAttributes, nonHeapPoolAttributes)) .add( "jvm.memory.init", metric -> @@ -83,7 +93,7 @@ void testJvmMetrics(String image) { .hasDescription("Measure of initial memory requested.") .hasUnit("By") .isUpDownCounter() - .hasDataPointsWithOneAttribute(jvmPoolName)) + .hasDataPointsWithAttributes(heapPoolAttributes, nonHeapPoolAttributes)) .add( "jvm.memory.used_after_last_gc", metric -> @@ -92,7 +102,8 @@ void testJvmMetrics(String image) { "Measure of memory used, as measured after the most recent garbage collection event on this pool.") .hasUnit("By") .isUpDownCounter() - .hasDataPointsWithOneAttribute(jvmPoolName)) + // note: there is no GC for non-heap memory + .hasDataPointsWithAttributes(heapPoolAttributes)) .add( "jvm.thread.count", metric -> diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/TargetSystemTest.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/TargetSystemTest.java index bc4856e7b14f..8dcee7738699 100644 --- a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/TargetSystemTest.java +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/TargetSystemTest.java @@ -89,8 +89,10 @@ void afterEach() { stop(targetSystem); targetSystem = null; - for (GenericContainer targetDependency : targetDependencies) { - stop(targetDependency); + if (targetDependencies != null) { + for (GenericContainer targetDependency : targetDependencies) { + stop(targetDependency); + } } targetDependencies = Collections.emptyList(); } From cf18b34e03c3aa6e914deb90d5b86e998625a50e Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue, 4 Mar 2025 11:36:26 +0100 Subject: [PATCH 11/20] add comment for jvm memory type on heap only --- .../jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml b/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml index d9c6a9e63f08..004ebaa3b95a 100644 --- a/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml +++ b/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml @@ -27,6 +27,7 @@ rules: metric: init desc: Measure of initial memory requested. # jvm.memory.used_after_last_gc + # note: metric attribute "jvm.memory.type" will always be "heap" as GC only manages heap CollectionUsage.used: metric: used_after_last_gc desc: Measure of memory used, as measured after the most recent garbage collection event on this pool. From b672f791bf6f18ecb23f0b0e1425cdd836fafbe3 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue, 4 Mar 2025 11:52:42 +0100 Subject: [PATCH 12/20] remove duplicated yaml --- .../src/main/resources/jmx/rules/jvm.yaml | 125 ------------------ 1 file changed, 125 deletions(-) delete mode 100644 instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/jvm.yaml diff --git a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/jvm.yaml b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/jvm.yaml deleted file mode 100644 index 22f9a6ea6ccf..000000000000 --- a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/jvm.yaml +++ /dev/null @@ -1,125 +0,0 @@ ---- - -rules: - - - bean: java.lang:type=MemoryPool,name=* - prefix: jvm.memory. - type: updowncounter - unit: By - metricAttribute: - jvm.memory.pool.name: param(name) - # TODO: here the value should be lower-cased https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13361 - # for now attribute is omitted to remain compliant with semconv - # jvm.memory.type: attribute(name) - mapping: - # jvm.memory.used - Usage.used: - metric: used - desc: Measure of memory used. - # jvm.memory.committed - Usage.committed: - metric: committed - desc: Measure of memory committed. - # jvm.memory.limit - Usage.max: - metric: limit - desc: Measure of max obtainable memory. - # jvm.memory.init (experimental) - Usage.init: - metric: init - desc: Measure of initial memory requested. - # jvm.memory.used_after_last_gc - CollectionUsage.used: - metric: used_after_last_gc - desc: Measure of memory used, as measured after the most recent garbage collection event on this pool. - - - bean: java.lang:type=Threading - prefix: jvm.thread. - mapping: - # jvm.thread.count - # limitation: 'jvm.thread.daemon' and 'jvm.thread.state' metric attributes are not provided - ThreadCount: - metric: count - type: updowncounter - unit: "{thread}" - desc: Number of executing platform threads. - - - bean: java.lang:type=ClassLoading - prefix: jvm.class. - type: updowncounter - unit: "{class}" - mapping: - # jvm.class.loaded - TotalLoadedClassCount: - metric: loaded - desc: Number of classes loaded since JVM start. - # jvm.class.unloaded - UnloadedClassCount: - metric: unloaded - desc: Number of classes unloaded since JVM start. - # jvm.class.count - LoadedClassCount: - metric: count - desc: Number of classes currently loaded. - - - bean: java.lang:type=OperatingSystem - prefix: jvm. - mapping: - # jvm.cpu.count - AvailableProcessors: - metric: cpu.count - type: updowncounter - unit: "{cpu}" - desc: Number of processors available to the Java virtual machine. - # jvm.cpu.time - # TODO requires time unit conversion to seconds be semconv compliant, so disabled for now - # https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13369 - # ProcessCpuTime: - # metric: jvm.cpu.time - # type: counter - # unit: ns - # desc: CPU time used by the process as reported by the JVM. - # # TODO: should we filter zero values ? - # jvm.cpu.recent_utilization - ProcessCpuLoad: - metric: cpu.recent_utilization - type: gauge - unit: '1' - desc: Recent CPU utilization for the process as reported by the JVM. - # TODO: should we filter zero values ? - # jvm.system.cpu.load_1m (experimental) - SystemLoadAverage: - metric: system.cpu.load_1m - type: gauge - unit: "{run_queue_item}" - desc: Average CPU load of the whole system for the last minute as reported by the JVM. - # TODO: should we filter zero values ? - # jvm.system.cpu.utilization (experimental) - SystemCpuLoad: - metric: system.cpu.utilization - type: gauge - unit: '1' - desc: Recent CPU utilization for the whole system as reported by the JVM. - # TODO: should we filter zero values ? - - - bean: java.nio:name=*,type=BufferPool - prefix: jvm.buffer. - type: updowncounter - metricAttribute: - jvm.buffer.pool.name: param(name) - mapping: - # jvm.buffer.memory.used (experimental) - MemoryUsed: - metric: memory.used - unit: By - desc: Measure of memory used by buffers. - # jvm.buffer.memory.limit (experimental) - TotalCapacity: - metric: memory.limit - unit: By - desc: Measure of total memory capacity of buffers. - # jvm.buffer.count (experimental) - Count: - metric: count - unit: "{buffer}" - desc: Number of buffers in the pool. From 3553639f8ab691dfad32683ea0477a8e809c5a50 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue, 4 Mar 2025 11:56:03 +0100 Subject: [PATCH 13/20] fix typo in readme --- instrumentation/jmx-metrics/javaagent/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/jmx-metrics/javaagent/README.md b/instrumentation/jmx-metrics/javaagent/README.md index 8272e893e327..7f285cc24659 100644 --- a/instrumentation/jmx-metrics/javaagent/README.md +++ b/instrumentation/jmx-metrics/javaagent/README.md @@ -302,7 +302,7 @@ rules: unit: By metricAttribute: jvm.memory.pool.name : param(name) - jvm.memory.type: lowercase(beanattr(type)) + jvm.memory.type: lowercase(beanattr(Type)) ``` For now, only the `lowercase` transformation is supported, other additions might be added in the future if needed. From 238862cc9433ed64cf3911d7b46f52aba9876fae Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue, 4 Mar 2025 17:11:59 +0100 Subject: [PATCH 14/20] add jvm.memory.type in jvm metrics docs --- instrumentation/jmx-metrics/library/jvm.md | 39 +++++++++++----------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/instrumentation/jmx-metrics/library/jvm.md b/instrumentation/jmx-metrics/library/jvm.md index e02dd9bde6a8..c8cf98eaa2be 100644 --- a/instrumentation/jmx-metrics/library/jvm.md +++ b/instrumentation/jmx-metrics/library/jvm.md @@ -4,32 +4,31 @@ Here is the list of metrics based on MBeans exposed by the JVM and that are defi Those metrics are defined in the [JVM runtime metrics semantic conventions](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/). -| Metric Name | semconv maturity | Type | Attributes | Description | -|---------------------------------------------------------------------------------------------------------------------------------------|:-----------------|---------------|---------------------------|----------------------------------------------------| -| [jvm.memory.used](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmmemoryused) | stable | UpDownCounter | jvm.memory.pool.name [^1] | Used memory | -| [jvm.memory.committed](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmmemorycommitted) | stable | UpDownCounter | jvm.memory.pool.name [^1] | Committed memory | -| [jvm.memory.limit](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmmemorylimit) | stable | UpDownCounter | jvm.memory.pool.name [^1] | Max obtainable memory | -| [jvm.memory.init](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmmemoryinit) | experimental | UpDownCounter | jvm.memory.pool.name [^1] | Initial memory requested | -| [jvm.memory.used_after_last_gc](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmmemoryused_after_last_gc) | stable | UpDownCounter | jvm.memory.pool.name [^1] | Memory used after latest GC | -| [jvm.thread.count](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmthreadcount) | stable | UpDownCounter | [^2] | Threads count | -| [jvm.class.loaded](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmclassloaded) | stable | Counter | | Classes loaded since JVM start | -| [jvm.class.unloaded](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmclassunloaded) | stable | Counter | | Classes unloaded since JVM start | -| [jvm.class.count](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmclasscount) | stable | UpDownCounter | | Classes currently loaded count | -| [jvm.cpu.count](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmcpucount) | stable | UpDownCounter | | Number of CPUs available | -| [jvm.cpu.recent_utilization](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmcpurecent_utilization) | stable | Gauge | | Recent CPU utilization for process reported by JVM | -| [jvm.system.cpu.load_1m](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmsystemcpuload_1m) | experimental | Gauge | | Average CPU load reported by JVM | -| [jvm.system.cpu.recent_utilization](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmcpurecent_utilization) | experimental | Gauge | | Recent CPU utilization reported by JVM | -| [jvm.buffer.memory.used](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmbuffermemoryused) | experimental | UpDownCounter | jvm.buffer.pool.name | Memory used by buffers | -| [jvm.buffer.memory.limit](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmbuffermemorylimit) | experimental | UpDownCounter | jvm.buffer.pool.name | Maximum memory usage for buffers | -| [jvm.buffer.memory.count](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmbuffermemorycount) | experimental | UpDownCounter | jvm.buffer.pool.name | Buffers count | +| Metric Name | semconv maturity | Type | Attributes | Description | +|---------------------------------------------------------------------------------------------------------------------------------------|:-----------------|---------------|---------------------------------------|----------------------------------------------------| +| [jvm.memory.used](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmmemoryused) | stable | UpDownCounter | jvm.memory.pool.name, jvm.memory.type | Used memory | +| [jvm.memory.committed](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmmemorycommitted) | stable | UpDownCounter | jvm.memory.pool.name, jvm.memory.type | Committed memory | +| [jvm.memory.limit](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmmemorylimit) | stable | UpDownCounter | jvm.memory.pool.name, jvm.memory.type | Max obtainable memory | +| [jvm.memory.init](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmmemoryinit) | experimental | UpDownCounter | jvm.memory.pool.name, jvm.memory.type | Initial memory requested | +| [jvm.memory.used_after_last_gc](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmmemoryused_after_last_gc) | stable | UpDownCounter | jvm.memory.pool.name, jvm.memory.type | Memory used after latest GC | +| [jvm.thread.count](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmthreadcount) | stable | UpDownCounter | [^1] | Threads count | +| [jvm.class.loaded](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmclassloaded) | stable | Counter | | Classes loaded since JVM start | +| [jvm.class.unloaded](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmclassunloaded) | stable | Counter | | Classes unloaded since JVM start | +| [jvm.class.count](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmclasscount) | stable | UpDownCounter | | Classes currently loaded count | +| [jvm.cpu.count](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmcpucount) | stable | UpDownCounter | | Number of CPUs available | +| [jvm.cpu.recent_utilization](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmcpurecent_utilization) | stable | Gauge | | Recent CPU utilization for process reported by JVM | +| [jvm.system.cpu.load_1m](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmsystemcpuload_1m) | experimental | Gauge | | Average CPU load reported by JVM | +| [jvm.system.cpu.recent_utilization](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmcpurecent_utilization) | experimental | Gauge | | Recent CPU utilization reported by JVM | +| [jvm.buffer.memory.used](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmbuffermemoryused) | experimental | UpDownCounter | jvm.buffer.pool.name | Memory used by buffers | +| [jvm.buffer.memory.limit](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmbuffermemorylimit) | experimental | UpDownCounter | jvm.buffer.pool.name | Maximum memory usage for buffers | +| [jvm.buffer.memory.count](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmbuffermemorycount) | experimental | UpDownCounter | jvm.buffer.pool.name | Buffers count | ## Limitations and unsupported metrics There are a few limitations to the JVM metrics that are captured through the JMX interface with declarative YAML. Using the [runtime-telemetry](../../runtime-telemetry) modules with instrumentation allow to capture metrics without those limitations. -[^1]: `jvm.memory.type` attribute is not supported yet due to [#13361](https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13361) -[^2]: `jvm.thread.daemon` and `jvm.thread.state` attributes are not supported. +[^1]: `jvm.thread.daemon` and `jvm.thread.state` attributes are not supported. - [jvm.gc.duration](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmgcduration) metric is not supported as it is only exposed through JMX notifications which are not supported with YAML. - [jvm.cpu.time](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmcputime) metric is not supported yet due to lack of unit conversion, see [#13369](https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13369) for details. From 0304bd8b783f73c74d6f661152893e21dee62d83 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 26 Mar 2025 16:05:51 +0100 Subject: [PATCH 15/20] add test for jvm.cpu.time --- .../src/main/resources/jmx/rules/jvm.yaml | 19 ++++++++----------- .../jmx/rules/JvmTargetSystemTest.java | 5 +++++ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml b/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml index 004ebaa3b95a..ad4fb5272a07 100644 --- a/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml +++ b/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml @@ -63,6 +63,8 @@ rules: - bean: java.lang:type=OperatingSystem prefix: jvm. + # TODO: drop negative values once https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/13589 is merged + # dropNegativeValues: true mapping: # jvm.cpu.count AvailableProcessors: @@ -71,35 +73,30 @@ rules: unit: "{cpu}" desc: Number of processors available to the Java virtual machine. # jvm.cpu.time - # TODO requires time unit conversion to seconds be semconv compliant, so disabled for now - # https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13369 - # ProcessCpuTime: - # metric: jvm.cpu.time - # type: counter - # unit: ns - # desc: CPU time used by the process as reported by the JVM. - # # TODO: should we filter zero values ? + ProcessCpuTime: + metric: jvm.cpu.time + type: counter + sourceUnit: ns + unit: s + desc: CPU time used by the process as reported by the JVM. # jvm.cpu.recent_utilization ProcessCpuLoad: metric: cpu.recent_utilization type: gauge unit: '1' desc: Recent CPU utilization for the process as reported by the JVM. - # TODO: should we filter zero values ? # jvm.system.cpu.load_1m (experimental) SystemLoadAverage: metric: system.cpu.load_1m type: gauge unit: "{run_queue_item}" desc: Average CPU load of the whole system for the last minute as reported by the JVM. - # TODO: should we filter zero values ? # jvm.system.cpu.utilization (experimental) SystemCpuLoad: metric: system.cpu.utilization type: gauge unit: '1' desc: Recent CPU utilization for the whole system as reported by the JVM. - # TODO: should we filter zero values ? - bean: java.nio:name=*,type=BufferPool prefix: jvm.buffer. diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java index 27c3a27a120f..26f9d374d34b 100644 --- a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java @@ -145,6 +145,11 @@ void testJvmMetrics(String image) { .hasUnit("{cpu}") .isUpDownCounter() .hasDataPointsWithoutAttributes()) + .add("jvm.cpu.time", metric -> metric + .hasDescription("CPU time used by the process as reported by the JVM.") + .hasUnit("s") + .isCounter() + .hasDataPointsWithoutAttributes()) .add( "jvm.cpu.recent_utilization", metric -> From d767ed9e2c51fbe83dde898401102aa9cbba2e3a Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 26 Mar 2025 16:07:27 +0100 Subject: [PATCH 16/20] reformat --- .../jmx/rules/JvmTargetSystemTest.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java index 26f9d374d34b..1f2665d26b1e 100644 --- a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java @@ -145,11 +145,14 @@ void testJvmMetrics(String image) { .hasUnit("{cpu}") .isUpDownCounter() .hasDataPointsWithoutAttributes()) - .add("jvm.cpu.time", metric -> metric - .hasDescription("CPU time used by the process as reported by the JVM.") - .hasUnit("s") - .isCounter() - .hasDataPointsWithoutAttributes()) + .add( + "jvm.cpu.time", + metric -> + metric + .hasDescription("CPU time used by the process as reported by the JVM.") + .hasUnit("s") + .isCounter() + .hasDataPointsWithoutAttributes()) .add( "jvm.cpu.recent_utilization", metric -> From ce040de6225cc32bf6aadd92f4457fa11dc39d71 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 26 Mar 2025 16:16:05 +0100 Subject: [PATCH 17/20] fix jvm.cpu.time metric name --- .../jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml b/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml index ad4fb5272a07..2914332398ca 100644 --- a/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml +++ b/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml @@ -74,7 +74,7 @@ rules: desc: Number of processors available to the Java virtual machine. # jvm.cpu.time ProcessCpuTime: - metric: jvm.cpu.time + metric: cpu.time type: counter sourceUnit: ns unit: s From d081c2946eec7636d272bb70a75fc0eb12553b33 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue, 25 Feb 2025 16:09:41 +0100 Subject: [PATCH 18/20] add test code to test yaml jmx metrics --- .../jmx-metrics/javaagent/README.md | 2 +- .../jmx-metrics/library/build.gradle.kts | 26 ++ .../jmx/rules/MetricsVerifier.java | 124 ++++++++ .../jmx/rules/TargetSystemTest.java | 299 ++++++++++++++++++ .../rules/assertions/AttributeMatcher.java | 61 ++++ .../assertions/AttributeMatcherGroup.java | 60 ++++ .../rules/assertions/DataPointAttributes.java | 53 ++++ .../jmx/rules/assertions/JmxAssertj.java | 18 ++ .../jmx/rules/assertions/MetricAssert.java | 266 ++++++++++++++++ 9 files changed, 908 insertions(+), 1 deletion(-) create mode 100644 instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/MetricsVerifier.java create mode 100644 instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/TargetSystemTest.java create mode 100644 instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/AttributeMatcher.java create mode 100644 instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/AttributeMatcherGroup.java create mode 100644 instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/DataPointAttributes.java create mode 100644 instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/JmxAssertj.java create mode 100644 instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/MetricAssert.java diff --git a/instrumentation/jmx-metrics/javaagent/README.md b/instrumentation/jmx-metrics/javaagent/README.md index b91ce05a7b79..753c62da8239 100644 --- a/instrumentation/jmx-metrics/javaagent/README.md +++ b/instrumentation/jmx-metrics/javaagent/README.md @@ -302,7 +302,7 @@ rules: unit: By metricAttribute: jvm.memory.pool.name : param(name) - jvm.memory.type: lowercase(beanattr(type)) + jvm.memory.type: lowercase(beanattr(Type)) ``` For now, only the `lowercase` transformation is supported, other additions might be added in the future if needed. diff --git a/instrumentation/jmx-metrics/library/build.gradle.kts b/instrumentation/jmx-metrics/library/build.gradle.kts index 375e5e77d932..86f0cb0515a8 100644 --- a/instrumentation/jmx-metrics/library/build.gradle.kts +++ b/instrumentation/jmx-metrics/library/build.gradle.kts @@ -1,3 +1,5 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + plugins { id("otel.library-instrumentation") } @@ -6,4 +8,28 @@ dependencies { implementation("org.snakeyaml:snakeyaml-engine") testImplementation(project(":testing-common")) + testImplementation("org.testcontainers:testcontainers") + + testImplementation("org.testcontainers:junit-jupiter") + testImplementation("com.linecorp.armeria:armeria-junit5:1.31.3") + testImplementation("com.linecorp.armeria:armeria-junit5:1.31.3") + testImplementation("com.linecorp.armeria:armeria-grpc:1.31.3") + testImplementation("io.opentelemetry.proto:opentelemetry-proto:1.5.0-alpha") +} + +tasks { + test { + // get packaged agent jar for testing + val shadowTask = project(":javaagent").tasks.named("shadowJar").get() + + dependsOn(shadowTask) + + inputs.files(layout.files(shadowTask)) + .withPropertyName("javaagent") + .withNormalizer(ClasspathNormalizer::class) + + doFirst { + jvmArgs("-Dio.opentelemetry.javaagent.path=${shadowTask.archiveFile.get()}") + } + } } diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/MetricsVerifier.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/MetricsVerifier.java new file mode 100644 index 000000000000..b89659806d65 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/MetricsVerifier.java @@ -0,0 +1,124 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jmx.rules; + +import static io.opentelemetry.instrumentation.jmx.rules.assertions.JmxAssertj.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.instrumentation.jmx.rules.assertions.MetricAssert; +import io.opentelemetry.proto.metrics.v1.Metric; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class MetricsVerifier { + + private final Map> assertions = new HashMap<>(); + private boolean strictMode = true; + + private MetricsVerifier() {} + + /** + * Create instance of MetricsVerifier configured to fail verification if any metric was not + * verified because there is no assertion defined for it. This behavior can be changed by calling + * {@link #disableStrictMode()} method. + * + * @return new instance of MetricsVerifier + * @see #disableStrictMode() + */ + public static MetricsVerifier create() { + return new MetricsVerifier(); + } + + /** + * Disable strict checks of metric assertions. It means that all metrics checks added after + * calling this method will not enforce asserting all metric properties and will not detect + * duplicate property assertions. Also, there will be no error reported if any of metrics was + * skipped because no assertion was added for it. + * + * @return this + * @see #verify(List) + * @see #add(String, Consumer) + */ + @CanIgnoreReturnValue + public MetricsVerifier disableStrictMode() { + strictMode = false; + return this; + } + + /** + * Add assertion for given metric + * + * @param metricName name of metric to be verified by provided assertion + * @param assertion an assertion to verify properties of the metric + * @return this + */ + @CanIgnoreReturnValue + public MetricsVerifier add(String metricName, Consumer assertion) { + if (assertions.containsKey(metricName)) { + throw new IllegalArgumentException("Duplicate assertion for metric " + metricName); + } + assertions.put( + metricName, + metric -> { + MetricAssert metricAssert = assertThat(metric); + metricAssert.setStrict(strictMode); + assertion.accept(metricAssert); + metricAssert.strictCheck(); + }); + return this; + } + + /** + * Execute all defined assertions against provided list of metrics. Error is reported if any of + * defined assertions failed. Error is also reported if any of expected metrics was not present in + * the metrics list, unless strict mode is disabled with {@link #disableStrictMode()}. + * + * @param metrics list of metrics to be verified + * @see #add(String, Consumer) + * @see #disableStrictMode() + */ + public void verify(List metrics) { + verifyAllExpectedMetricsWereReceived(metrics); + + Set unverifiedMetrics = new HashSet<>(); + + for (Metric metric : metrics) { + String metricName = metric.getName(); + Consumer assertion = assertions.get(metricName); + + if (assertion != null) { + assertion.accept(metric); + } else { + unverifiedMetrics.add(metricName); + } + } + + if (strictMode && !unverifiedMetrics.isEmpty()) { + fail("Metrics received but not verified because no assertion exists: " + unverifiedMetrics); + } + } + + private void verifyAllExpectedMetricsWereReceived(List metrics) { + Set receivedMetricNames = + metrics.stream().map(Metric::getName).collect(Collectors.toSet()); + Set assertionNames = new HashSet<>(assertions.keySet()); + + assertionNames.removeAll(receivedMetricNames); + if (!assertionNames.isEmpty()) { + fail( + "Metrics expected but not received: " + + assertionNames + + "\nReceived only: " + + receivedMetricNames); + } + } +} diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/TargetSystemTest.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/TargetSystemTest.java new file mode 100644 index 000000000000..8dcee7738699 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/TargetSystemTest.java @@ -0,0 +1,299 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jmx.rules; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; + +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.instrumentation.jmx.yaml.JmxConfig; +import io.opentelemetry.instrumentation.jmx.yaml.JmxRule; +import io.opentelemetry.instrumentation.jmx.yaml.RuleParser; +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 io.opentelemetry.proto.metrics.v1.Metric; +import io.opentelemetry.proto.metrics.v1.ResourceMetrics; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +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; +import org.testcontainers.utility.MountableFile; + +/** Base class for testing YAML metric definitions with a real target system */ +public class TargetSystemTest { + + private static final Logger logger = LoggerFactory.getLogger(TargetSystemTest.class); + private static final Logger targetSystemLogger = LoggerFactory.getLogger("targetSystem"); + + private static final String AGENT_PATH = "/opentelemetry-instrumentation-javaagent.jar"; + + private static final Network network = Network.newNetwork(); + + private static OtlpGrpcServer otlpServer; + private static Path agentPath; + private static String otlpEndpoint; + + private GenericContainer targetSystem; + private Collection> targetDependencies; + + @BeforeAll + static void beforeAll() { + otlpServer = new OtlpGrpcServer(); + otlpServer.start(); + Testcontainers.exposeHostPorts(otlpServer.httpPort()); + otlpEndpoint = "http://host.testcontainers.internal:" + otlpServer.httpPort(); + + String path = System.getProperty("io.opentelemetry.javaagent.path"); + assertThat(path).isNotNull(); + agentPath = Paths.get(path); + assertThat(agentPath).isReadable().isNotEmptyFile(); + } + + @BeforeEach + void beforeEach() { + otlpServer.reset(); + } + + @AfterEach + void afterEach() { + stop(targetSystem); + targetSystem = null; + + if (targetDependencies != null) { + for (GenericContainer targetDependency : targetDependencies) { + stop(targetDependency); + } + } + targetDependencies = Collections.emptyList(); + } + + private static void stop(@Nullable GenericContainer container) { + if (container != null && container.isRunning()) { + container.stop(); + } + } + + @AfterAll + static void afterAll() { + try { + otlpServer.stop().get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + protected static String javaAgentJvmArgument() { + return "-javaagent:" + AGENT_PATH; + } + + protected static List javaPropertiesToJvmArgs(Map config) { + return config.entrySet().stream() + .map(e -> String.format("-D%s=%s", e.getKey(), e.getValue())) + .collect(Collectors.toList()); + } + + /** + * Generates otel configuration for JMX testing with instrumentation agent + * + * @param yamlFiles JMX metrics definitions in YAML + * @return map of otel configuration properties for JMX testing + */ + protected static Map otelConfigProperties(List yamlFiles) { + Map config = new HashMap<>(); + // only export metrics + config.put("otel.logs.exporter", "none"); + config.put("otel.traces.exporter", "none"); + config.put("otel.metrics.exporter", "otlp"); + // use test grpc endpoint + config.put("otel.exporter.otlp.endpoint", otlpEndpoint); + config.put("otel.exporter.otlp.protocol", "grpc"); + // short export interval for testing + config.put("otel.metric.export.interval", "5s"); + // disable runtime telemetry metrics + config.put("otel.instrumentation.runtime-telemetry.enabled", "false"); + // set yaml config files to test + config.put("otel.jmx.target", "tomcat"); + config.put( + "otel.jmx.config", + yamlFiles.stream() + .map(TargetSystemTest::containerYamlPath) + .collect(Collectors.joining(","))); + return config; + } + + /** + * Starts the target system + * + * @param target target system to start + */ + protected void startTarget(GenericContainer target) { + startTarget(target, Collections.emptyList()); + } + + /** + * Starts the target system with its dependencies first + * + * @param target target system + * @param targetDependencies dependencies of target system + */ + protected void startTarget( + GenericContainer target, Collection> targetDependencies) { + + // If there are any containers that must be started before target then initialize them. + // Then make target depending on them, so it is started after dependencies + this.targetDependencies = targetDependencies; + for (GenericContainer container : targetDependencies) { + container.withNetwork(network); + target.dependsOn(container); + } + + targetSystem = + target.withLogConsumer(new Slf4jLogConsumer(targetSystemLogger)).withNetwork(network); + targetSystem.start(); + } + + protected static void copyFilesToTarget(GenericContainer target, List yamlFiles) { + // copy agent to target system + target.withCopyFileToContainer(MountableFile.forHostPath(agentPath), AGENT_PATH); + + // copy yaml files to target system + for (String file : yamlFiles) { + String resourcePath = yamlResourcePath(file); + String destPath = containerYamlPath(file); + logger.info("copying yaml from resources {} to container {}", resourcePath, destPath); + target.withCopyFileToContainer(MountableFile.forClasspathResource(resourcePath), destPath); + } + } + + private static String yamlResourcePath(String yaml) { + return "jmx/rules/" + yaml; + } + + private static String containerYamlPath(String yaml) { + return "/" + yaml; + } + + /** + * Validates YAML definition by parsing it to check for syntax errors + * + * @param yaml path to YAML resource (in classpath) + */ + protected void validateYamlSyntax(String yaml) { + String path = yamlResourcePath(yaml); + try (InputStream input = TargetSystemTest.class.getClassLoader().getResourceAsStream(path)) { + JmxConfig config; + // try-catch to provide a slightly better error + try { + config = RuleParser.get().loadConfig(input); + } catch (RuntimeException e) { + fail("Failed to parse yaml file " + path, e); + throw e; + } + + // make sure all the rules in that file are valid + for (JmxRule rule : config.getRules()) { + try { + rule.buildMetricDef(); + } catch (Exception e) { + fail("Failed to build metric definition " + rule.getBean(), e); + } + } + } catch (IOException e) { + fail("Failed to read yaml file " + path, e); + } + } + + protected void verifyMetrics(MetricsVerifier metricsVerifier) { + await() + .atMost(Duration.ofSeconds(60)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted( + () -> { + List receivedMetrics = otlpServer.getMetrics(); + assertThat(receivedMetrics).describedAs("No metric received").isNotEmpty(); + + List metrics = + receivedMetrics.stream() + .map(ExportMetricsServiceRequest::getResourceMetricsList) + .flatMap(rm -> rm.stream().map(ResourceMetrics::getScopeMetricsList)) + .flatMap(Collection::stream) + .filter( + // TODO: disabling batch span exporter might help remove unwanted metrics + sm -> sm.getScope().getName().equals("io.opentelemetry.jmx")) + .flatMap(sm -> sm.getMetricsList().stream()) + .collect(Collectors.toList()); + + assertThat(metrics).describedAs("Metrics received but not from JMX").isNotEmpty(); + + metricsVerifier.verify(metrics); + }); + } + + /** Minimal OTLP gRPC backend to capture metrics */ + 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) { + + // verbose but helpful to diagnose what is received + logger.debug("receiving metrics {}", request); + + metricRequests.add(request); + responseObserver.onNext(ExportMetricsServiceResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + }) + .build()); + sb.http(0); + } + } +} diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/AttributeMatcher.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/AttributeMatcher.java new file mode 100644 index 000000000000..33721e66cc0c --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/AttributeMatcher.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jmx.rules.assertions; + +import javax.annotation.Nullable; + +/** Implements functionality of matching data point attributes. */ +public class AttributeMatcher { + private final String attributeName; + @Nullable private final String attributeValue; + + /** + * Create instance used to match data point attribute with any value. + * + * @param attributeName matched attribute name + */ + AttributeMatcher(String attributeName) { + this(attributeName, null); + } + + /** + * Create instance used to match data point attribute with te same name and with the same value. + * + * @param attributeName attribute name + * @param attributeValue attribute value + */ + AttributeMatcher(String attributeName, @Nullable String attributeValue) { + this.attributeName = attributeName; + this.attributeValue = attributeValue; + } + + /** + * Return name of data point attribute that this AttributeMatcher is supposed to match value with. + * + * @return name of validated attribute + */ + public String getAttributeName() { + return attributeName; + } + + @Override + public String toString() { + return attributeValue == null + ? '{' + attributeName + '}' + : '{' + attributeName + '=' + attributeValue + '}'; + } + + /** + * Verify if this matcher is matching provided attribute value. If this matcher holds null value + * then it is matching any attribute value. + * + * @param value a value to be matched + * @return true if this matcher is matching provided value, false otherwise. + */ + boolean matchesValue(String value) { + return attributeValue == null || attributeValue.equals(value); + } +} diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/AttributeMatcherGroup.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/AttributeMatcherGroup.java new file mode 100644 index 000000000000..90e6a0da77c5 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/AttributeMatcherGroup.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jmx.rules.assertions; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + +/** Group of attribute matchers */ +public class AttributeMatcherGroup { + + // stored as a Map for easy lookup by name + private final Map matchers; + + /** + * Constructor for a set of attribute matchers + * + * @param matchers collection of matchers to build a group from + * @throws IllegalStateException if there is any duplicate key + */ + AttributeMatcherGroup(Collection matchers) { + this.matchers = + matchers.stream().collect(Collectors.toMap(AttributeMatcher::getAttributeName, m -> m)); + } + + /** + * Checks if attributes match this attribute matcher group + * + * @param attributes attributes to check as map + * @return {@literal true} when the attributes match all attributes from this group + */ + public boolean matches(Map attributes) { + if (attributes.size() != matchers.size()) { + return false; + } + + for (Map.Entry entry : attributes.entrySet()) { + AttributeMatcher matcher = matchers.get(entry.getKey()); + if (matcher == null) { + // no matcher for this key: unexpected key + return false; + } + + if (!matcher.matchesValue(entry.getValue())) { + // value does not match: unexpected value + return false; + } + } + + return true; + } + + @Override + public String toString() { + return matchers.values().toString(); + } +} diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/DataPointAttributes.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/DataPointAttributes.java new file mode 100644 index 000000000000..c72099e85eac --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/DataPointAttributes.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jmx.rules.assertions; + +import java.util.Arrays; + +/** + * Utility class implementing convenience static methods to construct data point attribute matchers + * and sets of matchers. + */ +public class DataPointAttributes { + private DataPointAttributes() {} + + /** + * Create instance of matcher that should be used to check if data point attribute with given name + * has value identical to the one provided as a parameter (exact match). + * + * @param name name of the data point attribute to check + * @param value expected value of checked data point attribute + * @return instance of matcher + */ + public static AttributeMatcher attribute(String name, String value) { + return new AttributeMatcher(name, value); + } + + /** + * Create instance of matcher that should be used to check if data point attribute with given name + * exists. Any value of the attribute is considered as matching (any value match). + * + * @param name name of the data point attribute to check + * @return instance of matcher + */ + public static AttributeMatcher attributeWithAnyValue(String name) { + return new AttributeMatcher(name); + } + + /** + * Creates a group of attribute matchers that should be used to verify data point attributes. + * + * @param attributes list of matchers to create group. It must contain matchers with unique names. + * @return group of attribute matchers + * @throws IllegalArgumentException if provided list contains two or more matchers with the same + * attribute name + * @see MetricAssert#hasDataPointsWithAttributes(AttributeMatcherGroup...) for detailed + * description off the algorithm used for matching + */ + public static AttributeMatcherGroup attributeGroup(AttributeMatcher... attributes) { + return new AttributeMatcherGroup(Arrays.asList(attributes)); + } +} diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/JmxAssertj.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/JmxAssertj.java new file mode 100644 index 000000000000..45a2d0cd6588 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/JmxAssertj.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jmx.rules.assertions; + +import io.opentelemetry.proto.metrics.v1.Metric; + +/** Dedicated Assertj extension to provide convenient fluent API for metrics testing */ +// TODO: we should contribute this back to sdk-testing +// This has been intentionally not named `*Assertions` to prevent checkstyle rule to be triggered +public class JmxAssertj extends org.assertj.core.api.Assertions { + + public static MetricAssert assertThat(Metric metric) { + return new MetricAssert(metric); + } +} diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/MetricAssert.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/MetricAssert.java new file mode 100644 index 000000000000..2ead92ae7668 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/assertions/MetricAssert.java @@ -0,0 +1,266 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jmx.rules.assertions; + +import static io.opentelemetry.instrumentation.jmx.rules.assertions.DataPointAttributes.attributeGroup; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.metrics.v1.Metric; +import io.opentelemetry.proto.metrics.v1.NumberDataPoint; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.internal.Integers; +import org.assertj.core.internal.Iterables; +import org.assertj.core.internal.Objects; + +public class MetricAssert extends AbstractAssert { + + private static final Objects objects = Objects.instance(); + private static final Iterables iterables = Iterables.instance(); + private static final Integers integers = Integers.instance(); + + private boolean strict; + + private boolean descriptionChecked; + private boolean unitChecked; + private boolean typeChecked; + private boolean dataPointAttributesChecked; + + MetricAssert(Metric actual) { + super(actual, MetricAssert.class); + } + + public void setStrict(boolean strict) { + this.strict = strict; + } + + public void strictCheck() { + strictCheck("description", /* expectedCheckStatus= */ true, descriptionChecked); + strictCheck("unit", /* expectedCheckStatus= */ true, unitChecked); + strictCheck("type", /* expectedCheckStatus= */ true, typeChecked); + strictCheck( + "data point attributes", /* expectedCheckStatus= */ true, dataPointAttributesChecked); + } + + private void strictCheck( + String metricProperty, boolean expectedCheckStatus, boolean actualCheckStatus) { + if (!strict) { + return; + } + String failMsgPrefix = expectedCheckStatus ? "Missing" : "Duplicate"; + info.description( + "%s assertion on %s for metric '%s'", failMsgPrefix, metricProperty, actual.getName()); + objects.assertEqual(info, actualCheckStatus, expectedCheckStatus); + } + + /** + * Verifies metric description + * + * @param description expected description + * @return this + */ + @CanIgnoreReturnValue + public MetricAssert hasDescription(String description) { + isNotNull(); + + info.description("unexpected description for metric '%s'", actual.getName()); + objects.assertEqual(info, actual.getDescription(), description); + strictCheck("description", /* expectedCheckStatus= */ false, descriptionChecked); + descriptionChecked = true; + return this; + } + + /** + * Verifies metric unit + * + * @param unit expected unit + * @return this + */ + @CanIgnoreReturnValue + public MetricAssert hasUnit(String unit) { + isNotNull(); + + info.description("unexpected unit for metric '%s'", actual.getName()); + objects.assertEqual(info, actual.getUnit(), unit); + strictCheck("unit", /* expectedCheckStatus= */ false, unitChecked); + unitChecked = true; + return this; + } + + /** + * Verifies the metric is a gauge + * + * @return this + */ + @CanIgnoreReturnValue + public MetricAssert isGauge() { + isNotNull(); + + info.description("gauge expected for metric '%s'", actual.getName()); + objects.assertEqual(info, actual.hasGauge(), true); + strictCheck("type", /* expectedCheckStatus= */ false, typeChecked); + typeChecked = true; + return this; + } + + @CanIgnoreReturnValue + private MetricAssert hasSum(boolean monotonic) { + isNotNull(); + + info.description("sum expected for metric '%s'", actual.getName()); + objects.assertEqual(info, actual.hasSum(), true); + + String sumType = monotonic ? "monotonic" : "non-monotonic"; + info.description("sum for metric '%s' is expected to be %s", actual.getName(), sumType); + objects.assertEqual(info, actual.getSum().getIsMonotonic(), monotonic); + return this; + } + + /** + * Verifies the metric is a counter + * + * @return this + */ + @CanIgnoreReturnValue + public MetricAssert isCounter() { + // counters have a monotonic sum as their value can't decrease + hasSum(true); + strictCheck("type", /* expectedCheckStatus= */ false, typeChecked); + typeChecked = true; + return this; + } + + /** + * Verifies the metric is an up-down counter + * + * @return this + */ + @CanIgnoreReturnValue + public MetricAssert isUpDownCounter() { + // up down counters are non-monotonic as their value can increase & decrease + hasSum(false); + strictCheck("type", /* expectedCheckStatus= */ false, typeChecked); + typeChecked = true; + return this; + } + + /** + * Verifies that there is no attribute in any of data points. + * + * @return this + */ + @CanIgnoreReturnValue + public MetricAssert hasDataPointsWithoutAttributes() { + isNotNull(); + + return checkDataPoints( + dataPoints -> { + dataPointsCommonCheck(dataPoints); + + // all data points must not have any attribute + for (NumberDataPoint dataPoint : dataPoints) { + info.description( + "no attribute expected on data point for metric '%s'", actual.getName()); + iterables.assertEmpty(info, dataPoint.getAttributesList()); + } + }); + } + + @CanIgnoreReturnValue + private MetricAssert checkDataPoints(Consumer> listConsumer) { + // in practice usually one set of data points is provided but the + // protobuf does not enforce that, so we have to ensure checking at least one + int count = 0; + if (actual.hasGauge()) { + count++; + listConsumer.accept(actual.getGauge().getDataPointsList()); + } + if (actual.hasSum()) { + count++; + listConsumer.accept(actual.getSum().getDataPointsList()); + } + info.description("at least one set of data points expected for metric '%s'", actual.getName()); + integers.assertGreaterThan(info, count, 0); + + strictCheck( + "data point attributes", /* expectedCheckStatus= */ false, dataPointAttributesChecked); + dataPointAttributesChecked = true; + return this; + } + + private void dataPointsCommonCheck(List dataPoints) { + info.description("unable to retrieve data points from metric '%s'", actual.getName()); + objects.assertNotNull(info, dataPoints); + + // at least one data point must be reported + info.description("at least one data point expected for metric '%s'", actual.getName()); + iterables.assertNotEmpty(info, dataPoints); + } + + /** + * Verifies that all metric data points have the same expected one attribute + * + * @param expectedAttribute attribute matcher to validate data points attributes + * @return this + */ + @CanIgnoreReturnValue + public final MetricAssert hasDataPointsWithOneAttribute(AttributeMatcher expectedAttribute) { + return hasDataPointsWithAttributes(attributeGroup(expectedAttribute)); + } + + /** + * Verifies that every data point attributes is matched exactly by one of the matcher groups + * provided. Also, each matcher group must match at least one data point attributes set. Data + * point attributes are matched by matcher group if each attribute is matched by one matcher and + * each matcher matches one attribute. In other words: number of attributes is the same as number + * of matchers and there is 1:1 matching between them. + * + * @param matcherGroups array of attribute matcher groups + * @return this + */ + @CanIgnoreReturnValue + public final MetricAssert hasDataPointsWithAttributes(AttributeMatcherGroup... matcherGroups) { + return checkDataPoints( + dataPoints -> { + dataPointsCommonCheck(dataPoints); + + boolean[] matchedSets = new boolean[matcherGroups.length]; + + // validate each datapoint attributes match exactly one of the provided attributes sets + for (NumberDataPoint dataPoint : dataPoints) { + Map dataPointAttributes = + dataPoint.getAttributesList().stream() + .collect( + Collectors.toMap(KeyValue::getKey, kv -> kv.getValue().getStringValue())); + int matchCount = 0; + for (int i = 0; i < matcherGroups.length; i++) { + if (matcherGroups[i].matches(dataPointAttributes)) { + matchedSets[i] = true; + matchCount++; + } + } + + info.description( + "data point attributes '%s' for metric '%s' must match exactly one of the attribute sets '%s'.\nActual data points: %s", + dataPointAttributes, actual.getName(), Arrays.asList(matcherGroups), dataPoints); + integers.assertEqual(info, matchCount, 1); + } + + // check that all attribute sets matched at least once + for (int i = 0; i < matchedSets.length; i++) { + info.description( + "no data point matched attribute set '%s' for metric '%s'", + matcherGroups[i], actual.getName()); + objects.assertEqual(info, matchedSets[i], true); + } + }); + } +} From 5f403f650bbe8059fed8ae09bebe44e3887eeed3 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:28:48 +0200 Subject: [PATCH 19/20] remove todo now that we filter negative values --- .../jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml b/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml index 2914332398ca..3be8dcd90571 100644 --- a/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml +++ b/instrumentation/jmx-metrics/library/src/main/resources/jmx/rules/jvm.yaml @@ -63,8 +63,7 @@ rules: - bean: java.lang:type=OperatingSystem prefix: jvm. - # TODO: drop negative values once https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/13589 is merged - # dropNegativeValues: true + dropNegativeValues: true mapping: # jvm.cpu.count AvailableProcessors: From 0d1d1f20b44d3d2830be2ae63ec706f1ed98cae7 Mon Sep 17 00:00:00 2001 From: SylvainJuge <763082+SylvainJuge@users.noreply.github.com> Date: Tue, 15 Apr 2025 13:55:20 +0200 Subject: [PATCH 20/20] Update instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java Co-authored-by: Jay DeLuca --- .../instrumentation/jmx/rules/JvmTargetSystemTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java index 1f2665d26b1e..a272903162bb 100644 --- a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/JvmTargetSystemTest.java @@ -20,7 +20,7 @@ import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; -public class JvmTargetSystemTest extends TargetSystemTest { +class JvmTargetSystemTest extends TargetSystemTest { @ParameterizedTest @ValueSource(