672 lines
19 KiB
TypeScript
672 lines
19 KiB
TypeScript
|
|
import * as v8 from 'v8';
|
||
|
|
import * as fs from 'fs';
|
||
|
|
import { PerformanceObserver, monitorEventLoopDelay } from 'perf_hooks';
|
||
|
|
|
||
|
|
// ── Metric types ────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
export interface IGaugeConfig {
|
||
|
|
name: string;
|
||
|
|
help: string;
|
||
|
|
registers?: Registry[];
|
||
|
|
labelNames?: string[];
|
||
|
|
collect?: () => void | Promise<void>;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ICounterConfig {
|
||
|
|
name: string;
|
||
|
|
help: string;
|
||
|
|
registers?: Registry[];
|
||
|
|
labelNames?: string[];
|
||
|
|
collect?: () => void | Promise<void>;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface IHistogramConfig {
|
||
|
|
name: string;
|
||
|
|
help: string;
|
||
|
|
registers?: Registry[];
|
||
|
|
labelNames?: string[];
|
||
|
|
buckets?: number[];
|
||
|
|
collect?: () => void | Promise<void>;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface IMetric {
|
||
|
|
name: string;
|
||
|
|
help: string;
|
||
|
|
type: string;
|
||
|
|
collect?: () => void | Promise<void>;
|
||
|
|
getLines(): Promise<string[]>;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Registry ────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
export class Registry {
|
||
|
|
private metricsList: IMetric[] = [];
|
||
|
|
|
||
|
|
registerMetric(metric: IMetric): void {
|
||
|
|
this.metricsList.push(metric);
|
||
|
|
}
|
||
|
|
|
||
|
|
async metrics(): Promise<string> {
|
||
|
|
const lines: string[] = [];
|
||
|
|
for (const m of this.metricsList) {
|
||
|
|
if (m.collect) {
|
||
|
|
await m.collect();
|
||
|
|
}
|
||
|
|
lines.push(`# HELP ${m.name} ${m.help}`);
|
||
|
|
lines.push(`# TYPE ${m.name} ${m.type}`);
|
||
|
|
lines.push(...(await m.getLines()));
|
||
|
|
}
|
||
|
|
return lines.join('\n') + '\n';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Gauge ───────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
export class Gauge implements IMetric {
|
||
|
|
public name: string;
|
||
|
|
public help: string;
|
||
|
|
public type = 'gauge';
|
||
|
|
public collect?: () => void | Promise<void>;
|
||
|
|
private value = 0;
|
||
|
|
private labelledValues = new Map<string, number>();
|
||
|
|
constructor(config: IGaugeConfig) {
|
||
|
|
this.name = config.name;
|
||
|
|
this.help = config.help;
|
||
|
|
this.collect = config.collect;
|
||
|
|
if (config.registers) {
|
||
|
|
for (const r of config.registers) {
|
||
|
|
r.registerMetric(this);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
set(labelsOrValue: Record<string, string> | number, value?: number): void {
|
||
|
|
if (typeof labelsOrValue === 'number') {
|
||
|
|
this.value = labelsOrValue;
|
||
|
|
} else {
|
||
|
|
const key = this.labelsToKey(labelsOrValue);
|
||
|
|
this.labelledValues.set(key, value!);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
inc(labelsOrAmount?: Record<string, string> | number, amount?: number): void {
|
||
|
|
if (labelsOrAmount === undefined) {
|
||
|
|
this.value += 1;
|
||
|
|
} else if (typeof labelsOrAmount === 'number') {
|
||
|
|
this.value += labelsOrAmount;
|
||
|
|
} else {
|
||
|
|
const key = this.labelsToKey(labelsOrAmount);
|
||
|
|
const cur = this.labelledValues.get(key) || 0;
|
||
|
|
this.labelledValues.set(key, cur + (amount ?? 1));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async getLines(): Promise<string[]> {
|
||
|
|
const lines: string[] = [];
|
||
|
|
if (this.labelledValues.size > 0) {
|
||
|
|
for (const [key, val] of this.labelledValues) {
|
||
|
|
lines.push(`${this.name}{${key}} ${formatValue(val)}`);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
lines.push(`${this.name} ${formatValue(this.value)}`);
|
||
|
|
}
|
||
|
|
return lines;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Reset all values */
|
||
|
|
reset(): void {
|
||
|
|
this.value = 0;
|
||
|
|
this.labelledValues.clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
private labelsToKey(labels: Record<string, string>): string {
|
||
|
|
return Object.entries(labels)
|
||
|
|
.map(([k, v]) => `${k}="${v}"`)
|
||
|
|
.join(',');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Counter ─────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
export class Counter implements IMetric {
|
||
|
|
public name: string;
|
||
|
|
public help: string;
|
||
|
|
public type = 'counter';
|
||
|
|
public collect?: () => void | Promise<void>;
|
||
|
|
private value = 0;
|
||
|
|
private labelledValues = new Map<string, number>();
|
||
|
|
|
||
|
|
constructor(config: ICounterConfig) {
|
||
|
|
this.name = config.name;
|
||
|
|
this.help = config.help;
|
||
|
|
this.collect = config.collect;
|
||
|
|
if (config.registers) {
|
||
|
|
for (const r of config.registers) {
|
||
|
|
r.registerMetric(this);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
inc(labelsOrAmount?: Record<string, string> | number, amount?: number): void {
|
||
|
|
if (labelsOrAmount === undefined) {
|
||
|
|
this.value += 1;
|
||
|
|
} else if (typeof labelsOrAmount === 'number') {
|
||
|
|
this.value += labelsOrAmount;
|
||
|
|
} else {
|
||
|
|
const key = this.labelsToKey(labelsOrAmount);
|
||
|
|
const cur = this.labelledValues.get(key) || 0;
|
||
|
|
this.labelledValues.set(key, cur + (amount ?? 1));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async getLines(): Promise<string[]> {
|
||
|
|
const lines: string[] = [];
|
||
|
|
if (this.labelledValues.size > 0) {
|
||
|
|
for (const [key, val] of this.labelledValues) {
|
||
|
|
lines.push(`${this.name}{${key}} ${formatValue(val)}`);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
lines.push(`${this.name} ${formatValue(this.value)}`);
|
||
|
|
}
|
||
|
|
return lines;
|
||
|
|
}
|
||
|
|
|
||
|
|
reset(): void {
|
||
|
|
this.value = 0;
|
||
|
|
this.labelledValues.clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
private labelsToKey(labels: Record<string, string>): string {
|
||
|
|
return Object.entries(labels)
|
||
|
|
.map(([k, v]) => `${k}="${v}"`)
|
||
|
|
.join(',');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Histogram ───────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
export class Histogram implements IMetric {
|
||
|
|
public name: string;
|
||
|
|
public help: string;
|
||
|
|
public type = 'histogram';
|
||
|
|
public collect?: () => void | Promise<void>;
|
||
|
|
private bucketBounds: number[];
|
||
|
|
private bucketCounts: number[];
|
||
|
|
private sum = 0;
|
||
|
|
private count = 0;
|
||
|
|
private labelledData = new Map<
|
||
|
|
string,
|
||
|
|
{ bucketCounts: number[]; sum: number; count: number }
|
||
|
|
>();
|
||
|
|
|
||
|
|
constructor(config: IHistogramConfig) {
|
||
|
|
this.name = config.name;
|
||
|
|
this.help = config.help;
|
||
|
|
this.bucketBounds = config.buckets || [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];
|
||
|
|
this.bucketCounts = new Array(this.bucketBounds.length).fill(0);
|
||
|
|
this.collect = config.collect;
|
||
|
|
if (config.registers) {
|
||
|
|
for (const r of config.registers) {
|
||
|
|
r.registerMetric(this);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
observe(labelsOrValue: Record<string, string> | number, value?: number): void {
|
||
|
|
if (typeof labelsOrValue === 'number') {
|
||
|
|
this.observeUnlabelled(labelsOrValue);
|
||
|
|
} else {
|
||
|
|
const key = this.labelsToKey(labelsOrValue);
|
||
|
|
let data = this.labelledData.get(key);
|
||
|
|
if (!data) {
|
||
|
|
data = {
|
||
|
|
bucketCounts: new Array(this.bucketBounds.length).fill(0),
|
||
|
|
sum: 0,
|
||
|
|
count: 0,
|
||
|
|
};
|
||
|
|
this.labelledData.set(key, data);
|
||
|
|
}
|
||
|
|
data.sum += value!;
|
||
|
|
data.count += 1;
|
||
|
|
for (let i = 0; i < this.bucketBounds.length; i++) {
|
||
|
|
if (value! <= this.bucketBounds[i]) {
|
||
|
|
data.bucketCounts[i]++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private observeUnlabelled(val: number): void {
|
||
|
|
this.sum += val;
|
||
|
|
this.count += 1;
|
||
|
|
for (let i = 0; i < this.bucketBounds.length; i++) {
|
||
|
|
if (val <= this.bucketBounds[i]) {
|
||
|
|
this.bucketCounts[i]++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async getLines(): Promise<string[]> {
|
||
|
|
const lines: string[] = [];
|
||
|
|
if (this.labelledData.size > 0) {
|
||
|
|
for (const [key, data] of this.labelledData) {
|
||
|
|
for (let i = 0; i < this.bucketBounds.length; i++) {
|
||
|
|
lines.push(
|
||
|
|
`${this.name}_bucket{${key},le="${this.bucketBounds[i]}"} ${data.bucketCounts[i]}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
lines.push(`${this.name}_bucket{${key},le="+Inf"} ${data.count}`);
|
||
|
|
lines.push(`${this.name}_sum{${key}} ${formatValue(data.sum)}`);
|
||
|
|
lines.push(`${this.name}_count{${key}} ${data.count}`);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
for (let i = 0; i < this.bucketBounds.length; i++) {
|
||
|
|
lines.push(
|
||
|
|
`${this.name}_bucket{le="${this.bucketBounds[i]}"} ${this.bucketCounts[i]}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
lines.push(`${this.name}_bucket{le="+Inf"} ${this.count}`);
|
||
|
|
lines.push(`${this.name}_sum ${formatValue(this.sum)}`);
|
||
|
|
lines.push(`${this.name}_count ${this.count}`);
|
||
|
|
}
|
||
|
|
return lines;
|
||
|
|
}
|
||
|
|
|
||
|
|
reset(): void {
|
||
|
|
this.sum = 0;
|
||
|
|
this.count = 0;
|
||
|
|
this.bucketCounts.fill(0);
|
||
|
|
this.labelledData.clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
private labelsToKey(labels: Record<string, string>): string {
|
||
|
|
return Object.entries(labels)
|
||
|
|
.map(([k, v]) => `${k}="${v}"`)
|
||
|
|
.join(',');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Default Metrics Collectors ──────────────────────────────────────────────
|
||
|
|
|
||
|
|
export function collectDefaultMetrics(registry: Registry): void {
|
||
|
|
registerProcessCpuTotal(registry);
|
||
|
|
registerProcessStartTime(registry);
|
||
|
|
registerProcessMemory(registry);
|
||
|
|
registerProcessOpenFds(registry);
|
||
|
|
registerProcessMaxFds(registry);
|
||
|
|
registerEventLoopLag(registry);
|
||
|
|
registerProcessHandles(registry);
|
||
|
|
registerProcessRequests(registry);
|
||
|
|
registerProcessResources(registry);
|
||
|
|
registerHeapSizeAndUsed(registry);
|
||
|
|
registerHeapSpaces(registry);
|
||
|
|
registerVersion(registry);
|
||
|
|
registerGc(registry);
|
||
|
|
}
|
||
|
|
|
||
|
|
function registerProcessCpuTotal(registry: Registry): void {
|
||
|
|
const userGauge = new Gauge({
|
||
|
|
name: 'process_cpu_user_seconds_total',
|
||
|
|
help: 'Total user CPU time spent in seconds.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
const now = process.cpuUsage();
|
||
|
|
userGauge.set(now.user / 1e6);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const systemGauge = new Gauge({
|
||
|
|
name: 'process_cpu_system_seconds_total',
|
||
|
|
help: 'Total system CPU time spent in seconds.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
const now = process.cpuUsage();
|
||
|
|
systemGauge.set(now.system / 1e6);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
new Gauge({
|
||
|
|
name: 'process_cpu_seconds_total',
|
||
|
|
help: 'Total user and system CPU time spent in seconds.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
const now = process.cpuUsage();
|
||
|
|
this.set((now.user + now.system) / 1e6);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function registerProcessStartTime(registry: Registry): void {
|
||
|
|
const startTimeSeconds = Math.floor(Date.now() / 1000 - process.uptime());
|
||
|
|
new Gauge({
|
||
|
|
name: 'process_start_time_seconds',
|
||
|
|
help: 'Start time of the process since unix epoch in seconds.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
this.set(startTimeSeconds);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function registerProcessMemory(registry: Registry): void {
|
||
|
|
new Gauge({
|
||
|
|
name: 'process_resident_memory_bytes',
|
||
|
|
help: 'Resident memory size in bytes.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
this.set(process.memoryUsage.rss());
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
new Gauge({
|
||
|
|
name: 'process_virtual_memory_bytes',
|
||
|
|
help: 'Virtual memory size in bytes.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
try {
|
||
|
|
const status = fs.readFileSync('/proc/self/status', 'utf8');
|
||
|
|
const match = status.match(/VmSize:\s+(\d+)\s+kB/);
|
||
|
|
if (match) {
|
||
|
|
this.set(parseInt(match[1], 10) * 1024);
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
// not on Linux — skip
|
||
|
|
}
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
new Gauge({
|
||
|
|
name: 'process_heap_bytes',
|
||
|
|
help: 'Process heap size in bytes.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
this.set(process.memoryUsage().heapUsed);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function registerProcessOpenFds(registry: Registry): void {
|
||
|
|
new Gauge({
|
||
|
|
name: 'process_open_fds',
|
||
|
|
help: 'Number of open file descriptors.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
try {
|
||
|
|
const fds = fs.readdirSync('/proc/self/fd');
|
||
|
|
this.set(fds.length);
|
||
|
|
} catch {
|
||
|
|
this.set(0);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function registerProcessMaxFds(registry: Registry): void {
|
||
|
|
new Gauge({
|
||
|
|
name: 'process_max_fds',
|
||
|
|
help: 'Maximum number of open file descriptors.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
try {
|
||
|
|
const limits = fs.readFileSync('/proc/self/limits', 'utf8');
|
||
|
|
const match = limits.match(/Max open files\s+(\d+)/);
|
||
|
|
if (match) {
|
||
|
|
this.set(parseInt(match[1], 10));
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
this.set(0);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function registerEventLoopLag(registry: Registry): void {
|
||
|
|
let histogram: ReturnType<typeof monitorEventLoopDelay> | null = null;
|
||
|
|
try {
|
||
|
|
histogram = monitorEventLoopDelay({ resolution: 10 });
|
||
|
|
histogram.enable();
|
||
|
|
} catch {
|
||
|
|
// Not available in this runtime
|
||
|
|
}
|
||
|
|
|
||
|
|
new Gauge({
|
||
|
|
name: 'nodejs_eventloop_lag_seconds',
|
||
|
|
help: 'Lag of event loop in seconds.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
if (histogram) {
|
||
|
|
this.set(histogram.mean / 1e9);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
new Gauge({
|
||
|
|
name: 'nodejs_eventloop_lag_min_seconds',
|
||
|
|
help: 'The minimum recorded event loop delay.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
if (histogram) {
|
||
|
|
this.set(histogram.min / 1e9);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
new Gauge({
|
||
|
|
name: 'nodejs_eventloop_lag_max_seconds',
|
||
|
|
help: 'The maximum recorded event loop delay.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
if (histogram) {
|
||
|
|
this.set(histogram.max / 1e9);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
new Gauge({
|
||
|
|
name: 'nodejs_eventloop_lag_mean_seconds',
|
||
|
|
help: 'The mean of the recorded event loop delays.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
if (histogram) {
|
||
|
|
this.set(histogram.mean / 1e9);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
new Gauge({
|
||
|
|
name: 'nodejs_eventloop_lag_stddev_seconds',
|
||
|
|
help: 'The standard deviation of the recorded event loop delays.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
if (histogram) {
|
||
|
|
this.set(histogram.stddev / 1e9);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
for (const p of [50, 90, 99]) {
|
||
|
|
new Gauge({
|
||
|
|
name: `nodejs_eventloop_lag_p${p}_seconds`,
|
||
|
|
help: `The ${p}th percentile of the recorded event loop delays.`,
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
if (histogram) {
|
||
|
|
this.set(histogram.percentile(p) / 1e9);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function registerProcessHandles(registry: Registry): void {
|
||
|
|
new Gauge({
|
||
|
|
name: 'nodejs_active_handles_total',
|
||
|
|
help: 'Number of active libuv handles grouped by handle type.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
const handles = (process as any)._getActiveHandles?.();
|
||
|
|
this.set(handles ? handles.length : 0);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function registerProcessRequests(registry: Registry): void {
|
||
|
|
new Gauge({
|
||
|
|
name: 'nodejs_active_requests_total',
|
||
|
|
help: 'Number of active libuv requests grouped by request type.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
const requests = (process as any)._getActiveRequests?.();
|
||
|
|
this.set(requests ? requests.length : 0);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function registerProcessResources(registry: Registry): void {
|
||
|
|
new Gauge({
|
||
|
|
name: 'nodejs_active_resources_total',
|
||
|
|
help: 'Number of active resources that are currently keeping the event loop alive.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
try {
|
||
|
|
const resources = (process as any).getActiveResourcesInfo?.();
|
||
|
|
this.set(resources ? resources.length : 0);
|
||
|
|
} catch {
|
||
|
|
this.set(0);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function registerHeapSizeAndUsed(registry: Registry): void {
|
||
|
|
new Gauge({
|
||
|
|
name: 'nodejs_heap_size_total_bytes',
|
||
|
|
help: 'Process heap size from Node.js in bytes.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
this.set(process.memoryUsage().heapTotal);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
new Gauge({
|
||
|
|
name: 'nodejs_heap_size_used_bytes',
|
||
|
|
help: 'Process heap size used from Node.js in bytes.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
this.set(process.memoryUsage().heapUsed);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
new Gauge({
|
||
|
|
name: 'nodejs_external_memory_bytes',
|
||
|
|
help: 'Node.js external memory size in bytes.',
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
this.set(process.memoryUsage().external);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function registerHeapSpaces(registry: Registry): void {
|
||
|
|
const spaceGauge = new Gauge({
|
||
|
|
name: 'nodejs_heap_space_size_total_bytes',
|
||
|
|
help: 'Process heap space size total from Node.js in bytes.',
|
||
|
|
labelNames: ['space'],
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
spaceGauge.reset();
|
||
|
|
const spaces = v8.getHeapSpaceStatistics();
|
||
|
|
for (const space of spaces) {
|
||
|
|
spaceGauge.set({ space: space.space_name }, space.space_size);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const usedGauge = new Gauge({
|
||
|
|
name: 'nodejs_heap_space_size_used_bytes',
|
||
|
|
help: 'Process heap space size used from Node.js in bytes.',
|
||
|
|
labelNames: ['space'],
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
usedGauge.reset();
|
||
|
|
const spaces = v8.getHeapSpaceStatistics();
|
||
|
|
for (const space of spaces) {
|
||
|
|
usedGauge.set({ space: space.space_name }, space.space_used_size);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const availableGauge = new Gauge({
|
||
|
|
name: 'nodejs_heap_space_size_available_bytes',
|
||
|
|
help: 'Process heap space size available from Node.js in bytes.',
|
||
|
|
labelNames: ['space'],
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
availableGauge.reset();
|
||
|
|
const spaces = v8.getHeapSpaceStatistics();
|
||
|
|
for (const space of spaces) {
|
||
|
|
availableGauge.set({ space: space.space_name }, space.space_available_size);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function registerVersion(registry: Registry): void {
|
||
|
|
const versionParts = process.version.slice(1).split('.').map(Number);
|
||
|
|
const gauge = new Gauge({
|
||
|
|
name: 'nodejs_version_info',
|
||
|
|
help: 'Node.js version info.',
|
||
|
|
labelNames: ['version', 'major', 'minor', 'patch'],
|
||
|
|
registers: [registry],
|
||
|
|
collect() {
|
||
|
|
gauge.set(
|
||
|
|
{
|
||
|
|
version: process.version,
|
||
|
|
major: String(versionParts[0]),
|
||
|
|
minor: String(versionParts[1]),
|
||
|
|
patch: String(versionParts[2]),
|
||
|
|
},
|
||
|
|
1
|
||
|
|
);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function registerGc(registry: Registry): void {
|
||
|
|
const gcHistogram = new Histogram({
|
||
|
|
name: 'nodejs_gc_duration_seconds',
|
||
|
|
help: 'Garbage collection duration by kind, in seconds.',
|
||
|
|
labelNames: ['kind'],
|
||
|
|
buckets: [0.001, 0.01, 0.1, 1, 2, 5],
|
||
|
|
registers: [registry],
|
||
|
|
});
|
||
|
|
|
||
|
|
const kindLabels: Record<number, string> = {
|
||
|
|
1: 'Scavenge',
|
||
|
|
2: 'Mark/Sweep/Compact',
|
||
|
|
4: 'IncrementalMarking',
|
||
|
|
8: 'ProcessWeakCallbacks',
|
||
|
|
15: 'All',
|
||
|
|
};
|
||
|
|
|
||
|
|
try {
|
||
|
|
const obs = new PerformanceObserver((list) => {
|
||
|
|
for (const entry of list.getEntries()) {
|
||
|
|
const gcEntry = entry as any;
|
||
|
|
const kind = kindLabels[gcEntry.detail?.kind ?? gcEntry.kind] || 'Unknown';
|
||
|
|
gcHistogram.observe({ kind }, entry.duration / 1000);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
obs.observe({ entryTypes: ['gc'] });
|
||
|
|
} catch {
|
||
|
|
// GC observation not available
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
function formatValue(v: number): string {
|
||
|
|
if (Number.isInteger(v)) return String(v);
|
||
|
|
return v.toString();
|
||
|
|
}
|