Skip to content

Commit a69fc5c

Browse files
committed
chore(instrumentation-runtime-node): addnodejs.eventloop.time, fix tests
1 parent 5efdd9d commit a69fc5c

13 files changed

+259
-119
lines changed

package-lock.json

Lines changed: 19 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/node/instrumentation-runtime-node/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"author": "OpenTelemetry Authors",
2121
"license": "Apache-2.0",
2222
"engines": {
23-
"node": ">=14.10.0"
23+
"node": ">=17.4.0"
2424
},
2525
"keywords": [
2626
"perf_hooks",
@@ -45,7 +45,7 @@
4545
"@opentelemetry/api": "^1.3.0",
4646
"@opentelemetry/sdk-metrics": "^1.20.0",
4747
"@types/mocha": "^10.0.6",
48-
"@types/node": "^20.11.2",
48+
"@types/node": "^22.1.0",
4949
"mocha": "7.2.0",
5050
"nyc": "^15.1.0",
5151
"rimraf": "5.0.5",

plugins/node/instrumentation-runtime-node/src/instrumentation.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { EventLoopDelayCollector } from './metrics/eventLoopDelayCollector';
2323
import { GCCollector } from './metrics/gcCollector';
2424
import { HeapSpacesSizeAndUsedCollector } from './metrics/heapSpacesSizeAndUsedCollector';
2525
import { ConventionalNamePrefix } from './types/ConventionalNamePrefix';
26+
import {EventLoopTimeCollector} from "./metrics/eventLoopTimeCollector";
2627

2728
const DEFAULT_CONFIG: RuntimeNodeInstrumentationConfig = {
2829
monitoringPrecision: 10,
@@ -42,10 +43,11 @@ export class RuntimeNodeInstrumentation extends InstrumentationBase {
4243
this._config,
4344
ConventionalNamePrefix.NodeJs
4445
),
45-
new EventLoopDelayCollector(
46+
new EventLoopTimeCollector(
4647
this._config,
4748
ConventionalNamePrefix.NodeJs
4849
),
50+
new EventLoopDelayCollector(this._config, ConventionalNamePrefix.NodeJs),
4951
new GCCollector(this._config, ConventionalNamePrefix.V8js),
5052
new HeapSpacesSizeAndUsedCollector(
5153
this._config,

plugins/node/instrumentation-runtime-node/src/metrics/baseCollector.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { MetricCollector } from '../types/metricCollector';
1717
import { Meter } from '@opentelemetry/api';
1818
import { RuntimeNodeInstrumentationConfig } from '../types';
1919

20-
2120
export abstract class BaseCollector implements MetricCollector {
2221
protected _config: RuntimeNodeInstrumentationConfig = {};
2322

@@ -41,7 +40,6 @@ export abstract class BaseCollector implements MetricCollector {
4140
this.internalEnable();
4241
}
4342

44-
4543
public abstract updateMetricInstruments(meter: Meter): void;
4644

4745
protected abstract internalEnable(): void;

plugins/node/instrumentation-runtime-node/src/metrics/eventLoopDelayCollector.ts

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import {RuntimeNodeInstrumentationConfig} from '../types';
17-
import {Meter} from '@opentelemetry/api';
16+
import { RuntimeNodeInstrumentationConfig } from '../types';
17+
import { Meter } from '@opentelemetry/api';
1818
import * as perf_hooks from 'node:perf_hooks';
19-
import {IntervalHistogram} from 'node:perf_hooks';
20-
import {BaseCollector} from './baseCollector';
19+
import { IntervalHistogram } from 'node:perf_hooks';
20+
import { BaseCollector } from './baseCollector';
2121

2222
enum NodeJsEventLoopDelay {
2323
min = 'eventloop.delay.min',
@@ -26,7 +26,7 @@ enum NodeJsEventLoopDelay {
2626
stddev = 'eventloop.delay.stddev',
2727
p50 = 'eventloop.delay.p50',
2828
p90 = 'eventloop.delay.p90',
29-
p99 = 'eventloop.delay.p99'
29+
p99 = 'eventloop.delay.p99',
3030
}
3131

3232
export const metricNames: Record<
@@ -53,7 +53,7 @@ export const metricNames: Record<
5353
},
5454
[NodeJsEventLoopDelay.p99]: {
5555
description: 'Event loop 99 percentile delay.',
56-
}
56+
},
5757
};
5858

5959
export interface EventLoopLagInformation {
@@ -132,10 +132,11 @@ export class EventLoopDelayCollector extends BaseCollector {
132132

133133
meter.addBatchObservableCallback(
134134
async observableResult => {
135-
if(!this._config.enabled) return
135+
if (!this._config.enabled) return;
136136

137137
const data = this.scrape();
138138
if (data === undefined) return;
139+
if (this._histogram.count < 5) return; // Don't return histogram data if we have less than 5 samples
139140

140141
observableResult.observe(delayMin, data.min);
141142
observableResult.observe(delayMax, data.max);
@@ -147,15 +148,7 @@ export class EventLoopDelayCollector extends BaseCollector {
147148

148149
this._histogram.reset();
149150
},
150-
[
151-
delayMin,
152-
delayMax,
153-
delayMean,
154-
delayStddev,
155-
delayp50,
156-
delayp90,
157-
delayp99,
158-
]
151+
[delayMin, delayMax, delayMean, delayStddev, delayp50, delayp90, delayp99]
159152
);
160153
}
161154

@@ -179,7 +172,6 @@ export class EventLoopDelayCollector extends BaseCollector {
179172
};
180173
}
181174

182-
183175
private checkNan(value: number) {
184176
return isNaN(value) ? 0 : value;
185177
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { EventLoopUtilization, performance } from 'node:perf_hooks';
17+
import { RuntimeNodeInstrumentationConfig } from '../types';
18+
import { Meter } from '@opentelemetry/api';
19+
import { BaseCollector } from './baseCollector';
20+
21+
const { eventLoopUtilization: eventLoopUtilizationCollector } = performance;
22+
23+
export const NODEJS_EVENT_LOOP_TIME = 'eventloop.time';
24+
25+
export class EventLoopTimeCollector extends BaseCollector {
26+
constructor(
27+
config: RuntimeNodeInstrumentationConfig = {},
28+
namePrefix: string
29+
) {
30+
super(config, namePrefix);
31+
}
32+
33+
public updateMetricInstruments(meter: Meter): void {
34+
const timeCounter = meter.createObservableCounter(
35+
`${this.namePrefix}.${NODEJS_EVENT_LOOP_TIME}`,
36+
{
37+
description:
38+
'Cumulative duration of time the event loop has been in each state.',
39+
unit: 's',
40+
}
41+
);
42+
43+
meter.addBatchObservableCallback(
44+
async observableResult => {
45+
if (!this._config.enabled) return;
46+
47+
const data = this.scrape();
48+
if (data === undefined) return;
49+
50+
observableResult.observe(timeCounter, data.active / 1000, {
51+
[`${this.namePrefix}.eventloop.state`]: 'active',
52+
});
53+
observableResult.observe(timeCounter, data.idle / 1000, {
54+
[`${this.namePrefix}.eventloop.state`]: 'idle',
55+
});
56+
},
57+
[timeCounter]
58+
);
59+
}
60+
61+
protected internalDisable(): void {}
62+
63+
protected internalEnable(): void {}
64+
65+
private scrape(): EventLoopUtilization {
66+
return eventLoopUtilizationCollector();
67+
}
68+
}

plugins/node/instrumentation-runtime-node/src/metrics/eventLoopUtilizationCollector.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,14 @@ export class EventLoopUtilizationCollector extends BaseCollector {
4040
}
4141
)
4242
.addCallback(async observableResult => {
43-
if(!this._config.enabled) return
43+
if (!this._config.enabled) return;
4444

4545
const elu = eventLoopUtilizationCollector(this.scrape());
4646
observableResult.observe(elu.utilization);
4747
});
4848
}
4949

50-
protected internalDisable(): void {
51-
52-
}
50+
protected internalDisable(): void {}
5351

5452
protected internalEnable(): void {}
5553

plugins/node/instrumentation-runtime-node/src/metrics/gcCollector.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class GCCollector extends BaseCollector {
3939
) {
4040
super(config, namePrefix);
4141
this._observer = new perf_hooks.PerformanceObserver(list => {
42-
if(!this._config.enabled) return
42+
if (!this._config.enabled) return;
4343

4444
const entry = list.getEntries()[0];
4545
// Node < 16 uses entry.kind
@@ -50,9 +50,7 @@ export class GCCollector extends BaseCollector {
5050
const kind = entry.detail ? kinds[entry.detail.kind] : kinds[entry.kind];
5151
this._gcDurationByKindHistogram?.record(
5252
entry.duration / 1000,
53-
Object.assign(
54-
{ [`${this.namePrefix}.gc.type`]: kind }
55-
)
53+
Object.assign({ [`${this.namePrefix}.gc.type`]: kind })
5654
);
5755
});
5856
}

plugins/node/instrumentation-runtime-node/src/metrics/heapSpacesSizeAndUsedCollector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export class HeapSpacesSizeAndUsedCollector extends BaseCollector {
8484

8585
meter.addBatchObservableCallback(
8686
observableResult => {
87-
if(!this._config.enabled) return
87+
if (!this._config.enabled) return;
8888

8989
const data = this.scrape();
9090
if (data === undefined) return;

plugins/node/instrumentation-runtime-node/test/event_loop_delay.test.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import { TestMetricReader } from './testMetricsReader';
2121
import { metricNames } from '../src/metrics/eventLoopDelayCollector';
2222
import { ConventionalNamePrefix } from '../src/types/ConventionalNamePrefix';
2323

24-
const MEASUREMENT_INTERVAL = 10;
2524

2625
describe(`${ConventionalNamePrefix.NodeJs}.eventloop`, function () {
2726
let metricReader: TestMetricReader;
@@ -37,13 +36,13 @@ describe(`${ConventionalNamePrefix.NodeJs}.eventloop`, function () {
3736
it(`should write ${ConventionalNamePrefix.NodeJs}.${metricName} after monitoringPrecision`, async function () {
3837
// arrange
3938
const instrumentation = new RuntimeNodeInstrumentation({
40-
monitoringPrecision: MEASUREMENT_INTERVAL,
39+
monitoringPrecision: 10,
4140
});
4241
instrumentation.setMeterProvider(meterProvider);
4342

4443
// act
4544
await new Promise(resolve =>
46-
setTimeout(resolve, MEASUREMENT_INTERVAL * 5)
45+
setTimeout(resolve, 100)
4746
);
4847
const { resourceMetrics, errors } = await metricReader.collect();
4948

@@ -56,8 +55,7 @@ describe(`${ConventionalNamePrefix.NodeJs}.eventloop`, function () {
5655
const scopeMetrics = resourceMetrics.scopeMetrics;
5756
const metric = scopeMetrics[0].metrics.find(
5857
x =>
59-
x.descriptor.name ===
60-
`${ConventionalNamePrefix.NodeJs}.${metricName}`
58+
x.descriptor.name === `${ConventionalNamePrefix.NodeJs}.${metricName}`
6159
);
6260

6361
assert.notEqual(

0 commit comments

Comments
 (0)