datadog 2.33.0 → 2.35.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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +99 -1
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +20 -0
  4. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +2 -15
  5. data/ext/datadog_profiling_native_extension/macos_sampler_thread.h +55 -0
  6. data/ext/datadog_profiling_native_extension/stack_recorder.c +6 -11
  7. data/lib/datadog/ai_guard/configuration.rb +1 -0
  8. data/lib/datadog/ai_guard/contrib/rack/request_middleware.rb +53 -39
  9. data/lib/datadog/ai_guard/evaluation.rb +6 -1
  10. data/lib/datadog/ai_guard/ext.rb +12 -1
  11. data/lib/datadog/appsec/api_security/route_extractor.rb +3 -0
  12. data/lib/datadog/appsec/component.rb +4 -1
  13. data/lib/datadog/appsec/compressed_json.rb +2 -2
  14. data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +3 -3
  15. data/lib/datadog/appsec/contrib/rack/ext.rb +1 -1
  16. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +9 -40
  17. data/lib/datadog/appsec/default_header_tags.rb +48 -0
  18. data/lib/datadog/core/configuration/components.rb +8 -1
  19. data/lib/datadog/core/configuration/settings.rb +16 -7
  20. data/lib/datadog/core/configuration/supported_configurations.rb +10 -0
  21. data/lib/datadog/core/environment/ext.rb +4 -0
  22. data/lib/datadog/core/environment/identity.rb +15 -1
  23. data/lib/datadog/core/environment/process.rb +50 -27
  24. data/lib/datadog/core/remote/client/capabilities.rb +11 -2
  25. data/lib/datadog/core/remote/transport/http/config.rb +5 -5
  26. data/lib/datadog/core/telemetry/request.rb +0 -2
  27. data/lib/datadog/core/transport/response.rb +1 -1
  28. data/lib/datadog/core/utils/{base64.rb → base64_codec.rb} +3 -2
  29. data/lib/datadog/core/utils/{array.rb → enumerable_compat.rb} +2 -2
  30. data/lib/datadog/core/utils/hash.rb +0 -23
  31. data/lib/datadog/core/utils/spawn_monkey_patch.rb +46 -16
  32. data/lib/datadog/data_streams/pathway_context.rb +3 -3
  33. data/lib/datadog/di/code_tracker.rb +43 -22
  34. data/lib/datadog/di/contrib/active_record.rb +6 -2
  35. data/lib/datadog/di/instrumenter.rb +24 -4
  36. data/lib/datadog/di/probe_notification_builder.rb +1 -1
  37. data/lib/datadog/di/remote.rb +4 -4
  38. data/lib/datadog/di/serializer.rb +5 -5
  39. data/lib/datadog/di/utils.rb +42 -14
  40. data/lib/datadog/opentelemetry/configuration/settings.rb +65 -0
  41. data/lib/datadog/opentelemetry/ext.rb +9 -0
  42. data/lib/datadog/opentelemetry/logs.rb +98 -0
  43. data/lib/datadog/opentelemetry/metrics.rb +10 -46
  44. data/lib/datadog/opentelemetry/sdk/configurator.rb +40 -0
  45. data/lib/datadog/opentelemetry/sdk/logs_exporter.rb +37 -0
  46. data/lib/datadog/opentelemetry/signal_configuration.rb +53 -0
  47. data/lib/datadog/opentelemetry.rb +1 -0
  48. data/lib/datadog/profiling/collectors/thread_context.rb +0 -4
  49. data/lib/datadog/profiling/component.rb +3 -11
  50. data/lib/datadog/profiling/ext/dir_monkey_patches.rb +3 -2
  51. data/lib/datadog/profiling/stack_recorder.rb +0 -4
  52. data/lib/datadog/symbol_database/component.rb +409 -0
  53. data/lib/datadog/symbol_database/configuration.rb +2 -2
  54. data/lib/datadog/symbol_database/extractor.rb +32 -4
  55. data/lib/datadog/symbol_database/remote.rb +175 -0
  56. data/lib/datadog/symbol_database/scope_batcher.rb +8 -0
  57. data/lib/datadog/symbol_database/service_version.rb +11 -2
  58. data/lib/datadog/symbol_database/symbol.rb +6 -3
  59. data/lib/datadog/symbol_database/uploader.rb +62 -8
  60. data/lib/datadog/tracing/contrib/action_cable/events/broadcast.rb +4 -1
  61. data/lib/datadog/tracing/contrib/action_cable/events/perform_action.rb +4 -1
  62. data/lib/datadog/tracing/contrib/action_cable/events/transmit.rb +4 -1
  63. data/lib/datadog/tracing/contrib/action_cable/instrumentation.rb +4 -1
  64. data/lib/datadog/tracing/contrib/action_mailer/event.rb +4 -1
  65. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +1 -0
  66. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/instrumentation.rb +8 -0
  67. data/lib/datadog/tracing/contrib/action_view/events/render_partial.rb +4 -1
  68. data/lib/datadog/tracing/contrib/action_view/events/render_template.rb +4 -1
  69. data/lib/datadog/tracing/contrib/active_job/events/discard.rb +4 -1
  70. data/lib/datadog/tracing/contrib/active_job/events/enqueue.rb +4 -1
  71. data/lib/datadog/tracing/contrib/active_job/events/enqueue_at.rb +4 -1
  72. data/lib/datadog/tracing/contrib/active_job/events/enqueue_retry.rb +4 -1
  73. data/lib/datadog/tracing/contrib/active_job/events/perform.rb +4 -1
  74. data/lib/datadog/tracing/contrib/active_job/events/retry_stopped.rb +4 -1
  75. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -4
  76. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +1 -4
  77. data/lib/datadog/tracing/contrib/active_support/cache/instrumentation.rb +1 -4
  78. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +1 -5
  79. data/lib/datadog/tracing/contrib/dalli/instrumentation.rb +1 -5
  80. data/lib/datadog/tracing/contrib/delayed_job/plugin.rb +2 -0
  81. data/lib/datadog/tracing/contrib/delayed_job/server_internal_tracer/worker.rb +1 -0
  82. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +1 -5
  83. data/lib/datadog/tracing/contrib/elasticsearch/quantize.rb +2 -2
  84. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +1 -5
  85. data/lib/datadog/tracing/contrib/ethon/multi_patch.rb +1 -8
  86. data/lib/datadog/tracing/contrib/excon/middleware.rb +1 -5
  87. data/lib/datadog/tracing/contrib/ext.rb +3 -1
  88. data/lib/datadog/tracing/contrib/faraday/middleware.rb +1 -5
  89. data/lib/datadog/tracing/contrib/grape/endpoint.rb +3 -0
  90. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +1 -0
  91. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/client.rb +1 -5
  92. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/server.rb +1 -5
  93. data/lib/datadog/tracing/contrib/hanami/action_tracer.rb +1 -0
  94. data/lib/datadog/tracing/contrib/hanami/renderer_policy_tracing.rb +1 -0
  95. data/lib/datadog/tracing/contrib/hanami/router_tracing.rb +1 -0
  96. data/lib/datadog/tracing/contrib/http/instrumentation.rb +1 -5
  97. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +1 -5
  98. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +1 -5
  99. data/lib/datadog/tracing/contrib/kafka/event.rb +1 -0
  100. data/lib/datadog/tracing/contrib/mongodb/parsers.rb +5 -5
  101. data/lib/datadog/tracing/contrib/mongodb/subscribers.rb +2 -5
  102. data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +1 -5
  103. data/lib/datadog/tracing/contrib/opensearch/patcher.rb +1 -5
  104. data/lib/datadog/tracing/contrib/opensearch/quantize.rb +2 -2
  105. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +1 -5
  106. data/lib/datadog/tracing/contrib/presto/instrumentation.rb +3 -5
  107. data/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb +3 -0
  108. data/lib/datadog/tracing/contrib/propagation/sql_comment/mode.rb +2 -2
  109. data/lib/datadog/tracing/contrib/que/tracer.rb +1 -0
  110. data/lib/datadog/tracing/contrib/racecar/event.rb +1 -5
  111. data/lib/datadog/tracing/contrib/rack/header_tagging.rb +23 -0
  112. data/lib/datadog/tracing/contrib/rack/middlewares.rb +1 -0
  113. data/lib/datadog/tracing/contrib/rack/trace_proxy_middleware.rb +2 -0
  114. data/lib/datadog/tracing/contrib/rails/runner.rb +2 -0
  115. data/lib/datadog/tracing/contrib/rake/instrumentation.rb +4 -2
  116. data/lib/datadog/tracing/contrib/redis/tags.rb +0 -5
  117. data/lib/datadog/tracing/contrib/redis/trace_middleware.rb +2 -0
  118. data/lib/datadog/tracing/contrib/resque/resque_job.rb +1 -0
  119. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +1 -5
  120. data/lib/datadog/tracing/contrib/roda/ext.rb +1 -0
  121. data/lib/datadog/tracing/contrib/roda/instrumentation.rb +4 -1
  122. data/lib/datadog/tracing/contrib/sequel/database.rb +1 -0
  123. data/lib/datadog/tracing/contrib/sequel/dataset.rb +1 -0
  124. data/lib/datadog/tracing/contrib/sequel/utils.rb +0 -5
  125. data/lib/datadog/tracing/contrib/shoryuken/tracer.rb +1 -0
  126. data/lib/datadog/tracing/contrib/sidekiq/client_tracer.rb +1 -0
  127. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/heartbeat.rb +2 -0
  128. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/job_fetch.rb +1 -0
  129. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/redis_info.rb +1 -0
  130. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/scheduled_poller.rb +2 -0
  131. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/stop.rb +1 -0
  132. data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +3 -2
  133. data/lib/datadog/tracing/contrib/sinatra/tracer.rb +1 -0
  134. data/lib/datadog/tracing/contrib/sinatra/tracer_middleware.rb +1 -0
  135. data/lib/datadog/tracing/contrib/sneakers/tracer.rb +1 -0
  136. data/lib/datadog/tracing/contrib/sucker_punch/instrumentation.rb +1 -0
  137. data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +1 -5
  138. data/lib/datadog/tracing/contrib/utils/quantization/{hash.rb → hash_formatter.rb} +1 -1
  139. data/lib/datadog/tracing/distributed/datadog_tags_codec.rb +0 -13
  140. data/lib/datadog/tracing/distributed/trace_context.rb +0 -28
  141. data/lib/datadog/tracing/metadata/ext.rb +10 -0
  142. data/lib/datadog/tracing/span_operation.rb +13 -0
  143. data/lib/datadog/tracing/trace_operation.rb +22 -0
  144. data/lib/datadog/tracing/tracer.rb +9 -0
  145. data/lib/datadog/tracing/transport/traces.rb +2 -2
  146. data/lib/datadog/version.rb +1 -1
  147. metadata +16 -10
@@ -0,0 +1,409 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'extractor'
4
+ require_relative 'logger'
5
+ require_relative 'scope_batcher'
6
+ require_relative 'uploader'
7
+ require_relative '../core/utils/time'
8
+
9
+ module Datadog
10
+ module SymbolDatabase
11
+ # Main coordinator for symbol database upload functionality.
12
+ #
13
+ # Responsibilities:
14
+ # - Lifecycle management: Initialization, shutdown, upload triggering
15
+ # - Coordination: Connects Extractor → ScopeBatcher → Uploader
16
+ # - Remote config handling: start_upload called by Remote module on config changes
17
+ # - Debounce: extraction is deferred by EXTRACT_DEBOUNCE_INTERVAL seconds so
18
+ # reconfigurations during boot coalesce into a single extraction on the
19
+ # final Component instance.
20
+ #
21
+ # Upload flow:
22
+ # 1. Remote config sends upload_symbols: true (or force_upload mode)
23
+ # 2. start_upload called — schedules extraction EXTRACT_DEBOUNCE_INTERVAL
24
+ # seconds in the future on a per-instance scheduler thread.
25
+ # 3. When the timer fires (no further start_upload calls reset it),
26
+ # extract_and_upload runs: ObjectSpace iteration → Extractor → ScopeBatcher.
27
+ # 4. ScopeBatcher batches and triggers Uploader.
28
+ # 5. A class-level flag is set so subsequent Component instances created via
29
+ # Datadog reconfiguration do not re-upload.
30
+ #
31
+ # Created by: Components#initialize (in Core::Configuration::Components)
32
+ # Accessed by: Remote config receiver via Datadog.send(:components).symbol_database
33
+ # Requires: Remote config enabled (unless force mode)
34
+ #
35
+ # @api private
36
+ class Component
37
+ # Debounce window for extraction. Multiple start_upload calls within this
38
+ # window coalesce; the timer fires once after the window of inactivity.
39
+ # Long enough to absorb reconfiguration cascades during Rails boot.
40
+ EXTRACT_DEBOUNCE_INTERVAL = 5 # seconds
41
+
42
+ # Class-level state: tracks whether any Component instance in this process
43
+ # has performed an extract+upload. Survives Component replacement during
44
+ # Datadog reconfiguration so duplicate uploads are prevented.
45
+ @uploaded_this_process = false
46
+ @upload_done_mutex = Mutex.new
47
+ @upload_done_cv = ConditionVariable.new
48
+
49
+ class << self
50
+ attr_reader :upload_done_mutex, :upload_done_cv
51
+
52
+ # Whether any Component instance in this process has completed an
53
+ # upload. Cross-instance flag — used to dedupe uploads across
54
+ # Component rebuilds within a single Ruby process.
55
+ # @return [Boolean]
56
+ def uploaded_this_process?
57
+ @upload_done_mutex.synchronize { @uploaded_this_process }
58
+ end
59
+
60
+ # Mark the current process as having completed a symbol upload.
61
+ # Called by the Component instance that successfully completes an
62
+ # upload; subsequent start_upload calls on any instance short-circuit.
63
+ # @return [void]
64
+ def mark_uploaded
65
+ @upload_done_mutex.synchronize do
66
+ @uploaded_this_process = true
67
+ @upload_done_cv.broadcast
68
+ end
69
+ end
70
+
71
+ # Reset class-level upload state. Test-only.
72
+ # @api private
73
+ def reset_uploaded_this_process_for_tests!
74
+ @upload_done_mutex.synchronize { @uploaded_this_process = false }
75
+ end
76
+ end
77
+
78
+ # Build a new Component if feature is enabled and dependencies met.
79
+ # @param settings [Configuration::Settings] Tracer settings
80
+ # @param agent_settings [Configuration::AgentSettings] Agent configuration
81
+ # @param logger [Logger] Logger instance
82
+ # @param telemetry [Core::Telemetry::Component, nil] Telemetry component for error reporting
83
+ # @return [Component, nil] Component instance or nil if not enabled/requirements not met
84
+ def self.build(settings, agent_settings, logger, telemetry: nil)
85
+ symdb_logger = SymbolDatabase::Logger.new(settings, logger)
86
+
87
+ unless settings.respond_to?(:symbol_database) && settings.symbol_database.enabled
88
+ symdb_logger.debug("symdb: symbol database upload not enabled, skipping")
89
+ return
90
+ end
91
+
92
+ # Symbol database requires MRI Ruby 2.6+.
93
+ # Configuration accessors (settings.symbol_database.*) remain available on all
94
+ # platforms — only the component (upload) is disabled on unsupported engines/versions.
95
+ # environment_supported? logs the specific reason (engine or version) internally.
96
+ return nil unless environment_supported?(symdb_logger)
97
+
98
+ # Requires remote config (unless force mode)
99
+ if !settings.remote&.enabled && !settings.symbol_database.internal.force_upload
100
+ symdb_logger.debug("symdb: remote config not available and force_upload not set, skipping")
101
+ return nil
102
+ end
103
+
104
+ new(settings, agent_settings, symdb_logger, telemetry: telemetry).tap do |component|
105
+ # Defer extraction if force upload mode — wait for app boot to complete
106
+ component.schedule_deferred_upload if settings.symbol_database.internal.force_upload
107
+ end
108
+ end
109
+
110
+ attr_reader :settings, :logger, :last_upload_time, :last_upload_scope_count, :upload_in_progress
111
+
112
+ # Initialize component.
113
+ # @param settings [Configuration::Settings] Tracer settings
114
+ # @param agent_settings [Configuration::AgentSettings] Agent configuration
115
+ # @param logger [Logger] Logger instance
116
+ # @param telemetry [Core::Telemetry::Component, nil] Telemetry component for error reporting
117
+ def initialize(settings, agent_settings, logger, telemetry: nil)
118
+ @settings = settings
119
+ @agent_settings = agent_settings
120
+ @logger = logger
121
+ @telemetry = telemetry
122
+
123
+ @extractor = Extractor.new(logger: logger, settings: settings)
124
+ @uploader = Uploader.new(settings: settings, agent_settings: agent_settings, logger: logger, telemetry: telemetry)
125
+ @scope_batcher = ScopeBatcher.new(@uploader, logger: logger)
126
+
127
+ @last_upload_time = nil
128
+ @last_upload_scope_count = nil
129
+ @mutex = Mutex.new
130
+ @upload_in_progress = false
131
+ @upload_in_progress_cv = ConditionVariable.new
132
+ @shutdown = false
133
+
134
+ # Per-instance scheduler state. The scheduler thread is started lazily
135
+ # on the first start_upload call.
136
+ @scheduler_mutex = Mutex.new
137
+ @scheduler_cv = ConditionVariable.new
138
+ @scheduled_at = nil
139
+ @scheduler_signaled = false
140
+ @scheduler_thread = nil
141
+ end
142
+
143
+ # Schedule a deferred upload that waits for app boot to complete.
144
+ #
145
+ # In Rails: registers ActiveSupport.on_load(:after_initialize). When the
146
+ # hook has already fired (e.g., this Component was built by a reconfigure
147
+ # after Rails finished initializing), the callback runs immediately.
148
+ #
149
+ # In non-Rails: triggers start_upload immediately.
150
+ #
151
+ # Each Component registers its own callback. Old Components that have
152
+ # been shut down short-circuit in start_upload via @shutdown.
153
+ # Cross-process deduplication is handled by the class-level
154
+ # uploaded_this_process? flag, not by guarding registration.
155
+ #
156
+ # @return [void]
157
+ def schedule_deferred_upload
158
+ if defined?(::ActiveSupport) && defined?(::Rails::Railtie)
159
+ # Capture self — on_load runs the block via instance_exec on the
160
+ # loaded object (Rails::Application), so a bare `start_upload`
161
+ # would resolve against it.
162
+ component = self
163
+ logger = @logger
164
+ ::ActiveSupport.on_load(:after_initialize) do
165
+ # Only auto-trigger when Rails has eager-loaded application
166
+ # classes during initialization. In dev (eager_load=false)
167
+ # there is nothing complete to extract; the auto-deferred
168
+ # upload would race with explicit triggers and produce
169
+ # under-extracted uploads.
170
+ if defined?(::Rails) && ::Rails.application&.config&.eager_load # steep:ignore NoMethod
171
+ component.start_upload
172
+ else
173
+ logger.debug { "symdb: skipping auto-deferred upload (eager_load disabled)" }
174
+ end
175
+ end
176
+ else
177
+ start_upload
178
+ end
179
+ end
180
+
181
+ # Whether this component has been shut down.
182
+ # @return [Boolean]
183
+ def shutdown?
184
+ @scheduler_mutex.synchronize { @shutdown }
185
+ end
186
+
187
+ # Schedule symbol upload (triggered by remote config or force mode).
188
+ # The actual extraction is debounced by EXTRACT_DEBOUNCE_INTERVAL seconds —
189
+ # subsequent calls within the window restart the timer.
190
+ # Thread-safe: can be called concurrently from multiple remote config updates.
191
+ # @return [void]
192
+ def start_upload
193
+ return if Component.uploaded_this_process?
194
+
195
+ @scheduler_mutex.synchronize do
196
+ return if @shutdown
197
+
198
+ @scheduled_at = Datadog::Core::Utils::Time.get_time + EXTRACT_DEBOUNCE_INTERVAL
199
+ @scheduler_signaled = true
200
+ @scheduler_cv.signal
201
+ ensure_scheduler_thread
202
+ end
203
+ rescue => e
204
+ @logger.debug { "symdb: error scheduling upload: #{e.class}: #{e.message}" }
205
+ @telemetry&.report(e, description: 'symdb: error scheduling upload')
206
+ end
207
+
208
+ # Stop symbol upload (cancel the scheduler).
209
+ # Thread-safe: can be called concurrently from multiple remote config updates.
210
+ # @return [void]
211
+ def stop_upload
212
+ @scheduler_mutex.synchronize do
213
+ @scheduled_at = nil
214
+ @scheduler_signaled = true
215
+ @scheduler_cv.signal
216
+ end
217
+ end
218
+
219
+ # Block until any Component in this process has finished an extract+upload,
220
+ # or until the timeout elapses. Used by short-lived scripts that trigger
221
+ # an upload via force_upload and need to wait before exiting.
222
+ # @param timeout [Numeric] Maximum seconds to wait
223
+ # @return [Boolean] true if an upload completed; false on timeout
224
+ def wait_for_idle(timeout: 30)
225
+ deadline = Datadog::Core::Utils::Time.get_time + timeout
226
+ Component.upload_done_mutex.synchronize do
227
+ # Read @uploaded_this_process directly: we already hold
228
+ # Component.upload_done_mutex here, and uploaded_this_process?
229
+ # would try to re-acquire it (non-reentrant), deadlocking.
230
+ until Component.instance_variable_get(:@uploaded_this_process)
231
+ remaining = deadline - Datadog::Core::Utils::Time.get_time
232
+ return false if remaining <= 0
233
+ Component.upload_done_cv.wait(Component.upload_done_mutex, remaining)
234
+ end
235
+ end
236
+ true
237
+ end
238
+
239
+ # Shutdown component and cleanup resources.
240
+ # Cancels the per-instance scheduler so any pending debounced extraction
241
+ # is dropped. Waits for an in-flight extraction to complete before
242
+ # returning. Does not touch class-level state, so a sibling Component
243
+ # built after shutdown can still upload.
244
+ # @return [void]
245
+ def shutdown!
246
+ @scheduler_mutex.synchronize do
247
+ @shutdown = true
248
+ @scheduler_signaled = true
249
+ @scheduler_cv.signal
250
+ end
251
+ @scheduler_thread&.join(5)
252
+ @scheduler_thread = nil
253
+
254
+ @mutex.synchronize do
255
+ if @upload_in_progress
256
+ @upload_in_progress_cv.wait(@mutex, 5)
257
+ end
258
+ end
259
+
260
+ @scope_batcher.shutdown
261
+ end
262
+
263
+ private
264
+
265
+ # Check whether the runtime environment supports symbol database upload.
266
+ # Only MRI Ruby 2.6+ is supported. JRuby and TruffleRuby are not supported
267
+ # because ObjectSpace iteration and Method#source_location behave differently.
268
+ # Configuration accessors remain available on all platforms — this only gates
269
+ # the component (upload) itself.
270
+ # @param logger [Logger]
271
+ # @return [Boolean]
272
+ def self.environment_supported?(logger)
273
+ if RUBY_ENGINE != 'ruby'
274
+ logger.debug { "symdb: not supported on #{RUBY_ENGINE}, skipping" }
275
+ return false
276
+ end
277
+ if RUBY_VERSION < '2.6'
278
+ logger.debug { "symdb: requires Ruby 2.6+, running #{RUBY_VERSION}, skipping" }
279
+ return false
280
+ end
281
+ true
282
+ end
283
+ private_class_method :environment_supported?
284
+
285
+ # Start the scheduler thread if not already running.
286
+ # Must be called from within @scheduler_mutex.synchronize.
287
+ # @return [void]
288
+ def ensure_scheduler_thread
289
+ return if @scheduler_thread&.alive?
290
+ @scheduler_thread = Thread.new { scheduler_loop }
291
+ end
292
+
293
+ # Scheduler thread main loop. Waits for the debounce window to elapse,
294
+ # then runs extract_and_upload exactly once for this Component.
295
+ # @return [void]
296
+ def scheduler_loop
297
+ loop do
298
+ # should_fire = true means the debounce deadline elapsed without further
299
+ # signals; extract_and_upload runs once after the mutex is released.
300
+ should_fire = false
301
+
302
+ @scheduler_mutex.synchronize do
303
+ return if @shutdown
304
+ return if Component.uploaded_this_process?
305
+
306
+ # Copy to local so Steep narrows `Float?` to `Float` in the else branch.
307
+ # Steep does not track narrowing on instance variables across nil checks.
308
+ scheduled_at = @scheduled_at
309
+ if scheduled_at.nil?
310
+ # Nothing scheduled (e.g. stop_upload cleared it). Wait
311
+ # indefinitely for a signal, then re-evaluate on next loop.
312
+ @scheduler_signaled = false
313
+ @scheduler_cv.wait(@scheduler_mutex)
314
+ else
315
+ remaining = scheduled_at - Datadog::Core::Utils::Time.get_time
316
+ if remaining > 0
317
+ # Wait until the debounce deadline. Any signal (start_upload,
318
+ # stop_upload, shutdown!) wakes us early; we always re-loop
319
+ # and recompute rather than firing immediately on wake.
320
+ @scheduler_signaled = false
321
+ @scheduler_cv.wait(@scheduler_mutex, remaining)
322
+ else
323
+ # Deadline elapsed without further signal — fire after releasing the mutex.
324
+ should_fire = true
325
+ end
326
+ end
327
+ end
328
+
329
+ # `next` inside `synchronize` only exits the synchronize block — not the
330
+ # surrounding loop. Use an explicit flag so the loop only fires
331
+ # extract_and_upload when the debounce deadline has actually elapsed.
332
+ next unless should_fire
333
+
334
+ # Outside the mutex.
335
+ return if @shutdown
336
+ if Component.uploaded_this_process?
337
+ return
338
+ end
339
+
340
+ extract_and_upload
341
+ Component.mark_uploaded
342
+ return
343
+ end
344
+ rescue => e
345
+ @logger.debug { "symdb: scheduler error: #{e.class}: #{e.message}" }
346
+ @telemetry&.report(e, description: 'symdb: scheduler error')
347
+ end
348
+
349
+ # Extract symbols from all loaded modules and upload.
350
+ # @return [void]
351
+ def extract_and_upload
352
+ @mutex.synchronize { @upload_in_progress = true }
353
+
354
+ begin
355
+ @logger.trace { "symdb: starting extraction and upload" }
356
+ start_time = Datadog::Core::Utils::Time.get_time
357
+
358
+ # Extract symbols from all loaded modules grouped by source file.
359
+ # extract_all handles ObjectSpace iteration, filtering, and FQN-based nesting.
360
+ file_scopes = @extractor.extract_all
361
+ extracted_count = 0
362
+ file_scopes.each do |scope|
363
+ @scope_batcher.add_scope(scope)
364
+ extracted_count += 1
365
+ log_scope_tree(scope, 0)
366
+ end
367
+
368
+ @logger.debug do
369
+ extraction_duration = Datadog::Core::Utils::Time.get_time - start_time
370
+ targetable_count = count_targetable_methods(file_scopes)
371
+ "symdb: extracted #{extracted_count} scopes (#{targetable_count} methods with targetable lines) in #{'%.2f' % extraction_duration}s"
372
+ end
373
+
374
+ # Flush any remaining scopes (triggers upload)
375
+ @scope_batcher.flush
376
+
377
+ @last_upload_time = Datadog::Core::Utils::Time.now
378
+ @last_upload_scope_count = extracted_count
379
+ rescue => e
380
+ @logger.debug { "symdb: extraction error: #{e.class}: #{e.message}" }
381
+ @telemetry&.report(e, description: 'symdb: extraction error')
382
+ ensure
383
+ @mutex.synchronize do
384
+ @upload_in_progress = false
385
+ @upload_in_progress_cv.signal
386
+ end
387
+ end
388
+ end
389
+
390
+ def log_scope_tree(scope, depth)
391
+ indent = ' ' * depth
392
+ @logger.trace { "symdb: #{indent}#{scope.scope_type} #{scope.name}" }
393
+ scope.scopes&.each { |child| log_scope_tree(child, depth + 1) }
394
+ end
395
+
396
+ def count_targetable_methods(file_scopes)
397
+ count = 0
398
+ file_scopes.each do |file_scope|
399
+ file_scope.scopes&.each do |class_or_module|
400
+ class_or_module.scopes&.each do |method_scope|
401
+ count += 1 if method_scope.scope_type == 'METHOD' && method_scope.targetable_lines?
402
+ end
403
+ end
404
+ end
405
+ count
406
+ end
407
+ end
408
+ end
409
+ end
@@ -7,7 +7,7 @@ module Datadog
7
7
  # Configuration settings for symbol database upload feature.
8
8
  #
9
9
  # Public environment variable:
10
- # - DD_SYMBOL_DATABASE_UPLOAD_ENABLED (default: true) - Feature gate
10
+ # - DD_SYMBOL_DATABASE_UPLOAD_ENABLED (default: false) - Feature gate
11
11
  #
12
12
  # Extended into: Core::Configuration::Settings (via extend)
13
13
  # Accessed as: Datadog.configuration.symbol_database.enabled
@@ -31,7 +31,7 @@ module Datadog
31
31
  option :enabled do |o|
32
32
  o.type :bool
33
33
  o.env 'DD_SYMBOL_DATABASE_UPLOAD_ENABLED'
34
- o.default true
34
+ o.default false
35
35
  end
36
36
 
37
37
  # Settings in the 'internal' group are for internal Datadog
@@ -3,12 +3,16 @@
3
3
  require_relative 'scope'
4
4
  require_relative 'symbol'
5
5
  require_relative 'file_hash'
6
- require_relative '../core/utils/array'
6
+ require_relative '../core/utils/enumerable_compat'
7
7
 
8
8
  module Datadog
9
9
  module SymbolDatabase
10
10
  # Extracts symbol metadata from loaded Ruby modules and classes via introspection.
11
11
  #
12
+ # Instance created by Component with injected dependencies (logger, settings).
13
+ # All methods are instance methods accessing @logger, @settings directly —
14
+ # no parameter threading needed.
15
+ #
12
16
  # Uses Ruby's reflection APIs (Module#constants, Class#instance_methods, Method#parameters)
13
17
  # to build hierarchical Scope structures representing code organization.
14
18
  # Filters to user code only (excludes gems, stdlib, test files).
@@ -80,6 +84,11 @@ module Datadog
80
84
  MODULE_SINGLETON_CLASS_PRED = Module.instance_method(:singleton_class?)
81
85
  private_constant :MODULE_SINGLETON_CLASS_PRED
82
86
 
87
+ # Cached UnboundMethod for Module#name — avoids resolving it on every
88
+ # safe_mod_name call. Some classes override .name (e.g. Faker::Travel::Airport),
89
+ # so we bind the original Module#name to get the real module name safely.
90
+ MODULE_NAME = Module.instance_method(:name)
91
+
83
92
  # @param logger [Logger] Logger instance (SymbolDatabase::Logger facade or compatible)
84
93
  # @param settings [Configuration::Settings] Tracer settings
85
94
  def initialize(logger:, settings:)
@@ -150,7 +159,7 @@ module Datadog
150
159
  # @param mod [Module] The module
151
160
  # @return [String, nil] Module name or nil
152
161
  def safe_mod_name(mod)
153
- Module.instance_method(:name).bind(mod).call
162
+ MODULE_NAME.bind(mod).call
154
163
  rescue => e
155
164
  @logger.debug { "symdb: safe_mod_name failed: #{e.class}: #{e.message}" }
156
165
  nil
@@ -584,7 +593,7 @@ module Datadog
584
593
 
585
594
  return [] if params.nil? || params.empty?
586
595
 
587
- Core::Utils::Array.filter_map(params) do |param_type, param_name|
596
+ Core::Utils::EnumerableCompat.filter_map(params) do |param_type, param_name|
588
597
  # Skip block parameters for MVP
589
598
  next if param_type == :block
590
599
 
@@ -605,10 +614,26 @@ module Datadog
605
614
 
606
615
  # ── extract_all helpers ──────────────────────────────────────────────
607
616
 
617
+ # Sleep between chunks of modules processed in collect_extractable_modules so
618
+ # request-handling threads have guaranteed CPU time while extraction is in
619
+ # flight. Unlike Thread.pass (which only offers the GVL among runnable
620
+ # threads and leaves the extractor immediately re-runnable), sleep removes
621
+ # the extractor thread from the runnable set for a fixed duration, capping
622
+ # its CPU share at sleep_work_ratio regardless of GVL scheduling.
623
+ #
624
+ # The cadence is measured in modules that pass the singleton-class fast-path
625
+ # skip — singleton classes are discarded in microseconds and counting them
626
+ # would add wall-clock delay disproportionate to the work being done (e.g.
627
+ # on heavily monkey-patched processes that retain large singleton chains).
628
+ SLEEP_EVERY_N_MODULES = 100
629
+ SLEEP_SECONDS = 0.001
630
+ private_constant :SLEEP_EVERY_N_MODULES, :SLEEP_SECONDS
631
+
608
632
  # Pass 1: Collect all extractable modules with methods grouped by source file.
609
633
  # @return [Hash] { mod_name => { mod:, methods_by_file: { path => [{name:, method:, type:}] } } }
610
634
  def collect_extractable_modules
611
635
  entries = {}
636
+ seen = 0
612
637
 
613
638
  ObjectSpace.each_object(Module) do |mod|
614
639
  # Singleton classes (per-object metaclasses) are never user-code classes.
@@ -620,6 +645,9 @@ module Datadog
620
645
  # processes. Ruby 2.7+ optimized this path; the skip is a no-op there.
621
646
  next if MODULE_SINGLETON_CLASS_PRED.bind(mod).call
622
647
 
648
+ seen += 1
649
+ sleep SLEEP_SECONDS if (seen % SLEEP_EVERY_N_MODULES).zero?
650
+
623
651
  mod_name = safe_mod_name(mod)
624
652
  next unless mod_name
625
653
  next unless user_code_module?(mod)
@@ -782,7 +810,7 @@ module Datadog
782
810
  # @return [Scope] Scope object
783
811
  def convert_node_to_scope(node)
784
812
  # Build method scopes from collected method entries
785
- method_scopes = Core::Utils::Array.filter_map(node[:methods]) do |method_info|
813
+ method_scopes = Core::Utils::EnumerableCompat.filter_map(node[:methods]) do |method_info|
786
814
  build_instance_method_scope(node[:mod], method_info[:name], method_info[:method])
787
815
  end
788
816
 
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module SymbolDatabase
5
+ # Provides remote configuration integration for symbol database.
6
+ #
7
+ # Responsibilities:
8
+ # - Registers with Core::Remote as a receiver for LIVE_DEBUGGING_SYMBOL_DB product
9
+ # - Processes remote config changes (insert/update/delete)
10
+ # - Calls Component.start_upload when upload_symbols: true
11
+ # - Calls Component.stop_upload when config deleted or upload_symbols: false
12
+ #
13
+ # Flow:
14
+ # 1. Remote config system calls receiver with repository and changes
15
+ # 2. For each change, process_change called
16
+ # 3. parse_config extracts upload_symbols flag
17
+ # 4. enable_upload or disable_upload called on component
18
+ #
19
+ # Created by: Symbol database initialization
20
+ # Accessed by: Core::Remote system when configurations change
21
+ # Requires: Component must exist (accessed via Datadog.send(:components).symbol_database)
22
+ #
23
+ # @api private
24
+ module Remote
25
+ PRODUCT = 'LIVE_DEBUGGING_SYMBOL_DB'
26
+
27
+ class << self
28
+ # Declare products this receiver handles.
29
+ # @return [Array<String>] Product names
30
+ def products
31
+ [PRODUCT]
32
+ end
33
+
34
+ # Declare capabilities for this receiver.
35
+ # @return [Array] Capabilities (none for symbol database)
36
+ def capabilities
37
+ []
38
+ end
39
+
40
+ # Create receivers for remote configuration.
41
+ # @return [Array<Receiver>] Array of receivers
42
+ def receivers(_telemetry)
43
+ receiver do |repository, changes|
44
+ telemetry = lookup_telemetry
45
+ component = begin
46
+ Datadog.send(:components, allow_initialization: false)&.symbol_database
47
+ rescue => e
48
+ Datadog.logger.debug { "symdb: failed to look up component in RC receiver: #{e.class}: #{e.message}" }
49
+ telemetry&.report(e, description: 'symdb: failed to look up component in RC receiver')
50
+ nil
51
+ end
52
+
53
+ if component
54
+ changes.each do |change|
55
+ process_change(component, change, telemetry)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ # Create a single receiver for the product.
62
+ # @param products [Array<String>] Product names to match
63
+ # @return [Array<Receiver>] Receiver array
64
+ def receiver(products = [PRODUCT], &block)
65
+ matcher = Core::Remote::Dispatcher::Matcher::Product.new(products)
66
+ [Core::Remote::Dispatcher::Receiver.new(matcher, &block)]
67
+ end
68
+
69
+ private
70
+
71
+ # Look up the telemetry component for error reporting. Returns nil if the
72
+ # component tree isn't built yet (very early boot) or the lookup raises.
73
+ # `allow_initialization: false` avoids triggering component-tree construction
74
+ # from inside an RC receiver callback.
75
+ # @return [Core::Telemetry::Component, nil]
76
+ # @api private
77
+ def lookup_telemetry
78
+ Datadog.send(:components, allow_initialization: false)&.telemetry
79
+ rescue
80
+ nil
81
+ end
82
+
83
+ # Process a single configuration change.
84
+ # @param component [Component] Symbol database component
85
+ # @param change [Change] Configuration change (:insert, :update, :delete)
86
+ # @param telemetry [Core::Telemetry::Component, nil] Telemetry for error reporting
87
+ # @return [void]
88
+ # @api private
89
+ def process_change(component, change, telemetry)
90
+ case change.type
91
+ when :insert
92
+ # @type var change: ::Datadog::Core::Remote::Configuration::Repository::Change::Inserted
93
+ enable_upload(component, change.content)
94
+ change.content.applied
95
+ when :update
96
+ # @type var change: ::Datadog::Core::Remote::Configuration::Repository::Change::Updated
97
+ disable_upload(component)
98
+ enable_upload(component, change.content)
99
+ change.content.applied
100
+ when :delete
101
+ # @type var change: ::Datadog::Core::Remote::Configuration::Repository::Change::Deleted
102
+ disable_upload(component)
103
+ change.previous&.applied
104
+ else
105
+ component.logger.debug { "symdb: unrecognized change type: #{change.type}" }
106
+ # Steep cannot narrow `change.content` from a respond_to? check — it sees
107
+ # the Repository::Change union type where `Deleted` lacks `content`.
108
+ change.content.errored("Unrecognized change type: #{change.type}") if change.respond_to?(:content) # steep:ignore NoMethod
109
+ end
110
+ rescue => e
111
+ component.logger.debug { "symdb: error processing remote config change: #{e.class}: #{e.message}" }
112
+ telemetry&.report(e, description: 'symdb: error processing remote config change')
113
+ # Rescue runs regardless of which branch raised — Steep cannot narrow the
114
+ # union type from a respond_to? check.
115
+ content_obj = change.respond_to?(:content) ? change.content : change.previous # steep:ignore NoMethod
116
+ content_obj&.errored(e.to_s)
117
+ end
118
+
119
+ # Enable upload if config has upload_symbols: true.
120
+ # @param component [Component] Symbol database component
121
+ # @param content [Content] Remote config content
122
+ # @return [void]
123
+ # @api private
124
+ def enable_upload(component, content)
125
+ config = parse_config(content, component.logger)
126
+
127
+ unless config
128
+ return
129
+ end
130
+
131
+ if config['upload_symbols']
132
+ component.logger.debug { "symdb: upload enabled via remote config" }
133
+ component.start_upload
134
+ else
135
+ component.logger.debug { "symdb: upload disabled in config" }
136
+ end
137
+ end
138
+
139
+ # Disable upload.
140
+ # @param component [Component] Symbol database component
141
+ # @return [void]
142
+ # @api private
143
+ def disable_upload(component)
144
+ component.logger.debug { "symdb: upload disabled via remote config" }
145
+ component.stop_upload
146
+ end
147
+
148
+ # Parse and validate remote config content.
149
+ # @param content [Content] Remote config content
150
+ # @param logger [SymbolDatabase::Logger] Logger for invalid-config diagnostics
151
+ # @return [Hash, nil] Parsed config or nil if invalid
152
+ # @api private
153
+ #
154
+ # JSON::ParserError is intentionally NOT rescued here — it propagates to
155
+ # process_change's rescue, which logs and reports to telemetry. Catching
156
+ # it locally would swallow the error from telemetry observability.
157
+ def parse_config(content, logger)
158
+ config = JSON.parse(content.data)
159
+
160
+ unless config.is_a?(Hash)
161
+ logger.debug { "symdb: invalid config format: expected Hash, got #{config.class}" }
162
+ return nil
163
+ end
164
+
165
+ unless config.key?('upload_symbols')
166
+ logger.debug { "symdb: missing 'upload_symbols' key in config" }
167
+ return nil
168
+ end
169
+
170
+ config
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end