Send Node.js runtime metrics
This guide walks through the steps to make a Node.js service emit runtime metrics in the OpenTelemetry format so that they appear in the Runtime tab for the service in the APM entity drilldown.
Use this guide if your service is Node.js-based but the Runtime tab shows a zero state, or if you are setting up a new Node.js service and want to ensure the tab is populated from day one.
The Runtime tab reads only the runtime metrics emitted by @opentelemetry/instrumentation-runtime-node - the nodejs.* (event loop) and v8js.* (V8 heap and garbage collection) namespaces defined by the OpenTelemetry semantic conventions. Metrics that follow other naming conventions, such as the defaults exposed by prom-client, are not recognized.
What you need
- A Node.js service running Node.js 22.15.0 or later (required by
@opentelemetry/instrumentation-runtime-node). - The OpenTelemetry Node.js runtime instrumentation and an OTLP metrics exporter (installed in Step 1).
- A Coralogix Send-Your-Data API key.
- The OpenTelemetry endpoint for your Coralogix domain.
- Permission to set environment variables or modify the service start command.
Step 1: Install the runtime instrumentation
Add the runtime instrumentation, the Node SDK, and an OTLP metrics exporter to your project.
npm install --save @opentelemetry/sdk-node
npm install --save @opentelemetry/sdk-metrics
npm install --save @opentelemetry/exporter-metrics-otlp-grpc
npm install --save @opentelemetry/instrumentation-runtime-node
This example uses the gRPC metrics exporter. To export over HTTP instead, install @opentelemetry/exporter-metrics-otlp-http and use it in place of the gRPC package in the steps below.
Step 2: Configure the OTLP metrics exporter
Create an instrumentation file that registers RuntimeNodeInstrumentation with the Node SDK and a periodic metric reader that exports over OTLP to Coralogix. Load this file before your application code.
// instrumentation.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-grpc');
const {
RuntimeNodeInstrumentation,
} = require('@opentelemetry/instrumentation-runtime-node');
const sdk = new NodeSDK({
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter(),
}),
instrumentations: [new RuntimeNodeInstrumentation()],
});
sdk.start();
RuntimeNodeInstrumentation also accepts a monitoringPrecision option (milliseconds, default 10) that controls how often the event-loop delay metrics are sampled.
With no arguments, OTLPMetricExporter reads its endpoint and headers from environment variables. Set them before the process starts:
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.
To configure the exporter in code instead, pass url and headers to OTLPMetricExporter({ ... }).
Step 3: Set the resource attributes
The Runtime tab correlates metrics to a service through the service.name resource attribute, and groups Node.js instances in the selector and heatmap by k8s.pod.name. When k8s.pod.name is absent, the backend falls back to other resource attributes - such as container.name, host.name, and host.id - to tell instances apart, but for pod-level granularity, set k8s.pod.name explicitly.
Set the service name and the other attributes through environment variables that the SDK reads at startup:
export OTEL_SERVICE_NAME="<ServiceName>"
export OTEL_RESOURCE_ATTRIBUTES="cx.application.name=<CXApplicationName>,cx.subsystem.name=<CXSubsystemName>"
In Kubernetes, k8s.pod.name and other pod-level attributes are not auto-detected by the Node SDK - its default resource detectors fill in host.name, host.id, and process attributes only. 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
Instrumentationcustom resource. The Operator injects the Node.js instrumentation and the right resource attributes automatically. - Collector-side enrichment: run the
k8sattributesprocessor in your OpenTelemetry Collector. The processor adds Kubernetes attributes to telemetry as it passes through, so the application itself does not need to know about Kubernetes.
Without one of these, k8s.pod.name is missing and the backend distinguishes instances only by the coarser attributes it can detect (such as host.name, the Kubernetes node), which groups multiple pods on the same node together - too coarse for per-pod debugging.
| Attribute | Required | Purpose |
|---|---|---|
service.name | Yes | Identifies the service in the catalog. Without it, metrics arrive but cannot be correlated to a service row. Set it via OTEL_SERVICE_NAME. |
service.namespace | Recommended | Disambiguates services with the same name in different teams or environments. |
k8s.pod.name | Yes for per-pod filtering | The attribute the instances selector and heatmap group by. Set it explicitly via the Downward API, OTel Operator, or Collector enrichment for pod-level granularity. |
host.name | Auto-detected | Backend fallback for telling instances apart when k8s.pod.name is not present (host-level, not pod-level). |
deployment.environment.name | Recommended | Required for the environment filter. |
Step 4: Restart the service
Restart the service so it loads the instrumentation file before your application code. Preload it with the --require flag:
node --require ./instrumentation.js app.js
Or through NODE_OPTIONS:
export NODE_OPTIONS="--require ./instrumentation.js"
node app.js
The instrumentation file must be loaded before the rest of the application so that the SDK starts first.
Step 5: Verify metrics are flowing
- In the Coralogix toolbar, go to Explore, then Metrics.
- Search for a Node.js runtime metric - for example
v8js.memory.heap.usedornodejs.eventloop.utilization- filtered by your service name. - Confirm at least one data point has arrived in the last few minutes.
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 tab handles this mapping automatically - you only need to know the label name when you query the metrics directly.
If no nodejs.* or v8js.* metrics arrive at all:
- Confirm the instrumentation file is loaded before application code (the
--requireflag orNODE_OPTIONS). - Confirm the
OTEL_EXPORTER_OTLP_HEADERSvalue contains a valid Send-Your-Data API key. - Confirm the service has network access to the OpenTelemetry endpoint for your domain.
- Enable SDK diagnostic logging to surface OTLP export errors.
Step 6: Open the Runtime tab
- In the Coralogix toolbar, select APM.
- Select your service.
- Select the Runtime tab.
The widgets populate as soon as nodejs.* and v8js.* metrics for the service are present in the selected time window. If you still see the zero state, return to Step 5 and confirm the metrics are reaching Coralogix.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Runtime tab is hidden | The service's detected language is not Node.js, so the tab is not shown for it | Confirm the instrumentation is running and service.name matches the service in the catalog |
| Tab shows the zero state | No nodejs.* or v8js.* metrics arriving for the service | Run through Step 5 - most often the instrumentation file is not preloaded before application code |
| Prometheus-style runtime metrics appear in Explore but the tab stays empty | Metrics use prom-client naming, not the OpenTelemetry conventions | Switch to @opentelemetry/instrumentation-runtime-node, which emits the nodejs.* and v8js.* namespaces |
| Widgets render but the per-instance filter and heatmap show only one instance | Resource detection has not picked up k8s.pod.name or host.name | Set k8s.pod.name via the Downward API, OTel Operator, or Collector enrichment |
| Metrics arrive in Explore but the catalog does not show the service | service.name is missing or differs from the spans' service name | Set service.name consistently for traces and metrics |
Next steps
Read and interpret the metrics in the Node.js runtime metrics tab.