diff --git a/lib/datadog/core/configuration/settings.rb b/lib/datadog/core/configuration/settings.rb index 45835ddcdcf..31cddb33bf4 100644 --- a/lib/datadog/core/configuration/settings.rb +++ b/lib/datadog/core/configuration/settings.rb @@ -654,6 +654,33 @@ def initialize(*_) end end + # The monotonic clock time provider used by Datadog. This option is internal and is used by `datadog-ci` + # gem to avoid traces' durations being skewed by timecop. + # + # It must respect the interface of [Datadog::Core::Utils::Time#get_time] method. + # + # For [Timecop](https://rubygems.org/gems/timecop), for example, + # `->(unit = :float_second) { ::Process.clock_gettime_without_mock(::Process::CLOCK_MONOTONIC, unit) }` + # allows Datadog features to use the real monotonic time when time is frozen with + # `Timecop.mock_process_clock = true`. + # + # @default `->(unit = :float_second) { ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, unit)}` + # @return [Proc] + option :get_time_provider do |o| + o.default_proc { |unit = :float_second| ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, unit) } + o.type :proc + + o.after_set do |get_time_provider| + Core::Utils::Time.get_time_provider = get_time_provider + end + + o.resetter do |_value| + ->(unit = :float_second) { ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, unit) }.tap do |default| + Core::Utils::Time.get_time_provider = default + end + end + end + # The `version` tag in Datadog. Use it to enable [Deployment Tracking](https://docs.datadoghq.com/tracing/deployment_tracking/). # @see https://docs.datadoghq.com/getting_started/tagging/unified_service_tagging # @default `DD_VERSION` environment variable, otherwise `nils` diff --git a/lib/datadog/core/utils/time.rb b/lib/datadog/core/utils/time.rb index 20b1c473914..79367097e05 100644 --- a/lib/datadog/core/utils/time.rb +++ b/lib/datadog/core/utils/time.rb @@ -34,6 +34,18 @@ def now_provider=(block) define_singleton_method(:now, &block) end + # Overrides the implementation of `#get_time + # with the provided callable. + # + # Overriding the method `#get_time` instead of + # indirectly calling `block` removes + # one level of method call overhead. + # + # @param block [Proc] block that accepts unit and returns timestamp in the requested unit + def get_time_provider=(block) + define_singleton_method(:get_time, &block) + end + def measure(unit = :float_second) before = get_time(unit) yield diff --git a/sig/datadog/core/utils/time.rbs b/sig/datadog/core/utils/time.rbs index 74aeb383b29..39a01face69 100644 --- a/sig/datadog/core/utils/time.rbs +++ b/sig/datadog/core/utils/time.rbs @@ -5,6 +5,7 @@ module Datadog def self?.get_time: (?::Symbol unit) -> ::Numeric def self?.now: () -> ::Time def self?.now_provider=: (^() -> ::Time block) -> void + def self?.get_time_provider=: (^(?::Symbol unit) -> ::Numeric block) -> void def self?.measure: (?::Symbol unit) { () -> void } -> ::Numeric def self?.as_utc_epoch_ns: (::Time time) -> ::Integer end diff --git a/spec/datadog/core/configuration/settings_spec.rb b/spec/datadog/core/configuration/settings_spec.rb index a9c12188b95..8703f477afc 100644 --- a/spec/datadog/core/configuration/settings_spec.rb +++ b/spec/datadog/core/configuration/settings_spec.rb @@ -1392,6 +1392,73 @@ end end + describe '#get_time_provider=' do + subject(:set_get_time_provider) { settings.get_time_provider = get_time_provider } + + after { settings.reset! } + + let(:get_time) { 1 } + + let(:get_time_new_milliseconds) { 42 } + let(:get_time_new_seconds) { 0.042 } + + let(:unit) { :float_second } + let(:get_time_provider) do + new_milliseconds = get_time_new_milliseconds # Capture for closure + new_seconds = get_time_new_seconds # Capture for closure + + ->(unit) { unit == :float_millisecond ? new_milliseconds : new_seconds } + end + + context 'when default' do + before { allow(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC, unit).and_return(1) } + + it 'delegates to Process.clock_gettime' do + expect(settings.get_time_provider.call(unit)).to eq(get_time) + expect(Datadog::Core::Utils::Time.get_time(unit)).to eq(get_time) + end + end + + context 'when given a value' do + before { set_get_time_provider } + + context 'when unit is :float_second' do + it 'returns the provided time in float seconds' do + expect(settings.get_time_provider.call(unit)).to eq(get_time_new_seconds) + expect(Datadog::Core::Utils::Time.get_time(unit)).to eq(get_time_new_seconds) + end + end + + context 'when unit is :float_millisecond' do + let(:unit) { :float_millisecond } + + it 'returns the provided time in float milliseconds' do + expect(settings.get_time_provider.call(unit)).to eq(get_time_new_milliseconds) + expect(Datadog::Core::Utils::Time.get_time(unit)).to eq(get_time_new_milliseconds) + end + end + end + + context 'then reset' do + let(:original_get_time) { 1 } + + before do + set_get_time_provider + allow(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC, unit).and_return(original_get_time) + end + + it 'returns the provided time' do + expect(settings.get_time_provider.call(unit)).to eq(get_time_new_seconds) + expect(Datadog::Core::Utils::Time.get_time(unit)).to eq(get_time_new_seconds) + + settings.reset! + + expect(settings.get_time_provider.call(unit)).to eq(original_get_time) + expect(Datadog::Core::Utils::Time.get_time(unit)).to eq(original_get_time) + end + end + end + # Important note: These settings are used as inputs of the AgentSettingsResolver and are used by all components # that consume its result (e.g. tracing, profiling, and telemetry, as of January 2023). describe '#agent' do diff --git a/spec/datadog/tracing/span_operation_spec.rb b/spec/datadog/tracing/span_operation_spec.rb index 6b10bebeb22..5eef0d6a69c 100644 --- a/spec/datadog/tracing/span_operation_spec.rb +++ b/spec/datadog/tracing/span_operation_spec.rb @@ -872,6 +872,27 @@ expect(span_op.end_time - span_op.start_time).to eq 0 end end + + context 'with get_time_provider set' do + let(:clock_increment) { 0.42 } + before do + incr = clock_increment + clock_time = clock_increment + Datadog.configure do |c| + # Use a custom clock provider that increments by `clock_increment` + c.get_time_provider = ->(_unit = :float_second) { clock_time += incr } + end + end + + after { without_warnings { Datadog.configuration.reset! } } + + it 'sets the duration to the provider increment' do + span_op.start + span_op.stop + + expect(span_op.duration).to be_within(0.01).of(clock_increment) + end + end end context 'with start_time provided' do