JVM Runtime Metrics
OpenTelemetry’s runtime-telemetry module is an alternative to prometheus-metrics-instrumentation-jvm for users who want JVM metrics following OTel semantic conventions.
Key advantages:
- Metric names follow OTel semantic conventions
- Java 17+ JFR support (context switches, network I/O, lock contention, memory allocation)
- Alignment with the broader OTel ecosystem
Since OpenTelemetry’s opentelemetry-exporter-prometheus
already depends on this library’s PrometheusRegistry,
no additional code is needed in this library — only the
OTel SDK wiring shown below.
implementation 'io.opentelemetry:opentelemetry-sdk'
implementation 'io.opentelemetry:opentelemetry-exporter-prometheus'
// Pick ONE of the following:
// Java 8+:
implementation 'io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java8'
// Java 17+ (adds JFR-based metrics):
// implementation 'io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java17'
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-prometheus</artifactId>
</dependency>
<!-- Pick ONE of the following -->
<!-- Java 8+ -->
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-runtime-telemetry-java8</artifactId>
</dependency>
<!-- Java 17+ (adds JFR-based metrics) -->
<!--
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-runtime-telemetry-java17</artifactId>
</dependency>
-->
If you only want OTel runtime metrics exposed as Prometheus, without any Prometheus Java client metrics:
import io.opentelemetry.exporter.prometheus.PrometheusHttpServer;
import io.opentelemetry.instrumentation.runtimemetrics.java8.RuntimeMetrics;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
PrometheusHttpServer prometheusServer =
PrometheusHttpServer.builder()
.setPort(9464)
.build();
OpenTelemetrySdk openTelemetry =
OpenTelemetrySdk.builder()
.setMeterProvider(
SdkMeterProvider.builder()
.registerMetricReader(prometheusServer)
.build())
.build();
RuntimeMetrics runtimeMetrics =
RuntimeMetrics.builder(openTelemetry).build();
// Close on shutdown to stop metric collection and server
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
runtimeMetrics.close();
prometheusServer.close();
}));
// Scrape at http://localhost:9464/metrics
If you already have Prometheus Java client metrics and want to
add OTel runtime metrics to the same /metrics
endpoint, use PrometheusMetricReader to bridge OTel
metrics into a PrometheusRegistry:
import io.prometheus.metrics.core.metrics.Counter;
import io.prometheus.metrics.exporter.httpserver.HTTPServer;
import io.prometheus.metrics.model.registry.PrometheusRegistry;
import io.opentelemetry.exporter.prometheus.PrometheusMetricReader;
import io.opentelemetry.instrumentation.runtimemetrics.java8.RuntimeMetrics;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
PrometheusRegistry registry =
new PrometheusRegistry();
// Register Prometheus metrics as usual
Counter myCounter = Counter.builder()
.name("my_requests_total")
.register(registry);
// Bridge OTel metrics into the same registry
PrometheusMetricReader reader =
PrometheusMetricReader.create();
registry.register(reader);
OpenTelemetrySdk openTelemetry =
OpenTelemetrySdk.builder()
.setMeterProvider(
SdkMeterProvider.builder()
.registerMetricReader(reader)
.build())
.build();
RuntimeMetrics runtimeMetrics =
RuntimeMetrics.builder(openTelemetry).build();
Runtime.getRuntime()
.addShutdownHook(new Thread(runtimeMetrics::close));
// Expose everything on one endpoint
HTTPServer.builder()
.port(9400)
.registry(registry)
.buildAndStart();
The examples/example-otel-jvm-runtime-metrics directory has a complete runnable example.
The RuntimeMetricsBuilder supports two configuration
options:
Adds a jvm.gc.cause attribute to the jvm.gc.duration
metric, indicating why the garbage collection occurred
(e.g. G1 Evacuation Pause, System.gc()):
RuntimeMetrics.builder(openTelemetry)
.captureGcCause()
.build();
Enables additional experimental metrics beyond the stable set. These are not yet part of the OTel semantic conventions and may change in future releases:
- Buffer pool metrics (direct and mapped byte buffers)
- Extended CPU metrics
- Extended memory pool metrics
- File descriptor metrics
RuntimeMetrics.builder(openTelemetry)
.emitExperimentalTelemetry()
.build();
Both options can be combined:
RuntimeMetrics.builder(openTelemetry)
.captureGcCause()
.emitExperimentalTelemetry()
.build();
Selective per-metric registration is not supported by the runtime-telemetry API — it is all-or-nothing with these two toggles.
The opentelemetry-runtime-telemetry-java17 variant adds
JFR-based metrics. You can selectively enable features:
import io.opentelemetry.instrumentation.runtimemetrics.java17.JfrFeature;
import io.opentelemetry.instrumentation.runtimemetrics.java17.RuntimeMetrics;
RuntimeMetrics.builder(openTelemetry)
.enableFeature(JfrFeature.BUFFER_METRICS)
.enableFeature(JfrFeature.NETWORK_IO_METRICS)
.enableFeature(JfrFeature.LOCK_METRICS)
.enableFeature(JfrFeature.CONTEXT_SWITCH_METRICS)
.build();
OTel metric names are converted to Prometheus format by the exporter. Examples:
| OTel name | Prometheus name |
|---|---|
jvm.memory.used | jvm_memory_used_bytes |
jvm.gc.duration | jvm_gc_duration_seconds |
jvm.thread.count | jvm_thread_count |
jvm.class.loaded | jvm_class_loaded |
jvm.cpu.recent_utilization | jvm_cpu_recent_utilization_ratio |
See Names for full details on how OTel names map to Prometheus names.