datadog 2.43.0 → 2.44.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -1
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +36 -5
  4. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +6 -7
  5. data/ext/datadog_profiling_native_extension/collectors_stack.c +6 -7
  6. data/ext/datadog_profiling_native_extension/collectors_stack.h +1 -1
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +65 -31
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -2
  9. data/ext/datadog_profiling_native_extension/extconf.rb +4 -2
  10. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +25 -0
  11. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +1 -0
  12. data/ext/datadog_profiling_native_extension/profiling.c +1 -0
  13. data/ext/datadog_profiling_native_extension/stack_recorder.c +4 -4
  14. data/ext/datadog_profiling_native_extension/time_helpers.h +3 -3
  15. data/ext/libdatadog_api/di.c +8 -3
  16. data/ext/libdatadog_api/extconf.rb +1 -1
  17. data/ext/libdatadog_api/feature_flags.c +8 -0
  18. data/ext/libdatadog_api/init.c +3 -1
  19. data/ext/libdatadog_api/otel_thread_context.c +38 -17
  20. data/ext/libdatadog_extconf_helpers.rb +14 -9
  21. data/lib/datadog/ai_guard/component.rb +17 -8
  22. data/lib/datadog/ai_guard/configuration/ext.rb +2 -0
  23. data/lib/datadog/ai_guard/configuration.rb +13 -0
  24. data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +48 -55
  25. data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +1 -1
  26. data/lib/datadog/ai_guard/contrib/ruby_llm/message_adapter.rb +76 -0
  27. data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +1 -0
  28. data/lib/datadog/ai_guard/contrib/ruby_llm/redaction_change_set.rb +135 -0
  29. data/lib/datadog/ai_guard/evaluation/client.rb +50 -0
  30. data/lib/datadog/ai_guard/evaluation/content_part.rb +12 -0
  31. data/lib/datadog/ai_guard/evaluation/message.rb +25 -7
  32. data/lib/datadog/ai_guard/evaluation/message_builder.rb +31 -0
  33. data/lib/datadog/ai_guard/evaluation/no_op_result.rb +6 -6
  34. data/lib/datadog/ai_guard/evaluation/outcome.rb +24 -0
  35. data/lib/datadog/ai_guard/evaluation/request.rb +4 -60
  36. data/lib/datadog/ai_guard/evaluation/response.rb +33 -0
  37. data/lib/datadog/ai_guard/evaluation/result.rb +11 -18
  38. data/lib/datadog/ai_guard/evaluation/tool_call.rb +15 -2
  39. data/lib/datadog/ai_guard/evaluation.rb +25 -11
  40. data/lib/datadog/ai_guard/ext.rb +1 -0
  41. data/lib/datadog/ai_guard/{api_client.rb → http_client.rb} +6 -4
  42. data/lib/datadog/ai_guard/metrics/telemetry.rb +43 -0
  43. data/lib/datadog/ai_guard/redaction/replacements.rb +97 -0
  44. data/lib/datadog/ai_guard/redaction/result.rb +29 -0
  45. data/lib/datadog/ai_guard/redaction.rb +80 -0
  46. data/lib/datadog/ai_guard.rb +58 -57
  47. data/lib/datadog/core/configuration/components.rb +9 -1
  48. data/lib/datadog/core/configuration/settings.rb +5 -2
  49. data/lib/datadog/core/configuration/supported_configurations.rb +8 -0
  50. data/lib/datadog/core/remote/client/capabilities.rb +10 -4
  51. data/lib/datadog/core/remote/component.rb +9 -2
  52. data/lib/datadog/core/remote/worker.rb +8 -0
  53. data/lib/datadog/di/instrumenter.rb +1 -1
  54. data/lib/datadog/di/probe_manager.rb +10 -5
  55. data/lib/datadog/di/proc_responder.rb +1 -1
  56. data/lib/datadog/open_feature/agentless/configuration_source.rb +246 -0
  57. data/lib/datadog/open_feature/agentless/parser.rb +43 -0
  58. data/lib/datadog/open_feature/agentless/response.rb +19 -0
  59. data/lib/datadog/open_feature/agentless/transport.rb +103 -0
  60. data/lib/datadog/open_feature/component.rb +57 -1
  61. data/lib/datadog/open_feature/configuration/agentless_endpoint.rb +90 -0
  62. data/lib/datadog/open_feature/configuration/source/resolution.rb +22 -0
  63. data/lib/datadog/open_feature/configuration/source.rb +43 -0
  64. data/lib/datadog/open_feature/configuration.rb +118 -0
  65. data/lib/datadog/open_feature/native_evaluator.rb +1 -14
  66. data/lib/datadog/open_feature/remote.rb +5 -5
  67. data/lib/datadog/opentelemetry/signal_configuration.rb +13 -9
  68. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +2 -2
  69. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -7
  70. data/lib/datadog/symbol_database/extractor.rb +84 -102
  71. data/lib/datadog/tracing/component.rb +2 -0
  72. data/lib/datadog/tracing/configuration/ext.rb +1 -0
  73. data/lib/datadog/tracing/configuration/settings.rb +6 -0
  74. data/lib/datadog/tracing/context.rb +24 -2
  75. data/lib/datadog/tracing/context_provider.rb +9 -5
  76. data/lib/datadog/tracing/contrib/grape/endpoint.rb +32 -5
  77. data/lib/datadog/tracing/metadata/ext.rb +3 -0
  78. data/lib/datadog/tracing/otel_thread_context.rb +76 -11
  79. data/lib/datadog/tracing/tracer.rb +15 -2
  80. data/lib/datadog/tracing/transport/native.rb +1 -1
  81. data/lib/datadog/tracing/transport/trace_formatter.rb +13 -3
  82. data/lib/datadog/tracing/transport/traces.rb +1 -1
  83. data/lib/datadog/version.rb +1 -1
  84. metadata +23 -7
  85. data/lib/datadog/ai_guard/evaluation/content_builder.rb +0 -31
@@ -5,6 +5,25 @@ module Datadog
5
5
  module Configuration
6
6
  # A settings class for the OpenFeature component.
7
7
  module Settings
8
+ DEFAULT_POLL_INTERVAL_SECONDS = 30
9
+ DEFAULT_REQUEST_TIMEOUT_SECONDS = 5
10
+ DEFAULT_INITIALIZATION_TIMEOUT_MS = 30_000
11
+
12
+ MAX_REQUEST_TIMEOUT_SECONDS = 300
13
+ MAX_INITIALIZATION_TIMEOUT_MS = 2_147_483_647
14
+
15
+ def self.configure_integer_option(option, environment_variable:, default:, setting_name:)
16
+ option.type :int
17
+ option.env environment_variable
18
+ option.env_parser do |value|
19
+ Integer(value, 10)
20
+ rescue ArgumentError
21
+ Datadog.logger.warn("#{setting_name} must be an integer; using the default")
22
+ default
23
+ end
24
+ option.default default
25
+ end
26
+
8
27
  def self.extended(base)
9
28
  base = base.singleton_class unless base.is_a?(Class)
10
29
  add_settings!(base)
@@ -15,10 +34,19 @@ module Datadog
15
34
  # Steep does not update `self` for this `class_eval` block.
16
35
  # @type self: Datadog::Core::Configuration::Base::_DslContext
17
36
  settings :open_feature do
37
+ # Deprecated switch retained for existing Remote Configuration adopters.
18
38
  option :enabled do |o|
19
39
  o.type :bool
20
40
  o.env "DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED"
21
41
  o.default false
42
+ o.after_set do |_, _, precedence|
43
+ next if precedence == Core::Configuration::Option::Precedence::DEFAULT
44
+
45
+ Datadog.logger.warn(
46
+ "DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED is deprecated; use " \
47
+ "DD_FEATURE_FLAGS_ENABLED and DD_FEATURE_FLAGS_CONFIGURATION_SOURCE instead"
48
+ )
49
+ end
22
50
  end
23
51
 
24
52
  # Opt-in gate for APM feature-flag span enrichment. When enabled,
@@ -42,6 +70,96 @@ module Datadog
42
70
  o.default true
43
71
  end
44
72
  end
73
+
74
+ settings :feature_flags do
75
+ option :enabled do |o|
76
+ o.type :bool
77
+ o.env "DD_FEATURE_FLAGS_ENABLED"
78
+ o.default true
79
+ end
80
+
81
+ option :configuration_source do |o|
82
+ o.type :string
83
+ o.env "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE"
84
+ o.default "agentless"
85
+ o.setter do |value|
86
+ value.to_s.strip.downcase
87
+ end
88
+ end
89
+
90
+ settings :agentless do
91
+ option :base_url do |o|
92
+ o.type :string, nilable: true
93
+ o.env "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL"
94
+ o.skip_telemetry true
95
+ o.setter do |value|
96
+ next unless value
97
+
98
+ stripped_value = value.to_s.strip
99
+ stripped_value.empty? ? nil : stripped_value
100
+ end
101
+ end
102
+
103
+ option :poll_interval_seconds do |o|
104
+ Settings.configure_integer_option(
105
+ o,
106
+ environment_variable: "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS",
107
+ default: Settings::DEFAULT_POLL_INTERVAL_SECONDS,
108
+ setting_name: "Feature Flags agentless poll interval",
109
+ )
110
+ o.setter do |value|
111
+ if value.is_a?(Integer) && value > 0
112
+ value
113
+ else
114
+ Datadog.logger.warn(
115
+ "Feature Flags agentless poll interval must be positive; using the default"
116
+ )
117
+ Settings::DEFAULT_POLL_INTERVAL_SECONDS
118
+ end
119
+ end
120
+ end
121
+
122
+ option :request_timeout_seconds do |o|
123
+ Settings.configure_integer_option(
124
+ o,
125
+ environment_variable: "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS",
126
+ default: Settings::DEFAULT_REQUEST_TIMEOUT_SECONDS,
127
+ setting_name: "Feature Flags agentless request timeout",
128
+ )
129
+ o.setter do |value|
130
+ if value.is_a?(Integer) && value > 0 && value <= Settings::MAX_REQUEST_TIMEOUT_SECONDS
131
+ value
132
+ else
133
+ Datadog.logger.warn(
134
+ "Feature Flags agentless request timeout must be within (0, " \
135
+ "#{Settings::MAX_REQUEST_TIMEOUT_SECONDS}]; using the default"
136
+ )
137
+ Settings::DEFAULT_REQUEST_TIMEOUT_SECONDS
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ option :initialization_timeout_ms do |o|
144
+ Settings.configure_integer_option(
145
+ o,
146
+ environment_variable: "DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS",
147
+ default: Settings::DEFAULT_INITIALIZATION_TIMEOUT_MS,
148
+ setting_name: "Feature Flags provider initialization timeout",
149
+ )
150
+ o.setter do |value|
151
+ if value.is_a?(Integer) && value > 0 && value <= Settings::MAX_INITIALIZATION_TIMEOUT_MS
152
+ value
153
+ else
154
+ Datadog.logger.warn(
155
+ "Feature Flags provider initialization timeout must be within (0, " \
156
+ "#{Settings::MAX_INITIALIZATION_TIMEOUT_MS}]; using the default"
157
+ )
158
+ Settings::DEFAULT_INITIALIZATION_TIMEOUT_MS
159
+ end
160
+ end
161
+ end
162
+ end
45
163
  end
46
164
  end
47
165
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
3
  require_relative "../core/feature_flags"
5
4
  require_relative "ext"
6
5
  require_relative "resolution_details"
@@ -16,7 +15,7 @@ module Datadog
16
15
  # in the format expected by `libdatadog` without any modifications
17
16
  def initialize(configuration)
18
17
  @configuration = Core::FeatureFlags::Configuration.new(configuration)
19
- @observe_full_evaluation_data = parse_observe_full_evaluation_data(configuration)
18
+ @observe_full_evaluation_data = @configuration.observe_full_evaluation_data
20
19
  end
21
20
 
22
21
  attr_reader :observe_full_evaluation_data
@@ -42,18 +41,6 @@ module Datadog
42
41
 
43
42
  private
44
43
 
45
- # Parse observe_full_evaluation_data from the top level of the UFC JSON (a sibling
46
- # of `environment`). Absent, null, or wrong-typed values return false.
47
- def parse_observe_full_evaluation_data(configuration)
48
- return false unless configuration.is_a?(String) && !configuration.empty?
49
-
50
- parsed = JSON.parse(configuration)
51
- parsed.is_a?(Hash) && parsed["observeFullEvaluationData"] == true
52
- rescue
53
- # This secondary policy parse must not reject configuration accepted by the native evaluator.
54
- false
55
- end
56
-
57
44
  def build_resolution_details(result, default_value)
58
45
  ResolutionDetails.new(
59
46
  value: result.variant.nil? ? default_value : result.value,
@@ -19,11 +19,11 @@ module Datadog
19
19
  FFE_PRODUCTS
20
20
  end
21
21
 
22
- def receivers(telemetry)
22
+ def receivers(telemetry, component_provider:)
23
23
  matcher = Core::Remote::Dispatcher::Matcher::Product.new(FFE_PRODUCTS)
24
24
  receiver = Core::Remote::Dispatcher::Receiver.new(matcher) do |repository, changes|
25
- engine = OpenFeature.engine
26
- next unless engine
25
+ component = component_provider.call
26
+ next unless component
27
27
 
28
28
  changes.each do |change|
29
29
  content = repository[change.path]
@@ -38,7 +38,7 @@ module Datadog
38
38
  when :insert, :update
39
39
  begin
40
40
  # @type var content: Core::Remote::Configuration::Content
41
- engine.reconfigure!(read_content(content))
41
+ component.reconfigure!(read_content(content))
42
42
  content.applied
43
43
  rescue EvaluationEngine::ReconfigurationError => e
44
44
  content.errored("Error applying OpenFeature configuration: #{e.class}: #{e.message}")
@@ -46,7 +46,7 @@ module Datadog
46
46
  when :delete
47
47
  # NOTE: For now, we treat deletion as clearing the configuration
48
48
  # In a multi-config scenario, we might track configs per path
49
- engine.reconfigure!(nil)
49
+ component.reconfigure!(nil)
50
50
  end
51
51
  end
52
52
  end
@@ -12,23 +12,24 @@ module Datadog
12
12
  def create_resource
13
13
  resource_attributes = {}
14
14
 
15
- @settings.tags&.each do |key, value| # steep:ignore
15
+ @settings.tags&.each do |key, value|
16
16
  otel_key = case key
17
17
  when "service" then "service.name"
18
- when "env" then "deployment.environment"
18
+ when "env", "deployment.environment.name", "deployment.environment" then next
19
19
  when "version" then "service.version"
20
20
  else key
21
21
  end
22
22
  resource_attributes[otel_key] = value
23
23
  end
24
24
 
25
- resource_attributes["service.name"] = @settings.service_without_fallback || resource_attributes["service.name"] || Datadog::Core::Environment::Ext::FALLBACK_SERVICE_NAME # steep:ignore
26
- resource_attributes["deployment.environment"] = @settings.env if @settings.env # steep:ignore
27
- resource_attributes["service.version"] = @settings.version if @settings.version # steep:ignore
25
+ resource_attributes["service.name"] = @settings.service_without_fallback || resource_attributes["service.name"] || Datadog::Core::Environment::Ext::FALLBACK_SERVICE_NAME
26
+ environment = @settings.env || @settings.tags["env"] || @settings.tags["deployment.environment.name"] || @settings.tags["deployment.environment"]
27
+ resource_attributes["deployment.environment.name"] = environment if environment
28
+ resource_attributes["service.version"] = @settings.version if @settings.version
28
29
 
29
- hostname = Datadog::Core::Environment::Socket.resolved_hostname(@settings) # steep:ignore
30
+ hostname = Datadog::Core::Environment::Socket.resolved_hostname(@settings)
30
31
  if hostname
31
- if hostname == @settings.hostname # steep:ignore
32
+ if hostname == @settings.hostname
32
33
  resource_attributes["host.name"] = hostname
33
34
  elsif !resource_attributes.key?("host.name")
34
35
  resource_attributes["host.name"] = hostname
@@ -41,9 +42,12 @@ module Datadog
41
42
  # Returns the signal-specific option value when explicitly set,
42
43
  # otherwise falls back to the general OTLP exporter config or computed_default.
43
44
  def config_or_exporter_fallback(signal:, option_name:, computed_default: nil)
44
- signal_settings = @settings.opentelemetry.public_send(signal) # steep:ignore
45
+ signal_settings = case signal
46
+ when :logs then @settings.opentelemetry.logs
47
+ when :metrics then @settings.opentelemetry.metrics
48
+ end
45
49
  if signal_settings.using_default?(option_name)
46
- @settings.opentelemetry.exporter.public_send(option_name) || computed_default # steep:ignore
50
+ @settings.opentelemetry.exporter.public_send(option_name) || computed_default
47
51
  else
48
52
  signal_settings.public_send(option_name)
49
53
  end
@@ -51,7 +51,7 @@ module Datadog
51
51
  # profiler overhead!
52
52
  dynamic_sampling_rate_enabled: true,
53
53
  skip_idle_samples_for_testing: false,
54
- idle_sampling_helper: IdleSamplingHelper.new(thread_context_collector: thread_context_collector)
54
+ idle_sampling_helper: IdleSamplingHelper.new
55
55
  )
56
56
  unless dynamic_sampling_rate_enabled
57
57
  Datadog.logger.warn(
@@ -98,7 +98,7 @@ module Datadog
98
98
 
99
99
  Datadog.logger.debug { "Starting thread for: #{self}" }
100
100
 
101
- @idle_sampling_helper.start
101
+ @idle_sampling_helper.start(self)
102
102
 
103
103
  @worker_thread = Thread.new do
104
104
  Thread.current.name = self.class.name
@@ -10,7 +10,6 @@ module Datadog
10
10
  class IdleSamplingHelper
11
11
  # @rbs @worker_thread: untyped
12
12
  # @rbs @start_stop_mutex: ::Thread::Mutex
13
- # @rbs @thread_context_collector: Datadog::Profiling::Collectors::ThreadContext?
14
13
 
15
14
  private
16
15
 
@@ -18,15 +17,14 @@ module Datadog
18
17
 
19
18
  public
20
19
 
21
- #: (thread_context_collector: Datadog::Profiling::Collectors::ThreadContext) -> void
22
- def initialize(thread_context_collector:)
20
+ #: () -> void
21
+ def initialize
23
22
  @worker_thread = nil
24
23
  @start_stop_mutex = Mutex.new
25
- @thread_context_collector = thread_context_collector
26
24
  end
27
25
 
28
- #: () -> (nil | true)
29
- def start
26
+ #: (CpuAndWallTimeWorker cpu_and_wall_time_worker) -> true?
27
+ def start(cpu_and_wall_time_worker)
30
28
  @start_stop_mutex.synchronize do
31
29
  return if @worker_thread&.alive?
32
30
 
@@ -39,7 +37,7 @@ module Datadog
39
37
  @worker_thread = Thread.new do
40
38
  Thread.current.name = self.class.name
41
39
 
42
- self.class._native_idle_sampling_loop(self, @thread_context_collector)
40
+ self.class._native_idle_sampling_loop(self, cpu_and_wall_time_worker)
43
41
 
44
42
  Datadog.logger.debug("IdleSamplingHelper thread stopping cleanly")
45
43
  rescue Exception => e # rubocop:disable Lint/RescueException
@@ -43,7 +43,7 @@ module Datadog
43
43
  # These are expected failures — no logging needed.
44
44
  #
45
45
  # 2. **Method-level rescues** (`rescue => e` with logging):
46
- # Catch failures in extract_method_scope, find_source_file, etc. Log at debug
46
+ # Catch failures in build_class_method_scopes, find_source_file, etc. Log at debug
47
47
  # for post-hoc diagnosis, return nil or empty array. One bad method/module
48
48
  # doesn't kill the entire class extraction.
49
49
  #
@@ -133,16 +133,16 @@ module Datadog
133
133
  return nil unless Module === mod
134
134
  mod_name = safe_mod_name(mod)
135
135
  return nil unless mod_name
136
-
137
- return nil unless user_code_module?(mod)
136
+ return nil unless user_code_module_name?(mod)
138
137
 
139
138
  source_file = find_source_file(mod)
140
139
  return nil unless source_file
140
+ return nil unless user_code_path?(source_file)
141
141
 
142
142
  inner_scope = if Class === mod
143
- extract_class_scope(mod)
143
+ extract_class_scope(mod, source_file)
144
144
  else
145
- extract_module_scope(mod)
145
+ extract_module_scope(mod, source_file)
146
146
  end
147
147
 
148
148
  wrap_in_file_scope(source_file, [inner_scope])
@@ -275,10 +275,12 @@ module Datadog
275
275
  false
276
276
  end
277
277
 
278
- # Check if module is from user code (not gems or stdlib)
279
- # @param mod [Module] The module to check
280
- # @return [Boolean] true if user code
281
- def user_code_module?(mod)
278
+ # Return whether +mod+ passes the name-based user-code exclusions
279
+ # (named, outside Datadog, not a Ruby root class). Does not resolve
280
+ # a source file.
281
+ # @param mod [Module]
282
+ # @return [Boolean]
283
+ def user_code_module_name?(mod)
282
284
  mod_name = safe_mod_name(mod)
283
285
  return false unless mod_name
284
286
 
@@ -297,6 +299,15 @@ module Datadog
297
299
  return false if mod.equal?(Object) || mod.equal?(BasicObject) ||
298
300
  mod.equal?(Kernel) || mod.equal?(Module) || mod.equal?(Class)
299
301
 
302
+ true
303
+ end
304
+
305
+ # Check if module is from user code (not gems or stdlib)
306
+ # @param mod [Module] The module to check
307
+ # @return [Boolean] true if user code
308
+ def user_code_module?(mod)
309
+ return false unless user_code_module_name?(mod)
310
+
300
311
  source_file = find_source_file(mod)
301
312
  return false unless source_file
302
313
 
@@ -331,6 +342,25 @@ module Datadog
331
342
  true
332
343
  end
333
344
 
345
+ # Resolve the UnboundMethod +mod+ itself declares for +method_name+,
346
+ # skipping prepended wrapper modules (e.g. a DI method-logpoint wrapper)
347
+ # that shadow it.
348
+ #
349
+ # @param mod [Module] Module or class expected to declare the method
350
+ # @param method_name [Symbol] Name of the method to resolve
351
+ # @return [UnboundMethod] Method whose owner is +mod+, or the
352
+ # prepend-resolved method when +mod+ owns no method in the super chain
353
+ def declared_instance_method(mod, method_name)
354
+ original_method = mod.instance_method(method_name)
355
+ method = original_method
356
+ until method.owner.equal?(mod)
357
+ super_method = method.super_method
358
+ return original_method unless super_method
359
+ method = super_method
360
+ end
361
+ method
362
+ end
363
+
334
364
  # Find source file for a module.
335
365
  # Prefers user code paths over gem/stdlib paths. ActiveRecord models have
336
366
  # generated methods (autosave callbacks) whose source is in the gem, but
@@ -349,7 +379,7 @@ module Datadog
349
379
 
350
380
  # Try instance methods first
351
381
  mod.instance_methods(false).each do |method_name|
352
- method = mod.instance_method(method_name)
382
+ method = declared_instance_method(mod, method_name)
353
383
  location = method.source_location
354
384
  next unless location
355
385
 
@@ -357,10 +387,15 @@ module Datadog
357
387
  return path if user_code_path?(path)
358
388
 
359
389
  fallback ||= path # steep:ignore
390
+ rescue Exception => e # standard:disable Lint/RescueException
391
+ Datadog::DI.reraise_if_fatal(e)
392
+ @logger.debug { "symdb: error resolving #{safe_mod_name(mod)}##{method_name}: #{e.class}: #{e.message}" }
360
393
  end
361
394
 
362
395
  # Try singleton methods
363
396
  mod.singleton_methods(false).each do |method_name|
397
+ # Method-logpoint probes prepend instance methods only, so singleton
398
+ # methods are never shadowed by a DI wrapper.
364
399
  method = mod.method(method_name)
365
400
  location = method.source_location
366
401
  next unless location
@@ -369,6 +404,9 @@ module Datadog
369
404
  return path if user_code_path?(path)
370
405
 
371
406
  fallback ||= path # steep:ignore
407
+ rescue Exception => e # standard:disable Lint/RescueException
408
+ Datadog::DI.reraise_if_fatal(e)
409
+ @logger.debug { "symdb: error resolving #{safe_mod_name(mod)}.#{method_name}: #{e.class}: #{e.message}" }
372
410
  end
373
411
 
374
412
  # Use const_source_location to find where this class/module is declared.
@@ -460,10 +498,9 @@ module Datadog
460
498
  # Extract MODULE scope (without file_hash — that belongs on the FILE root scope).
461
499
  # Does not include nested classes — nesting is handled by extract_all via FQN splitting.
462
500
  # @param mod [Module] The module
501
+ # @param source_file [String] Source file resolved by the caller
463
502
  # @return [Scope] The module scope
464
- def extract_module_scope(mod)
465
- source_file = find_source_file(mod)
466
-
503
+ def extract_module_scope(mod, source_file)
467
504
  Scope.new(
468
505
  scope_type: "MODULE",
469
506
  name: safe_mod_name(mod),
@@ -476,11 +513,11 @@ module Datadog
476
513
 
477
514
  # Extract CLASS scope
478
515
  # @param klass [Class] The class
516
+ # @param source_file [String] Source file resolved by the caller
479
517
  # @return [Scope] The class scope
480
- def extract_class_scope(klass)
481
- methods = klass.instance_methods(false)
482
- start_line, end_line = calculate_class_line_range(klass, methods)
483
- source_file = find_source_file(klass)
518
+ def extract_class_scope(klass, source_file)
519
+ method_scopes = build_class_method_scopes(klass)
520
+ start_line, end_line = line_range_from_scopes(method_scopes)
484
521
 
485
522
  Scope.new(
486
523
  scope_type: "CLASS",
@@ -489,36 +526,20 @@ module Datadog
489
526
  start_line: start_line,
490
527
  end_line: end_line,
491
528
  language_specifics: build_class_language_specifics(klass),
492
- scopes: extract_method_scopes(klass),
529
+ scopes: method_scopes,
493
530
  symbols: extract_scope_symbols(klass)
494
531
  )
495
532
  end
496
533
 
497
- # Calculate class line range from method locations.
498
- # Start from the earliest method start, end at the latest method end (derived
499
- # from iseq trace_points so methods spanning multiple lines aren't truncated).
500
- # @param klass [Class] The class
501
- # @param methods [Array<Symbol>] Method names
534
+ # Derive a [start_line, end_line] range from method scopes, ignoring
535
+ # UNKNOWN sentinel values. The end is the max of method end_lines so a
536
+ # multi-line method's last line is not truncated to its start_line.
537
+ # @param method_scopes [Array<Scope>]
502
538
  # @return [Array<Integer, Integer>] [start_line, end_line]
503
- def calculate_class_line_range(klass, methods)
504
- starts = []
505
- ends = []
506
- methods.each do |method_name|
507
- method = klass.instance_method(method_name)
508
- location = method.source_location
509
- next unless location && location[0]
510
- starts << location[1]
511
- _ranges, method_end = extract_targetable_lines(method, location[1])
512
- ends << method_end
513
- end
514
-
515
- return [UNKNOWN_MIN_LINE, UNKNOWN_MAX_LINE] if starts.empty?
516
-
517
- [starts.min, ends.max]
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}" }
521
- [UNKNOWN_MIN_LINE, UNKNOWN_MAX_LINE]
539
+ def line_range_from_scopes(method_scopes)
540
+ starts = method_scopes.map(&:start_line).reject { |line| line == UNKNOWN_MIN_LINE }
541
+ ends = method_scopes.map(&:end_line).reject { |line| line == UNKNOWN_MAX_LINE }
542
+ [starts.min || UNKNOWN_MIN_LINE, ends.max || UNKNOWN_MAX_LINE]
522
543
  end
523
544
 
524
545
  # Build language specifics for CLASS
@@ -566,66 +587,33 @@ module Datadog
566
587
  {}
567
588
  end
568
589
 
569
- # Extract method scopes from a class
570
- # @param klass [Class] The class
571
- # @return [Array<Scope>] Method scopes
572
- def extract_method_scopes(klass)
573
- scopes = []
574
-
575
- # Get all instance methods (public, protected, private)
576
- all_instance_methods = klass.instance_methods(false) +
590
+ # Build a METHOD scope for each user-code instance method +klass+
591
+ # declares.
592
+ # @param klass [Class]
593
+ # @return [Array<Scope>]
594
+ def build_class_method_scopes(klass)
595
+ method_names = (klass.instance_methods(false) +
577
596
  klass.protected_instance_methods(false) +
578
- klass.private_instance_methods(false)
579
- all_instance_methods.uniq!
597
+ klass.private_instance_methods(false)).uniq
580
598
 
581
- all_instance_methods.each do |method_name|
582
- method_scope = extract_method_scope(klass, method_name, :instance)
583
- scopes << method_scope if method_scope
584
- end
599
+ Core::Utils::EnumerableCompat.filter_map(method_names) do |method_name|
600
+ method = declared_instance_method(klass, method_name)
601
+ location = method.source_location
602
+ next unless location
603
+ next unless user_code_path?(location[0])
585
604
 
586
- scopes
605
+ build_instance_method_scope(klass, method_name, method)
606
+ rescue Exception => e # standard:disable Lint/RescueException
607
+ Datadog::DI.reraise_if_fatal(e)
608
+ @logger.debug { "symdb: error resolving #{safe_mod_name(klass)}##{method_name}: #{e.class}: #{e.message}" }
609
+ nil
610
+ end
587
611
  rescue Exception => e # standard:disable Lint/RescueException
588
612
  Datadog::DI.reraise_if_fatal(e)
589
613
  @logger.debug { "symdb: failed to extract methods from #{safe_mod_name(klass)}: #{e.class}: #{e.message}" }
590
614
  []
591
615
  end
592
616
 
593
- # Extract a single method scope
594
- # @param klass [Class] The class
595
- # @param method_name [Symbol] Method name
596
- # @param method_type [Symbol] :instance or :class
597
- # @return [Scope, nil] Method scope or nil
598
- def extract_method_scope(klass, method_name, method_type)
599
- method = klass.instance_method(method_name)
600
- location = method.source_location
601
-
602
- return nil unless location # Skip methods without source location
603
-
604
- source_file, line = location
605
- return nil unless user_code_path?(source_file) # Skip gem/stdlib methods
606
-
607
- targetable_lines, end_line = extract_targetable_lines(method, line)
608
-
609
- Scope.new(
610
- scope_type: "METHOD",
611
- name: method_name.to_s,
612
- source_file: source_file,
613
- start_line: line,
614
- end_line: end_line,
615
- targetable_lines: targetable_lines,
616
- language_specifics: {
617
- visibility: method_visibility(klass, method_name),
618
- method_type: method_type.to_s,
619
- arity: method.arity,
620
- },
621
- symbols: extract_method_parameters(method)
622
- )
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}" }
626
- nil
627
- end
628
-
629
617
  # Get method visibility
630
618
  # @param klass [Class] The class
631
619
  # @param method_name [Symbol] Method name
@@ -808,7 +796,7 @@ module Datadog
808
796
  # visibilities without an intermediate merged array.
809
797
  [mod.instance_methods(false), mod.private_instance_methods(false)].each do |method_names|
810
798
  method_names.each do |method_name|
811
- method = mod.instance_method(method_name)
799
+ method = declared_instance_method(mod, method_name)
812
800
  loc = method.source_location
813
801
  next unless loc
814
802
  next unless user_code_path?(loc[0])
@@ -852,7 +840,7 @@ module Datadog
852
840
  # live only as long as the tree node holds them; they are released when
853
841
  # convert_tree_to_scope finishes building the file's Scope.
854
842
  method_infos = Core::Utils::EnumerableCompat.filter_map(method_names) do |name|
855
- method = mod.instance_method(name)
843
+ method = declared_instance_method(mod, name)
856
844
  # Pass 1 (build_per_file_index) recorded this method under file_path.
857
845
  # If the method has been redefined in another file between the two
858
846
  # passes (e.g. a class reopened during a Rails reload while extract_all
@@ -989,13 +977,7 @@ module Datadog
989
977
  # Recurse into child scopes (nested modules/classes)
990
978
  child_scopes = node[:children].values.map { |child| convert_node_to_scope(child) }
991
979
 
992
- # Compute line range: start from the earliest method start, end at the latest
993
- # method end. Using max(start_line) would underreport the class's end_line for
994
- # classes whose last method spans multiple lines.
995
- starts = method_scopes.map(&:start_line).reject { |l| l == UNKNOWN_MIN_LINE } # steep:ignore
996
- ends = method_scopes.map(&:end_line).reject { |l| l == UNKNOWN_MAX_LINE } # steep:ignore
997
- start_line = starts.empty? ? UNKNOWN_MIN_LINE : starts.min
998
- end_line = ends.empty? ? UNKNOWN_MAX_LINE : ends.max
980
+ start_line, end_line = line_range_from_scopes(method_scopes)
999
981
 
1000
982
  # Extract symbols (constants, class variables) if we have the actual module object
1001
983
  symbols = node[:mod] ? extract_scope_symbols(node[:mod]) : []
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "otel_thread_context"
3
4
  require_relative "tracer"
4
5
  require_relative "flush"
5
6
  require_relative "sync_writer"
@@ -42,6 +43,7 @@ module Datadog
42
43
  default_service: settings.service,
43
44
  enabled: settings.tracing.enabled,
44
45
  logger: logger,
46
+ otel_thread_context: OTelThreadContext.build(settings.tracing),
45
47
  trace_flush: trace_flush,
46
48
  sampler: sampler_delegator,
47
49
  span_sampler: build_span_sampler(settings),
@@ -13,6 +13,7 @@ module Datadog
13
13
  ENV_BAGGAGE_TAG_KEYS = "DD_TRACE_BAGGAGE_TAG_KEYS"
14
14
  ENV_TRACE_ID_128_BIT_GENERATION_ENABLED = "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED"
15
15
  ENV_NATIVE_SPAN_EVENTS = "DD_TRACE_NATIVE_SPAN_EVENTS"
16
+ ENV_OTEL_CONTEXT_ENABLED = "DD_TRACE_OTEL_CTX_ENABLED"
16
17
  ENV_RESOURCE_RENAMING_ENABLED = "DD_TRACE_RESOURCE_RENAMING_ENABLED"
17
18
  ENV_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT = "DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT"
18
19
  ENV_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED = "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED"
@@ -255,6 +255,12 @@ module Datadog
255
255
  o.type :bool
256
256
  end
257
257
 
258
+ option :otel_thread_context_enabled do |o|
259
+ o.env Tracing::Configuration::Ext::ENV_OTEL_CONTEXT_ENABLED
260
+ o.default true
261
+ o.type :bool
262
+ end
263
+
258
264
  # Configures an alternative trace transport behavior, where
259
265
  # traces can be sent to the agent and backend before all spans
260
266
  # have finished.