fix(prometheus): clean up default Prometheus metric collectors on stop

This commit is contained in:
2026-03-23 14:51:14 +00:00
parent ed05c01abc
commit a01e21663f
4 changed files with 39 additions and 8 deletions

View File

@@ -1,5 +1,11 @@
# Changelog
## 2026-03-23 - 3.0.3 - fix(prometheus)
clean up default Prometheus metric collectors on stop
- Return cleanup handlers from default metric registration for event loop and GC observers.
- Dispose registered Prometheus default metric collectors when stopping Smartmetrics to prevent lingering observers.
## 2026-03-02 - 3.0.2 - fix(pidusage)
prune history entries for PIDs not present in the requested set to avoid stale data and memory growth

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartmetrics',
version: '3.0.2',
version: '3.0.3',
description: 'A package for easy collection and reporting of system and process metrics.'
}

View File

@@ -22,10 +22,11 @@ export class SmartMetrics {
// HTTP server for Prometheus endpoint
private prometheusServer?: plugins.http.Server;
private prometheusPort?: number;
private cleanupDefaultMetrics?: () => void;
public setup() {
this.registry = new plugins.prom.Registry();
plugins.prom.collectDefaultMetrics(this.registry);
this.cleanupDefaultMetrics = plugins.prom.collectDefaultMetrics(this.registry);
// Initialize custom gauges
this.cpuPercentageGauge = new plugins.prom.Gauge({
@@ -295,5 +296,9 @@ export class SmartMetrics {
public stop() {
this.started = false;
this.disablePrometheusEndpoint();
if (this.cleanupDefaultMetrics) {
this.cleanupDefaultMetrics();
this.cleanupDefaultMetrics = undefined;
}
}
}

View File

@@ -288,20 +288,25 @@ export class Histogram implements IMetric {
// ── Default Metrics Collectors ──────────────────────────────────────────────
export function collectDefaultMetrics(registry: Registry): void {
export function collectDefaultMetrics(registry: Registry): () => void {
registerProcessCpuTotal(registry);
registerProcessStartTime(registry);
registerProcessMemory(registry);
registerProcessOpenFds(registry);
registerProcessMaxFds(registry);
registerEventLoopLag(registry);
const cleanupEventLoop = registerEventLoopLag(registry);
registerProcessHandles(registry);
registerProcessRequests(registry);
registerProcessResources(registry);
registerHeapSizeAndUsed(registry);
registerHeapSpaces(registry);
registerVersion(registry);
registerGc(registry);
const cleanupGc = registerGc(registry);
return () => {
cleanupEventLoop();
cleanupGc();
};
}
function registerProcessCpuTotal(registry: Registry): void {
@@ -420,7 +425,7 @@ function registerProcessMaxFds(registry: Registry): void {
});
}
function registerEventLoopLag(registry: Registry): void {
function registerEventLoopLag(registry: Registry): () => void {
let histogram: ReturnType<typeof monitorEventLoopDelay> | null = null;
try {
histogram = monitorEventLoopDelay({ resolution: 10 });
@@ -496,6 +501,13 @@ function registerEventLoopLag(registry: Registry): void {
},
});
}
return () => {
if (histogram) {
histogram.disable();
histogram = null;
}
};
}
function registerProcessHandles(registry: Registry): void {
@@ -632,7 +644,7 @@ function registerVersion(registry: Registry): void {
});
}
function registerGc(registry: Registry): void {
function registerGc(registry: Registry): () => void {
const gcHistogram = new Histogram({
name: 'nodejs_gc_duration_seconds',
help: 'Garbage collection duration by kind, in seconds.',
@@ -649,8 +661,9 @@ function registerGc(registry: Registry): void {
15: 'All',
};
let obs: PerformanceObserver | null = null;
try {
const obs = new PerformanceObserver((list) => {
obs = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const gcEntry = entry as any;
const kind = kindLabels[gcEntry.detail?.kind ?? gcEntry.kind] || 'Unknown';
@@ -661,6 +674,13 @@ function registerGc(registry: Registry): void {
} catch {
// GC observation not available
}
return () => {
if (obs) {
obs.disconnect();
obs = null;
}
};
}
// ── Helpers ─────────────────────────────────────────────────────────────────