Skip to content

Commit 0b7d47b

Browse files
fix: use monotonic clock where possible (#782)
* fix: use monotonic clock where possible * fix some merge conflicts * always use relative timestamps * use correct base * fix: add start_timestamp tests * fix: events relative timestamp test * fix: add finish timestamp test * fix: rebase merge errors * chore: Apply francis feedback Co-authored-by: Francis Bogsanyi <[email protected]> * chore: time test cleanups * Simplify tests Co-authored-by: Robert Laurin <[email protected]> Co-authored-by: Robert <[email protected]>
1 parent 871ddb4 commit 0b7d47b

File tree

5 files changed

+162
-27
lines changed

5 files changed

+162
-27
lines changed

instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/tracer_middleware.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,7 @@ def create_frontend_span(env, extracted_context)
107107

108108
span = tracer.start_span('http_server.proxy',
109109
with_parent: extracted_context,
110-
attributes: {
111-
'start_time' => request_start_time.to_f
112-
},
110+
start_timestamp: request_start_time,
113111
kind: :server)
114112

115113
OpenTelemetry::Trace.context_with_span(span, parent_context: extracted_context)

sdk/lib/opentelemetry/sdk/trace/span.rb

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def add_attributes(attributes)
131131
#
132132
# @return [self] returns itself
133133
def add_event(name, attributes: nil, timestamp: nil)
134-
event = Event.new(name, truncate_attribute_values(attributes), wall_clock(timestamp))
134+
event = Event.new(name, truncate_attribute_values(attributes), relative_timestamp(timestamp))
135135

136136
@mutex.synchronize do
137137
if @ended
@@ -233,7 +233,7 @@ def finish(end_timestamp: nil)
233233
OpenTelemetry.logger.warn('Calling finish on an ended Span.')
234234
return self
235235
end
236-
@end_timestamp = wall_clock(end_timestamp)
236+
@end_timestamp = relative_timestamp(end_timestamp)
237237
@attributes = validated_attributes(@attributes).freeze
238238
@events.freeze
239239
@ended = true
@@ -276,7 +276,7 @@ def to_span_data
276276
end
277277

278278
# @api private
279-
def initialize(context, parent_context, name, kind, parent_span_id, span_limits, span_processors, attributes, links, start_timestamp, resource, instrumentation_library) # rubocop:disable Metrics/AbcSize
279+
def initialize(context, parent_context, parent_span, name, kind, parent_span_id, span_limits, span_processors, attributes, links, start_timestamp, resource, instrumentation_library) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
280280
super(span_context: context)
281281
@mutex = Mutex.new
282282
@name = name
@@ -291,17 +291,45 @@ def initialize(context, parent_context, name, kind, parent_span_id, span_limits,
291291
@total_recorded_events = 0
292292
@total_recorded_links = links&.size || 0
293293
@total_recorded_attributes = attributes&.size || 0
294-
@start_timestamp = wall_clock(start_timestamp)
295-
@end_timestamp = nil
296294
@attributes = attributes.nil? ? nil : Hash[attributes] # We need a mutable copy of attributes.
297295
trim_span_attributes(@attributes)
298296
@events = nil
299297
@links = trim_links(links, span_limits.link_count_limit, span_limits.link_attribute_count_limit)
298+
299+
# Times are hard. Whenever an explicit timestamp is provided
300+
# (for Events or for the Span start_timestamp or end_timestamp),
301+
# we use that as the recorded timestamp. An implicit Event timestamp
302+
# and end_timestamp is computed as a monotonic clock offset from
303+
# the realtime start_timestamp. The realtime start_timestamp is
304+
# computed as a monotonic clock offset from the realtime
305+
# start_timestamp of its parent span, if available, or it is
306+
# fetched from the realtime system clock.
307+
#
308+
# We therefore have 3 start timestamps. The first two are used
309+
# internally (and by child spans) to compute other timestamps.
310+
# The last is the start timestamp actually recorded in the
311+
# SpanData.
312+
@monotonic_start_timestamp = monotonic_now
313+
@realtime_start_timestamp = if parent_span.recording?
314+
relative_realtime(parent_span.realtime_start_timestamp, parent_span.monotonic_start_timestamp)
315+
else
316+
realtime_now
317+
end
318+
@start_timestamp = if start_timestamp
319+
time_in_nanoseconds(start_timestamp)
320+
else
321+
@realtime_start_timestamp
322+
end
323+
@end_timestamp = nil
300324
@span_processors.each { |processor| processor.on_start(self, parent_context) }
301325
end
302326

303327
# TODO: Java implementation overrides finalize to log if a span isn't finished.
304328

329+
protected
330+
331+
attr_reader :monotonic_start_timestamp, :realtime_start_timestamp
332+
305333
private
306334

307335
def validated_attributes(attrs)
@@ -376,9 +404,26 @@ def append_event(events, event) # rubocop:disable Metrics/AbcSize, Metrics/Cyclo
376404
events << event
377405
end
378406

379-
def wall_clock(timestamp)
380-
timestamp = (timestamp.to_r * 1_000_000_000).to_i unless timestamp.nil?
381-
timestamp || Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
407+
def relative_timestamp(timestamp)
408+
return time_in_nanoseconds(timestamp) unless timestamp.nil?
409+
410+
relative_realtime(realtime_start_timestamp, monotonic_start_timestamp)
411+
end
412+
413+
def time_in_nanoseconds(timestamp)
414+
(timestamp.to_r * 1_000_000_000).to_i
415+
end
416+
417+
def relative_realtime(realtime_base, monotonic_base)
418+
realtime_base + (monotonic_now - monotonic_base)
419+
end
420+
421+
def realtime_now
422+
Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
423+
end
424+
425+
def monotonic_now
426+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
382427
end
383428
end
384429
# rubocop:enable Metrics/ClassLength

sdk/lib/opentelemetry/sdk/trace/tracer.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ def start_span(name, with_parent: nil, attributes: nil, links: nil, start_timest
3131
name ||= 'empty'
3232

3333
with_parent ||= Context.current
34+
parent_span = OpenTelemetry::Trace.current_span(with_parent)
3435
parent_span_context = OpenTelemetry::Trace.current_span(with_parent).context
3536
if parent_span_context.valid?
3637
parent_span_id = parent_span_context.span_id
3738
trace_id = parent_span_context.trace_id
3839
end
39-
@tracer_provider.internal_create_span(name, kind, trace_id, parent_span_id, attributes, links, start_timestamp, with_parent, @instrumentation_library)
40+
41+
@tracer_provider.internal_create_span(name, kind, trace_id, parent_span_id, attributes, links, start_timestamp, with_parent, parent_span, @instrumentation_library)
4042
end
4143
end
4244
end

sdk/lib/opentelemetry/sdk/trace/tracer_provider.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,13 @@ def add_span_processor(span_processor)
126126
end
127127

128128
# @api private
129-
def internal_create_span(name, kind, trace_id, parent_span_id, attributes, links, start_timestamp, parent_context, instrumentation_library) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
129+
def internal_create_span(name, kind, trace_id, parent_span_id, attributes, links, start_timestamp, parent_context, parent_span, instrumentation_library) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
130+
parent_span_context = parent_span.context
131+
if parent_span_context.valid?
132+
parent_span_id = parent_span_context.span_id
133+
trace_id = parent_span_context.trace_id
134+
end
135+
name ||= 'empty'
130136
trace_id ||= @id_generator.generate_trace_id
131137
kind ||= :internal
132138
result = @sampler.should_sample?(trace_id: trace_id, parent_context: parent_context, links: links, name: name, kind: kind, attributes: attributes)
@@ -138,6 +144,7 @@ def internal_create_span(name, kind, trace_id, parent_span_id, attributes, links
138144
Span.new(
139145
context,
140146
parent_context,
147+
parent_span,
141148
name,
142149
kind,
143150
parent_span_id,

sdk/test/opentelemetry/sdk/trace/span_test.rb

Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
SpanKind = OpenTelemetry::Trace::SpanKind
1212
Status = OpenTelemetry::Trace::Status
1313
Context = OpenTelemetry::Context
14+
SpanLimits = OpenTelemetry::SDK::Trace::SpanLimits
1415

1516
let(:context) { OpenTelemetry::Trace::SpanContext.new }
1617
let(:mock_span_processor) { Minitest::Mock.new }
@@ -25,7 +26,7 @@
2526
)
2627
end
2728
let(:span) do
28-
Span.new(context, Context.empty, 'name', SpanKind::INTERNAL, nil, span_limits,
29+
Span.new(context, Context.empty, OpenTelemetry::Trace::Span::INVALID, 'name', SpanKind::INTERNAL, nil, span_limits,
2930
[], nil, nil, Time.now, nil, nil)
3031
end
3132

@@ -200,14 +201,31 @@
200201
_(events.first.attributes).must_equal(attrs)
201202
end
202203

203-
it 'add event with timestamp' do
204-
ts = Time.now
204+
it 'honours an explicit timestamp' do
205+
ts = Time.new('2021-11-23 12:00:00.000000 -0600')
205206
span.add_event('added', timestamp: ts)
206207
events = span.events
207208
_(events.size).must_equal(1)
208209
_(events.first.timestamp).must_equal(exportable_timestamp(ts))
209210
end
210211

212+
it 'sets the implicit event timestamp relative to the span start' do
213+
# Create a span with deterministic time values stored on it.
214+
test_span = mock_gettime(monotonic: 100, realtime: 1_000) do
215+
Span.new(context, Context.empty, OpenTelemetry::Trace::Span::INVALID, 'span', SpanKind::INTERNAL, nil, span_limits, [], nil, nil, nil, nil, nil)
216+
end
217+
218+
mock_gettime(monotonic: 200, realtime: 1_000_000) do
219+
test_span.add_event('record something with relative time')
220+
event = test_span.events[0]
221+
_(event).wont_be_nil
222+
223+
# The expect timestamp is that of the parent offset by
224+
# the drift in the monotonic clock
225+
_(event.timestamp).must_equal(1_000 + 100)
226+
end
227+
end
228+
211229
it 'does not add an event if span is ended' do
212230
span.finish
213231
span.add_event('will_not_be_added')
@@ -372,14 +390,29 @@
372390
_(span.finish).must_equal(span)
373391
end
374392

375-
it 'sets the end timestamp' do
376-
span.finish
377-
_(span.to_span_data.end_timestamp).wont_be_nil
393+
it 'honours an explicit end timestamp' do
394+
ts = Time.new('2021-11-23 12:00:00.000000 -0600')
395+
_(span.finish(end_timestamp: ts)).must_equal(span)
396+
_(span.end_timestamp).must_equal(exportable_timestamp(ts))
397+
end
398+
399+
it 'sets the end timestamp relative to the start time' do
400+
# Create a span with deterministic time values stored on it.
401+
test_span = mock_gettime(monotonic: 100, realtime: 1_000) do
402+
Span.new(context, Context.empty, OpenTelemetry::Trace::Span::INVALID, 'span', SpanKind::INTERNAL, nil, span_limits, [], nil, nil, nil, nil, nil)
403+
end
404+
405+
mock_gettime(monotonic: 200, realtime: 1_000_000) do
406+
test_span.finish
407+
# The expect timestamp is that of the parent offset by
408+
# the drift in the monotonic clock
409+
_(test_span.end_timestamp).must_equal(1_000 + 100)
410+
end
378411
end
379412

380413
it 'calls the span processor #on_finish callback' do
381414
mock_span_processor.expect(:on_start, nil) { |_| true }
382-
span = Span.new(context, Context.empty,
415+
span = Span.new(context, Context.empty, OpenTelemetry::Trace::Span::INVALID,
383416
'name', SpanKind::INTERNAL, nil, span_limits,
384417
[mock_span_processor], nil, nil, Time.now, nil, nil)
385418
mock_span_processor.expect(:on_finish, nil, [span])
@@ -405,54 +438,104 @@
405438
it 'calls the span processor #on_start callback' do
406439
yielded_span = nil
407440
mock_span_processor.expect(:on_start, nil) { |s| yielded_span = s }
408-
span = Span.new(context, Context.empty, 'name', SpanKind::INTERNAL, nil, span_limits,
441+
span = Span.new(context, Context.empty, OpenTelemetry::Trace::Span::INVALID, 'name', SpanKind::INTERNAL, nil, span_limits,
409442
[mock_span_processor], nil, nil, Time.now, nil, nil)
410443
_(yielded_span).must_equal(span)
411444
mock_span_processor.verify
412445
end
413446

414447
it 'trims excess attributes' do
415448
attributes = { 'foo': 'bar', 'other': 'attr' }
416-
span = Span.new(context, Context.empty, 'name', SpanKind::INTERNAL, nil, span_limits,
449+
span = Span.new(context, Context.empty, OpenTelemetry::Trace::Span::INVALID, 'name', SpanKind::INTERNAL, nil, span_limits,
417450
[], attributes, nil, Time.now, nil, nil)
418451
_(span.to_span_data.total_recorded_attributes).must_equal(2)
419452
_(span.attributes.length).must_equal(1)
420453
end
421454

422455
it 'truncates attributes if configured' do
423456
attributes = { 'foo': 'oldbaroldbaroldbaroldbaroldbaroldbar' }
424-
span = Span.new(context, Context.empty, 'name', SpanKind::INTERNAL, nil, span_limits,
457+
span = Span.new(context, Context.empty, OpenTelemetry::Trace::Span::INVALID, 'name', SpanKind::INTERNAL, nil, span_limits,
425458
[], attributes, nil, Time.now, nil, nil)
426459
_(span.attributes[:foo]).must_equal('oldbaroldbaroldbaroldbaroldba...')
427460
end
428461

429462
it 'counts attributes' do
430463
attributes = { 'foo': 'bar', 'other': 'attr' }
431-
span = Span.new(context, Context.empty, 'name', SpanKind::INTERNAL, nil, span_limits,
464+
span = Span.new(context, Context.empty, OpenTelemetry::Trace::Span::INVALID, 'name', SpanKind::INTERNAL, nil, span_limits,
432465
[], attributes, nil, Time.now, nil, nil)
433466
_(span.to_span_data.total_recorded_attributes).must_equal(2)
434467
end
435468

436469
it 'counts links' do
437470
links = [OpenTelemetry::Trace::Link.new(context), OpenTelemetry::Trace::Link.new(context)]
438-
span = Span.new(context, Context.empty, 'name', SpanKind::INTERNAL, nil, span_limits,
471+
span = Span.new(context, Context.empty, OpenTelemetry::Trace::Span::INVALID, 'name', SpanKind::INTERNAL, nil, span_limits,
439472
[], nil, links, Time.now, nil, nil)
440473
_(span.to_span_data.total_recorded_links).must_equal(2)
441474
end
442475

443476
it 'trims excess links' do
444477
links = [OpenTelemetry::Trace::Link.new(context), OpenTelemetry::Trace::Link.new(context)]
445-
span = Span.new(context, Context.empty, 'name', SpanKind::INTERNAL, nil, span_limits,
478+
span = Span.new(context, Context.empty, OpenTelemetry::Trace::Span::INVALID, 'name', SpanKind::INTERNAL, nil, span_limits,
446479
[], nil, links, Time.now, nil, nil)
447480
_(span.links.size).must_equal(1)
448481
end
449482

450483
it 'prunes invalid links' do
451484
invalid_context = OpenTelemetry::Trace::SpanContext.new(trace_id: OpenTelemetry::Trace::INVALID_TRACE_ID)
452485
links = [OpenTelemetry::Trace::Link.new(context), OpenTelemetry::Trace::Link.new(invalid_context)]
453-
span = Span.new(context, Context.empty, 'name', SpanKind::INTERNAL, nil, span_limits,
486+
span = Span.new(context, Context.empty, OpenTelemetry::Trace::Span::INVALID, 'name', SpanKind::INTERNAL, nil, span_limits,
454487
[], nil, links, Time.now, nil, nil)
455488
_(span.links.size).must_equal(1)
456489
end
490+
491+
it 'honours an explicit timestamp' do
492+
timestamp = Time.new('2021-11-23 12:00:00.000000 -0600')
493+
test_span = Span.new(context, Context.empty, OpenTelemetry::Trace::Span::INVALID, 'child span', SpanKind::INTERNAL, nil, span_limits, [], nil, [], timestamp, nil, nil)
494+
_(test_span.start_timestamp).must_equal(exportable_timestamp(timestamp))
495+
end
496+
497+
it 'uses the monotonic offset from the parent_span realtime start timestamp when parent_span is recording' do
498+
parent_span = mock_gettime(monotonic: 100, realtime: 1_000) do
499+
Span.new(context, Context.empty, OpenTelemetry::Trace::Span::INVALID, 'parent span', SpanKind::INTERNAL, nil, span_limits, [], nil, nil, nil, nil, nil)
500+
end
501+
502+
_(parent_span.recording?).must_equal(true)
503+
mock_gettime(monotonic: 200, realtime: 1_000_000) do
504+
test_span = Span.new(context, Context.empty, parent_span, 'child span', SpanKind::INTERNAL, nil, span_limits, [], nil, [], nil, nil, nil)
505+
506+
# We expect to see the start_timestamp to be the parent span timestamp
507+
# with the same offset we returned with our stubbed
508+
# monotonic_now value we returned above
509+
_(test_span.start_timestamp).must_equal(1_000 + 100)
510+
end
511+
end
512+
513+
it 'uses the realtime clock when the parent_span is not recording' do
514+
non_recording_span = OpenTelemetry::Trace.non_recording_span(span.context)
515+
516+
mock_gettime(monotonic: 100, realtime: 1_000) do
517+
test_span = Span.new(context, Context.empty, non_recording_span, 'name', SpanKind::INTERNAL, nil, span_limits, [], nil, [], nil, nil, nil)
518+
519+
# We expect for the timestamp to be the value returned from
520+
# the call to realtime_now as we have no timestamp and no parent
521+
# span time to try and get a relative offset from.
522+
_(test_span.start_timestamp).must_equal(1_000)
523+
end
524+
end
525+
end
526+
527+
def mock_gettime(monotonic:, realtime:)
528+
timestamps = {
529+
Process::CLOCK_MONOTONIC => monotonic,
530+
Process::CLOCK_REALTIME => realtime
531+
}
532+
533+
clock_gettime_mock = lambda do |clock_id, unit|
534+
_(timestamps).must_include(clock_id)
535+
_(unit).must_equal(:nanosecond)
536+
timestamps[clock_id]
537+
end
538+
539+
Process.stub(:clock_gettime, clock_gettime_mock) { yield }
457540
end
458541
end

0 commit comments

Comments
 (0)