Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(sdk-trace-base): make span start times resistant to hrtime clock drift #3129

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ All notable changes to this project will be documented in this file.

### :bug: (Bug Fix)

* fix(sdk-trace-base): make span start times resistant to hrtime clock drift
[#3129](https://github.com/open-telemetry/opentelemetry-js/issues/3129)

### :books: (Refine Doc)

### :house: (Internal)
Expand Down
12 changes: 12 additions & 0 deletions packages/opentelemetry-core/src/common/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ export function hrTimeDuration(
return [seconds, nanos];
}

/**
* Add an HrTime duration to an HrTime
*/
export function addHrTime(time: api.HrTime, duration: api.HrTime): api.HrTime {
const out: api.HrTime = [time[0] + duration[0], time[1] + duration[1]];
if (out[1] >= 1e9) {
out[1] -= 1e9;
out[0] += 1;
}
return out;
}

/**
* Convert hrTime to timestamp, for example "2019-05-14T17:00:00.000123456Z"
* @param time
Expand Down
18 changes: 18 additions & 0 deletions packages/opentelemetry-core/test/common/time.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
hrTimeToMicroseconds,
hrTimeToTimeStamp,
isTimeInput,
addHrTime,
} from '../../src/common/time';

describe('time', () => {
Expand Down Expand Up @@ -151,6 +152,23 @@ describe('time', () => {
});
});

describe('#addHrTime', () => {
it('should add two HrTimes', () => {
const startTime: api.HrTime = [22, 400000000];
const endTime: api.HrTime = [32, 200000000];

const output = addHrTime(startTime, endTime);
assert.deepStrictEqual(output, [54, 600000000]);
});
it('should overflow nanoseconds', () => {
const startTime: api.HrTime = [22, 600000000];
const endTime: api.HrTime = [32, 700000000];

const output = addHrTime(startTime, endTime);
assert.deepStrictEqual(output, [55, 300000000]);
});
});

describe('#hrTimeToTimeStamp', () => {
it('should return timestamp', () => {
const time: api.HrTime = [1573513121, 123456];
Expand Down
22 changes: 16 additions & 6 deletions packages/opentelemetry-sdk-trace-base/src/Span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
isTimeInput,
timeInputToHrTime,
sanitizeAttributes,
addHrTime,
} from '@opentelemetry/core';
import { Resource } from '@opentelemetry/resources';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
Expand Down Expand Up @@ -59,6 +60,7 @@ export class Span implements api.Span, ReadableSpan {
private readonly _spanProcessor: SpanProcessor;
private readonly _spanLimits: SpanLimits;
private readonly _attributeValueLengthLimit: number;
private readonly _hrStartTime: api.HrTime;

/** Constructs a new Span instance. */
constructor(
Expand All @@ -69,14 +71,15 @@ export class Span implements api.Span, ReadableSpan {
kind: api.SpanKind,
parentSpanId?: string,
links: api.Link[] = [],
startTime: api.TimeInput = hrTime()
startTime?: api.TimeInput
) {
this.name = spanName;
this._spanContext = spanContext;
this.parentSpanId = parentSpanId;
this.kind = kind;
this.links = links;
this.startTime = timeInputToHrTime(startTime);
this._hrStartTime = hrTime();
this.startTime = timeInputToHrTime(startTime ?? Date.now());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In principle it may happen that the process gets suspended for a few 100ms between line 81 and 82 resulting in two different start times - one used as start time one used to calc duration.

Not sure if it is worth to investigate a solution to avoid this. I could imagine that we could regularly update our own performance.timeOrigin instead of relying on that on from node.js.

I'm also not sure if Date.now() is worse performance wise as it might need to access the real time clock of the system and not just an high precision counter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we regularly update timeOrigin we lose the primary benefits of the hrtime anyway if it isn't monotonic

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hrtime should be monotonic. To my understanding it only drifts away.
But yes, if the correction happens between span start/end the result my be strange.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that's what i meant. if we apply a correction regularly we might apply the correction during a span and end up with a strange result like end before start which was the primary reason to use the monotonic clock in the first place.

this.resource = parentTracer.resource;
this.instrumentationLibrary = parentTracer.instrumentationLibrary;
this._spanLimits = parentTracer.getSpanLimits();
Expand All @@ -103,7 +106,7 @@ export class Span implements api.Span, ReadableSpan {

if (
Object.keys(this.attributes).length >=
this._spanLimits.attributeCountLimit! &&
this._spanLimits.attributeCountLimit! &&
!Object.prototype.hasOwnProperty.call(this.attributes, key)
) {
return this;
Expand Down Expand Up @@ -171,15 +174,22 @@ export class Span implements api.Span, ReadableSpan {
return this;
}

end(endTime: api.TimeInput = hrTime()): void {
end(endTime?: api.TimeInput): void {
if (this._isSpanEnded()) {
api.diag.error('You can only call end() on a span once.');
return;
}
this._ended = true;
this.endTime = timeInputToHrTime(endTime);

this._duration = hrTimeDuration(this.startTime, this.endTime);

if (endTime != null) {
this.endTime = timeInputToHrTime(endTime);
this._duration = hrTimeDuration(this._hrStartTime, this.endTime);
} else {
this._duration = hrTimeDuration(this._hrStartTime, hrTime());
this.endTime = addHrTime(this.startTime, this._duration);
}

if (this._duration[0] < 0) {
api.diag.warn(
'Inconsistent start and end time, startTime > endTime',
Expand Down