From ed6596f2b13de6a6e7317ebcdb2c0efc4c0b76ba Mon Sep 17 00:00:00 2001 From: LowArt Date: Mon, 22 Apr 2024 17:16:56 +0300 Subject: [PATCH] feat(prom-client) add implementation for collecting event loop lag, garbage collector, heap size and heap space --- .../src/instrumentation.ts | 77 +++---- .../src/metrics/baseCollector.ts | 73 +++++++ .../src/metrics/eventLoopLagCollector.ts | 203 ++++++++++++++++++ .../metrics/eventLoopUtilizationCollector.ts | 58 +++++ .../src/metrics/gcCollector.ts | 82 +++++++ .../src/metrics/heapSizeAndUsedCollector.ts | 93 ++++++++ .../metrics/heapSpacesSizeAndUsedCollector.ts | 109 ++++++++++ .../instrumentation-runtime-node/src/types.ts | 8 +- .../src/types/metricCollector.ts | 24 +++ 9 files changed, 675 insertions(+), 52 deletions(-) create mode 100644 plugins/node/instrumentation-runtime-node/src/metrics/baseCollector.ts create mode 100644 plugins/node/instrumentation-runtime-node/src/metrics/eventLoopLagCollector.ts create mode 100644 plugins/node/instrumentation-runtime-node/src/metrics/eventLoopUtilizationCollector.ts create mode 100644 plugins/node/instrumentation-runtime-node/src/metrics/gcCollector.ts create mode 100644 plugins/node/instrumentation-runtime-node/src/metrics/heapSizeAndUsedCollector.ts create mode 100644 plugins/node/instrumentation-runtime-node/src/metrics/heapSpacesSizeAndUsedCollector.ts create mode 100644 plugins/node/instrumentation-runtime-node/src/types/metricCollector.ts diff --git a/plugins/node/instrumentation-runtime-node/src/instrumentation.ts b/plugins/node/instrumentation-runtime-node/src/instrumentation.ts index c7733e2de4..a222757982 100644 --- a/plugins/node/instrumentation-runtime-node/src/instrumentation.ts +++ b/plugins/node/instrumentation-runtime-node/src/instrumentation.ts @@ -13,22 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EventLoopUtilization, performance } from 'node:perf_hooks'; -const { eventLoopUtilization } = performance; - import { InstrumentationBase } from '@opentelemetry/instrumentation'; import { VERSION } from './version'; import { RuntimeNodeInstrumentationConfig } from './types'; +import { MetricCollector } from './types/metricCollector'; +import { EventLoopUtilizationCollector } from './metrics/eventLoopUtilizationCollector'; +import { EventLoopLagCollector } from './metrics/eventLoopLagCollector'; +import { GCCollector } from './metrics/gcCollector'; +import { HeapSizeAndUsedCollector } from './metrics/heapSizeAndUsedCollector'; +import { HeapSpacesSizeAndUsedCollector } from './metrics/heapSpacesSizeAndUsedCollector'; -const ELUS_LENGTH = 2; const DEFAULT_CONFIG: RuntimeNodeInstrumentationConfig = { - eventLoopUtilizationMeasurementInterval: 5000, + monitoringPrecision: 5000, }; +const namePrefix = 'nodejs'; + export class RuntimeNodeInstrumentation extends InstrumentationBase { - private _ELUs: EventLoopUtilization[] = []; - private _interval: NodeJS.Timeout | undefined; + private _collectors: MetricCollector[] = []; constructor(config: RuntimeNodeInstrumentationConfig = {}) { super( @@ -36,37 +39,27 @@ export class RuntimeNodeInstrumentation extends InstrumentationBase { VERSION, Object.assign({}, DEFAULT_CONFIG, config) ); - } - - private _addELU() { - this._ELUs.unshift(eventLoopUtilization()); - if (this._ELUs.length > ELUS_LENGTH) { - this._ELUs.pop(); + this._collectors = [ + new EventLoopUtilizationCollector(this._config, namePrefix), + new EventLoopLagCollector(this._config, namePrefix), + new GCCollector(this._config, namePrefix), + new HeapSizeAndUsedCollector(this._config, namePrefix), + new HeapSpacesSizeAndUsedCollector(this._config, namePrefix), + ]; + if (this._config.enabled) { + for (const collector of this._collectors) { + collector.enable(); + } } } - private _clearELU() { - if (!this._ELUs) { - this._ELUs = []; - } - this._ELUs.length = 0; - } - // Called when a new `MeterProvider` is set // the Meter (result of @opentelemetry/api's getMeter) is available as this.meter within this method override _updateMetricInstruments() { - this.meter - .createObservableGauge('nodejs.event_loop.utilization', { - description: 'Event loop utilization', - unit: '1', - }) - .addCallback(async observableResult => { - if (this._ELUs.length !== ELUS_LENGTH) { - return; - } - const elu = eventLoopUtilization(...this._ELUs); - observableResult.observe(elu.utilization); - }); + if (!this._collectors) return; + for (const collector of this._collectors) { + collector.updateMetricInstruments(this.meter); + } } init() { @@ -74,22 +67,16 @@ export class RuntimeNodeInstrumentation extends InstrumentationBase { } override enable() { - this._clearELU(); - this._addELU(); - clearInterval(this._interval); - this._interval = setInterval( - () => this._addELU(), - (this._config as RuntimeNodeInstrumentationConfig) - .eventLoopUtilizationMeasurementInterval - ); + if (!this._collectors) return; - // unref so that it does not keep the process running if disable() is never called - this._interval?.unref(); + for (const collector of this._collectors) { + collector.enable(); + } } override disable() { - this._clearELU(); - clearInterval(this._interval); - this._interval = undefined; + for (const collector of this._collectors) { + collector.disable(); + } } } diff --git a/plugins/node/instrumentation-runtime-node/src/metrics/baseCollector.ts b/plugins/node/instrumentation-runtime-node/src/metrics/baseCollector.ts new file mode 100644 index 0000000000..438d4e9171 --- /dev/null +++ b/plugins/node/instrumentation-runtime-node/src/metrics/baseCollector.ts @@ -0,0 +1,73 @@ +/* + * 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 { MetricCollector } from '../types/metricCollector'; +import { Meter } from '@opentelemetry/api'; +import { clearInterval } from 'node:timers'; +import { RuntimeNodeInstrumentationConfig } from '../types'; + +export abstract class BaseCollector implements MetricCollector { + protected _config: RuntimeNodeInstrumentationConfig = {}; + + protected namePrefix: string; + private _interval: NodeJS.Timeout | undefined; + protected _scrapeQueue: T[] = []; + + constructor( + config: RuntimeNodeInstrumentationConfig = {}, + namePrefix: string + ) { + this._config = config; + this.namePrefix = namePrefix; + } + + public disable(): void { + this._clearQueue(); + clearInterval(this._interval); + this._interval = undefined; + + this.internalDisable(); + } + + public enable(): void { + this._clearQueue(); + clearInterval(this._interval); + this._interval = setInterval( + () => this._addTask(), + this._config.monitoringPrecision + ); + + // unref so that it does not keep the process running if disable() is never called + this._interval?.unref(); + + this.internalEnable(); + } + + private _clearQueue() { + this._scrapeQueue.length = 0; + } + + private _addTask() { + const taskResult = this.scrape(); + if (taskResult) { + this._scrapeQueue.push(taskResult); + } + } + + public abstract updateMetricInstruments(meter: Meter): void; + protected abstract internalEnable(): void; + protected abstract internalDisable(): void; + protected abstract scrape(): T; +} diff --git a/plugins/node/instrumentation-runtime-node/src/metrics/eventLoopLagCollector.ts b/plugins/node/instrumentation-runtime-node/src/metrics/eventLoopLagCollector.ts new file mode 100644 index 0000000000..401f1de6eb --- /dev/null +++ b/plugins/node/instrumentation-runtime-node/src/metrics/eventLoopLagCollector.ts @@ -0,0 +1,203 @@ +/* + * 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 { RuntimeNodeInstrumentationConfig } from '../types'; +import { Meter } from '@opentelemetry/api'; +import { IntervalHistogram } from 'node:perf_hooks'; +import { BaseCollector } from './baseCollector'; +import * as perf_hooks from 'node:perf_hooks'; + +const NODEJS_EVENTLOOP_LAG = 'event_loop.lag_seconds'; +const NODEJS_EVENTLOOP_LAG_MIN = 'event_loop.lag_min_seconds'; +const NODEJS_EVENTLOOP_LAG_MAX = 'event_loop.lag_max_seconds'; +const NODEJS_EVENTLOOP_LAG_MEAN = 'event_loop.lag_mean_seconds'; +const NODEJS_EVENTLOOP_LAG_STDDEV = 'event_loop.lag_stddev_seconds'; +const NODEJS_EVENTLOOP_LAG_P50 = 'event_loop.lag_p50_seconds'; +const NODEJS_EVENTLOOP_LAG_P90 = 'event_loop.lag_p90_seconds'; +const NODEJS_EVENTLOOP_LAG_P99 = 'event_loop.lag_p99_seconds'; + +export const metricNames = [ + { name: NODEJS_EVENTLOOP_LAG, description: 'Lag of event loop in seconds.' }, + { + name: NODEJS_EVENTLOOP_LAG_MIN, + description: 'The minimum recorded event loop delay.', + }, + { + name: NODEJS_EVENTLOOP_LAG_MAX, + description: 'The maximum recorded event loop delay.', + }, + { + name: NODEJS_EVENTLOOP_LAG_MEAN, + description: 'The mean of the recorded event loop delays.', + }, + { + name: NODEJS_EVENTLOOP_LAG_STDDEV, + description: 'The standard deviation of the recorded event loop delays.', + }, + { + name: NODEJS_EVENTLOOP_LAG_P50, + description: 'The 50th percentile of the recorded event loop delays.', + }, + { + name: NODEJS_EVENTLOOP_LAG_P90, + description: 'The 90th percentile of the recorded event loop delays.', + }, + { + name: NODEJS_EVENTLOOP_LAG_P99, + description: 'The 99th percentile of the recorded event loop delays.', + }, +]; + +export interface EventLoopLagInformation { + min: number; + max: number; + mean: number; + stddev: number; + p50: number; + p90: number; + p99: number; +} + +export class EventLoopLagCollector extends BaseCollector { + private _histogram: IntervalHistogram; + + constructor( + config: RuntimeNodeInstrumentationConfig = {}, + namePrefix: string + ) { + super(config, namePrefix); + this._histogram = perf_hooks.monitorEventLoopDelay({ + resolution: config.monitoringPrecision, + }); + } + + updateMetricInstruments(meter: Meter): void { + const lag = meter.createObservableGauge( + `${this.namePrefix}.${metricNames[0].name}`, + { + description: metricNames[0].description, + unit: '1', + } + ); + const lagMin = meter.createObservableGauge( + `${this.namePrefix}.${metricNames[1].name}`, + { + description: metricNames[1].description, + unit: '1', + } + ); + const lagMax = meter.createObservableGauge( + `${this.namePrefix}.${metricNames[2].name}`, + { + description: metricNames[2].description, + unit: '1', + } + ); + const lagMean = meter.createObservableGauge( + `${this.namePrefix}.${metricNames[3].name}`, + { + description: metricNames[3].description, + unit: '1', + } + ); + const lagStddev = meter.createObservableGauge( + `${this.namePrefix}.${metricNames[4].name}`, + { + description: metricNames[4].description, + unit: '1', + } + ); + const lagp50 = meter.createObservableGauge( + `${this.namePrefix}.${metricNames[5].name}`, + { + description: metricNames[5].description, + unit: '1', + } + ); + const lagp90 = meter.createObservableGauge( + `${this.namePrefix}.${metricNames[6].name}`, + { + description: metricNames[6].description, + unit: '1', + } + ); + const lagp99 = meter.createObservableGauge( + `${this.namePrefix}.${metricNames[7].name}`, + { + description: metricNames[7].description, + unit: '1', + } + ); + + meter.addBatchObservableCallback( + async observableResult => { + if (this._scrapeQueue.length === 0) return; + + const data = this._scrapeQueue.shift(); + if (data === undefined) return; + + const start = process.hrtime(); + const lagResult = await new Promise(res => { + setImmediate((start: [number, number]) => { + res(this._reportEventloopLag(start)); + }, start); + }); + + observableResult.observe(lag, lagResult); + observableResult.observe(lagMin, data.min); + observableResult.observe(lagMax, data.max); + observableResult.observe(lagMean, data.mean); + observableResult.observe(lagStddev, data.stddev); + observableResult.observe(lagp50, data.p50); + observableResult.observe(lagp90, data.p90); + observableResult.observe(lagp99, data.p99); + + this._histogram.reset(); + }, + [lag, lagMin, lagMax, lagMean, lagStddev, lagp50, lagp90, lagp99] + ); + } + + internalEnable(): void { + this._histogram.enable(); + } + + internalDisable(): void { + this._histogram.disable(); + } + + protected scrape(): EventLoopLagInformation { + return { + min: this.checkNan(this._histogram.min / 1e9), + max: this.checkNan(this._histogram.max / 1e9), + mean: this.checkNan(this._histogram.mean / 1e9), + stddev: this.checkNan(this._histogram.stddev / 1e9), + p50: this.checkNan(this._histogram.percentile(90) / 1e9), + p90: this.checkNan(this._histogram.percentile(90) / 1e9), + p99: this.checkNan(this._histogram.percentile(99) / 1e9), + }; + } + + private _reportEventloopLag(start: [number, number]): number { + const delta = process.hrtime(start); + const nanosec = delta[0] * 1e9 + delta[1]; + const seconds = nanosec / 1e9; + return seconds; + } + + private checkNan(value: number) { + return isNaN(value) ? 0 : value; + } +} diff --git a/plugins/node/instrumentation-runtime-node/src/metrics/eventLoopUtilizationCollector.ts b/plugins/node/instrumentation-runtime-node/src/metrics/eventLoopUtilizationCollector.ts new file mode 100644 index 0000000000..f44b400eb9 --- /dev/null +++ b/plugins/node/instrumentation-runtime-node/src/metrics/eventLoopUtilizationCollector.ts @@ -0,0 +1,58 @@ +/* + * 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 { EventLoopUtilization, performance } from 'node:perf_hooks'; +import { RuntimeNodeInstrumentationConfig } from '../types'; +import { Meter } from '@opentelemetry/api'; +import { BaseCollector } from './baseCollector'; + +const { eventLoopUtilization: eventLoopUtilizationCollector } = performance; + +const NODEJS_EVENT_LOOP_UTILIZATION = 'event_loop.utilization'; + +export class EventLoopUtilizationCollector extends BaseCollector { + constructor( + config: RuntimeNodeInstrumentationConfig = {}, + namePrefix: string + ) { + super(config, namePrefix); + } + + public updateMetricInstruments(meter: Meter): void { + meter + .createObservableGauge( + `${this.namePrefix}.${NODEJS_EVENT_LOOP_UTILIZATION}`, + { + description: 'Event loop utilization', + unit: '1', + } + ) + .addCallback(async observableResult => { + if (this._scrapeQueue.length === 0) { + return; + } + const elu = eventLoopUtilizationCollector(this._scrapeQueue.shift()); + observableResult.observe(elu.utilization); + }); + } + + protected internalDisable(): void {} + + protected internalEnable(): void {} + + protected scrape(): EventLoopUtilization { + return eventLoopUtilizationCollector(); + } +} diff --git a/plugins/node/instrumentation-runtime-node/src/metrics/gcCollector.ts b/plugins/node/instrumentation-runtime-node/src/metrics/gcCollector.ts new file mode 100644 index 0000000000..8991820c2a --- /dev/null +++ b/plugins/node/instrumentation-runtime-node/src/metrics/gcCollector.ts @@ -0,0 +1,82 @@ +/* + * 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 { RuntimeNodeInstrumentationConfig } from '../types'; +import { Meter } from '@opentelemetry/api'; +import { Histogram, ValueType } from '@opentelemetry/api'; +import { BaseCollector } from './baseCollector'; +import * as perf_hooks from 'node:perf_hooks'; +import { PerformanceObserver } from 'node:perf_hooks'; + +const NODEJS_GC_DURATION_SECONDS = 'gc.duration_seconds'; +const DEFAULT_GC_DURATION_BUCKETS = [0.001, 0.01, 0.1, 1, 2, 5]; + +const kinds: string[] = []; +kinds[perf_hooks.constants.NODE_PERFORMANCE_GC_MAJOR] = 'major'; +kinds[perf_hooks.constants.NODE_PERFORMANCE_GC_MINOR] = 'minor'; +kinds[perf_hooks.constants.NODE_PERFORMANCE_GC_INCREMENTAL] = 'incremental'; +kinds[perf_hooks.constants.NODE_PERFORMANCE_GC_WEAKCB] = 'weakcb'; + +export class GCCollector extends BaseCollector { + private _gcDurationByKindHistogram!: Histogram; + private _observer: PerformanceObserver; + + constructor( + config: RuntimeNodeInstrumentationConfig = {}, + namePrefix: string + ) { + super(config, namePrefix); + this._observer = new perf_hooks.PerformanceObserver(list => { + const entry = list.getEntries()[0]; + // Node < 16 uses entry.kind + // Node >= 16 uses entry.detail.kind + // See: https://nodejs.org/docs/latest-v16.x/api/deprecations.html#deprecations_dep0152_extension_performanceentry_properties + // @ts-ignore + const kind = entry.detail ? kinds[entry.detail.kind] : kinds[entry.kind]; + console.log(); + this._gcDurationByKindHistogram.record( + entry.duration / 1000, + Object.assign({ kind }) + ); + }); + } + + updateMetricInstruments(meter: Meter): void { + this._gcDurationByKindHistogram = meter.createHistogram( + `${this.namePrefix}.${NODEJS_GC_DURATION_SECONDS}`, + { + description: + 'Garbage collection duration by kind, one of major, minor, incremental or weakcb.', + unit: 'double', + valueType: ValueType.DOUBLE, + advice: { + explicitBucketBoundaries: DEFAULT_GC_DURATION_BUCKETS, + }, + } + ); + } + + internalEnable(): void { + this._observer.observe({ entryTypes: ['gc'] }); + } + + internalDisable(): void { + this._observer.disconnect(); + } + + protected scrape(): null { + return null; + } +} diff --git a/plugins/node/instrumentation-runtime-node/src/metrics/heapSizeAndUsedCollector.ts b/plugins/node/instrumentation-runtime-node/src/metrics/heapSizeAndUsedCollector.ts new file mode 100644 index 0000000000..7af05bb521 --- /dev/null +++ b/plugins/node/instrumentation-runtime-node/src/metrics/heapSizeAndUsedCollector.ts @@ -0,0 +1,93 @@ +/* + * 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 { RuntimeNodeInstrumentationConfig } from '../types'; +import { Meter } from '@opentelemetry/api'; +import { BaseCollector } from './baseCollector'; + +const NODEJS_HEAP_SIZE_TOTAL = 'heap_size_total_bytes'; +const NODEJS_HEAP_SIZE_USED = 'heap_size_used_bytes'; +const NODEJS_EXTERNAL_MEMORY = 'external_memory_bytes'; + +export const metricNames = [ + { + name: NODEJS_HEAP_SIZE_TOTAL, + description: 'Process heap size from Node.js in bytes.', + }, + { + name: NODEJS_HEAP_SIZE_USED, + description: 'Process heap size used from Node.js in bytes.', + }, + { + name: NODEJS_EXTERNAL_MEMORY, + description: 'Node.js external memory size in bytes.', + }, +]; + +export class HeapSizeAndUsedCollector extends BaseCollector { + constructor( + config: RuntimeNodeInstrumentationConfig = {}, + namePrefix: string + ) { + super(config, namePrefix); + } + + updateMetricInstruments(meter: Meter): void { + const heapSizeTotal = meter.createObservableGauge( + `${this.namePrefix}.${metricNames[0].name}`, + { + description: metricNames[0].description, + unit: '1', + } + ); + const heapSizeUsed = meter.createObservableGauge( + `${this.namePrefix}.${metricNames[1].name}`, + { + description: metricNames[1].description, + unit: '1', + } + ); + const externalMemUsed = meter.createObservableGauge( + `${this.namePrefix}.${metricNames[2].name}`, + { + description: metricNames[2].description, + unit: '1', + } + ); + + meter.addBatchObservableCallback( + observableResult => { + if (this._scrapeQueue.length === 0) return; + + const data = this._scrapeQueue.shift(); + if (data === undefined) return; + observableResult.observe(heapSizeTotal, data.heapTotal); + observableResult.observe(heapSizeUsed, data.heapUsed); + if (data.external !== undefined) { + observableResult.observe(externalMemUsed, data.external); + } + }, + [heapSizeTotal, heapSizeUsed, externalMemUsed] + ); + } + + internalEnable(): void {} + + internalDisable(): void {} + + protected scrape(): NodeJS.MemoryUsage { + return process.memoryUsage(); + } +} diff --git a/plugins/node/instrumentation-runtime-node/src/metrics/heapSpacesSizeAndUsedCollector.ts b/plugins/node/instrumentation-runtime-node/src/metrics/heapSpacesSizeAndUsedCollector.ts new file mode 100644 index 0000000000..62d817c126 --- /dev/null +++ b/plugins/node/instrumentation-runtime-node/src/metrics/heapSpacesSizeAndUsedCollector.ts @@ -0,0 +1,109 @@ +/* + * 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 { RuntimeNodeInstrumentationConfig } from '../types'; +import { Meter } from '@opentelemetry/api'; +import { BaseCollector } from './baseCollector'; +import * as v8 from 'node:v8'; +import { HeapSpaceInfo } from 'v8'; + +const NODEJS_HEAP_SPACE_TOTAL = 'heap_space_total_bytes'; +const NODEJS_HEAP_SPACE_USED = 'heap_space_used_bytes'; +const NODEJS_HEAP_SPACE_AVAILABLE = 'heap_space_available_bytes'; + +export const metricNames = [ + { + name: NODEJS_HEAP_SPACE_TOTAL, + description: 'Process heap space size total from Node.js in bytes.', + }, + { + name: NODEJS_HEAP_SPACE_USED, + description: 'Process heap space size used from Node.js in bytes.', + }, + { + name: NODEJS_HEAP_SPACE_AVAILABLE, + description: 'Process heap space size available from Node.js in bytes.', + }, +]; + +export class HeapSpacesSizeAndUsedCollector extends BaseCollector< + HeapSpaceInfo[] +> { + constructor( + config: RuntimeNodeInstrumentationConfig = {}, + namePrefix: string + ) { + super(config, namePrefix); + } + + updateMetricInstruments(meter: Meter): void { + const heapSpaceTotal = meter.createObservableGauge( + `${this.namePrefix}.${metricNames[0].name}`, + { + description: metricNames[0].description, + unit: 'bytes', + } + ); + const heapSpaceUsed = meter.createObservableGauge( + `${this.namePrefix}.${metricNames[1].name}`, + { + description: metricNames[1].description, + unit: 'bytes', + } + ); + const heapSpaceAvailable = meter.createObservableGauge( + `${this.namePrefix}.${metricNames[2].name}`, + { + description: metricNames[2].description, + unit: 'bytes', + } + ); + + meter.addBatchObservableCallback( + observableResult => { + if (this._scrapeQueue.length === 0) return; + + const data = this._scrapeQueue.shift(); + if (data === undefined) return; + for (const space of data) { + const spaceName = space.space_name.substring( + 0, + space.space_name.indexOf('_space') + ); + observableResult.observe(heapSpaceTotal, space.space_size, { + space: spaceName, + }); + observableResult.observe(heapSpaceUsed, space.space_used_size, { + space: spaceName, + }); + observableResult.observe( + heapSpaceAvailable, + space.space_available_size, + { space: spaceName } + ); + } + }, + [heapSpaceTotal, heapSpaceUsed, heapSpaceAvailable] + ); + } + + internalEnable(): void {} + + internalDisable(): void {} + + protected scrape(): HeapSpaceInfo[] { + return v8.getHeapSpaceStatistics(); + } +} diff --git a/plugins/node/instrumentation-runtime-node/src/types.ts b/plugins/node/instrumentation-runtime-node/src/types.ts index af0c5b12ef..68227c98f2 100644 --- a/plugins/node/instrumentation-runtime-node/src/types.ts +++ b/plugins/node/instrumentation-runtime-node/src/types.ts @@ -17,11 +17,5 @@ import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; export interface RuntimeNodeInstrumentationConfig extends InstrumentationConfig { - /** - * 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. - * @default 5000 - */ - eventLoopUtilizationMeasurementInterval?: number; + monitoringPrecision?: number; } diff --git a/plugins/node/instrumentation-runtime-node/src/types/metricCollector.ts b/plugins/node/instrumentation-runtime-node/src/types/metricCollector.ts new file mode 100644 index 0000000000..bfb7eda171 --- /dev/null +++ b/plugins/node/instrumentation-runtime-node/src/types/metricCollector.ts @@ -0,0 +1,24 @@ +/* + * 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 { Meter } from '@opentelemetry/api'; + +export interface MetricCollector { + updateMetricInstruments(meter: Meter): void; + + enable(): void; + + disable(): void; +}