Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed05c01abc | |||
| 8fd8b9a915 | |||
| 56757e1c71 | |||
| d2ee396cf7 | |||
| 5a22b87592 | |||
| b99d2cc04b | |||
| 32d75804c0 | |||
| 7c3197455f |
23
changelog.md
23
changelog.md
@@ -1,5 +1,28 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
- Deletes entries from the history map when a PID is not included in the current pids array
|
||||||
|
- Prevents accumulation of stale PID histories and potential memory growth
|
||||||
|
- Change implemented in ts/smartmetrics.pidusage.ts alongside the metrics result construction
|
||||||
|
|
||||||
|
## 2026-02-19 - 3.0.1 - fix(smartmetrics)
|
||||||
|
no code changes detected; no version bump or release required
|
||||||
|
|
||||||
|
- git diff contained no modifications
|
||||||
|
- current package.json version is 3.0.0
|
||||||
|
- no dependency or file changes to warrant a release
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartmetrics",
|
"name": "@push.rocks/smartmetrics",
|
||||||
"version": "2.0.11",
|
"version": "3.0.2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"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.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@@ -36,11 +36,7 @@
|
|||||||
],
|
],
|
||||||
"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",
|
|
||||||
"pidusage": "^4.0.1",
|
|
||||||
"prom-client": "^15.1.3"
|
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
59
pnpm-lock.yaml
generated
59
pnpm-lock.yaml
generated
@@ -14,18 +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:
|
|
||||||
specifier: ^0.6.0
|
|
||||||
version: 0.6.0
|
|
||||||
pidusage:
|
|
||||||
specifier: ^4.0.1
|
|
||||||
version: 4.0.1
|
|
||||||
prom-client:
|
|
||||||
specifier: ^15.1.3
|
|
||||||
version: 15.1.3
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@git.zone/tsbuild':
|
'@git.zone/tsbuild':
|
||||||
specifier: ^4.1.2
|
specifier: ^4.1.2
|
||||||
@@ -693,10 +681,6 @@ packages:
|
|||||||
'@napi-rs/wasm-runtime@1.1.1':
|
'@napi-rs/wasm-runtime@1.1.1':
|
||||||
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
|
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
|
||||||
|
|
||||||
'@opentelemetry/api@1.9.0':
|
|
||||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
|
||||||
engines: {node: '>=8.0.0'}
|
|
||||||
|
|
||||||
'@oxc-project/types@0.99.0':
|
'@oxc-project/types@0.99.0':
|
||||||
resolution: {integrity: sha512-LLDEhXB7g1m5J+woRSgfKsFPS3LhR9xRhTeIoEBm5WrkwMxn6eZ0Ld0c0K5eHB57ChZX6I3uSmmLjZ8pcjlRcw==}
|
resolution: {integrity: sha512-LLDEhXB7g1m5J+woRSgfKsFPS3LhR9xRhTeIoEBm5WrkwMxn6eZ0Ld0c0K5eHB57ChZX6I3uSmmLjZ8pcjlRcw==}
|
||||||
|
|
||||||
@@ -1871,9 +1855,6 @@ packages:
|
|||||||
'@types/node@25.3.0':
|
'@types/node@25.3.0':
|
||||||
resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==}
|
resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==}
|
||||||
|
|
||||||
'@types/pidusage@2.0.5':
|
|
||||||
resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==}
|
|
||||||
|
|
||||||
'@types/ping@0.4.4':
|
'@types/ping@0.4.4':
|
||||||
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
|
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
|
||||||
|
|
||||||
@@ -2077,9 +2058,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==}
|
resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
bintrees@1.0.2:
|
|
||||||
resolution: {integrity: sha1-SfiW1uhYpKSZ34XDj7OZua/4QPg=}
|
|
||||||
|
|
||||||
body-parser@1.20.3:
|
body-parser@1.20.3:
|
||||||
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
|
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
|
||||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||||
@@ -3519,15 +3497,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
pidtree@0.6.0:
|
|
||||||
resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
|
|
||||||
engines: {node: '>=0.10'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
pidusage@4.0.1:
|
|
||||||
resolution: {integrity: sha512-yCH2dtLHfEBnzlHUJymR/Z1nN2ePG3m392Mv8TFlTP1B0xkpMQNHAnfkY0n2tAi6ceKO6YWhxYfZ96V4vVkh/g==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
ping@0.4.4:
|
ping@0.4.4:
|
||||||
resolution: {integrity: sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==}
|
resolution: {integrity: sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
@@ -3552,10 +3521,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
|
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
prom-client@15.1.3:
|
|
||||||
resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==}
|
|
||||||
engines: {node: ^16 || ^18 || >=20}
|
|
||||||
|
|
||||||
property-information@7.1.0:
|
property-information@7.1.0:
|
||||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||||
|
|
||||||
@@ -3917,9 +3882,6 @@ packages:
|
|||||||
tar-stream@3.1.7:
|
tar-stream@3.1.7:
|
||||||
resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
|
resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
|
||||||
|
|
||||||
tdigest@0.1.2:
|
|
||||||
resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
|
|
||||||
|
|
||||||
text-decoder@1.2.3:
|
text-decoder@1.2.3:
|
||||||
resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==}
|
resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==}
|
||||||
|
|
||||||
@@ -5743,8 +5705,6 @@ snapshots:
|
|||||||
'@tybys/wasm-util': 0.10.1
|
'@tybys/wasm-util': 0.10.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@opentelemetry/api@1.9.0': {}
|
|
||||||
|
|
||||||
'@oxc-project/types@0.99.0': {}
|
'@oxc-project/types@0.99.0': {}
|
||||||
|
|
||||||
'@pdf-lib/standard-fonts@1.0.0':
|
'@pdf-lib/standard-fonts@1.0.0':
|
||||||
@@ -7762,8 +7722,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.18.2
|
undici-types: 7.18.2
|
||||||
|
|
||||||
'@types/pidusage@2.0.5': {}
|
|
||||||
|
|
||||||
'@types/ping@0.4.4': {}
|
'@types/ping@0.4.4': {}
|
||||||
|
|
||||||
'@types/qs@6.14.0': {}
|
'@types/qs@6.14.0': {}
|
||||||
@@ -7958,8 +7916,6 @@ snapshots:
|
|||||||
|
|
||||||
basic-ftp@5.0.5: {}
|
basic-ftp@5.0.5: {}
|
||||||
|
|
||||||
bintrees@1.0.2: {}
|
|
||||||
|
|
||||||
body-parser@1.20.3:
|
body-parser@1.20.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
@@ -9663,12 +9619,6 @@ snapshots:
|
|||||||
|
|
||||||
picomatch@4.0.3: {}
|
picomatch@4.0.3: {}
|
||||||
|
|
||||||
pidtree@0.6.0: {}
|
|
||||||
|
|
||||||
pidusage@4.0.1:
|
|
||||||
dependencies:
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
ping@0.4.4: {}
|
ping@0.4.4: {}
|
||||||
|
|
||||||
pkg-dir@4.2.0:
|
pkg-dir@4.2.0:
|
||||||
@@ -9689,11 +9639,6 @@ snapshots:
|
|||||||
|
|
||||||
progress@2.0.3: {}
|
progress@2.0.3: {}
|
||||||
|
|
||||||
prom-client@15.1.3:
|
|
||||||
dependencies:
|
|
||||||
'@opentelemetry/api': 1.9.0
|
|
||||||
tdigest: 0.1.2
|
|
||||||
|
|
||||||
property-information@7.1.0: {}
|
property-information@7.1.0: {}
|
||||||
|
|
||||||
proto-list@1.2.4: {}
|
proto-list@1.2.4: {}
|
||||||
@@ -10200,10 +10145,6 @@ snapshots:
|
|||||||
fast-fifo: 1.3.2
|
fast-fifo: 1.3.2
|
||||||
streamx: 2.22.1
|
streamx: 2.22.1
|
||||||
|
|
||||||
tdigest@0.1.2:
|
|
||||||
dependencies:
|
|
||||||
bintrees: 1.0.2
|
|
||||||
|
|
||||||
text-decoder@1.2.3:
|
text-decoder@1.2.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
b4a: 1.6.7
|
b4a: 1.6.7
|
||||||
|
|||||||
@@ -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 %")
|
||||||
|
|||||||
@@ -75,4 +75,4 @@ tap.test('should disable Prometheus endpoint', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
||||||
|
|||||||
@@ -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.2',
|
||||||
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.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,41 +5,82 @@ export class SmartMetrics {
|
|||||||
public started = false;
|
public started = false;
|
||||||
public sourceNameArg: string;
|
public sourceNameArg: string;
|
||||||
public logger: plugins.smartlog.Smartlog;
|
public logger: plugins.smartlog.Smartlog;
|
||||||
public registry: plugins.promClient.Registry;
|
public registry: plugins.prom.Registry;
|
||||||
public maxMemoryMB: number;
|
public maxMemoryMB: number;
|
||||||
|
|
||||||
// Prometheus gauges for custom metrics
|
// Prometheus gauges for custom metrics
|
||||||
private cpuPercentageGauge: plugins.promClient.Gauge<string>;
|
private cpuPercentageGauge: plugins.prom.Gauge;
|
||||||
private memoryPercentageGauge: plugins.promClient.Gauge<string>;
|
private memoryPercentageGauge: plugins.prom.Gauge;
|
||||||
private memoryUsageBytesGauge: plugins.promClient.Gauge<string>;
|
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;
|
||||||
private prometheusPort?: number;
|
private prometheusPort?: number;
|
||||||
|
|
||||||
public async setup() {
|
public setup() {
|
||||||
const collectDefaultMetrics = plugins.promClient.collectDefaultMetrics;
|
this.registry = new plugins.prom.Registry();
|
||||||
this.registry = new plugins.promClient.Registry();
|
plugins.prom.collectDefaultMetrics(this.registry);
|
||||||
collectDefaultMetrics({ register: this.registry });
|
|
||||||
|
|
||||||
// Initialize custom gauges
|
// Initialize custom gauges
|
||||||
this.cpuPercentageGauge = new plugins.promClient.Gauge({
|
this.cpuPercentageGauge = new plugins.prom.Gauge({
|
||||||
name: 'smartmetrics_cpu_percentage',
|
name: 'smartmetrics_cpu_percentage',
|
||||||
help: 'Current CPU usage percentage',
|
help: 'Current CPU usage percentage',
|
||||||
registers: [this.registry]
|
registers: [this.registry]
|
||||||
});
|
});
|
||||||
|
|
||||||
this.memoryPercentageGauge = new plugins.promClient.Gauge({
|
this.memoryPercentageGauge = new plugins.prom.Gauge({
|
||||||
name: 'smartmetrics_memory_percentage',
|
name: 'smartmetrics_memory_percentage',
|
||||||
help: 'Current memory usage percentage',
|
help: 'Current memory usage percentage',
|
||||||
registers: [this.registry]
|
registers: [this.registry]
|
||||||
});
|
});
|
||||||
|
|
||||||
this.memoryUsageBytesGauge = new plugins.promClient.Gauge({
|
this.memoryUsageBytesGauge = new plugins.prom.Gauge({
|
||||||
name: 'smartmetrics_memory_usage_bytes',
|
name: 'smartmetrics_memory_usage_bytes',
|
||||||
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) {
|
||||||
@@ -50,28 +91,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,20 +149,26 @@ export class SmartMetrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getMetrics() {
|
public async getMetrics() {
|
||||||
const pids = await plugins.pidtree(process.pid);
|
let pids: number[] = [];
|
||||||
const stats = await plugins.pidusage([process.pid, ...pids]);
|
try {
|
||||||
|
pids = await plugins.pidtree.getChildPids(process.pid);
|
||||||
|
} catch {
|
||||||
|
// pidtree can fail if process tree cannot be read
|
||||||
|
}
|
||||||
|
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]) continue;
|
if (!stat) continue;
|
||||||
cpuPercentage += stats[stat].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]) continue;
|
if (!stat) continue;
|
||||||
memoryUsageBytes += stats[stat].memory;
|
memoryUsageBytes += stat.memory;
|
||||||
}
|
}
|
||||||
|
|
||||||
let memoryPercentage =
|
let memoryPercentage =
|
||||||
@@ -126,8 +177,9 @@ export class SmartMetrics {
|
|||||||
memoryUsageBytes
|
memoryUsageBytes
|
||||||
)} / ${this.formatBytes(this.maxMemoryMB * 1024 * 1024)}`;
|
)} / ${this.formatBytes(this.maxMemoryMB * 1024 * 1024)}`;
|
||||||
|
|
||||||
console.log(`${cpuUsageText} ||| ${memoryUsageText} `);
|
// 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);
|
||||||
@@ -138,19 +190,35 @@ 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;
|
||||||
|
|
||||||
const returnMetrics: interfaces.IMetricsSnapshot = {
|
const returnMetrics: interfaces.IMetricsSnapshot = {
|
||||||
process_cpu_seconds_total,
|
process_cpu_seconds_total,
|
||||||
nodejs_active_handles_total,
|
nodejs_active_handles_total,
|
||||||
@@ -161,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;
|
||||||
}
|
}
|
||||||
@@ -168,7 +244,7 @@ export class SmartMetrics {
|
|||||||
public async getPrometheusFormattedMetrics(): Promise<string> {
|
public async getPrometheusFormattedMetrics(): Promise<string> {
|
||||||
// Update metrics to ensure gauges have latest values
|
// Update metrics to ensure gauges have latest values
|
||||||
await this.getMetrics();
|
await this.getMetrics();
|
||||||
|
|
||||||
// Return Prometheus text exposition format
|
// Return Prometheus text exposition format
|
||||||
return await this.registry.metrics();
|
return await this.registry.metrics();
|
||||||
}
|
}
|
||||||
@@ -178,7 +254,7 @@ export class SmartMetrics {
|
|||||||
this.logger.log('warn', 'Prometheus endpoint is already running');
|
this.logger.log('warn', 'Prometheus endpoint is already running');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.prometheusServer = plugins.http.createServer(async (req, res) => {
|
this.prometheusServer = plugins.http.createServer(async (req, res) => {
|
||||||
if (req.url === '/metrics' && req.method === 'GET') {
|
if (req.url === '/metrics' && req.method === 'GET') {
|
||||||
try {
|
try {
|
||||||
@@ -195,7 +271,7 @@ export class SmartMetrics {
|
|||||||
res.end('Not Found');
|
res.end('Not Found');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.prometheusPort = port;
|
this.prometheusPort = port;
|
||||||
this.prometheusServer.listen(port, () => {
|
this.prometheusServer.listen(port, () => {
|
||||||
this.logger.log('info', `Prometheus metrics endpoint available at http://localhost:${port}/metrics`);
|
this.logger.log('info', `Prometheus metrics endpoint available at http://localhost:${port}/metrics`);
|
||||||
@@ -206,12 +282,12 @@ export class SmartMetrics {
|
|||||||
if (!this.prometheusServer) {
|
if (!this.prometheusServer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = this.prometheusPort;
|
const port = this.prometheusPort;
|
||||||
this.prometheusServer.close(() => {
|
this.prometheusServer.close(() => {
|
||||||
this.logger.log('info', `Prometheus metrics endpoint on port ${port} has been shut down`);
|
this.logger.log('info', `Prometheus metrics endpoint on port ${port} has been shut down`);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.prometheusServer = undefined;
|
this.prometheusServer = undefined;
|
||||||
this.prometheusPort = undefined;
|
this.prometheusPort = undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
55
ts/smartmetrics.pidtree.ts
Normal file
55
ts/smartmetrics.pidtree.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
// Get all descendant PIDs of the given root PID by reading /proc/<pid>/stat.
|
||||||
|
// Returns an array of descendant PIDs (excludes the root itself).
|
||||||
|
export async function getChildPids(rootPid: number): Promise<number[]> {
|
||||||
|
const parentMap = new Map<number, number[]>(); // parent → children
|
||||||
|
|
||||||
|
let entries: string[];
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync('/proc');
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const pid = parseInt(entry, 10);
|
||||||
|
if (isNaN(pid)) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
||||||
|
// Format: pid (comm) state ppid ...
|
||||||
|
// comm can contain spaces and parentheses, so find the last ')' first
|
||||||
|
const closeParenIdx = stat.lastIndexOf(')');
|
||||||
|
if (closeParenIdx === -1) continue;
|
||||||
|
const afterComm = stat.slice(closeParenIdx + 2); // skip ') '
|
||||||
|
const fields = afterComm.split(' ');
|
||||||
|
const ppid = parseInt(fields[1], 10); // field index 1 after state is ppid
|
||||||
|
|
||||||
|
if (!parentMap.has(ppid)) {
|
||||||
|
parentMap.set(ppid, []);
|
||||||
|
}
|
||||||
|
parentMap.get(ppid)!.push(pid);
|
||||||
|
} catch {
|
||||||
|
// Process may have exited between readdir and readFile
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BFS from rootPid to collect all descendants
|
||||||
|
const result: number[] = [];
|
||||||
|
const queue: number[] = [rootPid];
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const current = queue.shift()!;
|
||||||
|
const children = parentMap.get(current);
|
||||||
|
if (children) {
|
||||||
|
for (const child of children) {
|
||||||
|
result.push(child);
|
||||||
|
queue.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
136
ts/smartmetrics.pidusage.ts
Normal file
136
ts/smartmetrics.pidusage.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
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
|
||||||
|
let clkTck: number | null = null;
|
||||||
|
let pageSize: number | null = null;
|
||||||
|
|
||||||
|
function getClkTck(): number {
|
||||||
|
if (clkTck === null) {
|
||||||
|
try {
|
||||||
|
clkTck = parseInt(execSync('getconf CLK_TCK', { encoding: 'utf8' }).trim(), 10);
|
||||||
|
} catch {
|
||||||
|
clkTck = 100; // standard Linux default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clkTck;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPageSize(): number {
|
||||||
|
if (pageSize === null) {
|
||||||
|
try {
|
||||||
|
pageSize = parseInt(execSync('getconf PAGESIZE', { encoding: 'utf8' }).trim(), 10);
|
||||||
|
} catch {
|
||||||
|
pageSize = 4096; // standard Linux default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// History for CPU delta tracking
|
||||||
|
interface ISnapshot {
|
||||||
|
utime: number;
|
||||||
|
stime: number;
|
||||||
|
timestamp: number; // hrtime in seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = new Map<number, ISnapshot>();
|
||||||
|
|
||||||
|
function readProcStat(pid: number): { utime: number; stime: number; rss: number } | null {
|
||||||
|
try {
|
||||||
|
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
||||||
|
// Format: pid (comm) state ppid ... fields
|
||||||
|
// utime is field 14, stime is field 15, rss is field 24 (1-indexed)
|
||||||
|
const closeParenIdx = stat.lastIndexOf(')');
|
||||||
|
if (closeParenIdx === -1) return null;
|
||||||
|
const afterComm = stat.slice(closeParenIdx + 2);
|
||||||
|
const fields = afterComm.split(' ');
|
||||||
|
// fields[0] = state (field 3), so utime = fields[11] (field 14), stime = fields[12] (field 15), rss = fields[21] (field 24)
|
||||||
|
const utime = parseInt(fields[11], 10);
|
||||||
|
const stime = parseInt(fields[12], 10);
|
||||||
|
const rss = parseInt(fields[21], 10);
|
||||||
|
return { utime, stime, rss };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hrtimeSeconds(): number {
|
||||||
|
const [sec, nsec] = process.hrtime();
|
||||||
|
return sec + nsec / 1e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPidUsageResult {
|
||||||
|
cpu: number; // raw per-core CPU% (can exceed 100%)
|
||||||
|
cpuCoreCount: number; // number of CPU cores on the machine
|
||||||
|
cpuNormalizedPercent: number; // cpu / coreCount — 0-100% of total machine
|
||||||
|
memory: number; // RSS in bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get CPU percentage and memory usage for the given PIDs.
|
||||||
|
* CPU% is calculated as a delta between successive calls.
|
||||||
|
*/
|
||||||
|
export async function getPidUsage(
|
||||||
|
pids: number[]
|
||||||
|
): Promise<Record<number, IPidUsageResult>> {
|
||||||
|
const tck = getClkTck();
|
||||||
|
const ps = getPageSize();
|
||||||
|
const result: Record<number, IPidUsageResult> = {};
|
||||||
|
|
||||||
|
for (const pid of pids) {
|
||||||
|
const stat = readProcStat(pid);
|
||||||
|
if (!stat) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = hrtimeSeconds();
|
||||||
|
const totalTicks = stat.utime + stat.stime;
|
||||||
|
const memoryBytes = stat.rss * ps;
|
||||||
|
|
||||||
|
const prev = history.get(pid);
|
||||||
|
if (prev) {
|
||||||
|
const elapsedSeconds = now - prev.timestamp;
|
||||||
|
const ticksDelta = totalTicks - (prev.utime + prev.stime);
|
||||||
|
const cpuSeconds = ticksDelta / tck;
|
||||||
|
const cpuPercent = elapsedSeconds > 0 ? (cpuSeconds / elapsedSeconds) * 100 : 0;
|
||||||
|
|
||||||
|
result[pid] = {
|
||||||
|
cpu: cpuPercent,
|
||||||
|
cpuCoreCount,
|
||||||
|
cpuNormalizedPercent: cpuPercent / cpuCoreCount,
|
||||||
|
memory: memoryBytes,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// First call for this PID — no delta available, report 0% cpu
|
||||||
|
result[pid] = {
|
||||||
|
cpu: 0,
|
||||||
|
cpuCoreCount,
|
||||||
|
cpuNormalizedPercent: 0,
|
||||||
|
memory: memoryBytes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update history
|
||||||
|
history.set(pid, {
|
||||||
|
utime: stat.utime,
|
||||||
|
stime: stat.stime,
|
||||||
|
timestamp: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prune history entries for PIDs no longer in the requested set
|
||||||
|
for (const histPid of history.keys()) {
|
||||||
|
if (!pids.includes(histPid)) {
|
||||||
|
history.delete(histPid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -12,9 +12,10 @@ import * as smartlog from '@push.rocks/smartlog';
|
|||||||
|
|
||||||
export { smartdelay, smartlog };
|
export { smartdelay, smartlog };
|
||||||
|
|
||||||
// third party scope
|
// own implementations (replacing pidtree, pidusage, prom-client)
|
||||||
import pidusage from 'pidusage';
|
import * as pidtree from './smartmetrics.pidtree.js';
|
||||||
import pidtree from 'pidtree';
|
import * as pidusage from './smartmetrics.pidusage.js';
|
||||||
import * as promClient from 'prom-client';
|
import * as prom from './smartmetrics.prom.js';
|
||||||
|
import * as sysusage from './smartmetrics.sysusage.js';
|
||||||
|
|
||||||
export { pidusage, pidtree, promClient };
|
export { pidtree, pidusage, prom, sysusage };
|
||||||
|
|||||||
671
ts/smartmetrics.prom.ts
Normal file
671
ts/smartmetrics.prom.ts
Normal file
@@ -0,0 +1,671 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
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