Skip to content

Commit 574733b

Browse files
committed
feat(webapp): add per-worker Node.js heap metrics
Extends the existing nodejs.* OTel gauges in tracer.server.ts with direct V8 heap + process memory readings via v8.getHeapStatistics() and process.memoryUsage(): - nodejs.memory.heap.used - V8 heap used after last GC - nodejs.memory.heap.total - V8 heap reserved - nodejs.memory.heap.limit - configured max-old-space-size - nodejs.memory.external - C++ objects bound to JS (Buffer, etc.) - nodejs.memory.array_buffers - ArrayBuffer/SharedArrayBuffer memory - nodejs.memory.rss - resident set size @opentelemetry/host-metrics already publishes process.memory.usage (RSS), but RSS overstates V8 heap by the external + native footprint. Without a direct heap metric it's impossible to size NODE_MAX_OLD_SPACE_SIZE against actual V8 usage. These gauges land in the same trigger.dev scope and carry the same per-worker tags (process.executable.name, service.instance.id) so they're queryable alongside the existing event-loop + handle metrics on a per-cluster-worker basis.
1 parent 41434b5 commit 574733b

2 files changed

Lines changed: 66 additions & 0 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Add per-worker Node.js heap metrics to the OTel meter — `nodejs.memory.heap.used`, `nodejs.memory.heap.total`, `nodejs.memory.heap.limit`, `nodejs.memory.external`, `nodejs.memory.array_buffers`, `nodejs.memory.rss`. Host-metrics only publishes RSS, which overstates V8 heap by the external + native footprint; these give direct heap visibility per cluster worker so `NODE_MAX_OLD_SPACE_SIZE` can be sized against observed heap peaks rather than RSS.

apps/webapp/app/v3/tracer.server.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
3838
import { PrismaInstrumentation } from "@prisma/instrumentation";
3939
import { HostMetrics } from "@opentelemetry/host-metrics";
4040
import { AwsInstrumentation as AwsSdkInstrumentation } from "@opentelemetry/instrumentation-aws-sdk";
41+
import v8 from "node:v8";
4142
import { awsEcsDetector, awsEc2Detector } from "@opentelemetry/resource-detector-aws";
4243
import {
4344
detectResources,
@@ -630,6 +631,39 @@ function configureNodejsMetrics({ meter }: { meter: Meter }) {
630631
unit: "1", // OpenTelemetry convention for ratios
631632
});
632633

634+
// V8 heap + process memory. `NODE_MAX_OLD_SPACE_SIZE` caps V8 old space
635+
// (reflected in `heap.limit`), but doesn't cap external/arrayBuffers/native
636+
// memory — which is why RSS can exceed the heap total. Tracking all of these
637+
// per-worker lets us size `NODE_MAX_OLD_SPACE_SIZE` against observed heap
638+
// peaks rather than RSS (which overstates heap by the external + native
639+
// footprint). `host-metrics` already publishes `process.memory.usage`
640+
// (RSS), but we duplicate it under `nodejs.memory.rss` so all the memory
641+
// numbers land in the same scope and are queryable together.
642+
const heapUsedGauge = meter.createObservableGauge("nodejs.memory.heap.used", {
643+
description: "V8 heap actively in use after the last GC",
644+
unit: "By",
645+
});
646+
const heapTotalGauge = meter.createObservableGauge("nodejs.memory.heap.total", {
647+
description: "V8 heap reserved (young + old generations)",
648+
unit: "By",
649+
});
650+
const heapLimitGauge = meter.createObservableGauge("nodejs.memory.heap.limit", {
651+
description: "V8 heap size limit (configured via --max-old-space-size)",
652+
unit: "By",
653+
});
654+
const externalMemoryGauge = meter.createObservableGauge("nodejs.memory.external", {
655+
description: "Memory used by C++ objects bound to JS (Buffer, etc.)",
656+
unit: "By",
657+
});
658+
const arrayBuffersGauge = meter.createObservableGauge("nodejs.memory.array_buffers", {
659+
description: "Memory allocated for ArrayBuffers and SharedArrayBuffers",
660+
unit: "By",
661+
});
662+
const rssGauge = meter.createObservableGauge("nodejs.memory.rss", {
663+
description: "Resident set size — total physical memory held by the process",
664+
unit: "By",
665+
});
666+
633667
// Get UV threadpool size (defaults to 4 if not set)
634668
const uvThreadpoolSize = parseInt(process.env.UV_THREADPOOL_SIZE || "4", 10);
635669

@@ -687,6 +721,9 @@ function configureNodejsMetrics({ meter }: { meter: Meter }) {
687721
// diff.utilization is between 0 and 1 (fraction of time "active")
688722
const utilization = Number.isFinite(diff.utilization) ? diff.utilization : 0;
689723

724+
const mem = process.memoryUsage();
725+
const heapStats = v8.getHeapStatistics();
726+
690727
return {
691728
threadpoolSize: uvThreadpoolSize,
692729
handlesByType,
@@ -702,6 +739,14 @@ function configureNodejsMetrics({ meter }: { meter: Meter }) {
702739
p99: eventLoopLagP99?.values?.[0]?.value ?? 0,
703740
utilization,
704741
},
742+
memory: {
743+
heapUsed: mem.heapUsed,
744+
heapTotal: mem.heapTotal,
745+
heapLimit: heapStats.heap_size_limit,
746+
external: mem.external,
747+
arrayBuffers: mem.arrayBuffers,
748+
rss: mem.rss,
749+
},
705750
};
706751
}
707752

@@ -714,6 +759,7 @@ function configureNodejsMetrics({ meter }: { meter: Meter }) {
714759
requestsByType,
715760
requestsTotal,
716761
eventLoop,
762+
memory,
717763
} = await readNodeMetrics();
718764

719765
// Observe UV threadpool size
@@ -739,6 +785,14 @@ function configureNodejsMetrics({ meter }: { meter: Meter }) {
739785
res.observe(eventLoopLagP90Gauge, eventLoop.p90);
740786
res.observe(eventLoopLagP99Gauge, eventLoop.p99);
741787
res.observe(eluGauge, eventLoop.utilization);
788+
789+
// Observe memory metrics (bytes)
790+
res.observe(heapUsedGauge, memory.heapUsed);
791+
res.observe(heapTotalGauge, memory.heapTotal);
792+
res.observe(heapLimitGauge, memory.heapLimit);
793+
res.observe(externalMemoryGauge, memory.external);
794+
res.observe(arrayBuffersGauge, memory.arrayBuffers);
795+
res.observe(rssGauge, memory.rss);
742796
},
743797
[
744798
uvThreadpoolSizeGauge,
@@ -753,6 +807,12 @@ function configureNodejsMetrics({ meter }: { meter: Meter }) {
753807
eventLoopLagP90Gauge,
754808
eventLoopLagP99Gauge,
755809
eluGauge,
810+
heapUsedGauge,
811+
heapTotalGauge,
812+
heapLimitGauge,
813+
externalMemoryGauge,
814+
arrayBuffersGauge,
815+
rssGauge,
756816
]
757817
);
758818
}

0 commit comments

Comments
 (0)