Skip to content

Send JVM metrics

This guide walks through the steps to make a Java, Scala, or Kotlin service emit JVM runtime metrics in the OpenTelemetry format so that they appear in the Runtime metrics tab in APM Service Catalog.

Use this guide if your service is JVM-based but the Runtime metrics tab shows an empty state, or if you are setting up a new JVM service and want to ensure the tab is populated from day one.

OpenTelemetry semantic conventions only

The Runtime metrics tab reads only metrics that follow the OpenTelemetry JVM semantic conventions — for example, jvm.memory.used, jvm.gc.duration, jvm.thread.count. Metrics that follow other naming conventions (such as Micrometer's jvm_memory_used_bytes) are not recognized.

What you need

  • A Java, Scala, or Kotlin service running on Java 8 or later.
  • OpenTelemetry Java agent 2.0.0 or later. Versions before 2.0 emit JVM metrics under the legacy process.runtime.jvm.* namespace, which the Runtime metrics tab does not query. The 2.0.0 release (12 January 2024) renamed these metrics to the stable jvm.* form defined by the OpenTelemetry semantic conventions. If your service is currently instrumented with a 1.x agent, upgrade before continuing.
  • A Coralogix Send-Your-Data API key.
  • The OpenTelemetry endpoint for your Coralogix domain.
  • Permission to set environment variables or modify the JVM startup command for the service.

Step 1: Download the OpenTelemetry Java agent

Download the latest opentelemetry-javaagent.jar from the OpenTelemetry Java instrumentation releases page.

Place the file somewhere accessible to the JVM process — for example /opt/otel/opentelemetry-javaagent.jar. In Kubernetes deployments, a common pattern is to mount it via an init container.

Step 2: Enable the metrics exporter

The OpenTelemetry Java agent does not send metrics by default — its default metrics exporter is none. To enable JVM runtime metrics, set the metrics exporter explicitly.

export OTEL_METRICS_EXPORTER="otlp"
export OTEL_EXPORTER_OTLP_METRICS_PROTOCOL="grpc"
export OTEL_EXPORTER_OTLP_ENDPOINT="<coralogix-otel-endpoint>"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer <CXPrivateKey>"

Replace <CXPrivateKey> with your Send-Your-Data API key and <coralogix-otel-endpoint> with the OpenTelemetry endpoint for your selected Coralogix domain.

Step 3: Set the resource attributes

The Runtime metrics tab correlates metrics to a service through the service.name resource attribute. The instance selector additionally uses the first detected of k8s.pod.name, container.name, host.name, host.id.

export OTEL_RESOURCE_ATTRIBUTES=service.name=<ServiceName>,application.name=<CXApplicationName>,cx.application.name=<CXApplicationName>,cx.subsystem.name=<CXSubsystemName>

In Kubernetes, k8s.pod.name and other pod-level attributes are not auto-detected by the Java agent — the agent only fills in container.id, host.name, and host.id automatically. To populate k8s.pod.name and k8s.namespace.name, choose one of:

  • Downward API environment variables: pass pod metadata into the container as env vars and append them to OTEL_RESOURCE_ATTRIBUTES. Example: OTEL_RESOURCE_ATTRIBUTES=...,k8s.pod.name=$(POD_NAME),k8s.namespace.name=$(POD_NAMESPACE).
  • OpenTelemetry Operator auto-instrumentation: install the OpenTelemetry Operator and create an Instrumentation custom resource. The Operator injects the Java agent and the right resource attributes automatically.
  • Collector-side enrichment: run the k8sattributes processor in your OpenTelemetry Collector. The processor adds Kubernetes attributes to telemetry as it passes through, so the agent itself does not need to know about Kubernetes.

Without one of these, the JVM instances selector falls back to host.name (the Kubernetes node), which groups multiple pods on the same node into a single filter value — too coarse for per-pod debugging.
AttributeRequiredPurpose
service.nameYesIdentifies the service in Service Catalog. Without it, metrics arrive but cannot be correlated to a service row.
service.namespaceRecommendedDisambiguates services with the same name in different teams or environments.
k8s.pod.nameYes for per-pod filteringRequired for the JVM instances selector to work at pod granularity. Set explicitly via the Downward API, OTel Operator, or Collector enrichment.
host.nameAuto-detectedPer-host filtering when k8s.pod.name is not present.
deployment.environment.nameRecommendedRequired for the environment filter.

Step 4: Attach the agent to the JVM

Pass the agent JAR with the -javaagent flag, either through JAVA_TOOL_OPTIONS or directly in the Java command.

Environment variable

export JAVA_TOOL_OPTIONS="-javaagent:/opt/otel/opentelemetry-javaagent.jar"
java -jar myapp.jar

Command line

java -javaagent:/opt/otel/opentelemetry-javaagent.jar \
     -jar myapp.jar

Docker

FROM eclipse-temurin:21-jre
COPY opentelemetry-javaagent.jar /opt/otel/opentelemetry-javaagent.jar
ENV JAVA_TOOL_OPTIONS="-javaagent:/opt/otel/opentelemetry-javaagent.jar"
COPY myapp.jar /app/myapp.jar
ENTRYPOINT ["java", "-jar", "/app/myapp.jar"]

Kubernetes

Add the agent as an init container that copies the JAR into a shared volume, mount the volume in your application container, and set JAVA_TOOL_OPTIONS accordingly. Alternatively, use the OpenTelemetry Operator to inject the agent automatically through an Instrumentation custom resource.

Step 5: Restart the service

Restart the service so it picks up the agent and the new environment variables. On startup, check the logs for the agent banner:

[otel.javaagent ...] [main] INFO io.opentelemetry.javaagent.tooling.VersionLogger - opentelemetry-javaagent - version: <version>

If the banner does not appear, the agent is not attached. Verify the path in -javaagent and that the file is readable by the user the JVM runs as.

Step 6: Verify metrics are flowing

  1. In the Coralogix toolbar, go to Explore, then Metrics.
  2. Search for jvm.memory.used filtered by your service name.
  3. Confirm at least one data point has arrived in the last few minutes.

Querying by service name in Explore

The OpenTelemetry resource attribute service.name (with a dot) is transformed during ingestion to the indexed metric label serviceName (camelCase). Use serviceName when filtering metrics in Explore or PromQL queries. The Runtime metrics tab handles this mapping automatically — you only need to know the label name when you query the metrics directly.

If jvm.memory.used appears but other metrics do not, your agent version may be older than the one that exposes the full stable metric set. Upgrade to the latest opentelemetry-javaagent.jar.

If no jvm.* metrics arrive at all:

  • Confirm OTEL_METRICS_EXPORTER is set to otlp (not none, which is the default).
  • Confirm the OTEL_EXPORTER_OTLP_HEADERS value contains a valid Send-Your-Data API key.
  • Confirm the service has network access to the OpenTelemetry endpoint for your domain.
  • Check the agent logs for export errors. The agent logs OTLP failures at WARN level.

Step 7: Open the Runtime metrics tab

  1. In the Coralogix toolbar, navigate to APM, then Service Catalog.
  2. Select your service.
  3. Select the Runtime metrics tab.

The widgets populate as soon as jvm.* metrics for the service are present in the selected time window. If you still see the empty state, return to Step 6 and confirm the metrics are reaching Coralogix.

These are not required for the Runtime metrics tab to render, but they make the data more useful:

  • jvm.gc.cause opt-in attribute: set -Dotel.instrumentation.runtime-telemetry.capture-gc-cause=true to surface GC cause in widget tooltips and for query filtering. (In agent 2.x. The flag is deprecated for removal in agent 3.0, where GC cause will always be captured.)
  • JFR-streamed metrics (Java 17 or later): on Java 17 or later, the agent can collect higher-fidelity runtime metrics from JDK Flight Recorder in addition to the stable JMX-sourced set. Enable with two properties:

    -Dotel.instrumentation.runtime-telemetry.emit-experimental-jfr-metrics=true
    -Dotel.instrumentation.runtime-telemetry.experimental.prefer-jfr=true
    

    The first property enables the experimental JFR metric set; the second prefers JFR over JMX for metrics available from both sources.

  • Deployment markers: emit deployment events alongside metrics to overlay deployment timestamps on the Runtime metrics charts.

Troubleshooting

SymptomLikely causeFix
Runtime metrics tab is hiddenThe service does not emit any spans or metrics, or its telemetry.sdk.language is non-JVM (Python, Go, and so on)Confirm the agent is running and service.name matches the service in the catalog
Tab shows the empty stateNo jvm.* metrics arriving for the serviceRun through Step 6 — most often OTEL_METRICS_EXPORTER is still set to none
Widgets render but per-instance filter shows only one instanceResource detection has not picked up k8s.pod.name or host.nameEnable the Kubernetes resource detector or set the attribute manually via OTEL_RESOURCE_ATTRIBUTES
Tab shows the empty state and process.runtime.jvm.* metrics appear in ExploreAgent is on the pre-2.0 release line, which emits the legacy metric namespaceUpgrade to opentelemetry-javaagent.jar 2.0.0 or later — the tab queries the jvm.* form only
Some widgets are empty (Heap by pool, Live set - memory after GC)Agent version is missing the stable JVM semantic-convention outputUpgrade to the latest opentelemetry-javaagent.jar 2.x
Metrics arrive in Explore but the catalog does not show the serviceservice.name is missing or differs from the spans' service nameSet service.name consistently for traces and metrics