@datadog/native-metrics

A native Node.js addon that collects CPU, V8 heap, garbage-collection, and event-loop metrics directly from libuv and V8 internals.

Library
npm
v4.0.0
8stars
Apache License 2.0

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
39/100Needs Attention
Development Activity64
Maintenance20
Community16
Maturity56
Momentum0

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
61/100Good
Architecture78
Code Quality62
Innovation58
Learning Curve45

@datadog/native-metrics is a small C++ addon for Node.js, built with N-API via node-addon-api, that exposes low-level runtime metrics no pure-JavaScript API can reach efficiently: per-GC-type pause histograms via V8’s GC prologue/epilogue callbacks, event-loop latency computed from libuv’s check/prepare handles, V8 heap-space statistics, and CPU usage deltas from uv_getrusage. It ships as a prebuilt binary (via node-gyp-build/prebuildify) so consumers install it without needing a local build toolchain in most cases.

The module is maintained by Datadog and used internally by dd-trace-js, the company’s Node.js APM tracer, to feed runtime metrics into Datadog’s monitoring product alongside distributed traces. It is intentionally narrow in scope — a start()/stop()/stats() API surface with optional selective watchers (loop, gc) — rather than a general observability toolkit, and is not typically installed directly by application developers outside of the Datadog tracer dependency chain.

What You Get

  • A start(...watchers) / stop() / stats() API that enables/disables and reads runtime metrics on demand
  • Per-GC-type pause histograms (scavenge, mark-sweep-compact, incremental marking, etc.) computed from V8 GC prologue/epilogue callbacks, with version-aware GC type naming across Node 18-22+
  • Event-loop delay measurement using libuv uv_prepare/uv_check handles to compute poll latency versus the configured backend timeout
  • V8 heap-space breakdown (space name, size, used size, available size, physical size) for every heap space reported by the isolate
  • CPU usage deltas (user/system time in microseconds) computed from uv_getrusage between successive stats() calls
  • Prebuilt binaries distributed via node-gyp-build, avoiding a local compiler toolchain for most supported platforms
  • Selective watcher activation (start('loop'), start('gc')) so only the metrics actually needed are collected

Common Use Cases

  • Feeding runtime metrics (GC pauses, event-loop lag, heap usage) into Datadog APM as a companion to dd-trace-js traces
  • Diagnosing event-loop blocking in a production Node.js service by sampling eventLoop histogram percentiles over time
  • Correlating garbage-collection pause spikes with request latency regressions in a monitored Node.js application
  • Building a custom lightweight runtime-metrics exporter for a Node.js process without pulling in a full APM SDK

Under The Hood

Architecture The addon is a single N-API Addon<NativeMetrics> subclass (src/metrics/main.cpp) that composes four independent metric collectors — Process, GarbageCollection, Heap, and EventLoop — behind one start/stop/stats surface exposed to JavaScript via index.js, which simply requires the platform binary through node-gyp-build. Each collector owns its own native state (a uv_rusage_t snapshot for CPU, a per-GC-type map of Histogram objects for GC, isolate heap-space queries for heap, and a pair of libuv prepare/check handles for the event loop) and is independently enable/disable-able, so start('gc') only wires up the GC prologue/epilogue callbacks without touching the libuv handles. EventLoop manages its own native lifetime carefully: it is heap-allocated and cleaned up via napi_add_async_cleanup_hook plus a reference-counted uv_close callback pattern (handle_count_ gates deletion until both the check and prepare handles have finished closing), correctly handling teardown while the event loop is still active. This is a tight, single-purpose native module rather than a layered application — its architecture is the discipline of keeping four unrelated OS/runtime metric sources behind one consistent JS-facing shape.

Tech Stack Written in C++17 (C++11 on Linux) against node-addon-api (N-API bindings), with node-gyp driving compilation via binding.gyp and node-gyp-build/prebuildify handling prebuilt-binary distribution so most installs skip compilation entirely. It links directly against libuv (bundled with Node.js) for event-loop and process-resource-usage primitives and against V8’s public C++ API for GC callbacks and heap-space statistics — both are Node.js’s own embedded dependencies rather than something this package vendors itself. JavaScript-side tooling is minimal: ESLint with the standard config, Mocha for the JS test runner, and a custom check_licenses.js script run as part of lint. CI (.github/workflows/build.yml) builds and tests across the platforms the prebuilt binaries target.

Code Quality Testing is handled through Mocha (test/metrics.spec.js, test/main.js) with real assertions against live native state — checking CPU deltas against process.cpuUsage(), asserting histogram shape and non-zero counts for GC and event-loop stats, and validating heap-space object shape — plus dedicated crash-repro and worker-thread-termination specs (test/crash-repro.js, test/worker-termination.js) that exercise the addon’s native cleanup paths under worker-thread teardown, a notoriously fragile area for native Node addons. ESLint with the standard config plus a custom license-checking script (scripts/check_licenses.js) run as part of lint. There is no static type system on the C++ side beyond what the compiler and N-API wrappers enforce, and no fuzzing or sanitizer-based testing is evident, but the presence of purpose-built crash/termination regression tests indicates the team has hardened this addon against real production failure modes rather than only testing the happy path.

API Design The public surface is intentionally minimal: start(...watchers), stop(), and stats(), with watchers as optional string arguments ('loop', 'gc') to enable only a subset of collectors. This keeps integration boilerplate near zero — a consumer needs three method calls and no configuration object — at the cost of a somewhat implicit contract (there’s no TypeScript definitions file, so watcher names and the shape of the returned stats object are documented only via the README/tests, not enforced by the compiler for JS consumers). The stats object itself is consistently shaped across metric families (each histogram exposes min/max/sum/avg/count/median/p95), which makes it easy to reason about even without formal API docs.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search