datadog 2.45.0 → 2.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -1
  3. data/ext/datadog_profiling_native_extension/collectors_stack.c +31 -14
  4. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +5 -1
  5. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +5 -0
  6. data/ext/datadog_profiling_native_extension/profiling.c +6 -0
  7. data/ext/libdatadog_api/di.c +30 -5
  8. data/ext/libdatadog_api/extconf.rb +2 -0
  9. data/ext/libdatadog_api/otel_thread_context.c +20 -14
  10. data/ext/libdatadog_api/process_discovery.c +15 -1
  11. data/ext/libdatadog_api/trace_exporter.c +105 -25
  12. data/ext/libdatadog_extconf_helpers.rb +1 -1
  13. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +29 -2
  14. data/lib/datadog/appsec/contrib/rails/patches/render_to_body_patch.rb +8 -7
  15. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +14 -2
  16. data/lib/datadog/appsec/contrib/sinatra/patches/json_patch.rb +8 -7
  17. data/lib/datadog/appsec/security_engine/engine.rb +2 -1
  18. data/lib/datadog/core/configuration/components.rb +40 -18
  19. data/lib/datadog/core/configuration/components_state.rb +8 -1
  20. data/lib/datadog/core/configuration/supported_configurations.rb +7 -2
  21. data/lib/datadog/core/process_discovery.rb +5 -1
  22. data/lib/datadog/core/remote/client/capabilities.rb +34 -29
  23. data/lib/datadog/core/remote/client.rb +44 -3
  24. data/lib/datadog/core/remote/component.rb +28 -12
  25. data/lib/datadog/core/remote/dispatcher.rb +47 -6
  26. data/lib/datadog/core/workers/runtime_metrics.rb +1 -1
  27. data/lib/datadog/di/boot.rb +1 -0
  28. data/lib/datadog/di/capture_expression_evaluator/result.rb +30 -0
  29. data/lib/datadog/di/capture_expression_evaluator.rb +39 -9
  30. data/lib/datadog/di/component.rb +108 -3
  31. data/lib/datadog/di/configuration.rb +53 -2
  32. data/lib/datadog/di/context.rb +7 -2
  33. data/lib/datadog/di/contrib/active_record.rb +8 -2
  34. data/lib/datadog/di/evaluation_failure.rb +28 -0
  35. data/lib/datadog/di/guardrails_telemetry/reason.rb +87 -0
  36. data/lib/datadog/di/guardrails_telemetry.rb +203 -0
  37. data/lib/datadog/di/instrumenter.rb +65 -15
  38. data/lib/datadog/di/logger.rb +30 -8
  39. data/lib/datadog/di/probe_manager.rb +48 -5
  40. data/lib/datadog/di/probe_notification_builder.rb +28 -26
  41. data/lib/datadog/di/probe_notifier_worker.rb +56 -6
  42. data/lib/datadog/di/remote.rb +46 -2
  43. data/lib/datadog/di/serializer.rb +147 -55
  44. data/lib/datadog/di/snapshot_event.rb +38 -0
  45. data/lib/datadog/di/transport/http.rb +13 -3
  46. data/lib/datadog/di/transport/input.rb +32 -7
  47. data/lib/datadog/open_feature/activation.rb +268 -0
  48. data/lib/datadog/open_feature/component.rb +31 -15
  49. data/lib/datadog/open_feature/configuration.rb +6 -0
  50. data/lib/datadog/open_feature/hooks/evaluation_telemetry_opt_out_hook.rb +25 -0
  51. data/lib/datadog/open_feature/provider.rb +304 -8
  52. data/lib/datadog/open_feature.rb +31 -1
  53. data/lib/datadog/profiling/collectors/info.rb +37 -6
  54. data/lib/datadog/profiling/component.rb +22 -20
  55. data/lib/datadog/profiling/exporter.rb +3 -5
  56. data/lib/datadog/tracing/component.rb +21 -2
  57. data/lib/datadog/tracing/configuration/ext.rb +2 -0
  58. data/lib/datadog/tracing/configuration/settings.rb +14 -1
  59. data/lib/datadog/tracing/context_provider.rb +8 -0
  60. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +10 -5
  61. data/lib/datadog/tracing/contrib/excon/middleware.rb +10 -3
  62. data/lib/datadog/tracing/contrib/faraday/middleware.rb +10 -4
  63. data/lib/datadog/tracing/contrib/http/instrumentation.rb +16 -1
  64. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +8 -2
  65. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +10 -4
  66. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +10 -3
  67. data/lib/datadog/tracing/contrib/utils/quantization/http.rb +102 -0
  68. data/lib/datadog/tracing/otel_thread_context.rb +3 -5
  69. data/lib/datadog/tracing/remote.rb +50 -19
  70. data/lib/datadog/tracing/transport/http/telemetry.rb +94 -0
  71. data/lib/datadog/tracing/transport/http/traces.rb +2 -0
  72. data/lib/datadog/tracing/transport/native/telemetry.rb +17 -0
  73. data/lib/datadog/tracing/transport/native.rb +16 -1
  74. data/lib/datadog/tracing/transport/telemetry.rb +62 -0
  75. data/lib/datadog/tracing/transport/traces.rb +82 -15
  76. data/lib/datadog/version.rb +1 -1
  77. metadata +19 -10
  78. data/lib/datadog/appsec/utils/hash_coercion.rb +0 -23
@@ -15,7 +15,7 @@ module Datadog
15
15
  #
16
16
  # @api private
17
17
  class Component
18
- attr_reader :logger, :client, :healthy, :worker
18
+ attr_reader :logger, :healthy, :worker
19
19
 
20
20
  def initialize(settings, capabilities, agent_settings, logger:)
21
21
  @logger = logger
@@ -28,6 +28,7 @@ module Datadog
28
28
 
29
29
  @barrier = Barrier.new(settings.remote.boot_timeout_seconds)
30
30
 
31
+ @client_mutex = Mutex.new
31
32
  @client = Client.new(@transport, @capabilities, settings: settings, logger: logger)
32
33
  @healthy = false
33
34
  logger.debug { "new remote configuration client: #{@client.id} products: #{@capabilities.products.sort.join(", ")}" }
@@ -40,7 +41,7 @@ module Datadog
40
41
  end
41
42
 
42
43
  begin
43
- @client.sync
44
+ client.sync
44
45
  @healthy ||= true
45
46
  rescue Client::SyncError => e
46
47
  # Transient errors due to network or agent. Logged the error but not via telemetry
@@ -60,9 +61,11 @@ module Datadog
60
61
  end
61
62
 
62
63
  # client state is unknown, state might be corrupted
63
- @client = Client.new(@transport, @capabilities, settings: settings, logger: logger)
64
+ new_client = @client_mutex.synchronize do
65
+ @client = Client.new(@transport, @capabilities, settings: settings, logger: logger)
66
+ end
64
67
  @healthy = false
65
- logger.debug { "new remote configuration client: #{@client.id} products: #{@capabilities.products.sort.join(", ")}" }
68
+ logger.debug { "new remote configuration client: #{new_client.id} products: #{@capabilities.products.sort.join(", ")}" }
66
69
 
67
70
  # TODO: bail out if too many errors?
68
71
  end
@@ -81,6 +84,10 @@ module Datadog
81
84
  @worker.started?
82
85
  end
83
86
 
87
+ def client
88
+ @client_mutex.synchronize { @client }
89
+ end
90
+
84
91
  # If the worker is not initialized, initialize it.
85
92
  #
86
93
  # Then, waits for one client sync to be executed if `kind` is `:once`.
@@ -98,9 +105,11 @@ module Datadog
98
105
  def after_fork
99
106
  @worker.after_fork
100
107
  @barrier = Barrier.new(@settings.remote.boot_timeout_seconds)
101
- @client = Client.new(@transport, @capabilities, settings: @settings, logger: @logger)
108
+ new_client = @client_mutex.synchronize do
109
+ @client = Client.new(@transport, @capabilities, settings: @settings, logger: @logger)
110
+ end
102
111
  @healthy = false
103
- logger.debug { "remote configuration client recreated after fork: #{@client.id} products: #{@capabilities.products.sort.join(", ")}" }
112
+ logger.debug { "remote configuration client recreated after fork: #{new_client.id} products: #{@capabilities.products.sort.join(", ")}" }
104
113
  end
105
114
 
106
115
  def add_products(*products)
@@ -111,6 +120,17 @@ module Datadog
111
120
  @capabilities.remove_products(*products)
112
121
  end
113
122
 
123
+ def register(capabilities:, products:, receivers:)
124
+ @client_mutex.synchronize do
125
+ @capabilities.register_runtime(
126
+ capabilities: capabilities,
127
+ products: products,
128
+ receivers: receivers,
129
+ )
130
+ @client.dispatcher.add_receivers(*receivers)
131
+ end
132
+ end
133
+
114
134
  # Barrier provides a mechanism to fence execution until a condition happens
115
135
  class Barrier
116
136
  def initialize(timeout = nil)
@@ -189,14 +209,10 @@ module Datadog
189
209
  #
190
210
  # Those checks are instead performed inside the worker loop.
191
211
  # This allows users to upgrade their agent while keeping their application running.
192
- def build(settings, agent_settings, logger:, telemetry:, open_feature_component_provider: nil)
212
+ def build(settings, agent_settings, logger:, telemetry:)
193
213
  return unless settings.remote.enabled
194
214
 
195
- capabilities = Client::Capabilities.new(
196
- settings,
197
- telemetry,
198
- open_feature_component_provider: open_feature_component_provider,
199
- )
215
+ capabilities = Client::Capabilities.new(settings, telemetry: telemetry)
200
216
  new(settings, capabilities, agent_settings, logger: logger)
201
217
  end
202
218
  end
@@ -5,24 +5,57 @@ module Datadog
5
5
  module Remote
6
6
  # Repository update dispatcher
7
7
  class Dispatcher
8
- attr_reader :receivers
9
-
10
8
  def initialize(receivers)
11
- @receivers = receivers
9
+ @receivers = receivers.dup
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ def receivers
14
+ @mutex.synchronize { @receivers.dup }
15
+ end
16
+
17
+ def add_receivers(*receivers)
18
+ @mutex.synchronize do
19
+ receivers.each { |receiver| @receivers << receiver unless @receivers.include?(receiver) }
20
+ end
21
+ nil
12
22
  end
13
23
 
14
24
  def dispatch(changes, repository)
15
25
  receivers.each do |receiver|
16
- matching_changes = changes.select { |c| receiver.match?(c.path) }
26
+ matching = matching_changes(changes, receiver)
27
+
28
+ receiver.call(repository, matching) if matching.any?
29
+ end
30
+ nil
31
+ end
32
+
33
+ # Calls each receiver that declares initial-reconcile participation and has no
34
+ # matching change in `changes`, passing an empty change list.
35
+ def reconcile(changes, repository)
36
+ receivers.each do |receiver|
37
+ next unless receiver.initial_reconcile?
17
38
 
18
- receiver.call(repository, matching_changes) if matching_changes.any?
39
+ receiver.call(repository, []) unless any_matching?(changes, receiver)
19
40
  end
41
+ nil
42
+ end
43
+
44
+ private
45
+
46
+ def matching_changes(changes, receiver)
47
+ changes.select { |change| receiver.match?(change.path) }
48
+ end
49
+
50
+ def any_matching?(changes, receiver)
51
+ changes.any? { |change| receiver.match?(change.path) }
20
52
  end
21
53
 
22
54
  # Store Matcher and block to be executed on a match
23
55
  class Receiver
24
- def initialize(matcher, &block)
56
+ def initialize(matcher, initial_reconcile: false, &block)
25
57
  @block = block
58
+ @initial_reconcile = initial_reconcile
26
59
  @matcher = matcher
27
60
  end
28
61
 
@@ -30,6 +63,14 @@ module Datadog
30
63
  @matcher.match?(path)
31
64
  end
32
65
 
66
+ # Whether #reconcile may call this receiver with an empty change
67
+ # list on the client's first sync. A participating receiver must
68
+ # act on the repository's current contents when the change list is
69
+ # empty.
70
+ def initial_reconcile?
71
+ @initial_reconcile
72
+ end
73
+
33
74
  def call(repository, changes)
34
75
  @block.call(repository, changes)
35
76
  end
@@ -32,7 +32,7 @@ module Datadog
32
32
  end
33
33
 
34
34
  # Workers::Async::Thread settings
35
- self.fork_policy = options.fetch(:fork_policy, Workers::Async::Thread::FORK_POLICY_STOP)
35
+ self.fork_policy = options.fetch(:fork_policy, Workers::Async::Thread::FORK_POLICY_RESTART)
36
36
 
37
37
  # Workers::IntervalLoop settings
38
38
  self.loop_base_interval = options.fetch(:interval, DEFAULT_FLUSH_INTERVAL)
@@ -7,6 +7,7 @@ require_relative "code_tracker"
7
7
  require_relative "component"
8
8
  require_relative "context"
9
9
  require_relative "capture_limits"
10
+ require_relative "guardrails_telemetry"
10
11
  require_relative "capture_expression"
11
12
  require_relative "capture_expression_evaluator"
12
13
  require_relative "instrumenter"
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ class CaptureExpressionEvaluator
6
+ # The result of evaluating a probe's capture expressions: the
7
+ # serialized values and the evaluation errors.
8
+ class Result
9
+ # Initializes the result.
10
+ #
11
+ # @param serialized_values [Hash{String => untyped}] serialized values keyed by capture expression name
12
+ # @param errors [Array<EvaluationFailure>] evaluation errors of the expressions that failed to evaluate or serialize
13
+ # @return [void]
14
+ def initialize(serialized_values:, errors:)
15
+ @serialized_values = serialized_values
16
+ @errors = errors
17
+ end
18
+
19
+ # The serialized values keyed by capture expression name.
20
+ # @return [Hash{String => untyped}]
21
+ attr_reader :serialized_values
22
+
23
+ # The evaluation errors; one entry per expression that failed to
24
+ # evaluate or serialize.
25
+ # @return [Array<EvaluationFailure>]
26
+ attr_reader :errors
27
+ end
28
+ end
29
+ end
30
+ end
@@ -2,17 +2,28 @@
2
2
 
3
3
  require_relative "capture_expression"
4
4
  require_relative "capture_limits"
5
+ require_relative "capture_expression_evaluator/result"
6
+ require_relative "evaluation_failure"
5
7
  require_relative "fatal_exceptions"
8
+ require_relative "guardrails_telemetry"
9
+ require_relative "../core/utils/time"
6
10
 
7
11
  module Datadog
8
12
  module DI
9
13
  class CaptureExpressionEvaluator
10
- TELEMETRY_NAMESPACE = "dynamic_instrumentation"
11
-
12
- def initialize(settings:, serializer:, logger:, telemetry: nil)
14
+ # Initializes the evaluator.
15
+ #
16
+ # @param settings [Datadog::Core::Configuration::Settings] tracer settings
17
+ # @param serializer [Serializer] serializer for captured expression values
18
+ # @param logger [DI::Logger] logger for evaluation failure diagnostics
19
+ # @param guardrails_telemetry [GuardrailsTelemetry] emitter for the canonical capture-timeout skip metric
20
+ # @param telemetry [Datadog::Core::Telemetry::Component, nil] component evaluation failures are reported through
21
+ # @return [void]
22
+ def initialize(settings:, serializer:, logger:, guardrails_telemetry:, telemetry: nil)
13
23
  @settings = settings
14
24
  @serializer = serializer
15
25
  @logger = logger
26
+ @guardrails_telemetry = guardrails_telemetry
16
27
  @telemetry = telemetry
17
28
  end
18
29
 
@@ -22,11 +33,25 @@ module Datadog
22
33
 
23
34
  attr_reader :logger
24
35
 
36
+ # The guardrails metric emitter.
37
+ # @return [GuardrailsTelemetry]
38
+ attr_reader :guardrails_telemetry
39
+
25
40
  attr_reader :telemetry
26
41
 
42
+ # Evaluates each of the probe's capture expressions against the given
43
+ # context, serializing the captured values under one capture deadline
44
+ # resolved through the serializer and shared across all expressions.
45
+ # An expression that fails to evaluate or serialize contributes an entry
46
+ # to the returned errors.
47
+ #
48
+ # @param probe [Probe] the probe whose capture expressions are evaluated
49
+ # @param context [Context] evaluation context the expressions are evaluated against
50
+ # @return [Result] the evaluation result: the serialized values keyed by
51
+ # capture expression name and the errors of the expressions that failed
52
+ # to evaluate or serialize
27
53
  def evaluate(probe, context)
28
- budget_ns = settings.dynamic_instrumentation.max_time_to_serialize_ms * 1_000_000
29
- deadline_ns = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :nanosecond) + budget_ns
54
+ deadline = serializer.serialization_deadline
30
55
 
31
56
  output = {}
32
57
  evaluation_errors = []
@@ -34,9 +59,13 @@ module Datadog
34
59
  probe.capture_expressions.each do |capture_expression|
35
60
  name = capture_expression.name
36
61
 
37
- if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :nanosecond) >= deadline_ns
62
+ if Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :float_second) >= deadline
38
63
  output[name] = {notCapturedReason: "timeout"}
39
- telemetry&.inc(TELEMETRY_NAMESPACE, "capture_expressions_skipped_by_timeout", 1)
64
+ telemetry&.inc(DI::TELEMETRY_NAMESPACE, "capture_expressions_skipped_by_timeout", 1)
65
+ guardrails_telemetry.skipped(
66
+ reason: GuardrailsTelemetry::Reason::EVALUATION_TIMEOUT,
67
+ probe: probe,
68
+ )
40
69
  next
41
70
  end
42
71
 
@@ -53,10 +82,11 @@ module Datadog
53
82
  attribute_count: limits[:attribute_count],
54
83
  length: limits[:length],
55
84
  collection_size: limits[:collection_size],
85
+ deadline: deadline,
56
86
  )
57
87
  rescue Exception => exc # standard:disable Lint/RescueException
58
88
  Datadog::DI.reraise_if_fatal(exc)
59
- evaluation_errors << {expr: name, message: "#{exc.class}: #{exc.message}"}
89
+ evaluation_errors << EvaluationFailure.new(expr: name, message: "#{exc.class}: #{exc.message}")
60
90
  logger.debug do
61
91
  "di: probe #{probe.id}: capture expression #{name}: evaluation failed: #{exc.class}: #{exc.message}"
62
92
  end
@@ -64,7 +94,7 @@ module Datadog
64
94
  end
65
95
  end
66
96
 
67
- [output, evaluation_errors]
97
+ Result.new(serialized_values: output, errors: evaluation_errors)
68
98
  end
69
99
  end
70
100
  end
@@ -16,6 +16,16 @@ module Datadog
16
16
  # resources and installed tracepoints upon shutdown.
17
17
  class Component
18
18
  class << self
19
+ # Builds and registers the DI component, or returns nil without
20
+ # building one when the settings have no dynamic_instrumentation
21
+ # section, DI is explicitly disabled, the runtime is unsupported,
22
+ # or Remote Configuration is disabled.
23
+ #
24
+ # @param settings [Datadog::Core::Configuration::Settings]
25
+ # @param agent_settings [Datadog::Core::Configuration::AgentSettings]
26
+ # @param logger [Core::Logger]
27
+ # @param telemetry [Core::Telemetry::Component, nil]
28
+ # @return [Component, nil] the built component, or nil when no component was built
19
29
  def build(settings, agent_settings, logger, telemetry: nil)
20
30
  return unless settings.respond_to?(:dynamic_instrumentation)
21
31
 
@@ -65,40 +75,66 @@ module Datadog
65
75
  end
66
76
  end
67
77
 
78
+ # Initializes the DI component.
79
+ #
80
+ # @param settings [Datadog::Core::Configuration::Settings] tracer settings
81
+ # @param agent_settings [Datadog::Core::Configuration::AgentSettings] agent connection settings for the transports
82
+ # @param logger [Datadog::Core::Logger] diagnostics logger, wrapped in the DI logger facade
83
+ # @param code_tracker [CodeTracker, nil] code tracker for line probe installation
84
+ # @param telemetry [Datadog::Core::Telemetry::Component, nil] component DI errors and guardrails metrics are emitted through
85
+ # @return [void]
68
86
  def initialize(settings, agent_settings, logger, code_tracker: nil, telemetry: nil)
69
87
  @settings = settings
70
88
  @agent_settings = agent_settings
71
89
  logger = DI::Logger.new(settings, logger)
72
90
  @logger = logger
73
91
  @telemetry = telemetry
92
+ @guardrails_telemetry = GuardrailsTelemetry.new(settings: settings, logger: logger, telemetry: telemetry)
74
93
  @code_tracker = code_tracker
75
94
  @redactor = Redactor.new(settings)
76
- @serializer = Serializer.new(settings, redactor, telemetry: telemetry)
77
- @instrumenter = Instrumenter.new(settings, serializer, logger, code_tracker: code_tracker, telemetry: telemetry)
95
+ @serializer = Serializer.new(settings, redactor, logger: logger,
96
+ guardrails_telemetry: guardrails_telemetry, telemetry: telemetry)
97
+ @capture_expression_evaluator = CaptureExpressionEvaluator.new(
98
+ settings: settings, serializer: serializer, logger: logger,
99
+ guardrails_telemetry: guardrails_telemetry, telemetry: telemetry,
100
+ )
101
+ @instrumenter = Instrumenter.new(settings, serializer, logger, code_tracker: code_tracker, telemetry: telemetry,
102
+ guardrails_telemetry: guardrails_telemetry,
103
+ capture_expression_evaluator: capture_expression_evaluator,)
78
104
  @probe_repository = ProbeRepository.new
79
- @probe_notification_builder = ProbeNotificationBuilder.new(settings, serializer, logger, telemetry: telemetry)
105
+ @probe_notification_builder = ProbeNotificationBuilder.new(settings, serializer, logger,
106
+ capture_expression_evaluator: capture_expression_evaluator,)
80
107
  @probe_notifier_worker = ProbeNotifierWorker.new(
81
108
  settings, logger,
82
109
  agent_settings: agent_settings,
83
110
  probe_repository: probe_repository,
84
111
  probe_notification_builder: probe_notification_builder,
85
112
  telemetry: telemetry,
113
+ guardrails_telemetry: guardrails_telemetry,
86
114
  )
87
115
  @probe_manager = ProbeManager.new(
88
116
  settings, instrumenter, probe_notification_builder, probe_notifier_worker, logger, probe_repository,
89
117
  telemetry: telemetry,
118
+ guardrails_telemetry: guardrails_telemetry,
90
119
  )
91
120
  # @started transitions are serialized by @lifecycle_mutex so that
92
121
  # concurrent RC callbacks (which run on the remote-config thread)
93
122
  # cannot race a foreground start! with a background stop!.
94
123
  @lifecycle_mutex = Mutex.new
95
124
  @started = false
125
+ @rc_enablement_signal_paths = []
96
126
  end
97
127
 
98
128
  attr_reader :settings
99
129
  attr_reader :agent_settings
100
130
  attr_reader :logger
101
131
  attr_reader :telemetry
132
+ # The guardrails metric emitter.
133
+ # @return [GuardrailsTelemetry]
134
+ attr_reader :guardrails_telemetry
135
+ # The capture expression evaluator.
136
+ # @return [CaptureExpressionEvaluator]
137
+ attr_reader :capture_expression_evaluator
102
138
  attr_reader :code_tracker
103
139
  attr_reader :instrumenter
104
140
  attr_reader :probe_repository
@@ -166,6 +202,75 @@ module Datadog
166
202
  @started
167
203
  end
168
204
 
205
+ # Whether the component is started while the customer left
206
+ # DD_DYNAMIC_INSTRUMENTATION_ENABLED at its default, the state produced
207
+ # by remote-configuration enablement.
208
+ #
209
+ # An explicit `enabled = false` takes precedence over a prior RC
210
+ # enablement: once the customer sets the option, this returns false
211
+ # even while the component is still started.
212
+ #
213
+ # @return [Boolean]
214
+ def implicitly_enabled?
215
+ started? && settings.dynamic_instrumentation.using_default?(:enabled)
216
+ end
217
+
218
+ # Whether an APM_TRACING merge that omitted dynamic_instrumentation_enabled
219
+ # withdraws this component's RC enable signal, so the component must stop.
220
+ #
221
+ # Returns true when the component is implicitly enabled and no unparseable
222
+ # content can be the signal carrier. A content at a recorded signal path can be
223
+ # the carrier. When no paths are recorded, every unparseable content can be the
224
+ # carrier.
225
+ #
226
+ # @param unparseable_contents [Array<Core::Remote::Configuration::Content>] the APM_TRACING contents that failed to parse at this merge
227
+ # @return [Boolean]
228
+ def rc_enablement_signal_withdrawn?(unparseable_contents)
229
+ return false unless implicitly_enabled?
230
+
231
+ recorded_signal_paths = rc_enablement_signal_paths
232
+ signal_source_unparseable =
233
+ if recorded_signal_paths.empty?
234
+ unparseable_contents.any?
235
+ else
236
+ unparseable_contents.any? { |content| recorded_signal_paths.include?(content.path) }
237
+ end
238
+ if signal_source_unparseable
239
+ if recorded_signal_paths.empty?
240
+ logger.trace do
241
+ "APM_TRACING RC: dynamic_instrumentation_enabled absent while an APM_TRACING config " \
242
+ "failed to parse; the enable signal carrier is unknown (no signal paths recorded), " \
243
+ "keeping the DI state"
244
+ end
245
+ else
246
+ logger.trace do
247
+ "APM_TRACING RC: dynamic_instrumentation_enabled absent while the config that carried " \
248
+ "the enable signal failed to parse; keeping the DI state"
249
+ end
250
+ end
251
+ false
252
+ else
253
+ logger.trace { "APM_TRACING RC: dynamic_instrumentation_enabled withdrawn; treating as false" }
254
+ true
255
+ end
256
+ end
257
+
258
+ # The repository paths of the APM_TRACING contents that supplied the
259
+ # dynamic_instrumentation_enabled value at the last merge where the field was
260
+ # present. The component holds the paths so that they outlive the RC client.
261
+ #
262
+ # @return [Array<Core::Remote::Configuration::Path>]
263
+ attr_reader :rc_enablement_signal_paths
264
+
265
+ # Replaces the recorded enable-signal carrier paths.
266
+ #
267
+ # @param paths [Array<Core::Remote::Configuration::Path>] the carrier paths, empty when the merged field was absent
268
+ # @return [void]
269
+ def record_rc_enablement_signal_paths(paths)
270
+ @rc_enablement_signal_paths = paths.dup
271
+ nil
272
+ end
273
+
169
274
  # Shuts down dynamic instrumentation permanently.
170
275
  #
171
276
  # Removes all code hooks and stops background threads.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../core/configuration/stable_config"
4
+
3
5
  module Datadog
4
6
  module DI
5
7
  # Configuration for DI
@@ -136,8 +138,35 @@ module Datadog
136
138
 
137
139
  option :max_time_to_serialize_ms do |o|
138
140
  o.type :int
139
- o.default 200
140
- o.env "DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE"
141
+ o.env "DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS"
142
+ o.default do
143
+ legacy = Settings.legacy_capture_timeout_value
144
+ if legacy
145
+ Datadog::Core.log_deprecation(key: :di_legacy_max_time_to_serialize) do
146
+ "The DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE environment variable is deprecated " \
147
+ "and will be removed in a future release. " \
148
+ "Please use DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS instead."
149
+ end
150
+
151
+ begin
152
+ Integer(legacy, 10)
153
+ rescue ArgumentError
154
+ raise ArgumentError,
155
+ "Expected environment variable DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE " \
156
+ "to be an int, but '#{legacy}' was provided."
157
+ end
158
+ else
159
+ 150
160
+ end
161
+ end
162
+ o.setter do |value, _old|
163
+ if value < 0
164
+ raise ArgumentError,
165
+ "The setting `dynamic_instrumentation.max_time_to_serialize_ms` must not be negative, " \
166
+ "but `#{value.inspect}` was provided."
167
+ end
168
+ value
169
+ end
141
170
  end
142
171
 
143
172
  # Settings in the 'internal' group are for internal Datadog
@@ -244,6 +273,28 @@ module Datadog
244
273
  end
245
274
  end
246
275
  end
276
+
277
+ # Resolves the deprecated capture-timeout name for the setting's
278
+ # default. The legacy value applies only when the canonical variable
279
+ # is configured nowhere (environment with its registry alias, local or
280
+ # fleet stable configuration), and the legacy itself resolves in the
281
+ # option machinery's precedence order: fleet stable configuration,
282
+ # environment, local stable configuration.
283
+ def self.legacy_capture_timeout_value
284
+ canonical_name = "DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS"
285
+ legacy_name = "DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE"
286
+
287
+ stable = Datadog::Core::Configuration::StableConfig.configuration
288
+ local = stable&.dig(:local, :config)
289
+ fleet = stable&.dig(:fleet, :config)
290
+
291
+ canonical = (fleet && DATADOG_ENV.get_environment_variable(canonical_name, source_env: fleet)) ||
292
+ DATADOG_ENV[canonical_name] ||
293
+ (local && DATADOG_ENV.get_environment_variable(canonical_name, source_env: local))
294
+ return nil if canonical
295
+
296
+ (fleet && fleet[legacy_name]) || DATADOG_ENV[legacy_name] || (local && local[legacy_name])
297
+ end
247
298
  end
248
299
  end
249
300
  end
@@ -61,9 +61,14 @@ module Datadog
61
61
  # Exception raised by the method, if any, for a method probe
62
62
  attr_reader :exception
63
63
 
64
- def serialized_locals
64
+ # Returns the locals captured at the probe hit in serialized form,
65
+ # or nil when no locals were captured.
66
+ #
67
+ # @param serialization_deadline [Float] absolute capture deadline in monotonic clock seconds
68
+ # @return [Hash{Symbol => Hash}, nil] serialized locals keyed by variable name
69
+ def serialized_locals(serialization_deadline:)
65
70
  # TODO cache?
66
- locals && serializer.serialize_vars(locals, **probe.snapshot_serializer_limits(settings))
71
+ locals && serializer.serialize_vars(locals, deadline: serialization_deadline, **probe.snapshot_serializer_limits(settings))
67
72
  end
68
73
 
69
74
  def fetch(var_name)
@@ -10,7 +10,7 @@ Datadog::DI::Serializer.register(
10
10
  # logic for "instances of classes derived from X", but a condition Proc
11
11
  # is more universal.
12
12
  condition: lambda { |value| ActiveRecord::Base === value }
13
- ) do |serializer, value, name:, depth:|
13
+ ) do |serializer, value, name:, depth:, deadline:|
14
14
  # +serializer+ is an instance of DI::Serializer.
15
15
  # Use it to perform the serialization to primitive values.
16
16
  #
@@ -30,12 +30,18 @@ Datadog::DI::Serializer.register(
30
30
  # nesting levels in the output.
31
31
  # This serializer could also potentially do its own depth limiting.
32
32
  #
33
+ # +deadline+ is the absolute monotonic-clock deadline (in seconds) of the
34
+ # capture point that is being serialized. Pass it through to
35
+ # +serialize_value+ so the custom-serialized subtree draws down the same
36
+ # capture budget as the rest of the snapshot.
37
+ #
33
38
  # Steep: steep thinks all of the arguments are nil here
34
39
  # Looks like it cannot handle kwargs in lambdas
35
40
  # @type var depth: Integer
41
+ # @type var deadline: Float
36
42
  value_to_serialize = {
37
43
  attributes: value.attributes,
38
44
  new_record: value.new_record?,
39
45
  }
40
- serializer.serialize_value(value_to_serialize, depth: depth, type: value.class)
46
+ serializer.serialize_value(value_to_serialize, depth: depth, type: value.class, deadline: deadline)
41
47
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ # An expression evaluation failure reported in a snapshot's evaluation
6
+ # errors: the expression that failed and the error description.
7
+ # @api private
8
+ class EvaluationFailure
9
+ # Initializes the failure.
10
+ #
11
+ # @param expr [String] name of the capture expression, or source of the condition or template expression, that failed to evaluate or serialize
12
+ # @param message [String] error description of the failed evaluation
13
+ # @return [void]
14
+ def initialize(expr:, message:)
15
+ @expr = expr
16
+ @message = message
17
+ end
18
+
19
+ # The name of the capture expression, or the source of the condition or
20
+ # template expression, that failed to evaluate or serialize.
21
+ # @return [String]
22
+ attr_reader :expr
23
+
24
+ # @return [String] the error description of the failed evaluation
25
+ attr_reader :message
26
+ end
27
+ end
28
+ end