datadog 2.35.0 → 2.37.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 (144) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +85 -1
  3. data/ext/datadog_profiling_native_extension/clock_id_from_mach.c +5 -12
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +71 -31
  5. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
  6. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +18 -4
  7. data/ext/datadog_profiling_native_extension/collectors_stack.c +37 -18
  8. data/ext/datadog_profiling_native_extension/collectors_stack.h +8 -2
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +596 -341
  10. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +11 -7
  11. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +7 -8
  12. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -12
  13. data/ext/datadog_profiling_native_extension/extconf.rb +13 -2
  14. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +4 -43
  15. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +15 -47
  16. data/ext/datadog_profiling_native_extension/heap_recorder.c +44 -26
  17. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +14 -35
  18. data/ext/datadog_profiling_native_extension/profiling.c +41 -4
  19. data/ext/datadog_profiling_native_extension/ruby_helpers.c +40 -34
  20. data/ext/datadog_profiling_native_extension/stack_recorder.c +24 -3
  21. data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -0
  22. data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.h +4 -2
  23. data/ext/libdatadog_api/datadog_ruby_common.c +7 -8
  24. data/ext/libdatadog_api/datadog_ruby_common.h +0 -12
  25. data/ext/libdatadog_api/di.c +76 -0
  26. data/ext/libdatadog_api/extconf.rb +6 -0
  27. data/ext/libdatadog_extconf_helpers.rb +1 -1
  28. data/lib/datadog/ai_guard/api_client.rb +6 -4
  29. data/lib/datadog/appsec/api_security/route_extractor.rb +13 -4
  30. data/lib/datadog/appsec/component.rb +1 -1
  31. data/lib/datadog/appsec/configuration.rb +13 -0
  32. data/lib/datadog/appsec/context.rb +4 -2
  33. data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +37 -4
  34. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +59 -4
  35. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +59 -5
  36. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +64 -19
  37. data/lib/datadog/appsec/contrib/graphql/integration.rb +1 -0
  38. data/lib/datadog/appsec/contrib/rack/buffered_input.rb +83 -0
  39. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +41 -3
  40. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +24 -7
  41. data/lib/datadog/appsec/contrib/rack/input_peeker.rb +79 -0
  42. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +21 -0
  43. data/lib/datadog/appsec/contrib/rails/gateway/request.rb +35 -1
  44. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +17 -1
  45. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +60 -7
  46. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +24 -3
  47. data/lib/datadog/appsec/default_header_tags.rb +10 -6
  48. data/lib/datadog/appsec/ext.rb +1 -0
  49. data/lib/datadog/appsec/metrics/collector.rb +18 -1
  50. data/lib/datadog/appsec/metrics/exporter.rb +9 -3
  51. data/lib/datadog/appsec/remote.rb +9 -3
  52. data/lib/datadog/appsec/route_normalizer/rails_route_pattern.rb +176 -0
  53. data/lib/datadog/appsec/route_normalizer/route_pattern.rb +378 -0
  54. data/lib/datadog/appsec/route_normalizer/route_text.rb +57 -0
  55. data/lib/datadog/appsec/route_normalizer.rb +80 -0
  56. data/lib/datadog/appsec/utils/http/body_reader.rb +61 -0
  57. data/lib/datadog/core/configuration/components.rb +83 -2
  58. data/lib/datadog/core/configuration/components_state.rb +6 -1
  59. data/lib/datadog/core/configuration/settings.rb +1 -5
  60. data/lib/datadog/core/configuration/supported_configurations.rb +4 -0
  61. data/lib/datadog/core/evp.rb +11 -0
  62. data/lib/datadog/core/remote/client/capabilities.rb +34 -7
  63. data/lib/datadog/core/remote/component.rb +1 -1
  64. data/lib/datadog/core/telemetry/event/app_started.rb +8 -22
  65. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +1 -1
  66. data/lib/datadog/core/utils/forking.rb +3 -1
  67. data/lib/datadog/core/utils/spawn_monkey_patch.rb +3 -1
  68. data/lib/datadog/core.rb +3 -0
  69. data/lib/datadog/di/base.rb +7 -2
  70. data/lib/datadog/di/code_tracker.rb +5 -2
  71. data/lib/datadog/di/component.rb +110 -36
  72. data/lib/datadog/di/error.rb +10 -0
  73. data/lib/datadog/di/fatal_exceptions.rb +26 -0
  74. data/lib/datadog/di/instrumenter.rb +381 -165
  75. data/lib/datadog/di/probe.rb +14 -0
  76. data/lib/datadog/di/probe_file_loader.rb +8 -3
  77. data/lib/datadog/di/probe_manager.rb +67 -9
  78. data/lib/datadog/di/probe_notification_builder.rb +4 -1
  79. data/lib/datadog/di/probe_notifier_worker.rb +52 -32
  80. data/lib/datadog/di/redactor.rb +16 -1
  81. data/lib/datadog/di/remote.rb +175 -14
  82. data/lib/datadog/di/serializer.rb +12 -5
  83. data/lib/datadog/di/transport/input.rb +8 -4
  84. data/lib/datadog/di.rb +63 -0
  85. data/lib/datadog/error_tracking/collector.rb +2 -1
  86. data/lib/datadog/error_tracking/component.rb +2 -2
  87. data/lib/datadog/kit/appsec/events/v2.rb +60 -2
  88. data/lib/datadog/kit/tracing/method_tracer.rb +4 -1
  89. data/lib/datadog/open_feature/component.rb +29 -6
  90. data/lib/datadog/open_feature/configuration.rb +8 -0
  91. data/lib/datadog/open_feature/ext.rb +2 -0
  92. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +286 -0
  93. data/lib/datadog/open_feature/flag_evaluation/writer.rb +433 -0
  94. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +101 -0
  95. data/lib/datadog/open_feature/hooks/{flag_eval_hook.rb → flag_eval_metrics_hook.rb} +1 -1
  96. data/lib/datadog/open_feature/native_evaluator.rb +22 -0
  97. data/lib/datadog/open_feature/provider.rb +59 -27
  98. data/lib/datadog/open_feature/transport.rb +55 -1
  99. data/lib/datadog/opentelemetry/sdk/logs_exporter.rb +5 -12
  100. data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +4 -11
  101. data/lib/datadog/opentelemetry/sdk/propagator.rb +9 -3
  102. data/lib/datadog/opentelemetry/sdk/span_processor.rb +4 -1
  103. data/lib/datadog/opentelemetry/sdk.rb +19 -0
  104. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +1 -1
  105. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -3
  106. data/lib/datadog/profiling/collectors/thread_context.rb +1 -0
  107. data/lib/datadog/profiling/component.rb +13 -15
  108. data/lib/datadog/profiling/ext/dir_monkey_patches.rb +3 -3
  109. data/lib/datadog/ruby_version.rb +25 -0
  110. data/lib/datadog/symbol_database/component.rb +453 -128
  111. data/lib/datadog/symbol_database/configuration.rb +12 -4
  112. data/lib/datadog/symbol_database/extensions.rb +19 -0
  113. data/lib/datadog/symbol_database/extractor.rb +326 -150
  114. data/lib/datadog/symbol_database/file_hash.rb +3 -1
  115. data/lib/datadog/symbol_database/remote.rb +8 -3
  116. data/lib/datadog/symbol_database/scope_batcher.rb +7 -3
  117. data/lib/datadog/symbol_database/uploader.rb +9 -2
  118. data/lib/datadog/symbol_database.rb +22 -0
  119. data/lib/datadog/tracing/configuration/ext.rb +13 -0
  120. data/lib/datadog/tracing/configuration/settings.rb +17 -0
  121. data/lib/datadog/tracing/contrib/configuration/resolver.rb +7 -0
  122. data/lib/datadog/tracing/contrib/grpc/distributed/propagation.rb +2 -0
  123. data/lib/datadog/tracing/contrib/grpc.rb +1 -0
  124. data/lib/datadog/tracing/contrib/http/distributed/propagation.rb +2 -0
  125. data/lib/datadog/tracing/contrib/http.rb +1 -0
  126. data/lib/datadog/tracing/contrib/karafka/distributed/propagation.rb +2 -0
  127. data/lib/datadog/tracing/contrib/karafka.rb +1 -0
  128. data/lib/datadog/tracing/contrib/rack/middlewares.rb +3 -1
  129. data/lib/datadog/tracing/contrib/rack/route_inference.rb +3 -1
  130. data/lib/datadog/tracing/contrib/sidekiq/distributed/propagation.rb +2 -0
  131. data/lib/datadog/tracing/contrib/sidekiq.rb +1 -0
  132. data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +2 -0
  133. data/lib/datadog/tracing/contrib/waterdrop.rb +1 -0
  134. data/lib/datadog/tracing/distributed/baggage.rb +2 -1
  135. data/lib/datadog/tracing/distributed/propagation.rb +33 -1
  136. data/lib/datadog/tracing/distributed/trace_context.rb +11 -2
  137. data/lib/datadog/tracing/ext.rb +9 -0
  138. data/lib/datadog/tracing/remote.rb +34 -3
  139. data/lib/datadog/tracing/trace_digest.rb +7 -0
  140. data/lib/datadog/tracing/trace_operation.rb +4 -1
  141. data/lib/datadog/tracing/tracer.rb +1 -0
  142. data/lib/datadog/version.rb +1 -1
  143. data/lib/datadog.rb +5 -1
  144. metadata +22 -6
@@ -4,6 +4,7 @@ require_relative 'scope'
4
4
  require_relative 'symbol'
5
5
  require_relative 'file_hash'
6
6
  require_relative '../core/utils/enumerable_compat'
7
+ require_relative '../di/fatal_exceptions'
7
8
 
8
9
  module Datadog
9
10
  module SymbolDatabase
@@ -79,7 +80,7 @@ module Datadog
79
80
  # Cached unbound Module#singleton_class? — dispatched explicitly so user classes
80
81
  # that define their own `singleton_class?` (e.g. with required arguments) cannot
81
82
  # intercept the predicate and cause the module to be silently dropped from
82
- # extract_all. Cached at load time because collect_extractable_modules iterates
83
+ # extract_all. Cached at load time because build_per_file_index iterates
83
84
  # ObjectSpace.each_object(Module) over tens of thousands of modules.
84
85
  MODULE_SINGLETON_CLASS_PRED = Module.instance_method(:singleton_class?)
85
86
  private_constant :MODULE_SINGLETON_CLASS_PRED
@@ -89,6 +90,26 @@ module Datadog
89
90
  # so we bind the original Module#name to get the real module name safely.
90
91
  MODULE_NAME = Module.instance_method(:name)
91
92
 
93
+ # Cached UnboundMethods for the remaining class/module/object introspection
94
+ # used during extraction. Like MODULE_NAME, these bind the original
95
+ # implementations and are dispatched explicitly so that application code
96
+ # which overrides any of them (per-class, on a subclass, or in a singleton
97
+ # class) can neither intercept extraction nor be executed as a side effect
98
+ # of it. KERNEL_CLASS recovers an object's real class without calling a
99
+ # possibly-overridden #class.
100
+ CLASS_SUPERCLASS = Class.instance_method(:superclass)
101
+ MODULE_INCLUDED_MODULES = Module.instance_method(:included_modules)
102
+ MODULE_ANCESTORS = Module.instance_method(:ancestors)
103
+ MODULE_CLASS_VARIABLES = Module.instance_method(:class_variables)
104
+ MODULE_CONSTANTS = Module.instance_method(:constants)
105
+ MODULE_AUTOLOAD_P = Module.instance_method(:autoload?)
106
+ MODULE_CONST_GET = Module.instance_method(:const_get)
107
+ MODULE_CONST_DEFINED = Module.instance_method(:const_defined?)
108
+ KERNEL_CLASS = ::Kernel.instance_method(:class)
109
+ private_constant :CLASS_SUPERCLASS, :MODULE_INCLUDED_MODULES, :MODULE_ANCESTORS,
110
+ :MODULE_CLASS_VARIABLES, :MODULE_CONSTANTS, :MODULE_AUTOLOAD_P, :MODULE_CONST_GET,
111
+ :MODULE_CONST_DEFINED, :KERNEL_CLASS
112
+
92
113
  # @param logger [Logger] Logger instance (SymbolDatabase::Logger facade or compatible)
93
114
  # @param settings [Configuration::Settings] Tracer settings
94
115
  def initialize(logger:, settings:)
@@ -109,7 +130,7 @@ module Datadog
109
130
  # @param mod [Module, Class] The module or class to extract from
110
131
  # @return [Scope, nil] FILE scope wrapping extracted scope, or nil if filtered out
111
132
  def extract(mod)
112
- return nil unless mod.is_a?(Module)
133
+ return nil unless Module === mod
113
134
  mod_name = safe_mod_name(mod)
114
135
  return nil unless mod_name
115
136
 
@@ -118,14 +139,15 @@ module Datadog
118
139
  source_file = find_source_file(mod)
119
140
  return nil unless source_file
120
141
 
121
- inner_scope = if mod.is_a?(Class)
142
+ inner_scope = if Class === mod
122
143
  extract_class_scope(mod)
123
144
  else
124
145
  extract_module_scope(mod)
125
146
  end
126
147
 
127
148
  wrap_in_file_scope(source_file, [inner_scope])
128
- rescue => e
149
+ rescue Exception => e # standard:disable Lint/RescueException
150
+ Datadog::DI.reraise_if_fatal(e)
129
151
  @logger.debug { "symdb: failed to extract #{mod_name || '<unknown>'}: #{e.class}: #{e.message}" }
130
152
  nil
131
153
  end
@@ -134,21 +156,66 @@ module Datadog
134
156
  # Returns an array of FILE scopes with proper FQN-based nesting.
135
157
  #
136
158
  # Two-pass algorithm:
137
- # Pass 1: Iterate ObjectSpace, collect all extractable modules with methods grouped by file
138
- # Pass 2: Build FILE scope trees with nested MODULE/CLASS hierarchy from FQN splitting
159
+ # Pass 1 (`build_per_file_index`): iterate ObjectSpace once, building
160
+ # `{ file_path => [[mod_name, mod, [method_name_symbol, ...]], ...] }`.
161
+ # Stores Symbol method names + Module refs only; no UnboundMethod retention
162
+ # between passes.
163
+ # Pass 2 (`build_file_scope`): for each file in the index, resolve
164
+ # UnboundMethods just-in-time, build the nested MODULE/CLASS scope tree from
165
+ # FQN splitting, and produce one FILE Scope. The per-file working set is
166
+ # released as soon as the FILE scope is yielded (or accumulated into the
167
+ # returned Array, in legacy mode).
139
168
  #
140
169
  # This is the production path used by Component. Methods are split by source file,
141
170
  # so a class reopened across two files produces two FILE scopes, each with only
142
171
  # the methods defined in that file.
143
172
  #
144
- # @return [Array<Scope>] Array of FILE scopes
173
+ # Memory profile (with a block):
174
+ # - Pass 1 builds a per-file index containing only Symbol method names plus
175
+ # Module references. No UnboundMethod objects are retained between passes.
176
+ # - Pass 2 processes one file at a time. The peak per file is bounded by the
177
+ # number of methods that live in that one file across all its modules
178
+ # (typical Rails: tens of methods; pathological case: a single very large
179
+ # source file). Once a FILE scope is yielded and the caller stops referencing
180
+ # it, the entire per-file working set becomes garbage.
181
+ #
182
+ # This is O(largest_file + batch_buffer), not O(total_classes).
183
+ #
184
+ # Without a block, returns the full `Array<Scope>` (legacy form, used by specs).
185
+ # The Array itself still scales with the number of files, so block form is the
186
+ # one to use for production memory bounds.
187
+ #
188
+ # @yieldparam scope [Scope] FILE scope for one source file
189
+ # @return [Array<Scope>, nil] Array of FILE scopes when called without a block; nil when a block is given
145
190
  def extract_all
146
- entries = collect_extractable_modules
147
- file_trees = build_file_trees(entries)
148
- convert_trees_to_scopes(file_trees)
149
- rescue => e
191
+ index = build_per_file_index
192
+
193
+ if block_given?
194
+ # Drain the index destructively so each per-file entry becomes eligible for
195
+ # collection as soon as its FILE scope is yielded and consumed. Hash#shift
196
+ # returns [key, value] on a non-empty hash and nil when empty, so the
197
+ # `while (pair = ...)` form is the drain. Indexing pair[0]/pair[1] rather
198
+ # than destructuring avoids introducing names into method scope that would
199
+ # then shadow the else-branch's block parameters.
200
+ while (pair = index.shift)
201
+ scope = build_file_scope(pair[0], pair[1])
202
+ yield scope if scope
203
+ end
204
+ nil
205
+ else
206
+ # Legacy non-block form for specs. No memory bound — the full Array is
207
+ # materialized.
208
+ result = []
209
+ index.each do |path, file_entries|
210
+ scope = build_file_scope(path, file_entries)
211
+ result << scope if scope
212
+ end
213
+ result
214
+ end
215
+ rescue Exception => e # standard:disable Lint/RescueException
216
+ Datadog::DI.reraise_if_fatal(e)
150
217
  @logger.debug { "symdb: error in extract_all: #{e.class}: #{e.message}" }
151
- []
218
+ block_given? ? nil : []
152
219
  end
153
220
 
154
221
  private
@@ -160,11 +227,54 @@ module Datadog
160
227
  # @return [String, nil] Module name or nil
161
228
  def safe_mod_name(mod)
162
229
  MODULE_NAME.bind(mod).call
163
- rescue => e
230
+ rescue Exception => e # standard:disable Lint/RescueException
231
+ Datadog::DI.reraise_if_fatal(e)
164
232
  @logger.debug { "symdb: safe_mod_name failed: #{e.class}: #{e.message}" }
165
233
  nil
166
234
  end
167
235
 
236
+ # Verify that mod_name still resolves to mod through Ruby's constant
237
+ # table. Returns false when a Class/Module has been detached from its
238
+ # constant (via remove_const) but still carries the cached Module#name —
239
+ # see build_per_file_index for the failure mode this protects.
240
+ #
241
+ # Walks the namespace path segment-by-segment. For each segment:
242
+ # 1. Check for a pending autoload directly on the current namespace.
243
+ # If present, const_get would trigger it — loading customer code as
244
+ # a side effect of symbol extraction and raising LoadError if the
245
+ # target file is missing (LoadError is ScriptError, not StandardError,
246
+ # and would propagate past the outer rescue in
247
+ # build_per_file_index). Return false instead.
248
+ # 2. Otherwise, require the constant to be directly defined on this
249
+ # namespace (const_defined?(sym, false)) and descend via
250
+ # const_get(sym, false). The direct-only lookup means an ancestor's
251
+ # pending autoload at the same name does not affect the result: a
252
+ # subclass with its own binding resolves through the binding, an
253
+ # inherited autoload triggers nothing.
254
+ #
255
+ # Uses `current.autoload?(sym, false)` (the inherit=false form, added
256
+ # in Ruby 2.7) so the question is strictly "is there an autoload
257
+ # registered directly on this namespace?" and ancestors' pending
258
+ # autoloads at the same name do not affect the result.
259
+ # @param mod_name [String]
260
+ # @param mod [Module]
261
+ # @return [Boolean]
262
+ def resolves_to_same_module?(mod_name, mod)
263
+ current = Object
264
+ mod_name.split('::').each do |seg|
265
+ sym = seg.to_sym
266
+ return false if MODULE_AUTOLOAD_P.bind(current).call(sym, false)
267
+ return false unless MODULE_CONST_DEFINED.bind(current).call(sym, false)
268
+ current = MODULE_CONST_GET.bind(current).call(sym, false)
269
+ end
270
+ current.equal?(mod)
271
+ rescue NameError, ArgumentError, TypeError
272
+ # Expected "no" outcome for stale/detached classes — the whole point
273
+ # of this predicate. Per the rescue convention in this file's header
274
+ # comment: inner per-item rescues are expected failures, no logging.
275
+ false
276
+ end
277
+
168
278
  # Check if module is from user code (not gems or stdlib)
169
279
  # @param mod [Module] The module to check
170
280
  # @return [Boolean] true if user code
@@ -228,15 +338,10 @@ module Datadog
228
338
  # AR models get filtered out as gem code.
229
339
  #
230
340
  # For namespace-only modules (no instance or singleton methods), falls back to
231
- # Module#const_source_location (Ruby 2.7+) to locate the module via its constants.
341
+ # Module#const_source_location to locate the module via its constants.
232
342
  # This handles patterns like `module ApplicationCable; class Channel...; end; end`
233
343
  # where the namespace module itself has no methods but defines user-code classes.
234
344
  #
235
- # On Ruby 2.6 (where const_source_location is unavailable), namespace-only modules
236
- # and classes whose only methods are generated (e.g., AR models with only associations)
237
- # may not be found — the extraction silently omits them. This is a graceful degradation:
238
- # fewer symbols uploaded, no errors.
239
- #
240
345
  # @param mod [Module] The module
241
346
  # @return [String, nil] Source file path or nil
242
347
  def find_source_file(mod)
@@ -266,18 +371,19 @@ module Datadog
266
371
  fallback ||= path # steep:ignore
267
372
  end
268
373
 
269
- # Try const_source_location (Ruby 2.7+) to find where this class/module is declared.
374
+ # Use const_source_location to find where this class/module is declared.
270
375
  # This handles two cases:
271
376
  # 1. Classes with no user-defined methods (e.g. AR models with only associations) whose
272
377
  # generated methods point to gem code — we find the `class Foo` declaration instead.
273
378
  # 2. Namespace-only modules (`module Foo; class Bar; end; end`) with no methods at all.
274
- if Module.method_defined?(:const_source_location) && mod.name
379
+ mod_name = safe_mod_name(mod)
380
+ if mod_name
275
381
  # Look up the class/module by its last name component in its enclosing namespace.
276
- parts = mod.name.split('::')
382
+ parts = mod_name.split('::')
277
383
  const_name = parts.last
278
384
  namespace = if parts.length > 1
279
385
  begin
280
- Object.const_get(parts[0..-2].join('::')) # steep:ignore
386
+ MODULE_CONST_GET.bind(Object).call(parts[0..-2].join('::')) # steep:ignore
281
387
  rescue NameError
282
388
  nil
283
389
  end
@@ -288,7 +394,8 @@ module Datadog
288
394
  if namespace
289
395
  location = begin
290
396
  namespace.const_source_location(const_name)
291
- rescue => e
397
+ rescue Exception => e # standard:disable Lint/RescueException
398
+ Datadog::DI.reraise_if_fatal(e)
292
399
  @logger.debug { "symdb: const_source_location(#{const_name}) failed: #{e.class}: #{e.message}" }
293
400
  nil
294
401
  end
@@ -301,10 +408,11 @@ module Datadog
301
408
  end
302
409
 
303
410
  # Also scan constants defined by mod itself (namespace-only modules).
304
- mod.constants(false).each do |child_const_name|
411
+ MODULE_CONSTANTS.bind(mod).call(false).each do |child_const_name|
305
412
  location = begin
306
413
  mod.const_source_location(child_const_name)
307
- rescue => e
414
+ rescue Exception => e # standard:disable Lint/RescueException
415
+ Datadog::DI.reraise_if_fatal(e)
308
416
  @logger.debug { "symdb: const_source_location(#{child_const_name}) failed: #{e.class}: #{e.message}" }
309
417
  nil
310
418
  end
@@ -320,7 +428,8 @@ module Datadog
320
428
  end
321
429
 
322
430
  fallback
323
- rescue => e
431
+ rescue Exception => e # standard:disable Lint/RescueException
432
+ Datadog::DI.reraise_if_fatal(e)
324
433
  @logger.debug { "symdb: error finding source file for #{safe_mod_name(mod) || '<unknown>'}: #{e.class}: #{e.message}" }
325
434
  nil
326
435
  end
@@ -357,7 +466,7 @@ module Datadog
357
466
 
358
467
  Scope.new(
359
468
  scope_type: 'MODULE',
360
- name: mod.name,
469
+ name: safe_mod_name(mod),
361
470
  source_file: source_file,
362
471
  start_line: UNKNOWN_MIN_LINE,
363
472
  end_line: UNKNOWN_MAX_LINE,
@@ -375,7 +484,7 @@ module Datadog
375
484
 
376
485
  Scope.new(
377
486
  scope_type: 'CLASS',
378
- name: klass.name,
487
+ name: safe_mod_name(klass),
379
488
  source_file: source_file,
380
489
  start_line: start_line,
381
490
  end_line: end_line,
@@ -406,8 +515,9 @@ module Datadog
406
515
  return [UNKNOWN_MIN_LINE, UNKNOWN_MAX_LINE] if starts.empty?
407
516
 
408
517
  [starts.min, ends.max]
409
- rescue => e
410
- @logger.debug { "symdb: error calculating line range for #{klass.name}: #{e.class}: #{e.message}" }
518
+ rescue Exception => e # standard:disable Lint/RescueException
519
+ Datadog::DI.reraise_if_fatal(e)
520
+ @logger.debug { "symdb: error calculating line range for #{safe_mod_name(klass)}: #{e.class}: #{e.message}" }
411
521
  [UNKNOWN_MIN_LINE, UNKNOWN_MAX_LINE]
412
522
  end
413
523
 
@@ -421,8 +531,9 @@ module Datadog
421
531
  # Emitted as an array named super_classes — consistent with Java, .NET, and Python.
422
532
  # Array allows for multiple entries if future Ruby versions or mixins expand the chain.
423
533
  # Anonymous superclasses (class Foo < Class.new { ... }) have nil name; compact to skip.
424
- if klass.superclass && klass.superclass != Object && klass.superclass != BasicObject
425
- super_name = klass.superclass.name # steep:ignore
534
+ superclass = CLASS_SUPERCLASS.bind(klass).call
535
+ if superclass && !superclass.equal?(Object) && !superclass.equal?(BasicObject)
536
+ super_name = safe_mod_name(superclass)
426
537
  specifics[:super_classes] = [super_name] if super_name
427
538
  end
428
539
 
@@ -430,7 +541,7 @@ module Datadog
430
541
  # included_modules returns the entire ancestor chain's mixins, not only directly
431
542
  # included ones. This is intentional: the field reports "modules this class
432
543
  # responds to," which is what the consumer (UI navigation, probe context) needs.
433
- included = klass.included_modules.map(&:name).reject do |name|
544
+ included = MODULE_INCLUDED_MODULES.bind(klass).call.map { |m| safe_mod_name(m) }.reject do |name|
434
545
  name.nil? || EXCLUDED_COMMON_MODULES.any? { |prefix| name.start_with?(prefix) }
435
546
  end
436
547
  specifics[:included_modules] = included unless included.empty?
@@ -441,16 +552,17 @@ module Datadog
441
552
  # Single-pass collection avoids the intermediate arrays from take_while.map.compact.
442
553
  # Test coverage: spec/datadog/symbol_database/extractor_spec.rb tests prepend behavior.
443
554
  prepended = []
444
- klass.ancestors.each do |a|
445
- break if a == klass
446
- name = a.name
555
+ MODULE_ANCESTORS.bind(klass).call.each do |a|
556
+ break if a.equal?(klass)
557
+ name = safe_mod_name(a)
447
558
  prepended << name if name
448
559
  end
449
560
  specifics[:prepended_modules] = prepended unless prepended.empty?
450
561
 
451
562
  specifics
452
- rescue => e
453
- @logger.debug { "symdb: error building language specifics for #{klass.name}: #{e.class}: #{e.message}" }
563
+ rescue Exception => e # standard:disable Lint/RescueException
564
+ Datadog::DI.reraise_if_fatal(e)
565
+ @logger.debug { "symdb: error building language specifics for #{safe_mod_name(klass)}: #{e.class}: #{e.message}" }
454
566
  {}
455
567
  end
456
568
 
@@ -472,8 +584,9 @@ module Datadog
472
584
  end
473
585
 
474
586
  scopes
475
- rescue => e
476
- @logger.debug { "symdb: failed to extract methods from #{klass.name}: #{e.class}: #{e.message}" }
587
+ rescue Exception => e # standard:disable Lint/RescueException
588
+ Datadog::DI.reraise_if_fatal(e)
589
+ @logger.debug { "symdb: failed to extract methods from #{safe_mod_name(klass)}: #{e.class}: #{e.message}" }
477
590
  []
478
591
  end
479
592
 
@@ -507,8 +620,9 @@ module Datadog
507
620
  },
508
621
  symbols: extract_method_parameters(method)
509
622
  )
510
- rescue => e
511
- @logger.debug { "symdb: failed to extract method #{klass.name}##{method_name}: #{e.class}: #{e.message}" }
623
+ rescue Exception => e # standard:disable Lint/RescueException
624
+ Datadog::DI.reraise_if_fatal(e)
625
+ @logger.debug { "symdb: failed to extract method #{safe_mod_name(klass)}##{method_name}: #{e.class}: #{e.message}" }
512
626
  nil
513
627
  end
514
628
 
@@ -585,7 +699,8 @@ module Datadog
585
699
  def extract_method_parameters(method)
586
700
  method_name = begin
587
701
  method.name.to_s
588
- rescue => e
702
+ rescue Exception => e # standard:disable Lint/RescueException
703
+ Datadog::DI.reraise_if_fatal(e)
589
704
  @logger.debug { "symdb: method.name failed: #{e.class}: #{e.message}" }
590
705
  'unknown'
591
706
  end
@@ -607,14 +722,15 @@ module Datadog
607
722
  line: UNKNOWN_MIN_LINE, # Parameters available in entire method
608
723
  )
609
724
  end
610
- rescue => e
725
+ rescue Exception => e # standard:disable Lint/RescueException
726
+ Datadog::DI.reraise_if_fatal(e)
611
727
  @logger.debug { "symdb: failed to extract parameters from #{method_name}: #{e.class}: #{e.message}" }
612
728
  []
613
729
  end
614
730
 
615
731
  # ── extract_all helpers ──────────────────────────────────────────────
616
732
 
617
- # Sleep between chunks of modules processed in collect_extractable_modules so
733
+ # Sleep between chunks of modules processed in build_per_file_index so
618
734
  # request-handling threads have guaranteed CPU time while extraction is in
619
735
  # flight. Unlike Thread.pass (which only offers the GVL among runnable
620
736
  # threads and leaves the extractor immediately re-runnable), sleep removes
@@ -629,20 +745,25 @@ module Datadog
629
745
  SLEEP_SECONDS = 0.001
630
746
  private_constant :SLEEP_EVERY_N_MODULES, :SLEEP_SECONDS
631
747
 
632
- # Pass 1: Collect all extractable modules with methods grouped by source file.
633
- # @return [Hash] { mod_name => { mod:, methods_by_file: { path => [{name:, method:, type:}] } } }
634
- def collect_extractable_modules
635
- entries = {}
748
+ # Pass 1 (memory-bounded form): build a per-file index of
749
+ # `{ file_path => [[mod_name, mod, [method_name_symbol, ...]], ...] }`.
750
+ #
751
+ # Stores Symbol method names plus Module references only — no UnboundMethod
752
+ # objects retained between passes. UnboundMethods created here (to read
753
+ # `source_location`) become garbage as the inner loop ends.
754
+ #
755
+ # The Module references are pointer-sized and the modules are already kept
756
+ # alive in ObjectSpace, so adding them to the index costs no extra retention.
757
+ #
758
+ # @return [Hash{String=>Array<Array(String, Module, Array<Symbol>)>}]
759
+ def build_per_file_index
760
+ index = {}
636
761
  seen = 0
637
762
 
638
763
  ObjectSpace.each_object(Module) do |mod|
639
764
  # Singleton classes (per-object metaclasses) are never user-code classes.
640
765
  # They're not const-referenced, DI cannot instrument methods on a singular
641
- # object instance, and on Ruby 2.6 specifically, Module#name on unnamed
642
- # singleton classes with long ancestor chains (e.g. through monkey-patches
643
- # prepended into Kernel, common in dd-trace-rb test processes) is O(ancestors)
644
- # — measured ~20ms per call, which dominates extract_all on heavily-loaded
645
- # processes. Ruby 2.7+ optimized this path; the skip is a no-op there.
766
+ # object instance, so skipping them is both correct and cheap.
646
767
  next if MODULE_SINGLETON_CLASS_PRED.bind(mod).call
647
768
 
648
769
  seen += 1
@@ -650,84 +771,124 @@ module Datadog
650
771
 
651
772
  mod_name = safe_mod_name(mod)
652
773
  next unless mod_name
774
+ next unless resolves_to_same_module?(mod_name, mod)
653
775
  next unless user_code_module?(mod)
654
776
 
655
- methods_by_file = group_methods_by_file(mod)
777
+ file_to_names = collect_method_names_by_file(mod)
656
778
 
657
- # For modules/classes with no methods but valid source, use find_source_file as fallback.
658
- # This handles namespace modules and classes with only constants.
659
- if methods_by_file.empty?
779
+ # Namespace-only modules (no own methods) use find_source_file as the
780
+ # canonical file so the FILE scope still gets a MODULE entry.
781
+ if file_to_names.empty?
660
782
  source_file = find_source_file(mod)
661
- methods_by_file[source_file] = [] if source_file
783
+ file_to_names[source_file] = [] if source_file
662
784
  end
663
785
 
664
- next if methods_by_file.empty?
786
+ next if file_to_names.empty?
665
787
 
666
- entries[mod_name] = {mod: mod, methods_by_file: methods_by_file}
667
- rescue => e
668
- @logger.debug { "symdb: error collecting #{mod_name || '<unknown>'}: #{e.class}: #{e.message}" }
788
+ file_to_names.each do |file_path, method_names|
789
+ (index[file_path] ||= []) << [mod_name, mod, method_names]
790
+ end
791
+ rescue Exception => e # standard:disable Lint/RescueException
792
+ Datadog::DI.reraise_if_fatal(e)
793
+ @logger.debug { "symdb: error indexing #{mod_name || '<unknown>'}: #{e.class}: #{e.message}" }
669
794
  end
670
795
 
671
- entries
796
+ index
672
797
  end
673
798
 
674
- # Group a module's methods by their source file path.
675
- # @param mod [Module] The module
676
- # @return [Hash] { file_path => [{name:, method:, type:}] }
677
- def group_methods_by_file(mod)
799
+ # For a single module, return `{ file_path => [method_name_symbol, ...] }`.
800
+ # Stores only the method-name symbols and their file paths — UnboundMethod
801
+ # objects allocated to read `source_location` are not retained between
802
+ # passes, so they can be GC'd as soon as the inner loop ends.
803
+ def collect_method_names_by_file(mod)
678
804
  result = Hash.new { |h, k| h[k] = [] } # steep:ignore
679
805
 
680
- # Instance methods (public, protected, private)
681
- all_methods = mod.instance_methods(false) +
682
- mod.protected_instance_methods(false) +
683
- mod.private_instance_methods(false)
684
- all_methods.uniq!
685
-
686
- all_methods.each do |method_name|
687
- method = mod.instance_method(method_name)
688
- loc = method.source_location
689
- next unless loc
690
- next unless user_code_path?(loc[0])
691
-
692
- result[loc[0]] << {name: method_name, method: method, type: :instance}
693
- rescue => e
694
- @logger.debug { "symdb: error grouping method #{method_name}: #{e.class}: #{e.message}" }
806
+ # Module#instance_methods(false) already returns both public and protected
807
+ # methods, so iterating it plus private_instance_methods covers all three
808
+ # visibilities without an intermediate merged array.
809
+ [mod.instance_methods(false), mod.private_instance_methods(false)].each do |method_names|
810
+ method_names.each do |method_name|
811
+ method = mod.instance_method(method_name)
812
+ loc = method.source_location
813
+ next unless loc
814
+ next unless user_code_path?(loc[0])
815
+
816
+ result[loc[0]] << method_name
817
+ rescue Exception => e # standard:disable Lint/RescueException
818
+ Datadog::DI.reraise_if_fatal(e)
819
+ @logger.debug { "symdb: error indexing method #{method_name}: #{e.class}: #{e.message}" }
820
+ end
695
821
  end
696
822
 
697
823
  result
698
- rescue => e
699
- @logger.debug { "symdb: error grouping methods: #{e.class}: #{e.message}" }
824
+ rescue Exception => e # standard:disable Lint/RescueException
825
+ Datadog::DI.reraise_if_fatal(e)
826
+ @logger.debug { "symdb: error indexing methods: #{e.class}: #{e.message}" }
700
827
  {}
701
828
  end
702
829
 
703
- # Pass 2: Build per-file trees from collected entries.
704
- # Uses hash nodes during construction, converted to Scope objects at the end.
705
- #
706
- # Node structure: { name:, type:, children: {name => node}, methods: [], mod:, source_file:, fqn: }
830
+ # Pass 2: build the FILE scope for one source file by walking just the modules
831
+ # that contribute methods to it. Resolves UnboundMethods just-in-time per
832
+ # method; the per-method scratch is collected by GC as each module's loop body
833
+ # ends. The returned Scope is the only thing the caller needs to keep alive
834
+ # — once the caller drops it, the entire per-file working set is collectable.
707
835
  #
708
- # @param entries [Hash] Output from collect_extractable_modules
709
- # @return [Hash] { file_path => root_node }
710
- def build_file_trees(entries)
711
- file_trees = {}
712
-
713
- # Sort by FQN depth so parents are placed before children.
714
- # This ensures intermediate nodes created for parents have correct scope_type.
715
- sorted = entries.sort_by { |name, _| name.count(':') }
716
-
717
- sorted.each do |mod_name, entry|
718
- entry[:methods_by_file].each do |file_path, methods|
719
- root = file_trees[file_path] ||= {
720
- name: file_path, type: 'FILE', children: {},
721
- methods: [], mod: nil, source_file: file_path, fqn: nil
722
- }
723
- parts = mod_name.split('::')
724
- place_in_tree(root, parts, entry[:mod], mod_name, methods, file_path)
836
+ # @param file_path [String]
837
+ # @param entries [Array<Array(String, Module, Array<Symbol>)>] tuples produced by build_per_file_index
838
+ # @return [Scope, nil] FILE scope, or nil if nothing extractable
839
+ def build_file_scope(file_path, entries)
840
+ return nil if entries.empty?
841
+
842
+ root = {
843
+ name: file_path, type: 'FILE', children: {},
844
+ methods: [], mod: nil, source_file: file_path, fqn: nil,
845
+ }
846
+
847
+ # Sort by FQN depth so parent namespaces are placed before children.
848
+ sorted = entries.sort_by { |(mod_name, _, _)| mod_name.count(':') }
849
+
850
+ sorted.each do |mod_name, mod, method_names|
851
+ # Resolve UnboundMethods for this (mod, file) just-in-time. These objects
852
+ # live only as long as the tree node holds them; they are released when
853
+ # convert_tree_to_scope finishes building the file's Scope.
854
+ method_infos = Core::Utils::EnumerableCompat.filter_map(method_names) do |name|
855
+ method = mod.instance_method(name)
856
+ # Pass 1 (build_per_file_index) recorded this method under file_path.
857
+ # If the method has been redefined in another file between the two
858
+ # passes (e.g. a class reopened during a Rails reload while extract_all
859
+ # is iterating), the resolved UnboundMethod's source_location now
860
+ # points elsewhere. Drop the stale entry — the hot-load TracePoint
861
+ # enqueues the redefined class and the next debounce window extracts
862
+ # it under the new file_path.
863
+ loc = method.source_location
864
+ next nil unless loc && loc[0] == file_path
865
+ {name: name, method: method, type: :instance}
866
+ rescue Exception => e # standard:disable Lint/RescueException
867
+ Datadog::DI.reraise_if_fatal(e)
868
+ @logger.debug { "symdb: error resolving #{mod_name}##{name}: #{e.class}: #{e.message}" }
869
+ nil
725
870
  end
726
- rescue => e
727
- @logger.debug { "symdb: error building tree for #{mod_name}: #{e.class}: #{e.message}" }
871
+
872
+ # If Pass 1 recorded methods for this module but every one of them has
873
+ # moved out of file_path between the passes, drop the entry — otherwise
874
+ # the FILE scope would carry an empty CLASS/MODULE node at a location
875
+ # the module no longer lives in.
876
+ next if method_names.any? && method_infos.empty?
877
+
878
+ parts = mod_name.split('::')
879
+ place_in_tree(root, parts, mod, mod_name, method_infos, file_path)
880
+ rescue Exception => e # standard:disable Lint/RescueException
881
+ Datadog::DI.reraise_if_fatal(e)
882
+ @logger.debug { "symdb: error placing #{mod_name} in tree: #{e.class}: #{e.message}" }
728
883
  end
729
884
 
730
- file_trees
885
+ # steep:ignore:start
886
+ # Steep widens root[:children] to the union of all value types declared in
887
+ # the literal (String | Hash | Array | nil), losing the Hash narrowing.
888
+ return nil if root[:children].empty?
889
+ # steep:ignore:end
890
+
891
+ convert_tree_to_scope(file_path, root)
731
892
  end
732
893
 
733
894
  # Place a module/class in the file tree at the correct nesting depth.
@@ -755,12 +916,12 @@ module Datadog
755
916
  if leaf
756
917
  # Node exists (was created as intermediate or from another entry).
757
918
  # Update type and mod — the actual module object is authoritative.
758
- leaf[:type] = mod.is_a?(Class) ? 'CLASS' : 'MODULE'
919
+ leaf[:type] = (Class === mod) ? 'CLASS' : 'MODULE'
759
920
  leaf[:mod] = mod
760
921
  else
761
922
  leaf = {
762
923
  name: mod_name,
763
- type: mod.is_a?(Class) ? 'CLASS' : 'MODULE',
924
+ type: (Class === mod) ? 'CLASS' : 'MODULE',
764
925
  children: {}, methods: [],
765
926
  mod: mod, source_file: file_path,
766
927
  fqn: mod_name
@@ -777,32 +938,43 @@ module Datadog
777
938
  # @param fqn [String] Fully-qualified name (e.g. "Authentication::Strategies")
778
939
  # @return [String] 'CLASS' or 'MODULE'
779
940
  def resolve_scope_type(fqn)
780
- const = Object.const_get(fqn)
781
- const.is_a?(Class) ? 'CLASS' : 'MODULE'
782
- rescue => e
941
+ current = Object
942
+ fqn.split('::').each do |seg|
943
+ sym = seg.to_sym
944
+ pending_autoload = if RubyVersion.is?('>= 2.7')
945
+ MODULE_AUTOLOAD_P.bind(current).call(sym, false)
946
+ else
947
+ MODULE_AUTOLOAD_P.bind(current).call(sym)
948
+ end
949
+ return 'MODULE' if pending_autoload
950
+ return 'MODULE' unless MODULE_CONST_DEFINED.bind(current).call(sym, false)
951
+ current = MODULE_CONST_GET.bind(current).call(sym, false)
952
+ end
953
+ (Class === current) ? 'CLASS' : 'MODULE'
954
+ rescue Exception => e # standard:disable Lint/RescueException
955
+ Datadog::DI.reraise_if_fatal(e)
783
956
  @logger.debug { "symdb: resolve_scope_type(#{fqn}) failed: #{e.class}: #{e.message}, defaulting to MODULE" }
784
957
  'MODULE'
785
958
  end
786
959
 
787
- # Convert hash-based file trees to Scope objects.
788
- # @param file_trees [Hash] { file_path => root_node }
789
- # @return [Array<Scope>] Array of FILE scopes
790
- def convert_trees_to_scopes(file_trees)
791
- file_trees.map do |file_path, root|
792
- file_hash = FileHash.compute(file_path, logger: @logger)
793
- lang = {}
794
- lang[:file_hash] = file_hash if file_hash
795
-
796
- Scope.new(
797
- scope_type: 'FILE',
798
- name: file_path,
799
- source_file: file_path,
800
- start_line: UNKNOWN_MIN_LINE,
801
- end_line: UNKNOWN_MAX_LINE,
802
- language_specifics: lang,
803
- scopes: root[:children].values.map { |child| convert_node_to_scope(child) }
804
- )
805
- end
960
+ # Convert a single file tree (built by build_file_scope) to a FILE Scope.
961
+ # @param file_path [String] Source file path
962
+ # @param root [Hash] Tree node from build_file_scope
963
+ # @return [Scope] FILE scope
964
+ def convert_tree_to_scope(file_path, root)
965
+ file_hash = FileHash.compute(file_path, logger: @logger)
966
+ lang = {}
967
+ lang[:file_hash] = file_hash if file_hash
968
+
969
+ Scope.new(
970
+ scope_type: 'FILE',
971
+ name: file_path,
972
+ source_file: file_path,
973
+ start_line: UNKNOWN_MIN_LINE,
974
+ end_line: UNKNOWN_MAX_LINE,
975
+ language_specifics: lang,
976
+ scopes: root[:children].values.map { |child| convert_node_to_scope(child) },
977
+ )
806
978
  end
807
979
 
808
980
  # Convert a single hash node to a Scope object (recursive).
@@ -875,7 +1047,8 @@ module Datadog
875
1047
  },
876
1048
  symbols: extract_method_parameters(method)
877
1049
  )
878
- rescue => e
1050
+ rescue Exception => e # standard:disable Lint/RescueException
1051
+ Datadog::DI.reraise_if_fatal(e)
879
1052
  klass_name = klass ? (safe_mod_name(klass) || '<unknown>') : '<unknown>'
880
1053
  @logger.debug { "symdb: failed to build method scope #{klass_name}##{method_name}: #{e.class}: #{e.message}" }
881
1054
  nil
@@ -889,8 +1062,8 @@ module Datadog
889
1062
  symbols = []
890
1063
 
891
1064
  # Class variables (only for classes)
892
- if mod.is_a?(Class)
893
- mod.class_variables(false).each do |var_name|
1065
+ if Class === mod
1066
+ MODULE_CLASS_VARIABLES.bind(mod).call(false).each do |var_name|
894
1067
  symbols << Symbol.new(
895
1068
  symbol_type: 'STATIC_FIELD',
896
1069
  name: var_name.to_s,
@@ -901,30 +1074,33 @@ module Datadog
901
1074
 
902
1075
  # Constants (excluding nested modules/classes).
903
1076
  # Skip autoloaded constants to avoid triggering loading as a side effect.
904
- mod.constants(false).each do |const_name|
905
- next if mod.autoload?(const_name)
906
- const_value = mod.const_get(const_name)
907
- next if const_value.is_a?(Module)
1077
+ MODULE_CONSTANTS.bind(mod).call(false).each do |const_name|
1078
+ next if MODULE_AUTOLOAD_P.bind(mod).call(const_name)
1079
+ const_value = MODULE_CONST_GET.bind(mod).call(const_name)
1080
+ next if Module === const_value
908
1081
 
909
1082
  symbols << Symbol.new(
910
1083
  symbol_type: 'STATIC_FIELD',
911
1084
  name: const_name.to_s,
912
1085
  line: UNKNOWN_MIN_LINE,
913
- type: const_value.class.name
1086
+ type: safe_mod_name(KERNEL_CLASS.bind(const_value).call)
914
1087
  )
915
- rescue NameError, LoadError, NoMethodError => e # standard:disable Lint/ShadowedException
916
- # Expected: constant removed/undefined, autoload failure, or const value missing
917
- # #class. Logged separately from unexpected errors so the latter stand out in triage.
918
- # Lint/ShadowedException disabled: NameError/NoMethodError do descend from
919
- # StandardError, but Ruby's rescue-clause-order semantics ensure the bare rescue
920
- # below only catches exceptions not matched here.
1088
+ rescue NameError, LoadError, NoMethodError, TypeError => e # standard:disable Lint/ShadowedException
1089
+ # Expected: constant removed/undefined (NameError), autoload failure (LoadError),
1090
+ # or a value whose class cannot be read (NoMethodError/TypeError). Skipping one
1091
+ # constant here keeps the rest of the module's symbols. Logged separately from
1092
+ # unexpected errors so the latter stand out in triage. Lint/ShadowedException
1093
+ # disabled: these descend from StandardError, but Ruby's rescue-clause-order
1094
+ # semantics ensure the catch-all rescue below only catches exceptions not matched here.
921
1095
  @logger.debug { "symdb: skipping module constant #{const_name}: #{e.class}: #{e.message}" }
922
- rescue => e
1096
+ rescue Exception => e # standard:disable Lint/RescueException
1097
+ Datadog::DI.reraise_if_fatal(e)
923
1098
  @logger.debug { "symdb: unexpected error reading module constant #{const_name}: #{e.class}: #{e.message}" }
924
1099
  end
925
1100
 
926
1101
  symbols
927
- rescue => e
1102
+ rescue Exception => e # standard:disable Lint/RescueException
1103
+ Datadog::DI.reraise_if_fatal(e)
928
1104
  mod_name = safe_mod_name(mod) || '<unknown>'
929
1105
  @logger.debug { "symdb: failed to extract symbols from #{mod_name}: #{e.class}: #{e.message}" }
930
1106
  []