Skip to content

Commit 06c503c

Browse files
authored
fix(instrumentation-runtime-node): use absolute results in eventLoopUtilization computation (#3118)
1 parent 4730498 commit 06c503c

File tree

2 files changed

+93
-4
lines changed

2 files changed

+93
-4
lines changed

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import { METRIC_NODEJS_EVENTLOOP_UTILIZATION } from '../semconv';
2222
const { eventLoopUtilization: eventLoopUtilizationCollector } = performance;
2323

2424
export class EventLoopUtilizationCollector extends BaseCollector {
25-
private _lastValue?: EventLoopUtilization;
25+
// Value needs to be initialized the first time otherwise the first measurement would always be 1
26+
// See https://github.com/open-telemetry/opentelemetry-js-contrib/pull/3118#issuecomment-3429737955
27+
private _lastValue: EventLoopUtilization = eventLoopUtilizationCollector();
2628

2729
public updateMetricInstruments(meter: Meter): void {
2830
meter
@@ -33,9 +35,13 @@ export class EventLoopUtilizationCollector extends BaseCollector {
3335
.addCallback(async observableResult => {
3436
if (!this._config.enabled) return;
3537

36-
const elu = eventLoopUtilizationCollector(this._lastValue);
37-
observableResult.observe(elu.utilization);
38-
this._lastValue = elu;
38+
const currentELU = eventLoopUtilizationCollector();
39+
const deltaELU = eventLoopUtilizationCollector(
40+
currentELU,
41+
this._lastValue
42+
);
43+
this._lastValue = currentELU;
44+
observableResult.observe(deltaELU.utilization);
3945
});
4046
}
4147

packages/instrumentation-runtime-node/test/event_loop_utilization.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,87 @@ describe('nodejs.eventloop.utilization', function () {
9898
'expected one data point'
9999
);
100100
});
101+
102+
it('should correctly calculate utilization deltas across multiple measurements', async function () {
103+
// This test ensures the bug where delta of deltas was observed instead of deltas of absolute values
104+
// does not regress. See https://github.com/open-telemetry/opentelemetry-js-contrib/pull/3118
105+
// This bug would surface on the third callback invocation.
106+
107+
const instrumentation = new RuntimeNodeInstrumentation({});
108+
instrumentation.setMeterProvider(meterProvider);
109+
110+
// Helper function to create blocking work that results in high utilization
111+
const createBlockingWork = (durationMs: number) => {
112+
const start = Date.now();
113+
while (Date.now() - start < durationMs) {
114+
// Busy wait to block the event loop
115+
}
116+
};
117+
118+
// Helper function to collect metrics and extract utilization value
119+
const collectUtilization = async (): Promise<number> => {
120+
const { resourceMetrics } = await metricReader.collect();
121+
const scopeMetrics = resourceMetrics.scopeMetrics;
122+
const utilizationMetric = scopeMetrics[0].metrics.find(
123+
x => x.descriptor.name === METRIC_NODEJS_EVENTLOOP_UTILIZATION
124+
);
125+
126+
assert.notEqual(utilizationMetric, undefined, 'metric not found');
127+
assert.strictEqual(
128+
utilizationMetric!.dataPoints.length,
129+
1,
130+
'expected one data point'
131+
);
132+
133+
return utilizationMetric!.dataPoints[0].value as number;
134+
};
135+
136+
// Wait for some time to establish baseline utilization
137+
await new Promise(resolve => setTimeout(resolve, 200));
138+
139+
// First collection
140+
const firstUtilization = await collectUtilization();
141+
assert.notStrictEqual(
142+
firstUtilization,
143+
1,
144+
'Expected utilization in first measurement to be not 1'
145+
);
146+
147+
// Second measurement: Create blocking work and measure
148+
createBlockingWork(50);
149+
const secondUtilization = await collectUtilization();
150+
assert.strictEqual(
151+
secondUtilization,
152+
1,
153+
'Expected utilization in second measurement to be 1'
154+
);
155+
156+
// Third measurement: Create blocking work again and measure
157+
// This is where the bug would manifest - if we were observing delta of deltas,
158+
// this measurement would not be 1
159+
createBlockingWork(50);
160+
const thirdUtilization = await collectUtilization();
161+
assert.strictEqual(
162+
thirdUtilization,
163+
1,
164+
'Expected utilization in third measurement to be 1'
165+
);
166+
167+
// Fourth measurement (should be the same as the third measurement, just a sanity check)
168+
createBlockingWork(50);
169+
const fourthUtilization = await collectUtilization();
170+
assert.strictEqual(
171+
fourthUtilization,
172+
1,
173+
'Expected utilization in fourth measurement to be 1'
174+
);
175+
176+
// Fifth measurement: Do some NON-blocking work (sanity check, should be low)
177+
await new Promise(resolve => setTimeout(resolve, 50));
178+
const fifthUtilization = await collectUtilization();
179+
assert.ok(
180+
fifthUtilization < 1,
181+
`Expected utilization in fifth measurement to be less than 1, but got ${fifthUtilization}`
182+
);
183+
});
101184
});

0 commit comments

Comments
 (0)