Skip to content

Commit 5818129

Browse files
dyladanpichlermarcFlarna
authored
feat: anchored clock (#3134)
Co-authored-by: Marc Pichler <[email protected]> Co-authored-by: Gerhard Stöbich <[email protected]>
1 parent 32cb123 commit 5818129

File tree

6 files changed

+150
-22
lines changed

6 files changed

+150
-22
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ All notable changes to this project will be documented in this file.
1010

1111
### :bug: (Bug Fix)
1212

13+
* fix(sdk-trace-base): make span start times resistant to hrtime clock drift
14+
[#3129](https://github.com/open-telemetry/opentelemetry-js/issues/3129)
15+
1316
### :books: (Refine Doc)
1417

1518
* docs(metrics): add missing metrics packages to SDK reference documentation [#3239](https://github.com/open-telemetry/opentelemetry-js/pull/3239) @dyladan
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
17+
export interface Clock {
18+
/**
19+
* Return the current time in milliseconds from some epoch such as the Unix epoch or process start
20+
*/
21+
now(): number;
22+
}
23+
24+
25+
/**
26+
* A utility for returning wall times anchored to a given point in time. Wall time measurements will
27+
* not be taken from the system, but instead are computed by adding a monotonic clock time
28+
* to the anchor point.
29+
*
30+
* This is needed because the system time can change and result in unexpected situations like
31+
* spans ending before they are started. Creating an anchored clock for each local root span
32+
* ensures that span timings and durations are accurate while preventing span times from drifting
33+
* too far from the system clock.
34+
*
35+
* Only creating an anchored clock once per local trace ensures span times are correct relative
36+
* to each other. For example, a child span will never have a start time before its parent even
37+
* if the system clock is corrected during the local trace.
38+
*
39+
* Heavily inspired by the OTel Java anchored clock
40+
* https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/AnchoredClock.java
41+
*/
42+
export class AnchoredClock implements Clock {
43+
private _monotonicClock: Clock;
44+
private _epochMillis: number;
45+
private _performanceMillis: number;
46+
47+
/**
48+
* Create a new AnchoredClock anchored to the current time returned by systemClock.
49+
*
50+
* @param systemClock should be a clock that returns the number of milliseconds since January 1 1970 such as Date
51+
* @param monotonicClock should be a clock that counts milliseconds monotonically such as window.performance or perf_hooks.performance
52+
*/
53+
public constructor(systemClock: Clock, monotonicClock: Clock) {
54+
this._monotonicClock = monotonicClock;
55+
this._epochMillis = systemClock.now();
56+
this._performanceMillis = monotonicClock.now();
57+
}
58+
59+
/**
60+
* Returns the current time by adding the number of milliseconds since the
61+
* AnchoredClock was created to the creation epoch time
62+
*/
63+
public now(): number {
64+
const delta = this._monotonicClock.now() - this._performanceMillis;
65+
return this._epochMillis + delta;
66+
}
67+
}

packages/opentelemetry-core/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
export * from './baggage/propagation/W3CBaggagePropagator';
18+
export * from './common/anchored-clock';
1819
export * from './common/attributes';
1920
export * from './common/global-error-handler';
2021
export * from './common/logging-error-handler';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
17+
import assert = require('assert');
18+
import { AnchoredClock, otperformance } from '../../src';
19+
20+
describe('AnchoredClock', () => {
21+
it('should keep time', done => {
22+
const clock = new AnchoredClock(Date, otperformance);
23+
setTimeout(() => {
24+
// after about 100ms, the clocks are within 10ms of each other
25+
assert.ok(Math.abs(Date.now() - clock.now()) < 10);
26+
done();
27+
}, 100);
28+
});
29+
});

packages/opentelemetry-sdk-trace-base/src/Span.ts

+23-14
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,25 @@
1515
*/
1616

1717
import * as api from '@opentelemetry/api';
18+
import { Context, SpanAttributeValue } from '@opentelemetry/api';
1819
import {
19-
isAttributeValue,
20-
hrTime,
20+
Clock,
2121
hrTimeDuration,
2222
InstrumentationLibrary,
23+
isAttributeValue,
2324
isTimeInput,
24-
timeInputToHrTime,
25+
otperformance,
2526
sanitizeAttributes,
27+
timeInputToHrTime
2628
} from '@opentelemetry/core';
2729
import { Resource } from '@opentelemetry/resources';
2830
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
31+
import { ExceptionEventName } from './enums';
2932
import { ReadableSpan } from './export/ReadableSpan';
33+
import { SpanProcessor } from './SpanProcessor';
3034
import { TimedEvent } from './TimedEvent';
3135
import { Tracer } from './Tracer';
32-
import { SpanProcessor } from './SpanProcessor';
3336
import { SpanLimits } from './types';
34-
import { SpanAttributeValue, Context } from '@opentelemetry/api';
35-
import { ExceptionEventName } from './enums';
3637

3738
/**
3839
* This class represents a span.
@@ -59,8 +60,13 @@ export class Span implements api.Span, ReadableSpan {
5960
private readonly _spanProcessor: SpanProcessor;
6061
private readonly _spanLimits: SpanLimits;
6162
private readonly _attributeValueLengthLimit: number;
63+
private readonly _clock: Clock;
6264

63-
/** Constructs a new Span instance. */
65+
/**
66+
* Constructs a new Span instance.
67+
*
68+
* @deprecated calling Span constructor directly is not supported. Please use tracer.startSpan.
69+
* */
6470
constructor(
6571
parentTracer: Tracer,
6672
context: Context,
@@ -69,14 +75,16 @@ export class Span implements api.Span, ReadableSpan {
6975
kind: api.SpanKind,
7076
parentSpanId?: string,
7177
links: api.Link[] = [],
72-
startTime: api.TimeInput = hrTime()
78+
startTime?: api.TimeInput,
79+
clock: Clock = otperformance,
7380
) {
81+
this._clock = clock;
7482
this.name = spanName;
7583
this._spanContext = spanContext;
7684
this.parentSpanId = parentSpanId;
7785
this.kind = kind;
7886
this.links = links;
79-
this.startTime = timeInputToHrTime(startTime);
87+
this.startTime = timeInputToHrTime(startTime ?? clock.now());
8088
this.resource = parentTracer.resource;
8189
this.instrumentationLibrary = parentTracer.instrumentationLibrary;
8290
this._spanLimits = parentTracer.getSpanLimits();
@@ -103,7 +111,7 @@ export class Span implements api.Span, ReadableSpan {
103111

104112
if (
105113
Object.keys(this.attributes).length >=
106-
this._spanLimits.attributeCountLimit! &&
114+
this._spanLimits.attributeCountLimit! &&
107115
!Object.prototype.hasOwnProperty.call(this.attributes, key)
108116
) {
109117
return this;
@@ -147,7 +155,7 @@ export class Span implements api.Span, ReadableSpan {
147155
attributesOrStartTime = undefined;
148156
}
149157
if (typeof startTime === 'undefined') {
150-
startTime = hrTime();
158+
startTime = this._clock.now();
151159
}
152160

153161
const attributes = sanitizeAttributes(attributesOrStartTime);
@@ -171,15 +179,16 @@ export class Span implements api.Span, ReadableSpan {
171179
return this;
172180
}
173181

174-
end(endTime: api.TimeInput = hrTime()): void {
182+
end(endTime?: api.TimeInput): void {
175183
if (this._isSpanEnded()) {
176184
api.diag.error('You can only call end() on a span once.');
177185
return;
178186
}
179187
this._ended = true;
180-
this.endTime = timeInputToHrTime(endTime);
181188

189+
this.endTime = timeInputToHrTime(endTime ?? this._clock.now());
182190
this._duration = hrTimeDuration(this.startTime, this.endTime);
191+
183192
if (this._duration[0] < 0) {
184193
api.diag.warn(
185194
'Inconsistent start and end time, startTime > endTime',
@@ -195,7 +204,7 @@ export class Span implements api.Span, ReadableSpan {
195204
return this._ended === false;
196205
}
197206

198-
recordException(exception: api.Exception, time: api.TimeInput = hrTime()): void {
207+
recordException(exception: api.Exception, time: api.TimeInput = this._clock.now()): void {
199208
const attributes: api.SpanAttributes = {};
200209
if (typeof exception === 'string') {
201210
attributes[SemanticAttributes.EXCEPTION_MESSAGE] = exception;

packages/opentelemetry-sdk-trace-base/src/Tracer.ts

+27-8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
InstrumentationLibrary,
2020
sanitizeAttributes,
2121
isTracingSuppressed,
22+
AnchoredClock,
23+
otperformance,
2224
} from '@opentelemetry/core';
2325
import { Resource } from '@opentelemetry/resources';
2426
import { BasicTracerProvider } from './BasicTracerProvider';
@@ -67,17 +69,31 @@ export class Tracer implements api.Tracer {
6769
options: api.SpanOptions = {},
6870
context = api.context.active()
6971
): api.Span {
70-
if (isTracingSuppressed(context)) {
71-
api.diag.debug('Instrumentation suppressed, returning Noop Span');
72-
return api.trace.wrapSpanContext(api.INVALID_SPAN_CONTEXT);
73-
}
74-
7572
// remove span from context in case a root span is requested via options
7673
if (options.root) {
7774
context = api.trace.deleteSpan(context);
7875
}
76+
const parentSpan = api.trace.getSpan(context);
77+
let clock: AnchoredClock | undefined;
78+
if (parentSpan) {
79+
clock = (parentSpan as any)['_clock'];
80+
}
81+
82+
if (!clock) {
83+
clock = new AnchoredClock(Date, otperformance);
84+
if (parentSpan) {
85+
(parentSpan as any)['_clock'] = clock;
86+
}
87+
}
88+
89+
if (isTracingSuppressed(context)) {
90+
api.diag.debug('Instrumentation suppressed, returning Noop Span');
91+
const nonRecordingSpan = api.trace.wrapSpanContext(api.INVALID_SPAN_CONTEXT);
92+
(nonRecordingSpan as any)['_clock'] = clock;
93+
return nonRecordingSpan;
94+
}
7995

80-
const parentSpanContext = api.trace.getSpanContext(context);
96+
const parentSpanContext = parentSpan?.spanContext();
8197
const spanId = this._idGenerator.generateSpanId();
8298
let traceId;
8399
let traceState;
@@ -117,7 +133,9 @@ export class Tracer implements api.Tracer {
117133
const spanContext = { traceId, spanId, traceFlags, traceState };
118134
if (samplingResult.decision === api.SamplingDecision.NOT_RECORD) {
119135
api.diag.debug('Recording is off, propagating context in a non-recording span');
120-
return api.trace.wrapSpanContext(spanContext);
136+
const nonRecordingSpan = api.trace.wrapSpanContext(spanContext);
137+
(nonRecordingSpan as any)['_clock'] = clock;
138+
return nonRecordingSpan;
121139
}
122140

123141
const span = new Span(
@@ -128,7 +146,8 @@ export class Tracer implements api.Tracer {
128146
spanKind,
129147
parentSpanId,
130148
links,
131-
options.startTime
149+
options.startTime,
150+
clock,
132151
);
133152
// Set initial span attributes. The attributes object may have been mutated
134153
// by the sampler, so we sanitize the merged attributes before setting them.

0 commit comments

Comments
 (0)