BREAKING CHANGE(smartmetrics): add system-wide metrics collection, Prometheus gauges, and normalized CPU reporting
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-19 - 3.0.0 - BREAKING CHANGE(smartmetrics)
|
||||||
|
add system-wide metrics collection, Prometheus gauges, and normalized CPU reporting
|
||||||
|
|
||||||
|
- Add new sysusage plugin (ts/smartmetrics.sysusage.ts) that reads /proc/stat and /proc/meminfo (with os fallback) and returns system CPU, memory and load averages.
|
||||||
|
- Expose system-wide Prometheus gauges: smartmetrics_system_cpu_percent, smartmetrics_system_memory_used_percent, smartmetrics_system_memory_used_bytes, smartmetrics_system_load_avg_1, smartmetrics_system_load_avg_5, smartmetrics_system_load_avg_15.
|
||||||
|
- Extend IMetricsSnapshot with system fields: systemCpuPercent, systemMemTotalBytes, systemMemAvailableBytes, systemMemUsedBytes, systemMemUsedPercent, systemLoadAvg1, systemLoadAvg5, systemLoadAvg15 (this is a breaking TypeScript API change).
|
||||||
|
- Normalize per-process CPU in pidusage by adding cpuCoreCount and cpuNormalizedPercent and use cpuNormalizedPercent when aggregating CPU across the process tree.
|
||||||
|
- Export the new sysusage plugin from ts/smartmetrics.plugins.ts and wire system metrics into metric collection and Prometheus gauge updates.
|
||||||
|
|
||||||
## 2026-02-19 - 2.0.11 - fix(deps)
|
## 2026-02-19 - 2.0.11 - fix(deps)
|
||||||
bump dependencies, update build script, expand README and npm metadata
|
bump dependencies, update build script, expand README and npm metadata
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartmetrics',
|
name: '@push.rocks/smartmetrics',
|
||||||
version: '2.0.11',
|
version: '3.0.0',
|
||||||
description: 'A package for easy collection and reporting of system and process metrics.'
|
description: 'A package for easy collection and reporting of system and process metrics.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ export class SmartMetrics {
|
|||||||
private cpuPercentageGauge: plugins.prom.Gauge;
|
private cpuPercentageGauge: plugins.prom.Gauge;
|
||||||
private memoryPercentageGauge: plugins.prom.Gauge;
|
private memoryPercentageGauge: plugins.prom.Gauge;
|
||||||
private memoryUsageBytesGauge: plugins.prom.Gauge;
|
private memoryUsageBytesGauge: plugins.prom.Gauge;
|
||||||
|
private systemCpuPercentGauge: plugins.prom.Gauge;
|
||||||
|
private systemMemUsedPercentGauge: plugins.prom.Gauge;
|
||||||
|
private systemMemUsedBytesGauge: plugins.prom.Gauge;
|
||||||
|
private systemLoadAvg1Gauge: plugins.prom.Gauge;
|
||||||
|
private systemLoadAvg5Gauge: plugins.prom.Gauge;
|
||||||
|
private systemLoadAvg15Gauge: plugins.prom.Gauge;
|
||||||
|
|
||||||
// HTTP server for Prometheus endpoint
|
// HTTP server for Prometheus endpoint
|
||||||
private prometheusServer?: plugins.http.Server;
|
private prometheusServer?: plugins.http.Server;
|
||||||
@@ -39,6 +45,42 @@ export class SmartMetrics {
|
|||||||
help: 'Current memory usage in bytes',
|
help: 'Current memory usage in bytes',
|
||||||
registers: [this.registry]
|
registers: [this.registry]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.systemCpuPercentGauge = new plugins.prom.Gauge({
|
||||||
|
name: 'smartmetrics_system_cpu_percent',
|
||||||
|
help: 'System-wide CPU usage percentage',
|
||||||
|
registers: [this.registry]
|
||||||
|
});
|
||||||
|
|
||||||
|
this.systemMemUsedPercentGauge = new plugins.prom.Gauge({
|
||||||
|
name: 'smartmetrics_system_memory_used_percent',
|
||||||
|
help: 'System-wide memory usage percentage',
|
||||||
|
registers: [this.registry]
|
||||||
|
});
|
||||||
|
|
||||||
|
this.systemMemUsedBytesGauge = new plugins.prom.Gauge({
|
||||||
|
name: 'smartmetrics_system_memory_used_bytes',
|
||||||
|
help: 'System-wide memory used in bytes',
|
||||||
|
registers: [this.registry]
|
||||||
|
});
|
||||||
|
|
||||||
|
this.systemLoadAvg1Gauge = new plugins.prom.Gauge({
|
||||||
|
name: 'smartmetrics_system_load_avg_1',
|
||||||
|
help: 'System 1-minute load average',
|
||||||
|
registers: [this.registry]
|
||||||
|
});
|
||||||
|
|
||||||
|
this.systemLoadAvg5Gauge = new plugins.prom.Gauge({
|
||||||
|
name: 'smartmetrics_system_load_avg_5',
|
||||||
|
help: 'System 5-minute load average',
|
||||||
|
registers: [this.registry]
|
||||||
|
});
|
||||||
|
|
||||||
|
this.systemLoadAvg15Gauge = new plugins.prom.Gauge({
|
||||||
|
name: 'smartmetrics_system_load_avg_15',
|
||||||
|
help: 'System 15-minute load average',
|
||||||
|
registers: [this.registry]
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(loggerArg: plugins.smartlog.Smartlog, sourceNameArg: string) {
|
constructor(loggerArg: plugins.smartlog.Smartlog, sourceNameArg: string) {
|
||||||
@@ -115,17 +157,18 @@ export class SmartMetrics {
|
|||||||
}
|
}
|
||||||
const stats = await plugins.pidusage.getPidUsage([process.pid, ...pids]);
|
const stats = await plugins.pidusage.getPidUsage([process.pid, ...pids]);
|
||||||
|
|
||||||
|
// Aggregate normalized CPU (0-100% of total machine) across process tree
|
||||||
let cpuPercentage = 0;
|
let cpuPercentage = 0;
|
||||||
for (const stat of Object.keys(stats)) {
|
for (const stat of Object.values(stats)) {
|
||||||
if (!stats[stat as any]) continue;
|
if (!stat) continue;
|
||||||
cpuPercentage += stats[stat as any].cpu;
|
cpuPercentage += stat.cpuNormalizedPercent;
|
||||||
}
|
}
|
||||||
let cpuUsageText = `${Math.round(cpuPercentage * 100) / 100} %`;
|
let cpuUsageText = `${Math.round(cpuPercentage * 100) / 100} %`;
|
||||||
|
|
||||||
let memoryUsageBytes = 0;
|
let memoryUsageBytes = 0;
|
||||||
for (const stat of Object.keys(stats)) {
|
for (const stat of Object.values(stats)) {
|
||||||
if (!stats[stat as any]) continue;
|
if (!stat) continue;
|
||||||
memoryUsageBytes += stats[stat as any].memory;
|
memoryUsageBytes += stat.memory;
|
||||||
}
|
}
|
||||||
|
|
||||||
let memoryPercentage =
|
let memoryPercentage =
|
||||||
@@ -134,6 +177,9 @@ export class SmartMetrics {
|
|||||||
memoryUsageBytes
|
memoryUsageBytes
|
||||||
)} / ${this.formatBytes(this.maxMemoryMB * 1024 * 1024)}`;
|
)} / ${this.formatBytes(this.maxMemoryMB * 1024 * 1024)}`;
|
||||||
|
|
||||||
|
// Get system-wide metrics
|
||||||
|
const systemUsage = await plugins.sysusage.getSystemUsage();
|
||||||
|
|
||||||
// Update Prometheus gauges with current values
|
// Update Prometheus gauges with current values
|
||||||
if (this.cpuPercentageGauge) {
|
if (this.cpuPercentageGauge) {
|
||||||
this.cpuPercentageGauge.set(cpuPercentage);
|
this.cpuPercentageGauge.set(cpuPercentage);
|
||||||
@@ -144,16 +190,32 @@ export class SmartMetrics {
|
|||||||
if (this.memoryUsageBytesGauge) {
|
if (this.memoryUsageBytesGauge) {
|
||||||
this.memoryUsageBytesGauge.set(memoryUsageBytes);
|
this.memoryUsageBytesGauge.set(memoryUsageBytes);
|
||||||
}
|
}
|
||||||
|
if (this.systemCpuPercentGauge) {
|
||||||
|
this.systemCpuPercentGauge.set(systemUsage.cpuPercent);
|
||||||
|
}
|
||||||
|
if (this.systemMemUsedPercentGauge) {
|
||||||
|
this.systemMemUsedPercentGauge.set(systemUsage.memUsedPercent);
|
||||||
|
}
|
||||||
|
if (this.systemMemUsedBytesGauge) {
|
||||||
|
this.systemMemUsedBytesGauge.set(systemUsage.memUsedBytes);
|
||||||
|
}
|
||||||
|
if (this.systemLoadAvg1Gauge) {
|
||||||
|
this.systemLoadAvg1Gauge.set(systemUsage.loadAvg1);
|
||||||
|
}
|
||||||
|
if (this.systemLoadAvg5Gauge) {
|
||||||
|
this.systemLoadAvg5Gauge.set(systemUsage.loadAvg5);
|
||||||
|
}
|
||||||
|
if (this.systemLoadAvg15Gauge) {
|
||||||
|
this.systemLoadAvg15Gauge.set(systemUsage.loadAvg15);
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate Node.js metrics directly
|
// Calculate Node.js metrics directly
|
||||||
const cpuUsage = process.cpuUsage();
|
const cpuUsage = process.cpuUsage();
|
||||||
const process_cpu_seconds_total = (cpuUsage.user + cpuUsage.system) / 1000000; // Convert from microseconds to seconds
|
const process_cpu_seconds_total = (cpuUsage.user + cpuUsage.system) / 1000000;
|
||||||
|
|
||||||
const heapStats = plugins.v8.getHeapStatistics();
|
const heapStats = plugins.v8.getHeapStatistics();
|
||||||
const nodejs_heap_size_total_bytes = heapStats.total_heap_size;
|
const nodejs_heap_size_total_bytes = heapStats.total_heap_size;
|
||||||
|
|
||||||
// Note: Active handles and requests are internal Node.js metrics that require deprecated APIs
|
|
||||||
// We return 0 here, but the Prometheus default collectors will track the real values
|
|
||||||
const nodejs_active_handles_total = 0;
|
const nodejs_active_handles_total = 0;
|
||||||
const nodejs_active_requests_total = 0;
|
const nodejs_active_requests_total = 0;
|
||||||
|
|
||||||
@@ -167,6 +229,14 @@ export class SmartMetrics {
|
|||||||
memoryPercentage,
|
memoryPercentage,
|
||||||
memoryUsageBytes,
|
memoryUsageBytes,
|
||||||
memoryUsageText,
|
memoryUsageText,
|
||||||
|
systemCpuPercent: systemUsage.cpuPercent,
|
||||||
|
systemMemTotalBytes: systemUsage.memTotalBytes,
|
||||||
|
systemMemAvailableBytes: systemUsage.memAvailableBytes,
|
||||||
|
systemMemUsedBytes: systemUsage.memUsedBytes,
|
||||||
|
systemMemUsedPercent: systemUsage.memUsedPercent,
|
||||||
|
systemLoadAvg1: systemUsage.loadAvg1,
|
||||||
|
systemLoadAvg5: systemUsage.loadAvg5,
|
||||||
|
systemLoadAvg15: systemUsage.loadAvg15,
|
||||||
};
|
};
|
||||||
return returnMetrics;
|
return returnMetrics;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
export interface IMetricsSnapshot {
|
export interface IMetricsSnapshot {
|
||||||
|
// existing process/node fields
|
||||||
process_cpu_seconds_total: number;
|
process_cpu_seconds_total: number;
|
||||||
nodejs_active_handles_total: number;
|
nodejs_active_handles_total: number;
|
||||||
nodejs_active_requests_total: number;
|
nodejs_active_requests_total: number;
|
||||||
nodejs_heap_size_total_bytes: number;
|
nodejs_heap_size_total_bytes: number;
|
||||||
cpuPercentage: number;
|
cpuPercentage: number; // normalized to 0-100% of total machine
|
||||||
cpuUsageText: string;
|
cpuUsageText: string;
|
||||||
memoryPercentage: number;
|
memoryPercentage: number;
|
||||||
memoryUsageBytes: number;
|
memoryUsageBytes: number;
|
||||||
memoryUsageText: string;
|
memoryUsageText: string;
|
||||||
|
// system-wide fields
|
||||||
|
systemCpuPercent: number;
|
||||||
|
systemMemTotalBytes: number;
|
||||||
|
systemMemAvailableBytes: number;
|
||||||
|
systemMemUsedBytes: number;
|
||||||
|
systemMemUsedPercent: number;
|
||||||
|
systemLoadAvg1: number;
|
||||||
|
systemLoadAvg5: number;
|
||||||
|
systemLoadAvg15: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
// CPU core count (cached at module load)
|
||||||
|
const cpuCoreCount = typeof os.availableParallelism === 'function'
|
||||||
|
? os.availableParallelism()
|
||||||
|
: os.cpus().length;
|
||||||
|
|
||||||
// Cached system constants
|
// Cached system constants
|
||||||
let clkTck: number | null = null;
|
let clkTck: number | null = null;
|
||||||
let pageSize: number | null = null;
|
let pageSize: number | null = null;
|
||||||
@@ -61,8 +67,10 @@ function hrtimeSeconds(): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IPidUsageResult {
|
export interface IPidUsageResult {
|
||||||
cpu: number;
|
cpu: number; // raw per-core CPU% (can exceed 100%)
|
||||||
memory: number;
|
cpuCoreCount: number; // number of CPU cores on the machine
|
||||||
|
cpuNormalizedPercent: number; // cpu / coreCount — 0-100% of total machine
|
||||||
|
memory: number; // RSS in bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,12 +103,16 @@ export async function getPidUsage(
|
|||||||
|
|
||||||
result[pid] = {
|
result[pid] = {
|
||||||
cpu: cpuPercent,
|
cpu: cpuPercent,
|
||||||
|
cpuCoreCount,
|
||||||
|
cpuNormalizedPercent: cpuPercent / cpuCoreCount,
|
||||||
memory: memoryBytes,
|
memory: memoryBytes,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// First call for this PID — no delta available, report 0% cpu
|
// First call for this PID — no delta available, report 0% cpu
|
||||||
result[pid] = {
|
result[pid] = {
|
||||||
cpu: 0,
|
cpu: 0,
|
||||||
|
cpuCoreCount,
|
||||||
|
cpuNormalizedPercent: 0,
|
||||||
memory: memoryBytes,
|
memory: memoryBytes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,5 +16,6 @@ export { smartdelay, smartlog };
|
|||||||
import * as pidtree from './smartmetrics.pidtree.js';
|
import * as pidtree from './smartmetrics.pidtree.js';
|
||||||
import * as pidusage from './smartmetrics.pidusage.js';
|
import * as pidusage from './smartmetrics.pidusage.js';
|
||||||
import * as prom from './smartmetrics.prom.js';
|
import * as prom from './smartmetrics.prom.js';
|
||||||
|
import * as sysusage from './smartmetrics.sysusage.js';
|
||||||
|
|
||||||
export { pidtree, pidusage, prom };
|
export { pidtree, pidusage, prom, sysusage };
|
||||||
|
|||||||
94
ts/smartmetrics.sysusage.ts
Normal file
94
ts/smartmetrics.sysusage.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
|
||||||
|
export interface ISystemUsageResult {
|
||||||
|
cpuPercent: number; // 0-100% system-wide CPU utilization
|
||||||
|
memTotalBytes: number; // total physical RAM
|
||||||
|
memAvailableBytes: number; // available memory
|
||||||
|
memUsedBytes: number; // memTotal - memAvailable
|
||||||
|
memUsedPercent: number; // 0-100%
|
||||||
|
loadAvg1: number; // 1-min load average
|
||||||
|
loadAvg5: number; // 5-min load average
|
||||||
|
loadAvg15: number; // 15-min load average
|
||||||
|
}
|
||||||
|
|
||||||
|
// History for system CPU delta tracking
|
||||||
|
interface ICpuSnapshot {
|
||||||
|
idle: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let prevCpuSnapshot: ICpuSnapshot | null = null;
|
||||||
|
|
||||||
|
function readProcStat(): ICpuSnapshot | null {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync('/proc/stat', 'utf8');
|
||||||
|
const firstLine = content.split('\n')[0]; // "cpu user nice system idle iowait irq softirq steal ..."
|
||||||
|
const parts = firstLine.split(/\s+/).slice(1).map(Number);
|
||||||
|
// parts: [user, nice, system, idle, iowait, irq, softirq, steal, ...]
|
||||||
|
const idle = parts[3] + (parts[4] || 0); // idle + iowait
|
||||||
|
const total = parts.reduce((sum, v) => sum + v, 0);
|
||||||
|
return { idle, total };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMemoryInfo(): { totalBytes: number; availableBytes: number } {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync('/proc/meminfo', 'utf8');
|
||||||
|
let memTotal = 0;
|
||||||
|
let memAvailable = 0;
|
||||||
|
for (const line of content.split('\n')) {
|
||||||
|
if (line.startsWith('MemTotal:')) {
|
||||||
|
memTotal = parseInt(line.split(/\s+/)[1], 10) * 1024; // kB to bytes
|
||||||
|
} else if (line.startsWith('MemAvailable:')) {
|
||||||
|
memAvailable = parseInt(line.split(/\s+/)[1], 10) * 1024;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (memTotal > 0 && memAvailable > 0) {
|
||||||
|
return { totalBytes: memTotal, availableBytes: memAvailable };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fall through to os fallback
|
||||||
|
}
|
||||||
|
// Fallback using os module
|
||||||
|
const totalBytes = os.totalmem();
|
||||||
|
const availableBytes = os.freemem();
|
||||||
|
return { totalBytes, availableBytes };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSystemUsage(): Promise<ISystemUsageResult> {
|
||||||
|
// CPU
|
||||||
|
let cpuPercent = 0;
|
||||||
|
const currentSnapshot = readProcStat();
|
||||||
|
if (currentSnapshot && prevCpuSnapshot) {
|
||||||
|
const totalDelta = currentSnapshot.total - prevCpuSnapshot.total;
|
||||||
|
const idleDelta = currentSnapshot.idle - prevCpuSnapshot.idle;
|
||||||
|
if (totalDelta > 0) {
|
||||||
|
cpuPercent = ((totalDelta - idleDelta) / totalDelta) * 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentSnapshot) {
|
||||||
|
prevCpuSnapshot = currentSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory
|
||||||
|
const mem = getMemoryInfo();
|
||||||
|
const memUsedBytes = mem.totalBytes - mem.availableBytes;
|
||||||
|
const memUsedPercent = mem.totalBytes > 0 ? (memUsedBytes / mem.totalBytes) * 100 : 0;
|
||||||
|
|
||||||
|
// Load averages
|
||||||
|
const [loadAvg1, loadAvg5, loadAvg15] = os.loadavg();
|
||||||
|
|
||||||
|
return {
|
||||||
|
cpuPercent,
|
||||||
|
memTotalBytes: mem.totalBytes,
|
||||||
|
memAvailableBytes: mem.availableBytes,
|
||||||
|
memUsedBytes,
|
||||||
|
memUsedPercent,
|
||||||
|
loadAvg1,
|
||||||
|
loadAvg5,
|
||||||
|
loadAvg15,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user