diff --git a/.github/workflows/reusable-native-tests.yml b/.github/workflows/reusable-native-tests.yml index 24f6f0fd0109..ae58a0d84a8c 100644 --- a/.github/workflows/reusable-native-tests.yml +++ b/.github/workflows/reusable-native-tests.yml @@ -16,12 +16,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - id: read-java - run: echo "version=$(cat .java-version)" >> "$GITHUB_OUTPUT" - uses: graalvm/setup-graalvm@aafbedb8d382ed0ca6167d3a051415f20c859274 # v1.2.8.1 with: version: "latest" - java-version: "${{ steps.read-java.outputs.version }}" + java-version: "23" # earlier versions have different arguments to enable JFR components: "native-image" - name: Running test env: diff --git a/instrumentation-api/src/main/resources/META-INF/native-image/io.opentelemetry.instrumentation/opentelemetry-instrumentation-api/native-image.properties b/instrumentation-api/src/main/resources/META-INF/native-image/io.opentelemetry.instrumentation/opentelemetry-instrumentation-api/native-image.properties index 758891265640..83efd36a27e8 100644 --- a/instrumentation-api/src/main/resources/META-INF/native-image/io.opentelemetry.instrumentation/opentelemetry-instrumentation-api/native-image.properties +++ b/instrumentation-api/src/main/resources/META-INF/native-image/io.opentelemetry.instrumentation/opentelemetry-instrumentation-api/native-image.properties @@ -1,2 +1,4 @@ Args=\ - --initialize-at-build-time=io.opentelemetry.instrumentation.api.internal.cache.concurrentlinkedhashmap.ConcurrentLinkedHashMap + --initialize-at-build-time=io.opentelemetry.instrumentation.api.internal.cache.concurrentlinkedhashmap.ConcurrentLinkedHashMap \ + --initialize-at-build-time=io.opentelemetry.instrumentation.api.internal.cache.MapBackedCache \ + --initialize-at-build-time=io.opentelemetry.api.internal.InternalAttributeKeyImpl diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java17/Java17RuntimeMetricsInstaller.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java17/Java17RuntimeMetricsInstaller.java index ec4fab1528af..10a535f8d393 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java17/Java17RuntimeMetricsInstaller.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java17/Java17RuntimeMetricsInstaller.java @@ -7,12 +7,10 @@ import com.google.auto.service.AutoService; import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.runtimemetrics.java17.RuntimeMetrics; -import io.opentelemetry.instrumentation.runtimemetrics.java17.RuntimeMetricsBuilder; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.extension.AgentListener; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; /** An {@link AgentListener} that enables runtime metrics during agent startup. */ @AutoService(AgentListener.class) @@ -20,34 +18,7 @@ public class Java17RuntimeMetricsInstaller implements AgentListener { @Override public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredSdk) { - ConfigProperties config = AgentListener.resolveConfigProperties(autoConfiguredSdk); - - OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); - RuntimeMetricsBuilder builder = null; - /* - By default don't use any JFR metrics. May change this once semantic conventions are updated. - If enabled, default to only the metrics not already covered by runtime-telemetry-java8 - */ - boolean defaultEnabled = config.getBoolean("otel.instrumentation.common.default-enabled", true); - if (config.getBoolean("otel.instrumentation.runtime-telemetry-java17.enable-all", false)) { - builder = RuntimeMetrics.builder(openTelemetry).enableAllFeatures(); - } else if (config.getBoolean("otel.instrumentation.runtime-telemetry-java17.enabled", false)) { - builder = RuntimeMetrics.builder(openTelemetry); - } else if (config.getBoolean( - "otel.instrumentation.runtime-telemetry.enabled", defaultEnabled)) { - // This only uses metrics gathered by JMX - builder = RuntimeMetrics.builder(openTelemetry).disableAllFeatures(); - } - - if (builder != null) { - if (config.getBoolean( - "otel.instrumentation.runtime-telemetry.emit-experimental-telemetry", false)) { - builder.enableExperimentalJmxTelemetry(); - } - - RuntimeMetrics finalJfrTelemetry = builder.build(); - Thread cleanupTelemetry = new Thread(() -> finalJfrTelemetry.close()); - Runtime.getRuntime().addShutdownHook(cleanupTelemetry); - } + RuntimeMetrics.builder(GlobalOpenTelemetry.get()) + .startFromInstrumentationConfig(AgentInstrumentationConfig.get()); } } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/RuntimeMetrics.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/RuntimeMetrics.java index 96a98b7fcdbd..aabe280576cb 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/RuntimeMetrics.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/RuntimeMetrics.java @@ -101,7 +101,7 @@ private JfrRuntimeMetrics(OpenTelemetry openTelemetry, Predicate fea recordingStream.onEvent(handler.getEventName(), handler); }); recordingStream.onMetadata(event -> startUpLatch.countDown()); - Thread daemonRunner = new Thread(() -> recordingStream.start()); + Thread daemonRunner = new Thread(recordingStream::start, "OpenTelemetry JFR-Metrics-Runner"); daemonRunner.setDaemon(true); daemonRunner.start(); } @@ -138,7 +138,8 @@ CountDownLatch getStartUpLatch() { private static boolean isJfrAvailable() { try { Class.forName("jdk.jfr.FlightRecorder"); - } catch (ClassNotFoundException e) { + // UnsatisfiedLinkError or ClassNotFoundException + } catch (Exception e) { return false; } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/RuntimeMetricsBuilder.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/RuntimeMetricsBuilder.java index e73c535e5803..c6886403f01f 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/RuntimeMetricsBuilder.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/RuntimeMetricsBuilder.java @@ -7,19 +7,12 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.instrumentation.runtimemetrics.java8.Classes; -import io.opentelemetry.instrumentation.runtimemetrics.java8.Cpu; -import io.opentelemetry.instrumentation.runtimemetrics.java8.GarbageCollector; -import io.opentelemetry.instrumentation.runtimemetrics.java8.MemoryPools; -import io.opentelemetry.instrumentation.runtimemetrics.java8.Threads; -import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.ExperimentalBufferPools; -import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.ExperimentalCpu; -import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.ExperimentalMemoryPools; -import java.util.ArrayList; +import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig; +import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.JmxRuntimeMetricsFactory; import java.util.Arrays; -import java.util.Collections; import java.util.EnumMap; import java.util.List; +import java.util.function.Consumer; import javax.annotation.Nullable; /** Builder for {@link RuntimeMetrics}. */ @@ -31,6 +24,11 @@ public final class RuntimeMetricsBuilder { private boolean disableJmx = false; private boolean enableExperimentalJmxTelemetry = false; + private Consumer shutdownHook = + runnable -> { + Runtime.getRuntime() + .addShutdownHook(new Thread(runnable, "OpenTelemetry RuntimeMetricsShutdownHook")); + }; RuntimeMetricsBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; @@ -83,42 +81,57 @@ public RuntimeMetricsBuilder disableAllJmx() { return this; } - /** Disable telemetry collection associated with the {@link JfrFeature}. */ + /** Enable experimental JMX telemetry collection. */ @CanIgnoreReturnValue public RuntimeMetricsBuilder enableExperimentalJmxTelemetry() { enableExperimentalJmxTelemetry = true; return this; } - /** Build and start an {@link RuntimeMetrics} with the config from this builder. */ - public RuntimeMetrics build() { - List observables = buildObservables(); - RuntimeMetrics.JfrRuntimeMetrics jfrRuntimeMetrics = buildJfrMetrics(); - return new RuntimeMetrics(openTelemetry, observables, jfrRuntimeMetrics); + /** Set a custom shutdown hook for the {@link RuntimeMetrics}. */ + @CanIgnoreReturnValue + public RuntimeMetricsBuilder setShutdownHook(Consumer shutdownHook) { + this.shutdownHook = shutdownHook; + return this; } - @SuppressWarnings("CatchingUnchecked") - private List buildObservables() { - if (disableJmx) { - return Collections.emptyList(); + public void startFromInstrumentationConfig(InstrumentationConfig config) { + /* + By default, don't use any JFR metrics. May change this once semantic conventions are updated. + If enabled, default to only the metrics not already covered by runtime-telemetry-java8 + */ + boolean defaultEnabled = config.getBoolean("otel.instrumentation.common.default-enabled", true); + if (config.getBoolean("otel.instrumentation.runtime-telemetry-java17.enable-all", false)) { + this.enableAllFeatures(); + } else if (config.getBoolean("otel.instrumentation.runtime-telemetry-java17.enabled", false)) { + // default configuration + } else if (config.getBoolean( + "otel.instrumentation.runtime-telemetry.enabled", defaultEnabled)) { + // This only uses metrics gathered by JMX + this.disableAllFeatures(); + } else { + // nothing is enabled + return; } - try { - // Set up metrics gathered by JMX - List observables = new ArrayList<>(); - observables.addAll(Classes.registerObservers(openTelemetry)); - observables.addAll(Cpu.registerObservers(openTelemetry)); - observables.addAll(GarbageCollector.registerObservers(openTelemetry)); - observables.addAll(MemoryPools.registerObservers(openTelemetry)); - observables.addAll(Threads.registerObservers(openTelemetry)); - if (enableExperimentalJmxTelemetry) { - observables.addAll(ExperimentalBufferPools.registerObservers(openTelemetry)); - observables.addAll(ExperimentalCpu.registerObservers(openTelemetry)); - observables.addAll(ExperimentalMemoryPools.registerObservers(openTelemetry)); - } - return observables; - } catch (Exception e) { - throw new IllegalStateException("Error building RuntimeMetrics", e); + + if (config.getBoolean( + "otel.instrumentation.runtime-telemetry.emit-experimental-telemetry", false)) { + this.enableExperimentalJmxTelemetry(); } + + RuntimeMetrics runtimeMetrics = this.build(); + shutdownHook.accept(runtimeMetrics::close); + } + + /** Build and start an {@link RuntimeMetrics} with the config from this builder. */ + public RuntimeMetrics build() { + List observables = + disableJmx + ? List.of() + : JmxRuntimeMetricsFactory.buildObservables( + openTelemetry, enableExperimentalJmxTelemetry); + RuntimeMetrics.JfrRuntimeMetrics jfrRuntimeMetrics = buildJfrMetrics(); + return new RuntimeMetrics(openTelemetry, observables, jfrRuntimeMetrics); } @Nullable diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/Java8RuntimeMetricsInstaller.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/Java8RuntimeMetricsInstaller.java index b93bfa57e835..57a569a976dc 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/Java8RuntimeMetricsInstaller.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/Java8RuntimeMetricsInstaller.java @@ -7,21 +7,10 @@ import com.google.auto.service.AutoService; import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.instrumentation.runtimemetrics.java8.Classes; -import io.opentelemetry.instrumentation.runtimemetrics.java8.Cpu; -import io.opentelemetry.instrumentation.runtimemetrics.java8.GarbageCollector; -import io.opentelemetry.instrumentation.runtimemetrics.java8.MemoryPools; -import io.opentelemetry.instrumentation.runtimemetrics.java8.Threads; -import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.ExperimentalBufferPools; -import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.ExperimentalCpu; -import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.ExperimentalMemoryPools; -import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.JmxRuntimeMetricsUtil; +import io.opentelemetry.instrumentation.runtimemetrics.java8.RuntimeMetrics; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.extension.AgentListener; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import java.util.ArrayList; -import java.util.List; /** An {@link AgentListener} that enables runtime metrics during agent startup. */ @AutoService(AgentListener.class) @@ -29,30 +18,7 @@ public class Java8RuntimeMetricsInstaller implements AgentListener { @Override public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredSdk) { - ConfigProperties config = AgentListener.resolveConfigProperties(autoConfiguredSdk); - - boolean defaultEnabled = config.getBoolean("otel.instrumentation.common.default-enabled", true); - if (!config.getBoolean("otel.instrumentation.runtime-telemetry.enabled", defaultEnabled) - || Double.parseDouble(System.getProperty("java.specification.version")) >= 17) { - return; - } - - OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); - List observables = new ArrayList<>(); - observables.addAll(Classes.registerObservers(openTelemetry)); - observables.addAll(Cpu.registerObservers(openTelemetry)); - observables.addAll(GarbageCollector.registerObservers(openTelemetry)); - observables.addAll(MemoryPools.registerObservers(openTelemetry)); - observables.addAll(Threads.registerObservers(openTelemetry)); - - if (config.getBoolean( - "otel.instrumentation.runtime-telemetry.emit-experimental-telemetry", false)) { - observables.addAll(ExperimentalBufferPools.registerObservers(openTelemetry)); - observables.addAll(ExperimentalCpu.registerObservers(openTelemetry)); - observables.addAll(ExperimentalMemoryPools.registerObservers(openTelemetry)); - } - - Thread cleanupTelemetry = new Thread(() -> JmxRuntimeMetricsUtil.closeObservers(observables)); - Runtime.getRuntime().addShutdownHook(cleanupTelemetry); + RuntimeMetrics.builder(GlobalOpenTelemetry.get()) + .startFromInstrumentationConfig(AgentInstrumentationConfig.get()); } } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/RuntimeMetrics.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/RuntimeMetrics.java new file mode 100644 index 000000000000..dec3beac8685 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/RuntimeMetrics.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.runtimemetrics.java8; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.JmxRuntimeMetricsUtil; +import java.io.Closeable; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** The entry point class for runtime metrics support using JMX. */ +public final class RuntimeMetrics implements Closeable { + + private static final Logger logger = Logger.getLogger(RuntimeMetrics.class.getName()); + + private final AtomicBoolean isClosed = new AtomicBoolean(); + private final List observables; + + RuntimeMetrics(List observables) { + this.observables = Collections.unmodifiableList(observables); + } + + /** + * Create and start {@link RuntimeMetrics}. + * + *

Listens for select JMX beans, extracts data, and records to various metrics. Recording will + * continue until {@link #close()} is called. + * + * @param openTelemetry the {@link OpenTelemetry} instance used to record telemetry + */ + public static RuntimeMetrics create(OpenTelemetry openTelemetry) { + return new RuntimeMetricsBuilder(openTelemetry).build(); + } + + /** + * Create a builder for configuring {@link RuntimeMetrics}. + * + * @param openTelemetry the {@link OpenTelemetry} instance used to record telemetry + */ + public static RuntimeMetricsBuilder builder(OpenTelemetry openTelemetry) { + return new RuntimeMetricsBuilder(openTelemetry); + } + + /** Stop recording JMX metrics. */ + @Override + public void close() { + if (!isClosed.compareAndSet(false, true)) { + logger.log(Level.WARNING, "RuntimeMetrics is already closed"); + return; + } + + JmxRuntimeMetricsUtil.closeObservers(observables); + } +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/RuntimeMetricsBuilder.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/RuntimeMetricsBuilder.java new file mode 100644 index 000000000000..584c68433e1b --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/RuntimeMetricsBuilder.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.runtimemetrics.java8; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig; +import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.JmxRuntimeMetricsFactory; +import java.util.List; +import java.util.function.Consumer; + +/** Builder for {@link RuntimeMetrics}. */ +public final class RuntimeMetricsBuilder { + + private final OpenTelemetry openTelemetry; + + private boolean enableExperimentalJmxTelemetry = false; + private Consumer shutdownHook = + runnable -> { + Runtime.getRuntime() + .addShutdownHook(new Thread(runnable, "OpenTelemetry RuntimeMetricsShutdownHook")); + }; + + RuntimeMetricsBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + /** Enable all JMX telemetry collection. */ + @CanIgnoreReturnValue + public RuntimeMetricsBuilder enableExperimentalJmxTelemetry() { + enableExperimentalJmxTelemetry = true; + return this; + } + + /** Build and start an {@link RuntimeMetrics} with the config from this builder. */ + public RuntimeMetrics build() { + List observables = + JmxRuntimeMetricsFactory.buildObservables(openTelemetry, enableExperimentalJmxTelemetry); + return new RuntimeMetrics(observables); + } + + /** Set a custom shutdown hook for the {@link RuntimeMetrics}. */ + @CanIgnoreReturnValue + public RuntimeMetricsBuilder setShutdownHook(Consumer shutdownHook) { + this.shutdownHook = shutdownHook; + return this; + } + + public void startFromInstrumentationConfig(InstrumentationConfig config) { + boolean defaultEnabled = config.getBoolean("otel.instrumentation.common.default-enabled", true); + if (!config.getBoolean("otel.instrumentation.runtime-telemetry.enabled", defaultEnabled)) { + // nothing is enabled + return; + } + + if (config.getBoolean( + "otel.instrumentation.runtime-telemetry.emit-experimental-telemetry", false)) { + this.enableExperimentalJmxTelemetry(); + } + + RuntimeMetrics runtimeMetrics = this.build(); + shutdownHook.accept(runtimeMetrics::close); + } +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/Threads.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/Threads.java index d3af5bf00ea9..2b720ab813bd 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/Threads.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/Threads.java @@ -57,7 +57,7 @@ public final class Threads { /** Register observers for java runtime class metrics. */ public static List registerObservers(OpenTelemetry openTelemetry) { - return INSTANCE.registerObservers(openTelemetry, !isJava9OrNewer()); + return INSTANCE.registerObservers(openTelemetry, useThreads()); } private List registerObservers(OpenTelemetry openTelemetry, boolean useThread) { @@ -116,6 +116,12 @@ private static boolean isJava9OrNewer() { return THREAD_INFO_IS_DAEMON != null; } + private static boolean useThreads() { + // GraalVM native image does not support ThreadMXBean yet + // see https://github.com/oracle/graal/issues/6101 + return !isJava9OrNewer() || System.getProperty("org.graalvm.nativeimage.imagecode") != null; + } + private static Consumer java8Callback(ThreadMXBean threadBean) { return measurement -> { int daemonThreadCount = threadBean.getDaemonThreadCount(); diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/JmxRuntimeMetricsFactory.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/JmxRuntimeMetricsFactory.java new file mode 100644 index 000000000000..405fbb48c80e --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/JmxRuntimeMetricsFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.runtimemetrics.java8.internal; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.runtimemetrics.java8.Classes; +import io.opentelemetry.instrumentation.runtimemetrics.java8.Cpu; +import io.opentelemetry.instrumentation.runtimemetrics.java8.GarbageCollector; +import io.opentelemetry.instrumentation.runtimemetrics.java8.MemoryPools; +import io.opentelemetry.instrumentation.runtimemetrics.java8.Threads; +import java.util.ArrayList; +import java.util.List; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class JmxRuntimeMetricsFactory { + @SuppressWarnings("CatchingUnchecked") + public static List buildObservables( + OpenTelemetry openTelemetry, boolean enableExperimentalJmxTelemetry) { + try { + // Set up metrics gathered by JMX + List observables = new ArrayList<>(); + observables.addAll(Classes.registerObservers(openTelemetry)); + observables.addAll(Cpu.registerObservers(openTelemetry)); + observables.addAll(GarbageCollector.registerObservers(openTelemetry)); + observables.addAll(MemoryPools.registerObservers(openTelemetry)); + observables.addAll(Threads.registerObservers(openTelemetry)); + if (enableExperimentalJmxTelemetry) { + observables.addAll(ExperimentalBufferPools.registerObservers(openTelemetry)); + observables.addAll(ExperimentalCpu.registerObservers(openTelemetry)); + observables.addAll(ExperimentalMemoryPools.registerObservers(openTelemetry)); + } + return observables; + } catch (Exception e) { + throw new IllegalStateException("Error building RuntimeMetrics", e); + } + } + + private JmxRuntimeMetricsFactory() {} +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/resources/META-INF/native-image/reflect-config.json b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/resources/META-INF/native-image/reflect-config.json new file mode 100644 index 000000000000..1e250d63300c --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/resources/META-INF/native-image/reflect-config.json @@ -0,0 +1,10 @@ +[ + { + "name": "com.sun.management.OperatingSystemMXBean", + "allPublicMethods": true + }, + { + "name": "com.ibm.lang.management.OperatingSystemMXBean", + "allPublicMethods": true + } +] diff --git a/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts b/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts index d701141e3f65..99fbcbd98b8e 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts +++ b/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts @@ -53,6 +53,8 @@ dependencies { implementation(project(":instrumentation:logback:logback-mdc-1.0:library")) compileOnly("ch.qos.logback:logback-classic:1.0.0") implementation(project(":instrumentation:jdbc:library")) + implementation(project(":instrumentation:runtime-telemetry:runtime-telemetry-java8:library")) + implementation(project(":instrumentation:runtime-telemetry:runtime-telemetry-java17:library")) library("org.springframework.kafka:spring-kafka:2.9.0") library("org.springframework.boot:spring-boot-starter-actuator:$springBootVersion") diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/runtimemetrics/RuntimeMetricsAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/runtimemetrics/RuntimeMetricsAutoConfiguration.java new file mode 100644 index 000000000000..7c3a312131e8 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/runtimemetrics/RuntimeMetricsAutoConfiguration.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.runtimemetrics; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.ConfigPropertiesBridge; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import javax.annotation.PreDestroy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; + +/** + * Configures runtime metrics collection. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@ConditionalOnEnabledInstrumentation(module = "runtime-telemetry") +@Configuration +public class RuntimeMetricsAutoConfiguration { + + private static final Logger logger = + LoggerFactory.getLogger(RuntimeMetricsAutoConfiguration.class); + + private Runnable shutdownHook; + + @PreDestroy + public void stopMetrics() { + if (shutdownHook != null) { + shutdownHook.run(); + } + } + + @EventListener + public void handleApplicationReadyEvent(ApplicationReadyEvent event) { + ConfigurableApplicationContext applicationContext = event.getApplicationContext(); + OpenTelemetry openTelemetry = applicationContext.getBean(OpenTelemetry.class); + ConfigPropertiesBridge config = + new ConfigPropertiesBridge(applicationContext.getBean(ConfigProperties.class)); + + if (Double.parseDouble(System.getProperty("java.specification.version")) >= 17) { + logger.debug("Use runtime metrics instrumentation for Java 17+"); + io.opentelemetry.instrumentation.runtimemetrics.java17.RuntimeMetrics.builder(openTelemetry) + .setShutdownHook(runnable -> shutdownHook = runnable) + .startFromInstrumentationConfig(config); + } else { + logger.debug("Use runtime metrics instrumentation for Java 8"); + io.opentelemetry.instrumentation.runtimemetrics.java8.RuntimeMetrics.builder(openTelemetry) + .setShutdownHook(runnable -> shutdownHook = runnable) + .startFromInstrumentationConfig(config); + } + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/ConfigPropertiesBridge.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/ConfigPropertiesBridge.java index 49d50f94cc5c..8305aef6a4f0 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/ConfigPropertiesBridge.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/ConfigPropertiesBridge.java @@ -13,7 +13,13 @@ import java.util.Map; import javax.annotation.Nullable; -final class ConfigPropertiesBridge implements InstrumentationConfig { +/** + * Support for {@link ConfigProperties} in {@link InstrumentationConfig}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class ConfigPropertiesBridge implements InstrumentationConfig { private final ConfigProperties configProperties; diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 68a06350a09d..f179b7e7f9bd 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -476,6 +476,30 @@ "description": "Enables the DB statement sanitization.", "defaultValue": true }, + { + "name": "otel.instrumentation.runtime-telemetry.enabled", + "type": "java.lang.Boolean", + "description": "Enable runtime telemetry metrics.", + "defaultValue": true + }, + { + "name": "otel.instrumentation.runtime-telemetry.emit-experimental-telemetry", + "type": "java.lang.Boolean", + "description": "Enable the capture of experimental metrics.", + "defaultValue": false + }, + { + "name": "otel.instrumentation.runtime-telemetry-java17.enable-all", + "type": "java.lang.Boolean", + "description": "Enable the capture of all JFR based metrics.", + "defaultValue": false + }, + { + "name": "otel.instrumentation.runtime-telemetry-java17.enabled", + "type": "java.lang.Boolean", + "description": "Enable the capture of JFR based metrics.", + "defaultValue": false + }, { "name": "otel.instrumentation.spring-web.enabled", "type": "java.lang.Boolean", diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 049b3b068f1d..bf7f5dc46fe2 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -10,7 +10,8 @@ io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.r io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web.SpringWebInstrumentationAutoConfiguration,\ io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux.SpringWebfluxInstrumentationAutoConfiguration,\ io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webmvc.SpringWebMvc5InstrumentationAutoConfiguration,\ -io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.scheduling.SpringSchedulingInstrumentationAutoConfiguration +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.scheduling.SpringSchedulingInstrumentationAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.runtimemetrics.RuntimeMetricsAutoConfiguration org.springframework.context.ApplicationListener=\ io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.logging.LogbackAppenderApplicationListener diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 73b6f4a6f840..fda05853c855 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -11,3 +11,4 @@ io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.w io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web.RestClientInstrumentationAutoConfiguration io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webmvc.SpringWebMvc6InstrumentationAutoConfiguration io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.scheduling.SpringSchedulingInstrumentationAutoConfiguration +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.runtimemetrics.RuntimeMetricsAutoConfiguration diff --git a/smoke-tests-otel-starter/spring-boot-2/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-2/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java index 315e8155413b..65315b926d5b 100644 --- a/smoke-tests-otel-starter/spring-boot-2/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java +++ b/smoke-tests-otel-starter/spring-boot-2/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java @@ -16,6 +16,7 @@ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { // The headers are simply set here to make sure that headers can be parsed - "otel.exporter.otlp.headers.c=3" + "otel.exporter.otlp.headers.c=3", + "otel.instrumentation.runtime-telemetry.emit-experimental-telemetry=true", }) class OtelSpringStarterSmokeTest extends AbstractOtelSpringStarterSmokeTest {} diff --git a/smoke-tests-otel-starter/spring-boot-3/build.gradle.kts b/smoke-tests-otel-starter/spring-boot-3/build.gradle.kts index 78e01e6685e1..1392ad4844ab 100644 --- a/smoke-tests-otel-starter/spring-boot-3/build.gradle.kts +++ b/smoke-tests-otel-starter/spring-boot-3/build.gradle.kts @@ -66,6 +66,9 @@ graalvmNative { // Workaround for https://github.com/junit-team/junit5/issues/3405 buildArgs.add("--initialize-at-build-time=org.junit.platform.launcher.core.LauncherConfig") buildArgs.add("--initialize-at-build-time=org.junit.jupiter.engine.config.InstantiatingConfigurationParameterConverter") + + // enable JFR - see https://www.graalvm.org/latest/reference-manual/native-image/debugging-and-diagnostics/JFR/ + buildArgs.add("--enable-monitoring=jfr") } // See https://github.com/graalvm/native-build-tools/issues/572 diff --git a/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java index 315e8155413b..b94278398ff4 100644 --- a/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java +++ b/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java @@ -5,6 +5,8 @@ package io.opentelemetry.spring.smoketest; +import java.util.List; +import org.assertj.core.api.AbstractIterableAssert; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest( @@ -16,6 +18,31 @@ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { // The headers are simply set here to make sure that headers can be parsed - "otel.exporter.otlp.headers.c=3" + "otel.exporter.otlp.headers.c=3", + "otel.instrumentation.runtime-telemetry.emit-experimental-telemetry=true", + "otel.instrumentation.runtime-telemetry-java17.enable-all=true", }) -class OtelSpringStarterSmokeTest extends AbstractOtelSpringStarterSmokeTest {} +class OtelSpringStarterSmokeTest extends AbstractOtelSpringStarterSmokeTest { + + @Override + protected void assertAdditionalMetrics() { + // JFR based metrics + for (String metric : + List.of( + "jvm.cpu.limit", + "jvm.buffer.count", + "jvm.class.count", + "jvm.cpu.context_switch", + "jvm.cpu.longlock", + "jvm.system.cpu.utilization", + "jvm.gc.duration", + "jvm.memory.init", + "jvm.memory.used", + "jvm.memory.allocation", + "jvm.network.io", + "jvm.thread.count")) { + testing.waitAndAssertMetrics( + "io.opentelemetry.runtime-telemetry-java17", metric, AbstractIterableAssert::isNotEmpty); + } + } +} diff --git a/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractOtelSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractOtelSpringStarterSmokeTest.java index 0709434bc883..d4a0a2425a78 100644 --- a/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractOtelSpringStarterSmokeTest.java +++ b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractOtelSpringStarterSmokeTest.java @@ -29,6 +29,7 @@ import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import io.opentelemetry.semconv.incubating.ServiceIncubatingAttributes; import java.net.URI; +import java.util.Arrays; import java.util.Collections; import java.util.List; import org.assertj.core.api.AbstractCharSequenceAssert; @@ -210,6 +211,29 @@ void shouldSendTelemetry() { OtelSpringStarterSmokeTestController.TEST_HISTOGRAM, AbstractIterableAssert::isNotEmpty); + // runtime metrics + // from special logic for threads that is automatically detected in GraalVM native image + // see io.opentelemetry.instrumentation.runtimemetrics.java8.Threads + testing.waitAndAssertMetrics( + "io.opentelemetry.runtime-telemetry-java8", + "jvm.thread.count", + AbstractIterableAssert::isNotEmpty); + + // JMX based metrics + Arrays.asList( + "jvm.memory.used", + "jvm.system.cpu.load_1m", + "jvm.buffer.memory.usage", + "jvm.memory.init") + .forEach( + metricName -> + testing.waitAndAssertMetrics( + "io.opentelemetry.runtime-telemetry-java8", + metricName, + AbstractIterableAssert::isNotEmpty)); + + assertAdditionalMetrics(); + // Log List exportedLogRecords = testing.getExportedLogRecords(); assertThat(exportedLogRecords).as("No log record exported.").isNotEmpty(); @@ -228,6 +252,8 @@ void shouldSendTelemetry() { } } + protected void assertAdditionalMetrics() {} + @Test void databaseQuery() { testing.clearAllExportedData(); diff --git a/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestController.java b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestController.java index 7d02c29d5fc1..c4902b2d65d5 100644 --- a/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestController.java +++ b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestController.java @@ -15,8 +15,6 @@ public class OtelSpringStarterSmokeTestController { public static final String PING = "/ping"; - public static final String REST_CLIENT = "/rest-client"; - public static final String REST_TEMPLATE = "/rest-template"; public static final String TEST_HISTOGRAM = "histogram-test-otel-spring-starter"; public static final String METER_SCOPE_NAME = "scope"; private final LongHistogram histogram;