diff --git a/instrumentation/runtime-metrics/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/RuntimeMetricsInstaller.java b/instrumentation/runtime-metrics/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/RuntimeMetricsInstaller.java index a35bf41b1258..b0b04964916a 100644 --- a/instrumentation/runtime-metrics/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/RuntimeMetricsInstaller.java +++ b/instrumentation/runtime-metrics/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/RuntimeMetricsInstaller.java @@ -11,6 +11,7 @@ import io.opentelemetry.instrumentation.runtimemetrics.Classes; import io.opentelemetry.instrumentation.runtimemetrics.GarbageCollector; import io.opentelemetry.instrumentation.runtimemetrics.MemoryPools; +import io.opentelemetry.instrumentation.runtimemetrics.Threads; import io.opentelemetry.javaagent.extension.AgentListener; import io.opentelemetry.javaagent.tooling.config.AgentConfig; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; @@ -28,8 +29,9 @@ public void afterAgent(Config config, AutoConfiguredOpenTelemetrySdk unused) { if (new AgentConfig(config) .isInstrumentationEnabled(Collections.singleton("runtime-metrics"), DEFAULT_ENABLED)) { - MemoryPools.registerObservers(GlobalOpenTelemetry.get()); Classes.registerObservers(GlobalOpenTelemetry.get()); + MemoryPools.registerObservers(GlobalOpenTelemetry.get()); + Threads.registerObservers(GlobalOpenTelemetry.get()); if (config.getBoolean( "otel.instrumentation.runtime-metrics.experimental-metrics.enabled", false)) { diff --git a/instrumentation/runtime-metrics/javaagent/src/test/groovy/io/opentelemetry/instrumentation/javaagent/runtimemetrics/RuntimeMetricsTest.groovy b/instrumentation/runtime-metrics/javaagent/src/test/groovy/io/opentelemetry/instrumentation/javaagent/runtimemetrics/RuntimeMetricsTest.groovy index c8ee1553ac6f..527ba61f84a1 100644 --- a/instrumentation/runtime-metrics/javaagent/src/test/groovy/io/opentelemetry/instrumentation/javaagent/runtimemetrics/RuntimeMetricsTest.groovy +++ b/instrumentation/runtime-metrics/javaagent/src/test/groovy/io/opentelemetry/instrumentation/javaagent/runtimemetrics/RuntimeMetricsTest.groovy @@ -16,15 +16,16 @@ class RuntimeMetricsTest extends AgentInstrumentationSpecification { then: conditions.eventually { + assert getMetrics().any { it.name == "process.runtime.jvm.classes.loaded" } + assert getMetrics().any { it.name == "process.runtime.jvm.classes.unloaded" } + assert getMetrics().any { it.name == "process.runtime.jvm.classes.current_loaded" } assert getMetrics().any { it.name == "runtime.jvm.gc.time" } assert getMetrics().any { it.name == "runtime.jvm.gc.count" } assert getMetrics().any { it.name == "process.runtime.jvm.memory.init" } assert getMetrics().any { it.name == "process.runtime.jvm.memory.usage" } assert getMetrics().any { it.name == "process.runtime.jvm.memory.committed" } assert getMetrics().any { it.name == "process.runtime.jvm.memory.max" } - assert getMetrics().any { it.name == "process.runtime.jvm.classes.loaded" } - assert getMetrics().any { it.name == "process.runtime.jvm.classes.unloaded" } - assert getMetrics().any { it.name == "process.runtime.jvm.classes.current_loaded" } + assert getMetrics().any { it.name == "process.runtime.jvm.threads.count" } } } } diff --git a/instrumentation/runtime-metrics/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/Threads.java b/instrumentation/runtime-metrics/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/Threads.java new file mode 100644 index 000000000000..dc868ad89147 --- /dev/null +++ b/instrumentation/runtime-metrics/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/Threads.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.runtimemetrics; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.Meter; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; + +/** + * Registers measurements that generate metrics about JVM threads. + * + *

Example usage: + * + *

{@code
+ * Classes.registerObservers(GlobalOpenTelemetry.get());
+ * }
+ * + *

Example metrics being exported: + * + *

+ *   process.runtime.jvm.threads.count 4
+ * 
+ */ +public final class Threads { + + // Visible for testing + static final Threads INSTANCE = new Threads(); + + /** Register observers for java runtime class metrics. */ + public static void registerObservers(OpenTelemetry openTelemetry) { + INSTANCE.registerObservers(openTelemetry, ManagementFactory.getThreadMXBean()); + } + + // Visible for testing + void registerObservers(OpenTelemetry openTelemetry, ThreadMXBean threadBean) { + Meter meter = openTelemetry.getMeter("io.opentelemetry.runtime-metrics"); + + meter + .upDownCounterBuilder("process.runtime.jvm.threads.count") + .setDescription("Number of executing threads") + .setUnit("1") + .buildWithCallback( + observableMeasurement -> observableMeasurement.record(threadBean.getThreadCount())); + } + + private Threads() {} +} diff --git a/instrumentation/runtime-metrics/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/ThreadsTest.java b/instrumentation/runtime-metrics/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/ThreadsTest.java new file mode 100644 index 000000000000..b1128ce41faa --- /dev/null +++ b/instrumentation/runtime-metrics/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/ThreadsTest.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.runtimemetrics; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import java.lang.management.ThreadMXBean; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ThreadsTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Mock private ThreadMXBean threadBean; + + @Test + void registerObservers() { + when(threadBean.getThreadCount()).thenReturn(3); + + Threads.INSTANCE.registerObservers(testing.getOpenTelemetry(), threadBean); + + testing.waitAndAssertMetrics( + "io.opentelemetry.runtime-metrics", + "process.runtime.jvm.threads.count", + metrics -> + metrics.anySatisfy( + metricData -> + assertThat(metricData) + .hasDescription("Number of executing threads") + .hasUnit("1") + .hasLongSumSatisfying( + sum -> + sum.isNotMonotonic() + .hasPointsSatisfying( + point -> + point.hasValue(3).hasAttributes(Attributes.empty()))))); + } +}