diff --git a/packages/instrumentation-runtime-node/README.md b/packages/instrumentation-runtime-node/README.md index 890269f574..f9dfb8252b 100644 --- a/packages/instrumentation-runtime-node/README.md +++ b/packages/instrumentation-runtime-node/README.md @@ -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 diff --git a/packages/instrumentation-runtime-node/package.json b/packages/instrumentation-runtime-node/package.json index a8002a3e3e..fa7c2dd81c 100644 --- a/packages/instrumentation-runtime-node/package.json +++ b/packages/instrumentation-runtime-node/package.json @@ -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'", "version:update": "node ../../scripts/version-update.js" }, "author": "OpenTelemetry Authors", diff --git a/packages/instrumentation-runtime-node/src/instrumentation.ts b/packages/instrumentation-runtime-node/src/instrumentation.ts index 9d0c0b441a..11ad227600 100644 --- a/packages/instrumentation-runtime-node/src/instrumentation.ts +++ b/packages/instrumentation-runtime-node/src/instrumentation.ts @@ -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 { diff --git a/packages/instrumentation-runtime-node/src/metrics/gcCollector.ts b/packages/instrumentation-runtime-node/src/metrics/gcCollector.ts index 790b641cda..e73b78ae38 100644 --- a/packages/instrumentation-runtime-node/src/metrics/gcCollector.ts +++ b/packages/instrumentation-runtime-node/src/metrics/gcCollector.ts @@ -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'; @@ -61,7 +59,7 @@ export class GCCollector extends BaseCollector { unit: 's', valueType: ValueType.DOUBLE, advice: { - explicitBucketBoundaries: DEFAULT_GC_DURATION_BUCKETS, + explicitBucketBoundaries: this._config.gcDurationBuckets, }, } ); diff --git a/packages/instrumentation-runtime-node/src/types.ts b/packages/instrumentation-runtime-node/src/types.ts index 68227c98f2..18ac3a0806 100644 --- a/packages/instrumentation-runtime-node/src/types.ts +++ b/packages/instrumentation-runtime-node/src/types.ts @@ -18,4 +18,5 @@ import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; export interface RuntimeNodeInstrumentationConfig extends InstrumentationConfig { monitoringPrecision?: number; + gcDurationBuckets?: number[]; } diff --git a/packages/instrumentation-runtime-node/test/gc.test.ts b/packages/instrumentation-runtime-node/test/gc.test.ts new file mode 100644 index 0000000000..3e519564b6 --- /dev/null +++ b/packages/instrumentation-runtime-node/test/gc.test.ts @@ -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' + ); + }); +});