This commit is contained in:
2026-02-19 09:46:46 +00:00
parent 767a40e01e
commit 7c3197455f
5 changed files with 36 additions and 29 deletions

View File

@@ -17,7 +17,8 @@
"@git.zone/tsbundle": "^2.8.3", "@git.zone/tsbundle": "^2.8.3",
"@git.zone/tsrun": "^2.0.1", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.8", "@git.zone/tstest": "^3.1.8",
"@types/node": "^25.3.0" "@types/node": "^25.3.0",
"@types/pidusage": "^2.0.2"
}, },
"browserslist": [ "browserslist": [
"last 1 chrome versions" "last 1 chrome versions"
@@ -37,7 +38,6 @@
"dependencies": { "dependencies": {
"@push.rocks/smartdelay": "^3.0.5", "@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartlog": "^3.1.11", "@push.rocks/smartlog": "^3.1.11",
"@types/pidusage": "^2.0.2",
"pidtree": "^0.6.0", "pidtree": "^0.6.0",
"pidusage": "^4.0.1", "pidusage": "^4.0.1",
"prom-client": "^15.1.3" "prom-client": "^15.1.3"

6
pnpm-lock.yaml generated
View File

@@ -14,9 +14,6 @@ importers:
'@push.rocks/smartlog': '@push.rocks/smartlog':
specifier: ^3.1.11 specifier: ^3.1.11
version: 3.1.11 version: 3.1.11
'@types/pidusage':
specifier: ^2.0.2
version: 2.0.5
pidtree: pidtree:
specifier: ^0.6.0 specifier: ^0.6.0
version: 0.6.0 version: 0.6.0
@@ -42,6 +39,9 @@ importers:
'@types/node': '@types/node':
specifier: ^25.3.0 specifier: ^25.3.0
version: 25.3.0 version: 25.3.0
'@types/pidusage':
specifier: ^2.0.2
version: 2.0.5
packages: packages:

View File

@@ -71,7 +71,7 @@ SmartMetrics doesn't just monitor your main process it automatically discove
The library automatically detects available memory whether running on bare metal, in Docker containers, or with Node.js heap restrictions. It picks the most restrictive of: The library automatically detects available memory whether running on bare metal, in Docker containers, or with Node.js heap restrictions. It picks the most restrictive of:
1. **System total memory** (`os.totalmem()`) 1. **System total memory** (`os.totalmem()`)
2. **Docker cgroup limit** (`/sys/fs/cgroup/memory/memory.limit_in_bytes`) 2. **Docker cgroup limit** supports both cgroup v2 (`/sys/fs/cgroup/memory.max`) and cgroup v1 (`/sys/fs/cgroup/memory/memory.limit_in_bytes`)
3. **V8 heap size limit** (`v8.getHeapStatistics().heap_size_limit`) 3. **V8 heap size limit** (`v8.getHeapStatistics().heap_size_limit`)
This ensures accurate percentage calculations regardless of environment. This ensures accurate percentage calculations regardless of environment.
@@ -101,8 +101,8 @@ Retrieves current system metrics as a structured object.
```typescript ```typescript
{ {
process_cpu_seconds_total: number; // Total CPU time in seconds process_cpu_seconds_total: number; // Total CPU time in seconds
nodejs_active_handles_total: number; // Active handles count nodejs_active_handles_total: number; // Always 0 (deprecated Node.js API; real values tracked by Prometheus default collectors)
nodejs_active_requests_total: number; // Active requests count nodejs_active_requests_total: number; // Always 0 (deprecated Node.js API; real values tracked by Prometheus default collectors)
nodejs_heap_size_total_bytes: number; // V8 heap size in bytes nodejs_heap_size_total_bytes: number; // V8 heap size in bytes
cpuPercentage: number; // Aggregated CPU usage across all child processes cpuPercentage: number; // Aggregated CPU usage across all child processes
cpuUsageText: string; // Human-readable CPU usage (e.g. "12.5 %") cpuUsageText: string; // Human-readable CPU usage (e.g. "12.5 %")

View File

@@ -75,4 +75,4 @@ tap.test('should disable Prometheus endpoint', async () => {
} }
}); });
tap.start(); export default tap.start();

View File

@@ -17,7 +17,7 @@ export class SmartMetrics {
private prometheusServer?: plugins.http.Server; private prometheusServer?: plugins.http.Server;
private prometheusPort?: number; private prometheusPort?: number;
public async setup() { public setup() {
const collectDefaultMetrics = plugins.promClient.collectDefaultMetrics; const collectDefaultMetrics = plugins.promClient.collectDefaultMetrics;
this.registry = new plugins.promClient.Registry(); this.registry = new plugins.promClient.Registry();
collectDefaultMetrics({ register: this.registry }); collectDefaultMetrics({ register: this.registry });
@@ -50,28 +50,32 @@ export class SmartMetrics {
} }
private checkMemoryLimits() { private checkMemoryLimits() {
let heapStats = plugins.v8.getHeapStatistics(); const heapStats = plugins.v8.getHeapStatistics();
let maxHeapSizeMB = heapStats.heap_size_limit / 1024 / 1024; const maxHeapSizeMB = heapStats.heap_size_limit / 1024 / 1024;
let totalSystemMemoryMB = plugins.os.totalmem() / 1024 / 1024; const totalSystemMemoryMB = plugins.os.totalmem() / 1024 / 1024;
let dockerMemoryLimitMB = totalSystemMemoryMB; let dockerMemoryLimitMB = totalSystemMemoryMB;
// Try cgroup v2 first, then fall back to cgroup v1
try { try {
let dockerMemoryLimitBytes = plugins.fs.readFileSync( const cgroupV2 = plugins.fs.readFileSync('/sys/fs/cgroup/memory.max', 'utf8').trim();
'/sys/fs/cgroup/memory/memory.limit_in_bytes', if (cgroupV2 !== 'max') {
'utf8' dockerMemoryLimitMB = parseInt(cgroupV2, 10) / 1024 / 1024;
); }
dockerMemoryLimitMB = parseInt(dockerMemoryLimitBytes, 10) / 1024 / 1024; } catch {
} catch (error) { try {
// Ignore - this will fail if not running in a Docker container const cgroupV1 = plugins.fs.readFileSync(
'/sys/fs/cgroup/memory/memory.limit_in_bytes',
'utf8'
).trim();
dockerMemoryLimitMB = parseInt(cgroupV1, 10) / 1024 / 1024;
} catch {
// Not running in a container — use system memory
}
} }
// Set the maximum memory to the lower value between the Docker limit and the total system memory // Pick the most restrictive limit
this.maxMemoryMB = Math.min(totalSystemMemoryMB, dockerMemoryLimitMB, maxHeapSizeMB); this.maxMemoryMB = Math.min(totalSystemMemoryMB, dockerMemoryLimitMB, maxHeapSizeMB);
// If the maximum old space size limit is greater than the maximum available memory, throw an error
if (maxHeapSizeMB > this.maxMemoryMB) {
throw new Error('Node.js process can use more memory than is available');
}
} }
public start() { public start() {
@@ -104,7 +108,12 @@ export class SmartMetrics {
} }
public async getMetrics() { public async getMetrics() {
const pids = await plugins.pidtree(process.pid); let pids: number[] = [];
try {
pids = await plugins.pidtree(process.pid);
} catch {
// pidtree can fail if process tree cannot be read
}
const stats = await plugins.pidusage([process.pid, ...pids]); const stats = await plugins.pidusage([process.pid, ...pids]);
let cpuPercentage = 0; let cpuPercentage = 0;
@@ -126,8 +135,6 @@ export class SmartMetrics {
memoryUsageBytes memoryUsageBytes
)} / ${this.formatBytes(this.maxMemoryMB * 1024 * 1024)}`; )} / ${this.formatBytes(this.maxMemoryMB * 1024 * 1024)}`;
console.log(`${cpuUsageText} ||| ${memoryUsageText} `);
// 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);