docs

System Metrics

CPU, memory and runtime charts for each running instance, with panels that match the language the instance runs.

System Metrics shows how hard each running process works: CPU, memory, threads and the health of its language runtime. Use it when latency rises without a slow query or slow call to blame, or when a process restarts and you suspect memory.

The page lives at /o/:org/projects/:project/system-metrics.

Panels per runtime

Every instance gets the generic panels its metrics can fill. On top of those, it gets the panels of its own runtime and no others, so a Go service never shows an empty event loop chart. A panel with no data for the selected instance is left out.

Unhandled reads the runtime from the telemetry.sdk.language resource attribute, then from process.runtime.name. When neither names a runtime it knows, the instance gets every panel that its metrics fill.

When a panel lists several metrics for one line, the first one the instance reports wins. That way an SDK that sends both a current and a deprecated name is not counted twice.

RuntimePanelMetrics
AnyCPU Utilizationprocess.cpu.utilization, jvm.cpu.recent_utilization or the per-second rate of process.cpu.time; host line from system.cpu.utilization without the idle mode, averaged over CPUs
AnyMemory Usageprocess.memory.usage, or process.runtime.cpython.memory with type=rss
AnyHost Memorysystem.memory.utilization for the used state, or the used share of system.memory.usage
AnyThreadsprocess.thread.count, jvm.thread.count or process.runtime.cpython.thread_count
Node.jsV8 Heap Memoryv8js.memory.heap.used, v8js.memory.heap.limit
Node.jsEvent Loopnodejs.eventloop.utilization, nodejs.eventloop.delay.p99
GoGo Memorygo.memory.used (or process.runtime.go.mem.heap_alloc), go.memory.gc.goal, go.memory.limit
GoGoroutinesgo.goroutine.count (or process.runtime.go.goroutines)
JVMJVM Heapjvm.memory.used and jvm.memory.limit with jvm.memory.type=heap, summed over pools
JVMGarbage Collectionshare of time spent in GC, from the jvm.gc.duration histogram
PythonGarbage Collectioncollections per second, from cpython.gc.collections

Gauges and up-down counters are averaged per bucket. Counters such as process.cpu.time and cpython.gc.collections are charted as their increase per second, so a restart that resets the counter does not show up as a drop.

What each SDK sends

  • The AdonisJS agent sends the generic CPU and memory metrics and the Node.js runtime metrics. hostMetrics: false in the agent config turns off the CPU and memory ones.
  • Go services get the Go panels from go.opentelemetry.io/contrib/instrumentation/runtime. Recent releases send the go.* names; older ones send process.runtime.go.*. For CPU and memory, add go.opentelemetry.io/contrib/instrumentation/host.
  • Java services running the OpenTelemetry Java agent send the JVM metrics without extra setup.
  • Python services get the generic panels and the GC panel from opentelemetry-instrumentation-system-metrics.
  • Any other SDK fills the generic panels when it exports the process and system metrics above.

Instances

An instance is one process of one service. Unhandled identifies it by the service.instance.id resource attribute, falling back to host.name. Two processes that report the same ID share one line, and a process whose ID changes on every restart shows up as a new instance each time.

The agent sets the ID to the HOSTNAME environment variable plus the process id, so cluster and PM2 workers on one host get one instance each. Without HOSTNAME the ID is random. Either way, a restarted process is a new instance.

To control the ID, set it in the agent config through resourceAttributes. This example keeps it stable across restarts on the same host:

ts
// config/unhandled.ts
import { hostname } from 'node:os'
import env from '#start/env'
import { defineConfig } from '@unhandled/agent-adonisjs'

export default defineConfig({
  dsn: env.get('UNHANDLED_DSN'),
  environment: env.get('APP_ENV'),
  resourceAttributes: {
    'service.instance.id': `${hostname()}-web`,
  },
})

The page

The Instance picker lists every service and instance pair that reported any metric from the table above in the last 15 minutes, as service · instance · runtime. The first one is selected by default.

The 15 minutes count back from now, not from the end of the selected range. An instance that stopped is gone from the picker even when you look at yesterday, so you cannot chart a process after it shut down. When nothing reported in the last 15 minutes, the page says No running instances.

The charts cover the selected range for the chosen instance. A few of them need a word on reading:

  • V8 Heap Memory, Go Memory and JVM Heap draw memory in use against its limit. A used line that climbs toward the limit and never drops is a leak.
  • Event Loop shows utilization as a percentage and the P99 delay in milliseconds. A high delay means synchronous work blocks every request on that instance.
  • Goroutines that only ever grow point to goroutines that never return.

For other metrics, or to compare instances on one chart, use Metrics and group by service.instance.id or host.name, whichever your processes send.

Answering "why is the API slow every afternoon?"

  1. Set the range to 24h and pick the API instance.
  2. Look at Event Loop around the slow hours. A P99 delay spike points to CPU-bound work in the request path, such as JSON parsing of a large payload or synchronous crypto.
  3. If CPU Utilization sits near 100% at the same time, the instance is saturated. Add instances or move the heavy work into a job.
  4. If V8 Heap Memory climbs toward the limit, garbage collection pauses cause the delay. Check Requests for an endpoint that loads too much data at once.