Skip to content

Commit 969bb62

Browse files
authored
fix(tracing): make spans resilient to performance clock drift (#3434)
* fix(tracing): make spans resilient to performance clock drift * Fix changelog * Do not export getTimeOrigin * Apply shift to shim spans * Lint * Lint * Remove unused imports * Fix drift calculation * Remove bad import * Use Date.now for fetch span end * Fetch changelog * Test addHrTimes * lint * lint * lint * Fix flaky test * lint * Use perf timer for perf API * Apply date fix to xhr * Changelog * lint * Revert to previous addHrTimes impl * Review comments * Fix flaky test
1 parent 2dcc898 commit 969bb62

File tree

9 files changed

+190
-82
lines changed

9 files changed

+190
-82
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ For experimental package changes, see the [experimental CHANGELOG](experimental/
2222
* `telemetry.sdk.name`
2323
* `telemetry.sdk.language`
2424
* `telemetry.sdk.version`
25+
* fix(sdk-trace): make spans resilient to clock drift [#3434](https://github.com/open-telemetry/opentelemetry-js/pull/3434) @dyladan
2526
* fix(selenium-tests): updated webpack version for selenium test issue [#3456](https://github.com/open-telemetry/opentelemetry-js/issues/3456) @SaumyaBhushan
2627
* fix(sdk-metrics): fix duplicated registration of metrics for collectors [#3488](https://github.com/open-telemetry/opentelemetry-js/pull/3488) @legendecas
2728
* fix(core): fix precision loss in numberToHrtime [#3480](https://github.com/open-telemetry/opentelemetry-js/pull/3480) @legendecas

experimental/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ All notable changes to experimental packages in this project will be documented
2323
* fix(prometheus-sanitization): replace repeated `_` with a single `_` [3470](https://github.com/open-telemetry/opentelemetry-js/pull/3470) @samimusallam
2424
* fix(prometheus-serializer): correct string used for NaN [#3477](https://github.com/open-telemetry/opentelemetry-js/pull/3477) @JacksonWeber
2525
* fix(instrumentation-http): close server span when response finishes [#3407](https://github.com/open-telemetry/opentelemetry-js/pull/3407) @legendecas
26+
* fix(instrumentation-fetch): make spans resilient to clock drift by using Date.now [#3434](https://github.com/open-telemetry/opentelemetry-js/pull/3434) @dyladan
27+
* fix(instrumentation-xml-http-request): make spans resilient to clock drift by using Date.now [#3434](https://github.com/open-telemetry/opentelemetry-js/pull/3434) @dyladan
2628
* fix(sdk-node): fix exporter to be read only OTEL_TRACES_EXPORTER is set to a valid exporter [3492] @svetlanabrennan
2729

2830
### :books: (Refine Doc)

experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,12 +276,13 @@ export class FetchInstrumentation extends InstrumentationBase<
276276
spanData: SpanData,
277277
response: FetchResponse
278278
) {
279-
const endTime = core.hrTime();
279+
const endTime = core.millisToHrTime(Date.now());
280+
const performanceEndTime = core.hrTime();
280281
this._addFinalSpanAttributes(span, response);
281282

282283
setTimeout(() => {
283284
spanData.observer?.disconnect();
284-
this._findResourceAndAddNetworkEvents(span, spanData, endTime);
285+
this._findResourceAndAddNetworkEvents(span, spanData, performanceEndTime);
285286
this._tasksCount--;
286287
this._clearResources();
287288
span.end(endTime);

experimental/packages/opentelemetry-instrumentation-xml-http-request/src/xhr.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,8 @@ export class XMLHttpRequestInstrumentation extends InstrumentationBase<XMLHttpRe
389389
function endSpanTimeout(
390390
eventName: string,
391391
xhrMem: XhrMem,
392-
endTime: api.HrTime
392+
performanceEndTime: api.HrTime,
393+
endTime: number
393394
) {
394395
const callbackToRemoveEvents = xhrMem.callbackToRemoveEvents;
395396

@@ -405,7 +406,7 @@ export class XMLHttpRequestInstrumentation extends InstrumentationBase<XMLHttpRe
405406
span,
406407
spanUrl,
407408
sendStartTime,
408-
endTime
409+
performanceEndTime
409410
);
410411
span.addEvent(eventName, endTime);
411412
plugin._addFinalSpanAttributes(span, xhrMem, spanUrl);
@@ -427,13 +428,14 @@ export class XMLHttpRequestInstrumentation extends InstrumentationBase<XMLHttpRe
427428
if (xhrMem.span) {
428429
plugin._applyAttributesAfterXHR(xhrMem.span, xhr);
429430
}
430-
const endTime = hrTime();
431+
const performanceEndTime = hrTime();
432+
const endTime = Date.now();
431433

432434
// the timeout is needed as observer doesn't have yet information
433435
// when event "load" is called. Also the time may differ depends on
434436
// browser and speed of computer
435437
setTimeout(() => {
436-
endSpanTimeout(eventName, xhrMem, endTime);
438+
endSpanTimeout(eventName, xhrMem, performanceEndTime, endTime);
437439
}, OBSERVER_WAIT_TIME_MS);
438440
}
439441

packages/opentelemetry-core/src/common/time.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const SECOND_TO_NANOSECONDS = Math.pow(10, NANOSECOND_DIGITS);
2727
* Converts a number of milliseconds from epoch to HrTime([seconds, remainder in nanoseconds]).
2828
* @param epochMillis
2929
*/
30-
function numberToHrtime(epochMillis: number): api.HrTime {
30+
export function millisToHrTime(epochMillis: number): api.HrTime {
3131
const epochSeconds = epochMillis / 1000;
3232
// Decimals only.
3333
const seconds = Math.trunc(epochSeconds);
@@ -36,7 +36,7 @@ function numberToHrtime(epochMillis: number): api.HrTime {
3636
return [seconds, nanos];
3737
}
3838

39-
function getTimeOrigin(): number {
39+
export function getTimeOrigin(): number {
4040
let timeOrigin = performance.timeOrigin;
4141
if (typeof timeOrigin !== 'number') {
4242
const perf: TimeOriginLegacy = performance as unknown as TimeOriginLegacy;
@@ -50,21 +50,12 @@ function getTimeOrigin(): number {
5050
* @param performanceNow
5151
*/
5252
export function hrTime(performanceNow?: number): api.HrTime {
53-
const timeOrigin = numberToHrtime(getTimeOrigin());
54-
const now = numberToHrtime(
53+
const timeOrigin = millisToHrTime(getTimeOrigin());
54+
const now = millisToHrTime(
5555
typeof performanceNow === 'number' ? performanceNow : performance.now()
5656
);
5757

58-
let seconds = timeOrigin[0] + now[0];
59-
let nanos = timeOrigin[1] + now[1];
60-
61-
// Nanoseconds
62-
if (nanos > SECOND_TO_NANOSECONDS) {
63-
nanos -= SECOND_TO_NANOSECONDS;
64-
seconds += 1;
65-
}
66-
67-
return [seconds, nanos];
58+
return addHrTimes(timeOrigin, now);
6859
}
6960

7061
/**
@@ -82,10 +73,10 @@ export function timeInputToHrTime(time: api.TimeInput): api.HrTime {
8273
return hrTime(time);
8374
} else {
8475
// epoch milliseconds or performance.timeOrigin
85-
return numberToHrtime(time);
76+
return millisToHrTime(time);
8677
}
8778
} else if (time instanceof Date) {
88-
return numberToHrtime(time.getTime());
79+
return millisToHrTime(time.getTime());
8980
} else {
9081
throw TypeError('Invalid input type');
9182
}
@@ -175,3 +166,18 @@ export function isTimeInput(
175166
value instanceof Date
176167
);
177168
}
169+
170+
/**
171+
* Given 2 HrTime formatted times, return their sum as an HrTime.
172+
*/
173+
export function addHrTimes(time1: api.HrTime, time2: api.HrTime): api.HrTime {
174+
const out = [time1[0] + time2[0], time1[1] + time2[1]] as api.HrTime;
175+
176+
// Nanoseconds
177+
if (out[1] >= SECOND_TO_NANOSECONDS) {
178+
out[1] -= SECOND_TO_NANOSECONDS;
179+
out[0] += 1;
180+
}
181+
182+
return out;
183+
}

packages/opentelemetry-core/test/common/time.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
hrTimeToMicroseconds,
2828
hrTimeToTimeStamp,
2929
isTimeInput,
30+
addHrTimes,
3031
} from '../../src/common/time';
3132

3233
describe('time', () => {
@@ -224,4 +225,34 @@ describe('time', () => {
224225
assert.strictEqual(isTimeInput(undefined), false);
225226
});
226227
});
228+
229+
describe('#addHrTimes', () => {
230+
const NANOSECOND_DIGITS = 9;
231+
const SECOND_TO_NANOSECONDS = Math.pow(10, NANOSECOND_DIGITS);
232+
233+
it('should add two positive times', () => {
234+
const output = addHrTimes([10, 20], [30, 40]);
235+
assert.deepStrictEqual(output, [40, 60]);
236+
});
237+
it('should add two negative times', () => {
238+
const output = addHrTimes([-10, 20], [-30, 40]);
239+
assert.deepStrictEqual(output, [-40, 60]);
240+
});
241+
it('should add a positive and negative time (result positive)', () => {
242+
const output = addHrTimes([-10, 20], [30, 40]);
243+
assert.deepStrictEqual(output, [20, 60]);
244+
});
245+
it('should add a positive and negative time (result negative)', () => {
246+
const output = addHrTimes([10, 20], [-30, 40]);
247+
assert.deepStrictEqual(output, [-20, 60]);
248+
});
249+
it('should overflow nanoseconds to seconds', () => {
250+
const output = addHrTimes([10, SECOND_TO_NANOSECONDS - 10], [10, 20]);
251+
assert.deepStrictEqual(output, [21, 10]);
252+
});
253+
it('should overflow nanoseconds to seconds (negative)', () => {
254+
const output = addHrTimes([-10, SECOND_TO_NANOSECONDS - 10], [-10, 20]);
255+
assert.deepStrictEqual(output, [-19, 10]);
256+
});
257+
});
227258
});

0 commit comments

Comments
 (0)