diff --git a/packages/instrumentation-runtime-node/src/metrics/heapSpacesSizeAndUsedCollector.ts b/packages/instrumentation-runtime-node/src/metrics/heapSpacesSizeAndUsedCollector.ts index cbe4b278df..168bb418b2 100644 --- a/packages/instrumentation-runtime-node/src/metrics/heapSpacesSizeAndUsedCollector.ts +++ b/packages/instrumentation-runtime-node/src/metrics/heapSpacesSizeAndUsedCollector.ts @@ -23,6 +23,7 @@ import { BaseCollector } from './baseCollector'; import { ATTR_V8JS_HEAP_SPACE_NAME, METRIC_V8JS_MEMORY_HEAP_LIMIT, + METRIC_V8JS_MEMORY_HEAP_MAX, METRIC_V8JS_MEMORY_HEAP_USED, METRIC_V8JS_MEMORY_HEAP_SPACE_AVAILABLE_SIZE, METRIC_V8JS_MEMORY_HEAP_SPACE_PHYSICAL_SIZE, @@ -59,10 +60,19 @@ export class HeapSpacesSizeAndUsedCollector extends BaseCollector { } ); + const heapMax = meter.createObservableGauge(METRIC_V8JS_MEMORY_HEAP_MAX, { + description: + 'Maximum heap size allowed by the V8 engine, as set by --max-old-space-size or V8 defaults.', + unit: 'By', + }); + meter.addBatchObservableCallback( observableResult => { if (!this._config.enabled) return; + const heapStats = v8.getHeapStatistics(); + observableResult.observe(heapMax, heapStats.heap_size_limit); + const data = this.scrape(); if (data === undefined) return; for (const space of data) { @@ -93,7 +103,7 @@ export class HeapSpacesSizeAndUsedCollector extends BaseCollector { ); } }, - [heapLimit, heapSpaceUsed, heapSpaceAvailable, heapSpacePhysical] + [heapMax, heapLimit, heapSpaceUsed, heapSpaceAvailable, heapSpacePhysical] ); } diff --git a/packages/instrumentation-runtime-node/src/semconv.ts b/packages/instrumentation-runtime-node/src/semconv.ts index 709d38ad93..ac0376dc94 100644 --- a/packages/instrumentation-runtime-node/src/semconv.ts +++ b/packages/instrumentation-runtime-node/src/semconv.ts @@ -196,3 +196,13 @@ export const NODEJS_EVENTLOOP_STATE_VALUE_ACTIVE = 'active' as const; * @experimental This enum value is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. */ export const NODEJS_EVENTLOOP_STATE_VALUE_IDLE = 'idle' as const; + +/** + * Maximum heap size allowed by the V8 engine. + * + * @note The value can be retrieved from value `heap_size_limit` of [`v8.getHeapStatistics()`](https://nodejs.org/api/v8.html#v8getheapstatistics). + * This is the absolute ceiling the heap can grow to, controlled by `--max-old-space-size` or V8 defaults. + * + * @experimental This metric is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const METRIC_V8JS_MEMORY_HEAP_MAX = 'v8js.memory.heap.max' as const; diff --git a/packages/instrumentation-runtime-node/test/heap_size_limit.test.ts b/packages/instrumentation-runtime-node/test/heap_size_limit.test.ts new file mode 100644 index 0000000000..d9c916e963 --- /dev/null +++ b/packages/instrumentation-runtime-node/test/heap_size_limit.test.ts @@ -0,0 +1,142 @@ +/* + * 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 { DataPointType, MeterProvider } from '@opentelemetry/sdk-metrics'; +import { RuntimeNodeInstrumentation } from '../src'; +import { TestMetricReader } from './testMetricsReader'; +import { METRIC_V8JS_MEMORY_HEAP_MAX } from '../src/semconv'; + +const MEASUREMENT_INTERVAL = 10; + +describe('v8js.memory.heap.max', function () { + let metricReader: TestMetricReader; + let meterProvider: MeterProvider; + + beforeEach(() => { + metricReader = new TestMetricReader(); + meterProvider = new MeterProvider({ + readers: [metricReader], + }); + }); + + it(`should write ${METRIC_V8JS_MEMORY_HEAP_MAX} after monitoringPrecision`, async function () { + // arrange + const instrumentation = new RuntimeNodeInstrumentation({ + monitoringPrecision: MEASUREMENT_INTERVAL, + }); + instrumentation.setMeterProvider(meterProvider); + + // act + 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_MEMORY_HEAP_MAX + ); + + assert.notEqual( + metric, + undefined, + `${METRIC_V8JS_MEMORY_HEAP_MAX} not found` + ); + + assert.strictEqual( + metric!.dataPointType, + DataPointType.GAUGE, + 'expected gauge' + ); + + assert.strictEqual( + metric!.descriptor.name, + METRIC_V8JS_MEMORY_HEAP_MAX, + 'descriptor.name' + ); + }); + + it('should have a positive value representing the heap size limit', async function () { + // arrange + const instrumentation = new RuntimeNodeInstrumentation({ + monitoringPrecision: MEASUREMENT_INTERVAL, + }); + instrumentation.setMeterProvider(meterProvider); + + // act + await new Promise(resolve => setTimeout(resolve, MEASUREMENT_INTERVAL * 5)); + const { resourceMetrics, errors } = await metricReader.collect(); + + // assert + assert.deepEqual(errors, []); + const scopeMetrics = resourceMetrics.scopeMetrics; + const metric = scopeMetrics[0].metrics.find( + x => x.descriptor.name === METRIC_V8JS_MEMORY_HEAP_MAX + ); + + assert.notEqual( + metric, + undefined, + `${METRIC_V8JS_MEMORY_HEAP_MAX} not found` + ); + + if (metric!.dataPointType === DataPointType.GAUGE) { + assert.strictEqual( + metric!.dataPoints.length, + 1, + 'expected exactly one data point (global, not per-space)' + ); + const value = metric!.dataPoints[0].value as number; + assert.ok(value > 0, `expected positive heap_size_limit, got ${value}`); + } + }); + + it('should not have v8js.heap.space.name attribute (global metric)', async function () { + // arrange + const instrumentation = new RuntimeNodeInstrumentation({ + monitoringPrecision: MEASUREMENT_INTERVAL, + }); + instrumentation.setMeterProvider(meterProvider); + + // act + await new Promise(resolve => setTimeout(resolve, MEASUREMENT_INTERVAL * 5)); + const { resourceMetrics, errors } = await metricReader.collect(); + + // assert + assert.deepEqual(errors, []); + const scopeMetrics = resourceMetrics.scopeMetrics; + const metric = scopeMetrics[0].metrics.find( + x => x.descriptor.name === METRIC_V8JS_MEMORY_HEAP_MAX + ); + + assert.notEqual(metric, undefined); + + if (metric!.dataPointType === DataPointType.GAUGE) { + for (const dp of metric!.dataPoints) { + assert.strictEqual( + dp.attributes['v8js.heap.space.name'], + undefined, + 'v8js.memory.heap.max should not have v8js.heap.space.name attribute' + ); + } + } + }); +});