Skip to content
Merged
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 numberDiff line numberDiff line change
Expand Up@@ -47,6 +47,34 @@ jobs:
npm i
npm run lint

TestUnit:
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 --testPathIgnorePatterns "/node_modules/" "/tests/plugins/" --runInBand

build-matrix:
runs-on: ubuntu-latest
timeout-minutes: 5
Expand Down
16 changes: 10 additions & 6 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -72,19 +72,17 @@ Environment Variable | Description | Default
| `SW_AWS_SQS_CHECK_BODY` | Incoming SQS messages check inside the body for trace ID in order to allow linking outgoing SNS messages to incoming SQS. | `false` |
| `SW_AGENT_MAX_BUFFER_SIZE` | The maximum buffer size before sending the segment data to backend | `'1000'` |
| `SW_AGENT_TRACE_TIMEOUT` | The timeout for trace requests to backend services | `'10000'` |
| `SW_AGENT_NODEJS_RUNTIME_METRICS_REPORTER_ACTIVE` | Whether to report Node.js runtime metrics through MeterReportService (default: collect 1s, report 1s) | `true` |
| `SW_AGENT_NODEJS_RUNTIME_METRICS_COLLECT_PERIOD` | Runtime metric sample interval in milliseconds | `1000` |
| `SW_AGENT_NODEJS_RUNTIME_METRICS_REPORT_PERIOD` | Runtime metric report interval in milliseconds (aligned with Java JVM metrics upload interval) | `1000` |
| `SW_AGENT_NODEJS_RUNTIME_METRICS_BUFFER_SIZE` | Maximum buffered runtime metric samples before dropping oldest | `600` |
| `SW_AGENT_NODEJS_RUNTIME_METRICS_REPORTER_ACTIVE` | Whether to report Node.js runtime metrics through MeterReportService (default period 20s) | `true` |
| `SW_AGENT_NODEJS_RUNTIME_METRICS_REPORT_PERIOD` | Runtime metric sample + report interval in milliseconds (aligned with Java `meter.report_interval`) | `20000` |

Legacy env names `SW_AGENT_RUNTIME_METRICS_*`, `SW_AGENT_NVM_METRICS_*` and `SW_AGENT_NVM_JVM_*` are still accepted as deprecated aliases.
Legacy env names `SW_AGENT_RUNTIME_METRICS_*` / `SW_AGENT_NVM_*` for reporter active and report period are still accepted as deprecated aliases.


Note that the various ignore options like `SW_IGNORE_SUFFIX`, `SW_TRACE_IGNORE_PATH` and `SW_HTTP_IGNORE_METHOD` as well as endpoints which are not recorded due to exceeding `SW_AGENT_MAX_BUFFER_SIZE` all propagate their ignored status downstream to any other endpoints they may call. If that endpoint is running the Node Skywalking agent then regardless of its ignore settings it will not be recorded since its upstream parent was not recorded. This allows the elimination of entire trees of endpoints you are not interested in as well as eliminating partial traces if a span in the chain is ignored but calls out to other endpoints which are recorded as children of ROOT instead of the actual parent.

## 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 (sample and report every 20s). 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 +92,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
69 changes: 27 additions & 42 deletions src/agent/core/meter/MeterSender.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,9 +38,9 @@ export default class MeterSender implements BootService, GRPCChannelListener {
private channelManager?: GRPCChannelManager;
private status = GRPCChannelStatus.DISCONNECT;
private reporterClient?: MeterReportServiceClient;
private readonly buffer: RuntimeSnapshot[] = [];
private collectTimer?: NodeJS.Timeout;
private reportTimer?: NodeJS.Timeout;
/** Latest gauge snapshot only — stale samples have no value after reconnect. */
private latestSnapshot?: RuntimeSnapshot;
private timer?: NodeJS.Timeout;
private reporting?: Promise<void>;

private collector!: RuntimeMetricsCollector;
Expand All@@ -52,12 +52,12 @@ export default class MeterSender implements BootService, GRPCChannelListener {
}

boot(): void {
if (this.collectTimer || this.reportTimer) {
logger.warn('MeterSender timers already scheduled; skipping duplicate boot.');
if (this.timer) {
logger.warn('MeterSender timer already scheduled; skipping duplicate boot.');
return;
}

this.startTimers();
this.startTimer();
}

onComplete(): void {}
Expand All@@ -83,29 +83,19 @@ export default class MeterSender implements BootService, GRPCChannelListener {
);
}

private startTimers(): void {
this.collectTimer = setInterval(() => {
private startTimer(): void {
this.timer = setInterval(() => {
if (this.closed) {
return;
}
this.collectSample();
}, config.runtimeMetricsCollectPeriod || 1000) as NodeJS.Timeout;
this.collectTimer.unref();
this.reportTimer = setInterval(() => {
if (this.closed) {
return;
}
void this.reportBufferedMetrics();
}, config.runtimeMetricsReportPeriod || 1000) as NodeJS.Timeout;
this.reportTimer.unref();
}, config.runtimeMetricsReportPeriod || 20000) as NodeJS.Timeout;
this.timer.unref();
}

private collectSample(): void {
const maxBufferSize = config.runtimeMetricsBufferSize || 600;
if (this.buffer.length >= maxBufferSize) {
this.buffer.shift();
}
this.buffer.push(this.collector.sample());
this.latestSnapshot = this.collector.sample();
}

private reportBufferedMetrics(): Promise<void> {
Expand All@@ -132,7 +122,9 @@ export default class MeterSender implements BootService, GRPCChannelListener {
return;
}

if (this.buffer.length === 0 || this.status !== GRPCChannelStatus.CONNECTED || !this.reporterClient) {
const snapshot = this.latestSnapshot;
this.latestSnapshot = undefined;
if (!snapshot || this.status !== GRPCChannelStatus.CONNECTED || !this.reporterClient) {
resolve();
return;
}
Expand All@@ -142,7 +134,6 @@ export default class MeterSender implements BootService, GRPCChannelListener {
return;
}

const snapshots = this.buffer.splice(0, this.buffer.length);
const stream = this.reporterClient.collect(
new grpc.Metadata(),
{ deadline: Date.now() + (config.traceTimeout || 10000) },
Expand All@@ -157,18 +148,16 @@ export default class MeterSender implements BootService, GRPCChannelListener {

try {
let metadataWritten = false;
const timestamp = Date.now();
for (const snapshot of snapshots) {
for (const meterData of this.collector.toMeterData(snapshot)) {
if (!metadataWritten) {
meterData
.setService(config.serviceName)
.setServiceinstance(config.serviceInstance)
.setTimestamp(timestamp);
metadataWritten = true;
}
stream.write(meterData);
for (const meterData of this.collector.toMeterData(snapshot)) {
// Meter.proto: service / instance / timestamp on the first stream element only.
if (!metadataWritten) {
meterData
.setService(config.serviceName)
.setServiceinstance(config.serviceInstance)
.setTimestamp(snapshot.collectedAt);
metadataWritten = true;
}
stream.write(meterData);
}
} finally {
try {
Expand DownExpand Up@@ -204,17 +193,13 @@ export default class MeterSender implements BootService, GRPCChannelListener {

shutdown(): void {
this.closed = true;
if (this.collectTimer) {
clearInterval(this.collectTimer);
this.collectTimer = undefined;
}
if (this.reportTimer) {
clearInterval(this.reportTimer);
this.reportTimer = undefined;
if (this.timer) {
clearInterval(this.timer);
this.timer = undefined;
}
this.reporting = undefined;
this.reporterClient = undefined;
this.buffer.length = 0;
this.latestSnapshot = undefined;
this.collector.destroy();
this.channelManager = undefined;
logger.info('MeterSender destroyed and resources cleaned up');
Expand Down
6 changes: 6 additions & 0 deletions src/agent/core/meter/RuntimeMetricsCollector.ts
Original file line numberDiff line numberDiff 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
25 changes: 23 additions & 2 deletions src/agent/core/meter/RuntimeSampler.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,13 +21,20 @@ import os from 'os';
import v8 from 'v8';

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 +45,38 @@ 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 heapSpaces = v8.getHeapSpaceStatistics();
const oldSpaceUsed = heapSpaces.find((entry) => entry.space_name === 'old_space')?.space_used_size ?? 0;
const 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
Loading
Loading