Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,34 @@ jobs:
npm i
npm run lint

TestUnitRemote:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
node-version: [ 20, 22, 24 ]
steps:
- uses: actions/checkout@v4
with:
submodules: true

- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

- name: Set Up NodeJS ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Unit tests on Node@${{ matrix.node-version }}
run: |
npm i
npx jest tests/remote/ tests/config/ tests/runtime/ --runInBand

build-matrix:
runs-on: ubuntu-latest
timeout-minutes: 5
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Note that the various ignore options like `SW_IGNORE_SUFFIX`, `SW_TRACE_IGNORE_P

## Node.js Runtime Metrics

The agent reports six process-level meters (`instance_nodejs_*`) via `MeterReportService` by default (collect 1s, report 1s). Set `SW_AGENT_NODEJS_RUNTIME_METRICS_REPORTER_ACTIVE=false` to disable. Process CPU combines `process.cpuUsage()` user + system, normalized by logical CPU count (0–100%).
The agent reports twelve process-level meters (`instance_nodejs_*`) via `MeterReportService` by default (collect 1s, report 1s). Set `SW_AGENT_NODEJS_RUNTIME_METRICS_REPORTER_ACTIVE=false` to disable. Process CPU combines `process.cpuUsage()` user + system, normalized by logical CPU count (0–100%).

| Node.js source | Meter name | Notes |
| :--- | :--- | :--- |
Expand All @@ -94,6 +94,12 @@ The agent reports six process-level meters (`instance_nodejs_*`) via `MeterRepor
| `v8.getHeapStatistics().heap_size_limit` | `instance_nodejs_heap_limit` | bytes |
| `process.memoryUsage().rss` | `instance_nodejs_rss` | bytes |
| `process.memoryUsage().external` | `instance_nodejs_external_memory` | bytes |
| `process.memoryUsage().arrayBuffers` | `instance_nodejs_array_buffers` | bytes |
| `process.uptime()` | `instance_nodejs_uptime` | seconds |
| `v8.getHeapStatistics().peak_malloced_memory` | `instance_nodejs_peak_malloced_memory` | bytes |
| `v8.getHeapStatistics().malloced_memory` | `instance_nodejs_malloced_memory` | bytes |
| `v8.getHeapSpaceStatistics()` old_space | `instance_nodejs_old_space_used` | bytes |
| `v8.getHeapSpaceStatistics()` new_space | `instance_nodejs_new_space_used` | bytes |

Custom business metrics are not available through a public API; use [OpenTelemetry metrics](https://skywalking.apache.org/docs/main/latest/en/setup/backend/opentelemetry-receiver/) if you need those.

Expand Down
6 changes: 6 additions & 0 deletions src/agent/core/meter/RuntimeMetricsCollector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export default class RuntimeMetricsCollector {
['instance_nodejs_heap_limit', snapshot.heapSizeLimit],
['instance_nodejs_rss', snapshot.rss],
['instance_nodejs_external_memory', snapshot.external],
['instance_nodejs_array_buffers', snapshot.arrayBuffers],
['instance_nodejs_uptime', snapshot.uptime],
['instance_nodejs_peak_malloced_memory', snapshot.peakMallocedMemory],
['instance_nodejs_malloced_memory', snapshot.mallocedMemory],
['instance_nodejs_old_space_used', snapshot.oldSpaceUsed],
['instance_nodejs_new_space_used', snapshot.newSpaceUsed],
];

return gauges.map(([name, value]) =>
Expand Down
31 changes: 29 additions & 2 deletions src/agent/core/meter/RuntimeSampler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,23 @@

import os from 'os';
import v8 from 'v8';
import config from '../../../config/AgentConfig';

export type RuntimeSnapshot = {
collectedAt: number;
heapUsed: number;
heapTotal: number;
heapSizeLimit: number;
rss: number;
external: number;
cpuUserPercent: number;
cpuSystemPercent: number;
arrayBuffers: number;
uptime: number;
peakMallocedMemory: number;
mallocedMemory: number;
oldSpaceUsed: number;
newSpaceUsed: number;
};

export default class RuntimeSampler {
Expand All @@ -38,24 +46,43 @@ export default class RuntimeSampler {
sample(): RuntimeSnapshot {
const memory = process.memoryUsage();
const heapStats = v8.getHeapStatistics();
const cpuUsage = process.cpuUsage(this.lastCpuUsage);
const cpuNow = process.cpuUsage();
const cpuUsage = {
user: cpuNow.user - this.lastCpuUsage.user,
system: cpuNow.system - this.lastCpuUsage.system,
};
const now = process.hrtime.bigint();
const elapsedMicros = Number(now - this.lastCpuTimestamp) / 1000;
this.lastCpuUsage = process.cpuUsage();
this.lastCpuUsage = cpuNow;
this.lastCpuTimestamp = now;

const cpuScale = elapsedMicros > 0 ? 100 / elapsedMicros / this.logicalCpuCount : 0;
const cpuUserPercent = cpuUsage.user * cpuScale;
const cpuSystemPercent = cpuUsage.system * cpuScale;
const heapSpaceDetail = config.runtimeMetricsHeapSpaceDetail !== false;
let oldSpaceUsed = 0;
let newSpaceUsed = 0;
if (heapSpaceDetail) {
const heapSpaces = v8.getHeapSpaceStatistics();
oldSpaceUsed = heapSpaces.find((entry) => entry.space_name === 'old_space')?.space_used_size ?? 0;
newSpaceUsed = heapSpaces.find((entry) => entry.space_name === 'new_space')?.space_used_size ?? 0;
}

return {
collectedAt: Date.now(),
heapUsed: memory.heapUsed,
heapTotal: memory.heapTotal,
heapSizeLimit: heapStats.heap_size_limit,
rss: memory.rss,
external: memory.external,
cpuUserPercent,
cpuSystemPercent,
arrayBuffers: memory.arrayBuffers ?? 0,
uptime: process.uptime(),
peakMallocedMemory: heapStats.peak_malloced_memory,
mallocedMemory: heapStats.malloced_memory,
oldSpaceUsed,
newSpaceUsed,
};
}

Expand Down
5 changes: 5 additions & 0 deletions src/config/AgentConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export type AgentConfig = {
runtimeMetricsCollectPeriod?: number;
runtimeMetricsReportPeriod?: number;
runtimeMetricsBufferSize?: number;
runtimeMetricsHeapSpaceDetail?: boolean;
/** @deprecated use runtimeMetricsReporterActive */
nvmMetricsReporterActive?: boolean;
/** @deprecated use runtimeMetricsCollectPeriod */
Expand Down Expand Up @@ -279,6 +280,10 @@ const _config = {
10,
),
),
runtimeMetricsHeapSpaceDetail: ((): boolean => {
const configured = process.env.SW_AGENT_RUNTIME_METRICS_HEAP_SPACE_DETAIL;
return configured?.toLowerCase() !== 'false';
})(),
runtimeMetricsBufferSize: ((n) => (Number.isSafeInteger(n) && n > 0 ? n : 600))(
Number.parseInt(
process.env.SW_AGENT_NODEJS_RUNTIME_METRICS_BUFFER_SIZE ??
Expand Down
52 changes: 50 additions & 2 deletions tests/plugins/express/expected.data.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ segmentItems:

meterItems:
- serviceName: server
meterSize: 6
meterSize: 12
meters:
- meterId:
name: instance_nodejs_process_cpu
Expand All @@ -148,8 +148,32 @@ meterItems:
name: instance_nodejs_external_memory
tags: []
singleValue: gt 0
- meterId:
name: instance_nodejs_array_buffers
tags: []
singleValue: ge 0
- meterId:
name: instance_nodejs_uptime
tags: []
singleValue: gt 0
- meterId:
name: instance_nodejs_peak_malloced_memory
tags: []
singleValue: gt 0
- meterId:
name: instance_nodejs_malloced_memory
tags: []
singleValue: gt 0
- meterId:
name: instance_nodejs_old_space_used
tags: []
singleValue: gt 0
- meterId:
name: instance_nodejs_new_space_used
tags: []
singleValue: gt 0
- serviceName: client
meterSize: 6
meterSize: 12
meters:
- meterId:
name: instance_nodejs_process_cpu
Expand All @@ -175,3 +199,27 @@ meterItems:
name: instance_nodejs_external_memory
tags: []
singleValue: gt 0
- meterId:
name: instance_nodejs_array_buffers
tags: []
singleValue: ge 0
- meterId:
name: instance_nodejs_uptime
tags: []
singleValue: gt 0
- meterId:
name: instance_nodejs_peak_malloced_memory
tags: []
singleValue: gt 0
- meterId:
name: instance_nodejs_malloced_memory
tags: []
singleValue: gt 0
- meterId:
name: instance_nodejs_old_space_used
tags: []
singleValue: gt 0
- meterId:
name: instance_nodejs_new_space_used
tags: []
singleValue: gt 0
76 changes: 64 additions & 12 deletions tests/runtime/RuntimeMetricsCollector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@
/* eslint-env jest */

import RuntimeMetricsCollector from '../../src/agent/core/meter/RuntimeMetricsCollector';
import { RuntimeSnapshot } from '../../src/agent/core/meter/RuntimeSampler';

const EXPECTED_METER_NAMES = [
'instance_nodejs_process_cpu',
'instance_nodejs_heap_used',
'instance_nodejs_heap_total',
'instance_nodejs_heap_limit',
'instance_nodejs_rss',
'instance_nodejs_external_memory',
'instance_nodejs_array_buffers',
'instance_nodejs_uptime',
'instance_nodejs_peak_malloced_memory',
'instance_nodejs_malloced_memory',
'instance_nodejs_old_space_used',
'instance_nodejs_new_space_used',
];

describe('RuntimeMetricsCollector', () => {
let collector: RuntimeMetricsCollector;
Expand All @@ -37,21 +53,57 @@ describe('RuntimeMetricsCollector', () => {
const meters = collector.toMeterData(snapshot);
const names = meters.map((meter) => meter.getSinglevalue()?.getName());

expect(names).toEqual(
expect.arrayContaining([
'instance_nodejs_process_cpu',
'instance_nodejs_heap_used',
'instance_nodejs_heap_total',
'instance_nodejs_heap_limit',
'instance_nodejs_rss',
'instance_nodejs_external_memory',
]),
);

expect(names).toHaveLength(6);
expect(names).toEqual(EXPECTED_METER_NAMES);

for (const meter of meters) {
expect(meter.getSinglevalue()?.getValue()).toBeGreaterThanOrEqual(0);
}

expect(snapshot.uptime).toBeGreaterThanOrEqual(0);
expect(snapshot.oldSpaceUsed).toBeGreaterThanOrEqual(0);
expect(snapshot.newSpaceUsed).toBeGreaterThanOrEqual(0);
});

it('maps extended runtime snapshot values into meter single values', () => {
const snapshot: RuntimeSnapshot = {
collectedAt: 1_700_000_000_000,
heapUsed: 100,
heapTotal: 200,
heapSizeLimit: 300,
rss: 400,
external: 50,
cpuUserPercent: 1.2,
cpuSystemPercent: 0.8,
arrayBuffers: 16,
uptime: 42.5,
peakMallocedMemory: 2048,
mallocedMemory: 3072,
oldSpaceUsed: 88,
newSpaceUsed: 12,
};

const meters = collector.toMeterData(snapshot);
const values: Record<string, number | undefined> = {};
for (const meter of meters) {
const name = meter.getSinglevalue()?.getName();
if (name) {
values[name] = meter.getSinglevalue()?.getValue();
}
}

expect(values).toEqual({
instance_nodejs_process_cpu: 2,
instance_nodejs_heap_used: 100,
instance_nodejs_heap_total: 200,
instance_nodejs_heap_limit: 300,
instance_nodejs_rss: 400,
instance_nodejs_external_memory: 50,
instance_nodejs_array_buffers: 16,
instance_nodejs_uptime: 42.5,
instance_nodejs_peak_malloced_memory: 2048,
instance_nodejs_malloced_memory: 3072,
instance_nodejs_old_space_used: 88,
instance_nodejs_new_space_used: 12,
});
});
});
Loading