Skip to content
Open
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
7 changes: 4 additions & 3 deletions packages/instrumentation-runtime-node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ nodejs_performance_event_loop_utilization 0.010140079547955264

`RuntimeNodeInstrumentation`'s constructor accepts the following options:

| name | type | unit | default | description |
|---------------------------------------------|-------|-------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`monitoringPrecision`](./src/types.ts#L25) | `int` | millisecond | `10` | The approximate number of milliseconds for which to calculate event loop utilization averages. A larger value will result in more accurate averages at the expense of less granular data. Should be set to below the scrape interval of your metrics collector to avoid duplicated data points. |
| name | type | unit | default | description |
|---------------------------------------------|------------|-------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`monitoringPrecision`](./src/types.ts#L20) | `int` | millisecond | `10` | The approximate number of milliseconds for which to calculate event loop utilization averages. A larger value will result in more accurate averages at the expense of less granular data. Should be set to below the scrape interval of your metrics collector to avoid duplicated data points. |
| [`gcDurationBuckets`](./src/types.ts#L21) | `number[]` | second | `[0.01, 0.1, 1, 10]` | The histogram bucket boundaries to use for the GC duration histogram (`v8js.gc.duration`). |

## Useful links

Expand Down
2 changes: 1 addition & 1 deletion packages/instrumentation-runtime-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"compile": "tsc -p .",
"compile:with-dependencies": "nx run-many -t compile -p @opentelemetry/instrumentation-runtime-node",
"prepublishOnly": "npm run compile",
"test": "nyc --no-clean mocha 'test/**/*.test.ts'",
"test": "nyc --no-clean mocha --expose-gc 'test/**/*.test.ts'",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the test failures might be related to this change, what this parameter does? does it need some extra info to find specific files?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exposes the ability to trigger GC which is what the test uses.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how it would be possible to test this instrumentation collector without this flag which is I assume why there were no tests in the first place

I believe that v18 and v20 of Node that your CI tests with do not have this flag and therefore I do not know how one would test this collector on those versions. Up to you how to proceed, we could:

  1. Not test this collector (which is the current state of the codebase and my change does not make particularly any worse) and therefore delete my tests
  2. Disable these tests in certain CI matrix formations where we use older Node.js versions
  3. Find an alternative way to do this on those versions, however I do not know of a way to do that

Copy link
Contributor

@david-luna david-luna Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a script that can be used to skip tests given a certain major version (only major). A possible solution is to:

  • have a test script without the flag (works for all versions)
  • have a test script with the flag and skip for nodejs versions lower than 22
  • refactor the new tests to skip if the global.gc() method is not avaiable

Example of how the scripts would look like.

    "test": "npm run test:nogc && npm run test:gc",
    "test:nogc": "nyc --no-clean mocha 'test/**/*.test.ts'",
    "test:gc": "SKIP_TEST_IF_NODE_OLDER_THAN=22 nyc --no-clean mocha --require '../../scripts/skip-test-if.js' --expose-gc 'test/**/*.ts'",

WDYT?

"version:update": "node ../../scripts/version-update.js"
},
"author": "OpenTelemetry Authors",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { PACKAGE_VERSION, PACKAGE_NAME } from './version';

const DEFAULT_CONFIG: RuntimeNodeInstrumentationConfig = {
monitoringPrecision: 10,
gcDurationBuckets: [0.01, 0.1, 1, 10],
};

export class RuntimeNodeInstrumentation extends InstrumentationBase<RuntimeNodeInstrumentationConfig> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ import { Histogram, ValueType } from '@opentelemetry/api';
import { BaseCollector } from './baseCollector';
import { ATTR_V8JS_GC_TYPE, METRIC_V8JS_GC_DURATION } from '../semconv';

const DEFAULT_GC_DURATION_BUCKETS = [0.01, 0.1, 1, 10];

const kinds: string[] = [];
kinds[perf_hooks.constants.NODE_PERFORMANCE_GC_MAJOR] = 'major';
kinds[perf_hooks.constants.NODE_PERFORMANCE_GC_MINOR] = 'minor';
Expand Down Expand Up @@ -61,7 +59,7 @@ export class GCCollector extends BaseCollector {
unit: 's',
valueType: ValueType.DOUBLE,
advice: {
explicitBucketBoundaries: DEFAULT_GC_DURATION_BUCKETS,
explicitBucketBoundaries: this._config.gcDurationBuckets,
},
}
);
Expand Down
1 change: 1 addition & 0 deletions packages/instrumentation-runtime-node/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
export interface RuntimeNodeInstrumentationConfig
extends InstrumentationConfig {
monitoringPrecision?: number;
gcDurationBuckets?: number[];
}
132 changes: 132 additions & 0 deletions packages/instrumentation-runtime-node/test/gc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as assert from 'assert';
import { MeterProvider, DataPointType } from '@opentelemetry/sdk-metrics';
import { RuntimeNodeInstrumentation } from '../src';
import { TestMetricReader } from './testMetricsReader';
import { METRIC_V8JS_GC_DURATION } from '../src/semconv';

const MEASUREMENT_INTERVAL = 10;

// Helper to trigger GC by allocating memory
function triggerGC() {
const arrays = [];
for (let i = 0; i < 100; i++) {
arrays.push(new Array(10000).fill(i));
}
// Allow garbage collection by clearing references
arrays.length = 0;
if (global.gc) {
global.gc();
}
}

describe('v8js.gc.duration', function () {
let metricReader: TestMetricReader;
let meterProvider: MeterProvider;
let instrumentation: RuntimeNodeInstrumentation;

beforeEach(() => {
metricReader = new TestMetricReader();
meterProvider = new MeterProvider({
readers: [metricReader],
});
});

afterEach(() => {
instrumentation.disable();
});

it('should create histogram with default gcDurationBuckets', async function () {
// arrange
instrumentation = new RuntimeNodeInstrumentation({
monitoringPrecision: MEASUREMENT_INTERVAL,
});
instrumentation.setMeterProvider(meterProvider);

// act - trigger GC
triggerGC();
await new Promise(resolve =>
setTimeout(resolve, MEASUREMENT_INTERVAL * 10)
);
triggerGC();
await new Promise(resolve => setTimeout(resolve, MEASUREMENT_INTERVAL * 5));

const { resourceMetrics, errors } = await metricReader.collect();

// assert
assert.deepEqual(
errors,
[],
'expected no errors from the callback during collection'
);
const scopeMetrics = resourceMetrics.scopeMetrics;
const metric = scopeMetrics[0]?.metrics.find(
x => x.descriptor.name === METRIC_V8JS_GC_DURATION
);

assert.notEqual(metric, undefined, `${METRIC_V8JS_GC_DURATION} not found`);
assert.strictEqual(
metric!.dataPointType,
DataPointType.HISTOGRAM,
'expected histogram'
);
assert.strictEqual(
metric!.descriptor.unit,
's',
'expected unit to be seconds'
);
});

it('should allow custom gcDurationBuckets configuration', async function () {
// arrange
const customBuckets = [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1];
instrumentation = new RuntimeNodeInstrumentation({
monitoringPrecision: MEASUREMENT_INTERVAL,
gcDurationBuckets: customBuckets,
});
instrumentation.setMeterProvider(meterProvider);

// act - trigger GC
triggerGC();
await new Promise(resolve =>
setTimeout(resolve, MEASUREMENT_INTERVAL * 10)
);
triggerGC();
await new Promise(resolve => setTimeout(resolve, MEASUREMENT_INTERVAL * 5));

const { resourceMetrics, errors } = await metricReader.collect();

// assert
assert.deepEqual(
errors,
[],
'expected no errors from the callback during collection'
);
const scopeMetrics = resourceMetrics.scopeMetrics;
const metric = scopeMetrics[0]?.metrics.find(
x => x.descriptor.name === METRIC_V8JS_GC_DURATION
);

assert.notEqual(metric, undefined, `${METRIC_V8JS_GC_DURATION} not found`);
assert.strictEqual(
metric!.dataPointType,
DataPointType.HISTOGRAM,
'expected histogram'
);
});
});
Loading