diff --git a/.github/scripts/instrumentations.sh b/.github/scripts/instrumentations.sh index f277d5bf1b81..b2b9993e1145 100755 --- a/.github/scripts/instrumentations.sh +++ b/.github/scripts/instrumentations.sh @@ -245,6 +245,7 @@ readonly INSTRUMENTATIONS=( "rocketmq:rocketmq-client-4.8:javaagent:testExperimental" "rocketmq:rocketmq-client-5.0:javaagent:test" "rocketmq:rocketmq-client-5.0:javaagent:testExperimental" + "runtime-telemetry:library:check" "servlet:servlet-2.2:javaagent:test" "servlet:servlet-3.0:javaagent-testing:test" "servlet:servlet-5.0:jetty12-testing:test" diff --git a/docs/instrumentation-list.yaml b/docs/instrumentation-list.yaml index 2994f4a32600..059d8f086138 100644 --- a/docs/instrumentation-list.yaml +++ b/docs/instrumentation-list.yaml @@ -12524,13 +12524,359 @@ libraries: - name: messaging.system type: STRING runtime: + - name: runtime-telemetry + description: | + This instrumentation enables JVM runtime metrics using JMX (Java 8+) and JFR (Java 17+) to monitor CPU, memory, garbage collection, threads, classes, buffer pools, and file descriptors. + semantic_conventions: + - JVM_RUNTIME_METRICS + library_link: https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/runtime-telemetry + source_path: instrumentation/runtime-telemetry + scope: + name: io.opentelemetry.runtime-telemetry + has_standalone_library: true + configurations: + - name: otel.instrumentation.runtime-telemetry.emit-experimental-metrics + description: Enables the capture of experimental JVM runtime metrics. + type: boolean + default: false + - name: otel.instrumentation.runtime-telemetry.experimental.prefer-jfr + description: | + Prefers JFR over JMX for metrics where both collection methods are available (Java 17+). + type: boolean + default: false + - name: otel.instrumentation.runtime-telemetry.experimental.package-emitter.enabled + description: Enables creating events for JAR libraries used by the application. + type: boolean + default: false + - name: otel.instrumentation.runtime-telemetry.experimental.package-emitter.jars-per-second + description: The number of JAR files processed per second by the package emitter. + type: int + default: 10 + telemetry: + - when: Java17 + metrics: + - name: jvm.buffer.count + description: Number of buffers in the pool. + instrument: updowncounter + data_type: LONG_SUM + unit: '{buffer}' + attributes: + - name: jvm.buffer.pool.name + type: STRING + - name: jvm.buffer.memory.limit + description: Measure of total memory capacity of buffers. + instrument: updowncounter + data_type: LONG_SUM + unit: By + attributes: + - name: jvm.buffer.pool.name + type: STRING + - name: jvm.buffer.memory.used + description: Measure of memory used by buffers. + instrument: updowncounter + data_type: LONG_SUM + unit: By + attributes: + - name: jvm.buffer.pool.name + type: STRING + - name: jvm.class.count + description: Number of classes currently loaded. + instrument: updowncounter + data_type: LONG_SUM + unit: '{class}' + attributes: [] + - name: jvm.class.loaded + description: Number of classes loaded since JVM start. + instrument: counter + data_type: LONG_SUM + unit: '{class}' + attributes: [] + - name: jvm.class.unloaded + description: Number of classes unloaded since JVM start. + instrument: counter + data_type: LONG_SUM + unit: '{class}' + attributes: [] + - name: jvm.cpu.count + description: Number of processors available to the Java virtual machine. + instrument: updowncounter + data_type: LONG_SUM + unit: '{cpu}' + attributes: [] + - name: jvm.cpu.longlock + description: Long lock times + instrument: histogram + data_type: HISTOGRAM + unit: s + attributes: [] + - name: jvm.cpu.recent_utilization + description: Recent CPU utilization for the process as reported by the JVM. + instrument: gauge + data_type: DOUBLE_GAUGE + unit: '1' + attributes: [] + - name: jvm.cpu.time + description: CPU time used by the process as reported by the JVM. + instrument: counter + data_type: DOUBLE_SUM + unit: s + attributes: [] + - name: jvm.gc.duration + description: Duration of JVM garbage collection actions. + instrument: histogram + data_type: HISTOGRAM + unit: s + attributes: + - name: jvm.gc.action + type: STRING + - name: jvm.gc.name + type: STRING + - name: jvm.memory.committed + description: Measure of memory committed. + instrument: updowncounter + data_type: LONG_SUM + unit: By + attributes: + - name: jvm.memory.pool.name + type: STRING + - name: jvm.memory.type + type: STRING + - name: jvm.memory.init + description: Measure of initial memory requested. + instrument: updowncounter + data_type: LONG_SUM + unit: By + attributes: + - name: jvm.memory.pool.name + type: STRING + - name: jvm.memory.type + type: STRING + - name: jvm.memory.limit + description: Measure of max obtainable memory. + instrument: updowncounter + data_type: LONG_SUM + unit: By + attributes: + - name: jvm.memory.pool.name + type: STRING + - name: jvm.memory.type + type: STRING + - name: jvm.memory.used + description: Measure of memory used. + instrument: updowncounter + data_type: LONG_SUM + unit: By + attributes: + - name: jvm.memory.pool.name + type: STRING + - name: jvm.memory.type + type: STRING + - name: jvm.memory.used_after_last_gc + description: Measure of memory used, as measured after the most recent garbage + collection event on this pool. + instrument: updowncounter + data_type: LONG_SUM + unit: By + attributes: + - name: jvm.memory.pool.name + type: STRING + - name: jvm.memory.type + type: STRING + - name: jvm.system.cpu.utilization + description: Recent CPU utilization for the whole system as reported by the + JVM. + instrument: gauge + data_type: DOUBLE_GAUGE + unit: '1' + attributes: [] + - name: jvm.thread.count + description: Number of executing platform threads. + instrument: updowncounter + data_type: LONG_SUM + unit: '{thread}' + attributes: + - name: jvm.thread.daemon + type: BOOLEAN + - name: jvm.thread.state + type: STRING + - when: default + metrics: + - name: jvm.buffer.count + description: Number of buffers in the pool. + instrument: updowncounter + data_type: LONG_SUM + unit: '{buffer}' + attributes: + - name: jvm.buffer.pool.name + type: STRING + - name: jvm.buffer.memory.limit + description: Measure of total memory capacity of buffers. + instrument: updowncounter + data_type: LONG_SUM + unit: By + attributes: + - name: jvm.buffer.pool.name + type: STRING + - name: jvm.buffer.memory.used + description: Measure of memory used by buffers. + instrument: updowncounter + data_type: LONG_SUM + unit: By + attributes: + - name: jvm.buffer.pool.name + type: STRING + - name: jvm.class.count + description: Number of classes currently loaded. + instrument: updowncounter + data_type: LONG_SUM + unit: '{class}' + attributes: [] + - name: jvm.class.loaded + description: Number of classes loaded since JVM start. + instrument: counter + data_type: LONG_SUM + unit: '{class}' + attributes: [] + - name: jvm.class.unloaded + description: Number of classes unloaded since JVM start. + instrument: counter + data_type: LONG_SUM + unit: '{class}' + attributes: [] + - name: jvm.cpu.count + description: Number of processors available to the Java virtual machine. + instrument: updowncounter + data_type: LONG_SUM + unit: '{cpu}' + attributes: [] + - name: jvm.cpu.recent_utilization + description: Recent CPU utilization for the process as reported by the JVM. + instrument: gauge + data_type: DOUBLE_GAUGE + unit: '1' + attributes: [] + - name: jvm.cpu.time + description: CPU time used by the process as reported by the JVM. + instrument: counter + data_type: DOUBLE_SUM + unit: s + attributes: [] + - name: jvm.file_descriptor.count + description: Number of open file descriptors as reported by the JVM. + instrument: updowncounter + data_type: LONG_SUM + unit: '{file_descriptor}' + attributes: [] + - name: jvm.file_descriptor.limit + description: Measure of max open file descriptors as reported by the JVM. + instrument: updowncounter + data_type: LONG_SUM + unit: '{file_descriptor}' + attributes: [] + - name: jvm.gc.duration + description: Duration of JVM garbage collection actions. + instrument: histogram + data_type: HISTOGRAM + unit: s + attributes: + - name: jvm.gc.action + type: STRING + - name: jvm.gc.cause + type: STRING + - name: jvm.gc.name + type: STRING + - name: jvm.memory.committed + description: Measure of memory committed. + instrument: updowncounter + data_type: LONG_SUM + unit: By + attributes: + - name: jvm.memory.pool.name + type: STRING + - name: jvm.memory.type + type: STRING + - name: jvm.memory.init + description: Measure of initial memory requested. + instrument: updowncounter + data_type: LONG_SUM + unit: By + attributes: + - name: jvm.memory.pool.name + type: STRING + - name: jvm.memory.type + type: STRING + - name: jvm.memory.limit + description: Measure of max obtainable memory. + instrument: updowncounter + data_type: LONG_SUM + unit: By + attributes: + - name: jvm.memory.pool.name + type: STRING + - name: jvm.memory.type + type: STRING + - name: jvm.memory.used + description: Measure of memory used. + instrument: updowncounter + data_type: LONG_SUM + unit: By + attributes: + - name: jvm.memory.pool.name + type: STRING + - name: jvm.memory.type + type: STRING + - name: jvm.memory.used_after_last_gc + description: Measure of memory used, as measured after the most recent garbage + collection event on this pool. + instrument: updowncounter + data_type: LONG_SUM + unit: By + attributes: + - name: jvm.memory.pool.name + type: STRING + - name: jvm.memory.type + type: STRING + - name: jvm.system.cpu.load_1m + description: Average CPU load of the whole system for the last minute as reported + by the JVM. + instrument: gauge + data_type: DOUBLE_GAUGE + unit: '{run_queue_item}' + attributes: [] + - name: jvm.system.cpu.utilization + description: Recent CPU utilization for the whole system as reported by the + JVM. + instrument: gauge + data_type: DOUBLE_GAUGE + unit: '1' + attributes: [] + - name: jvm.thread.count + description: Number of executing platform threads. + instrument: updowncounter + data_type: LONG_SUM + unit: '{thread}' + attributes: + - name: jvm.thread.daemon + type: BOOLEAN + - name: jvm.thread.state + type: STRING - name: runtime-telemetry-java17 + description: | + DEPRECATED: This instrumentation enables JVM runtime metrics using JFR (Java 17+). Use the unified runtime-telemetry module instead, which provides both JMX and JFR support. + semantic_conventions: + - JVM_RUNTIME_METRICS + library_link: https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/runtime-telemetry/runtime-telemetry-java17 source_path: instrumentation/runtime-telemetry/runtime-telemetry-java17 minimum_java_version: 17 scope: name: io.opentelemetry.runtime-telemetry-java17 has_standalone_library: true - name: runtime-telemetry-java8 + description: | + DEPRECATED: This instrumentation enables JVM runtime metrics using JMX (Java 8+). Use the unified runtime-telemetry module instead, which provides both JMX and JFR support. + semantic_conventions: + - JVM_RUNTIME_METRICS + library_link: https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/runtime-telemetry/runtime-telemetry-java8 source_path: instrumentation/runtime-telemetry/runtime-telemetry-java8 scope: name: io.opentelemetry.runtime-telemetry-java8 diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/parsers/TelemetryParser.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/parsers/TelemetryParser.java index 438d3485cce0..e048f5ca3a3f 100644 --- a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/parsers/TelemetryParser.java +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/parsers/TelemetryParser.java @@ -55,7 +55,9 @@ class TelemetryParser { entry("io.opentelemetry.couchbase-3.2", singleton("com.couchbase.client.jvm")), entry("io.opentelemetry.couchbase-3.4", singleton("com.couchbase.client.jvm")), // servlet-5.0 tests use jetty-12.0 instrumentation - entry("io.opentelemetry.servlet-5.0", singleton("io.opentelemetry.jetty-12.0"))); + entry("io.opentelemetry.servlet-5.0", singleton("io.opentelemetry.jetty-12.0")), + // runtime-telemetry library tests use a meter named "test" + entry("io.opentelemetry.runtime-telemetry", singleton("test"))); } /** diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/utils/FileManager.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/utils/FileManager.java index ea3cd52dfbff..c06fb4379fa1 100644 --- a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/utils/FileManager.java +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/utils/FileManager.java @@ -85,7 +85,8 @@ public List findBuildGradleFiles(String instrumentationDirectory) { .filter( path -> path.getFileName().toString().equals("build.gradle.kts") - && !path.toString().contains("/testing/")) + && !path.toString().contains("/testing/") + && !isInNestedInstrumentationModule(path, rootPath)) .map(Path::toString) .collect(toList()); } catch (IOException e) { @@ -94,6 +95,31 @@ public List findBuildGradleFiles(String instrumentationDirectory) { } } + /** + * Checks if a file path is inside a nested instrumentation module. A nested module is identified + * by having a javaagent/ or library/ directory that is NOT at the root level. + * + * @param filePath The file path to check + * @param rootPath The root instrumentation directory path + * @return true if the file is in a nested instrumentation module + */ + private static boolean isInNestedInstrumentationModule(Path filePath, Path rootPath) { + Path relativePath = rootPath.relativize(filePath); + String relativeStr = relativePath.toString(); + + String[] segments = relativeStr.split("/"); + + // Find the first javaagent or library segment + for (int i = 0; i < segments.length; i++) { + if (segments[i].equals("javaagent") || segments[i].equals("library")) { + // If javaagent/library is not the first segment, it's a nested module + return i > 0; + } + } + + return false; + } + @Nullable public String getMetaDataFile(String instrumentationDirectory) { String metadataFile = rootDir + instrumentationDirectory + "/metadata.yaml"; diff --git a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/utils/FileManagerTest.java b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/utils/FileManagerTest.java index feeacee17fda..36ee11a0d4bf 100644 --- a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/utils/FileManagerTest.java +++ b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/utils/FileManagerTest.java @@ -52,4 +52,34 @@ void testExcludesCommonModules() { "instrumentation/elasticsearch/elasticsearch-rest-common-5.0")) .isFalse(); } + + @Test + void testFindBuildGradleFilesExcludesNestedModules() throws IOException { + // mimicking runtime-telemetry with nested instrumentation modules + Path runtimeTelemetry = tempDir.resolve("instrumentation/runtime-telemetry"); + Path javaagent = Files.createDirectories(runtimeTelemetry.resolve("javaagent")); + Path library = Files.createDirectories(runtimeTelemetry.resolve("library")); + Path nestedJava17 = + Files.createDirectories(runtimeTelemetry.resolve("runtime-telemetry-java17/javaagent")); + Path nestedJava8 = + Files.createDirectories(runtimeTelemetry.resolve("runtime-telemetry-java8/library")); + + Files.createFile(javaagent.resolve("build.gradle.kts")); + Files.createFile(library.resolve("build.gradle.kts")); + Files.createFile(nestedJava17.resolve("build.gradle.kts")); + Files.createFile(nestedJava8.resolve("build.gradle.kts")); + + List gradleFiles = + fileManager.findBuildGradleFiles("instrumentation/runtime-telemetry"); + + assertThat(gradleFiles).hasSize(2); + assertThat(gradleFiles) + .containsExactlyInAnyOrder( + javaagent.resolve("build.gradle.kts").toString(), + library.resolve("build.gradle.kts").toString()); + assertThat(gradleFiles) + .doesNotContain( + nestedJava17.resolve("build.gradle.kts").toString(), + nestedJava8.resolve("build.gradle.kts").toString()); + } } diff --git a/instrumentation/runtime-telemetry/library/build.gradle.kts b/instrumentation/runtime-telemetry/library/build.gradle.kts index 2a390f89da1b..1e322af6ee9a 100644 --- a/instrumentation/runtime-telemetry/library/build.gradle.kts +++ b/instrumentation/runtime-telemetry/library/build.gradle.kts @@ -69,6 +69,10 @@ dependencies { } tasks { + withType().configureEach { + systemProperty("collectMetadata", findProperty("collectMetadata")?.toString() ?: "false") + } + // Configure testJava17 compilation for Java 17 named("compileTestJava17Java") { dependsOn("compileJava17Java") @@ -106,6 +110,7 @@ tasks { } include("**/*G1GcMemoryMetricTest.*") jvmArgs("-XX:+UseG1GC") + systemProperty("metadataConfig", "Java17") } val testPS by registering(Test::class) { @@ -117,6 +122,7 @@ tasks { } include("**/*PsGcMemoryMetricTest.*") jvmArgs("-XX:+UseParallelGC") + systemProperty("metadataConfig", "Java17") } val testSerial by registering(Test::class) { @@ -128,6 +134,7 @@ tasks { } include("**/*SerialGcMemoryMetricTest.*") jvmArgs("-XX:+UseSerialGC") + systemProperty("metadataConfig", "Java17") } // Run other Java 17 tests (not GC-specific) @@ -140,6 +147,7 @@ tasks { excludeTestsMatching("*SerialGcMemoryMetricTest") excludeTestsMatching("*PsGcMemoryMetricTest") } + systemProperty("metadataConfig", "Java17") } test { diff --git a/instrumentation/runtime-telemetry/library/src/testJava17/java/io/opentelemetry/instrumentation/runtimetelemetry/JfrExtension.java b/instrumentation/runtime-telemetry/library/src/testJava17/java/io/opentelemetry/instrumentation/runtimetelemetry/JfrExtension.java index ccfd696785ac..868135307a79 100644 --- a/instrumentation/runtime-telemetry/library/src/testJava17/java/io/opentelemetry/instrumentation/runtimetelemetry/JfrExtension.java +++ b/instrumentation/runtime-telemetry/library/src/testJava17/java/io/opentelemetry/instrumentation/runtimetelemetry/JfrExtension.java @@ -6,16 +6,26 @@ package io.opentelemetry.instrumentation.runtimetelemetry; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static java.util.Collections.emptyMap; import static java.util.concurrent.TimeUnit.SECONDS; import static org.awaitility.Awaitility.await; import io.opentelemetry.instrumentation.runtimetelemetry.internal.JfrConfig; +import io.opentelemetry.instrumentation.testing.internal.MetaDataCollector; import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.testing.assertj.MetricAssert; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import java.io.File; +import java.io.IOException; import java.util.Collection; +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 jdk.jfr.FlightRecorder; import org.junit.jupiter.api.Assumptions; @@ -30,6 +40,9 @@ public class JfrExtension implements BeforeEachCallback, AfterEachCallback { private SdkMeterProvider meterProvider; private InMemoryMetricReader metricReader; private RuntimeTelemetry runtimeMetrics; + private final Map> metricsByScope = + new HashMap<>(); + private final Set instrumentationScopes = new HashSet<>(); public JfrExtension(Consumer jfrConfigConsumer) { this.jfrConfigConsumer = jfrConfigConsumer; @@ -58,13 +71,22 @@ public void beforeEach(ExtensionContext context) throws InterruptedException { } @Override - public void afterEach(ExtensionContext context) { + public void afterEach(ExtensionContext context) throws IOException { if (meterProvider != null) { meterProvider.close(); } if (runtimeMetrics != null) { runtimeMetrics.close(); } + + // Generates files in a `.telemetry` directory within the instrumentation module with all + // captured emitted metadata to be used by the instrumentation-docs Doc generator. + if (Boolean.getBoolean("collectMetadata")) { + String path = new File("").getAbsolutePath(); + + MetaDataCollector.writeTelemetryToFiles( + path, metricsByScope, emptyMap(), instrumentationScopes); + } } @SafeVarargs @@ -79,6 +101,26 @@ protected final void waitAndAssertMetrics(Consumer... assertions) for (Consumer assertion : assertions) { assertThat(metrics).anySatisfy(metric -> assertion.accept(assertThat(metric))); } + if (Boolean.getBoolean("collectMetadata")) { + collectEmittedMetrics(metrics.stream().toList()); + } }); } + + private void collectEmittedMetrics(List metrics) { + for (MetricData metric : metrics) { + Map scopeMap = + this.metricsByScope.computeIfAbsent( + metric.getInstrumentationScopeInfo(), m -> new HashMap<>()); + + if (!scopeMap.containsKey(metric.getName())) { + scopeMap.put(metric.getName(), metric); + } + + InstrumentationScopeInfo scopeInfo = metric.getInstrumentationScopeInfo(); + if (!scopeInfo.getName().equals("test")) { + instrumentationScopes.add(scopeInfo); + } + } + } } diff --git a/instrumentation/runtime-telemetry/metadata.yaml b/instrumentation/runtime-telemetry/metadata.yaml new file mode 100644 index 000000000000..a9a9de315713 --- /dev/null +++ b/instrumentation/runtime-telemetry/metadata.yaml @@ -0,0 +1,24 @@ +description: > + This instrumentation enables JVM runtime metrics using JMX (Java 8+) and JFR (Java 17+) to + monitor CPU, memory, garbage collection, threads, classes, buffer pools, and file descriptors. +semantic_conventions: + - JVM_RUNTIME_METRICS +library_link: https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/runtime-telemetry +configurations: + - name: otel.instrumentation.runtime-telemetry.emit-experimental-metrics + description: Enables the capture of experimental JVM runtime metrics. + type: boolean + default: false + - name: otel.instrumentation.runtime-telemetry.experimental.prefer-jfr + description: > + Prefers JFR over JMX for metrics where both collection methods are available (Java 17+). + type: boolean + default: false + - name: otel.instrumentation.runtime-telemetry.experimental.package-emitter.enabled + description: Enables creating events for JAR libraries used by the application. + type: boolean + default: false + - name: otel.instrumentation.runtime-telemetry.experimental.package-emitter.jars-per-second + description: The number of JAR files processed per second by the package emitter. + type: int + default: 10 diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/metadata.yaml b/instrumentation/runtime-telemetry/runtime-telemetry-java17/metadata.yaml new file mode 100644 index 000000000000..0dbaf5967cd8 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/metadata.yaml @@ -0,0 +1,6 @@ +description: > + DEPRECATED: This instrumentation enables JVM runtime metrics using JFR (Java 17+). Use the + unified runtime-telemetry module instead, which provides both JMX and JFR support. +semantic_conventions: + - JVM_RUNTIME_METRICS +library_link: https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/runtime-telemetry/runtime-telemetry-java17 diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/metadata.yaml b/instrumentation/runtime-telemetry/runtime-telemetry-java8/metadata.yaml new file mode 100644 index 000000000000..ea9c107b7faf --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/metadata.yaml @@ -0,0 +1,6 @@ +description: > + DEPRECATED: This instrumentation enables JVM runtime metrics using JMX (Java 8+). Use the + unified runtime-telemetry module instead, which provides both JMX and JFR support. +semantic_conventions: + - JVM_RUNTIME_METRICS +library_link: https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/runtime-telemetry/runtime-telemetry-java8