Skip to main content
Collect JVM runtime metrics such as memory usage, garbage collection, thread count, and more using OpenTelemetry’s runtime telemetry modules. See the JVM Metrics Semantic Conventions for details on the metrics collected.

Java 8 and Newer

The OpenTelemetry runtime-telemetry-java8 module provides JVM runtime metrics for Java 8 and newer using JMX.

Installation

libraryDependencies ++= Seq(
  "org.typelevel" %% "otel4s-oteljava" % "0.15.0", // <1>
  "io.opentelemetry.instrumentation" % "opentelemetry-runtime-telemetry-java8" % "@OPEN_TELEMETRY_INSTRUMENTATION_ALPHA_VERSION@" // <2>
)
  1. Add the otel4s-oteljava library
  2. Add the OpenTelemetry runtime metrics library

Registration

Register the runtime metrics producers manually:
import cats.effect.{IO, IOApp, Resource, Sync}
import cats.syntax.flatMap._
import cats.syntax.functor._
import io.opentelemetry.api.{OpenTelemetry => JOpenTelemetry}
import io.opentelemetry.instrumentation.runtimemetrics.java8._
import org.typelevel.otel4s.oteljava.OtelJava

object Service extends IOApp.Simple {
  
  def run: IO[Unit] =
    OtelJava
      .autoConfigured[IO]()
      .flatTap(otel4s => registerRuntimeMetrics(otel4s.underlying))
      .use { otel4s =>
        // Your application logic
        ???
      }
  
  private def registerRuntimeMetrics[F[_]: Sync](
      openTelemetry: JOpenTelemetry
  ): Resource[F, Unit] = {
    val acquire = Sync[F].delay(RuntimeMetrics.create(openTelemetry))
  
    Resource.fromAutoCloseable(acquire).void
  }
}

Metrics Collected

The Java 8 module collects these metrics:
MetricDescriptionUnit
process.runtime.jvm.memory.usageMemory usage by poolbytes
process.runtime.jvm.memory.committedCommitted memory by poolbytes
process.runtime.jvm.memory.limitMaximum memory by poolbytes
process.runtime.jvm.memory.usage_after_last_gcMemory after last GCbytes
process.runtime.jvm.gc.durationGC durationms
process.runtime.jvm.threads.countThread countthreads
process.runtime.jvm.classes.loadedLoaded classesclasses
process.runtime.jvm.classes.unloadedUnloaded classesclasses
process.runtime.jvm.cpu.utilizationCPU utilization1

Java 17 and Newer

The OpenTelemetry runtime-telemetry-java17 module provides enhanced JVM runtime metrics for Java 17 and newer using both JMX and JFR (Java Flight Recorder).

Installation

libraryDependencies ++= Seq(
  "org.typelevel" %% "otel4s-oteljava" % "0.15.0", // <1>
  "io.opentelemetry.instrumentation" % "opentelemetry-runtime-telemetry-java17" % "@OPEN_TELEMETRY_INSTRUMENTATION_ALPHA_VERSION@" // <2>
)
  1. Add the otel4s-oteljava library
  2. Add the OpenTelemetry runtime metrics library

Registration

Register the runtime metrics producers:
import cats.effect.{IO, IOApp, Resource, Sync}
import cats.syntax.flatMap._
import cats.syntax.functor._
import io.opentelemetry.api.{OpenTelemetry => JOpenTelemetry}
import io.opentelemetry.instrumentation.runtimemetrics.java17._
import org.typelevel.otel4s.oteljava.OtelJava

object Service extends IOApp.Simple {
  
  def run: IO[Unit] = 
    OtelJava
      .autoConfigured[IO]()
      .flatTap(otel4s => registerRuntimeMetrics(otel4s.underlying))
      .use { otel4s =>
        // Your application logic
        ???
      }

  private def registerRuntimeMetrics[F[_]: Sync](
      openTelemetry: JOpenTelemetry
  ): Resource[F, Unit] = {
    val acquire = Sync[F].delay(RuntimeMetrics.create(openTelemetry))

    Resource.fromAutoCloseable(acquire).void
  }
}

Additional Metrics

The Java 17 module collects all Java 8 metrics plus:
MetricDescriptionUnit
process.runtime.jvm.memory.initInitial memorybytes
process.runtime.jvm.system.cpu.utilizationSystem CPU utilization1
process.runtime.jvm.system.cpu.load_1m1-minute load average1
process.runtime.jvm.buffer.usageBuffer pool usagebytes
process.runtime.jvm.buffer.limitBuffer pool limitbytes
process.runtime.jvm.buffer.countNumber of buffersbuffers

Configuration Options

Custom Metric Intervals

Control metric collection intervals through environment variables:
# Metric export interval (default: 60000ms)
export OTEL_METRIC_EXPORT_INTERVAL=30000

# Metric export timeout (default: 30000ms)
export OTEL_METRIC_EXPORT_TIMEOUT=10000

Selective Metric Collection

Disable specific metric collectors if needed:
import io.opentelemetry.instrumentation.runtimemetrics.java8._

val metrics = RuntimeMetrics.builder(openTelemetry)
  .disableAllFeatures()
  .enableMemoryPools()
  .enableGarbageCollector()
  .build()

Usage Examples

Complete Application

import cats.effect.{IO, IOApp, Resource}
import io.opentelemetry.api.{OpenTelemetry => JOpenTelemetry}
import io.opentelemetry.instrumentation.runtimemetrics.java17._
import org.typelevel.otel4s.oteljava.OtelJava
import org.typelevel.otel4s.metrics.Meter
import scala.concurrent.duration._

object MetricsApp extends IOApp.Simple {
  
  def run: IO[Unit] = 
    resources.use { case (otel4s, meter) =>
      // Application will emit JVM metrics automatically
      runApplication(meter)
    }
    
  def resources: Resource[IO, (OtelJava[IO], Meter[IO])] =
    for {
      otel4s <- OtelJava.autoConfigured[IO]()
      _      <- registerJvmMetrics(otel4s.underlying)
      meter  <- Resource.eval(otel4s.meterProvider.get("my-service"))
    } yield (otel4s, meter)
  
  def registerJvmMetrics(
    openTelemetry: JOpenTelemetry
  ): Resource[IO, Unit] =
    Resource.fromAutoCloseable(
      IO.delay(RuntimeMetrics.create(openTelemetry))
    ).void
    
  def runApplication(meter: Meter[IO]): IO[Unit] =
    IO.sleep(5.minutes) // Keep running to observe metrics
}

With Custom Application Metrics

import cats.effect.{IO, IOApp, Resource}
import cats.syntax.flatMap._
import io.opentelemetry.api.{OpenTelemetry => JOpenTelemetry}
import io.opentelemetry.instrumentation.runtimemetrics.java17._
import org.typelevel.otel4s.oteljava.OtelJava
import org.typelevel.otel4s.metrics.{Counter, Meter}

object CombinedMetrics extends IOApp.Simple {
  
  def run: IO[Unit] =
    OtelJava.autoConfigured[IO]()
      .flatTap(otel4s => registerJvmMetrics(otel4s.underlying))
      .use { otel4s =>
        otel4s.meterProvider.get("my-service").flatMap { meter =>
          setupMetrics(meter).flatMap { metrics =>
            applicationLogic(metrics)
          }
        }
      }
  
  def registerJvmMetrics(
    openTelemetry: JOpenTelemetry
  ): Resource[IO, Unit] =
    Resource.fromAutoCloseable(
      IO.delay(RuntimeMetrics.create(openTelemetry))
    ).void
    
  def setupMetrics(meter: Meter[IO]): IO[AppMetrics] =
    for {
      requests <- meter.counter("http.requests").create
      errors   <- meter.counter("http.errors").create
    } yield AppMetrics(requests, errors)
    
  case class AppMetrics(
    requests: Counter[IO, Long],
    errors: Counter[IO, Long]
  )
  
  def applicationLogic(metrics: AppMetrics): IO[Unit] = ???
}

Monitoring Dashboards

Common Metrics to Watch

  1. Memory Pressure
    • process.runtime.jvm.memory.usage (heap)
    • process.runtime.jvm.memory.usage_after_last_gc
  2. GC Performance
    • process.runtime.jvm.gc.duration
    • GC frequency and pause times
  3. Thread Health
    • process.runtime.jvm.threads.count
    • Thread growth over time
  4. CPU Usage
    • process.runtime.jvm.cpu.utilization
    • process.runtime.jvm.system.cpu.utilization (Java 17+)

Best Practices

The Java 17 module provides more detailed metrics through JFR integration. If your application runs on Java 17 or newer, prefer the runtime-telemetry-java17 module.
process.runtime.jvm.memory.usage_after_last_gc is a better indicator of actual memory pressure than raw usage, as it shows memory that cannot be reclaimed.
JVM metrics change frequently. Consider using a shorter export interval (e.g., 30 seconds) for better granularity in production.
Combine JVM metrics with your application-specific metrics to understand the relationship between resource usage and application behavior.

Troubleshooting

Metrics Not Appearing

Ensure the runtime metrics are properly registered:
// Check that registration happens before application logic
OtelJava
  .autoConfigured[IO]()
  .flatTap(otel4s => registerRuntimeMetrics(otel4s.underlying)) // Register first
  .use { otel4s =>
    // Application logic
  }

High Memory Usage

Metric collection itself has minimal overhead, but if you suspect issues:
// Disable specific collectors
val metrics = RuntimeMetrics.builder(openTelemetry)
  .disableMemoryPools()  // Disable if not needed
  .enableGarbageCollector()
  .enableThreads()
  .build()

Next Steps

Metrics Guide

Learn how to create custom metrics

Configuration

Configure metric export settings

Build docs developers (and LLMs) love