datadog 2.36.0 → 2.38.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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -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 +29 -18
  5. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +17 -3
  6. data/ext/datadog_profiling_native_extension/collectors_stack.c +19 -24
  7. data/ext/datadog_profiling_native_extension/collectors_stack.h +0 -1
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +348 -213
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -0
  10. data/ext/datadog_profiling_native_extension/extconf.rb +11 -3
  11. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +5 -0
  12. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +2 -0
  13. data/ext/datadog_profiling_native_extension/ruby_helpers.c +7 -0
  14. data/ext/datadog_profiling_native_extension/time_helpers.h +12 -7
  15. data/ext/libdatadog_api/di.c +76 -0
  16. data/ext/libdatadog_api/extconf.rb +6 -0
  17. data/ext/libdatadog_extconf_helpers.rb +1 -1
  18. data/lib/datadog/ai_guard/api_client.rb +6 -4
  19. data/lib/datadog/appsec/api_security/route_extractor.rb +7 -4
  20. data/lib/datadog/appsec/configuration.rb +6 -0
  21. data/lib/datadog/appsec/context.rb +4 -2
  22. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +59 -4
  23. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +59 -5
  24. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +1 -1
  25. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +5 -0
  26. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +4 -0
  27. data/lib/datadog/appsec/contrib/rack/input_peeker.rb +4 -17
  28. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +21 -0
  29. data/lib/datadog/appsec/contrib/rails/gateway/request.rb +7 -1
  30. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +60 -7
  31. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +4 -0
  32. data/lib/datadog/appsec/ext.rb +1 -0
  33. data/lib/datadog/appsec/metrics/collector.rb +18 -1
  34. data/lib/datadog/appsec/metrics/exporter.rb +9 -3
  35. data/lib/datadog/appsec/remote.rb +9 -3
  36. data/lib/datadog/appsec/route_normalizer/rails_route_pattern.rb +176 -0
  37. data/lib/datadog/appsec/route_normalizer/route_pattern.rb +378 -0
  38. data/lib/datadog/appsec/route_normalizer/route_text.rb +57 -0
  39. data/lib/datadog/appsec/route_normalizer.rb +80 -0
  40. data/lib/datadog/appsec/utils/http/body_reader.rb +61 -0
  41. data/lib/datadog/core/configuration/agentless_settings_resolver.rb +2 -2
  42. data/lib/datadog/core/configuration/components.rb +82 -2
  43. data/lib/datadog/core/configuration/components_state.rb +6 -1
  44. data/lib/datadog/core/configuration/settings.rb +4 -7
  45. data/lib/datadog/core/configuration/supported_configurations.rb +3 -0
  46. data/lib/datadog/core/evp.rb +11 -0
  47. data/lib/datadog/core/knuth_sampler.rb +3 -1
  48. data/lib/datadog/core/metrics/client.rb +1 -1
  49. data/lib/datadog/core/remote/client/capabilities.rb +34 -7
  50. data/lib/datadog/core/telemetry/event/app_started.rb +8 -1
  51. data/lib/datadog/core/transport/http/builder.rb +1 -1
  52. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +1 -1
  53. data/lib/datadog/core/utils/time.rb +5 -1
  54. data/lib/datadog/di/base.rb +3 -1
  55. data/lib/datadog/di/boot.rb +3 -0
  56. data/lib/datadog/di/capture_expression.rb +21 -0
  57. data/lib/datadog/di/capture_expression_evaluator.rb +71 -0
  58. data/lib/datadog/di/capture_limits.rb +41 -0
  59. data/lib/datadog/di/code_tracker.rb +5 -2
  60. data/lib/datadog/di/component.rb +112 -38
  61. data/lib/datadog/di/configuration.rb +6 -0
  62. data/lib/datadog/di/context.rb +9 -3
  63. data/lib/datadog/di/el/compiler.rb +1 -1
  64. data/lib/datadog/di/error.rb +10 -0
  65. data/lib/datadog/di/fatal_exceptions.rb +26 -0
  66. data/lib/datadog/di/instrumenter.rb +412 -165
  67. data/lib/datadog/di/probe.rb +61 -2
  68. data/lib/datadog/di/probe_builder.rb +83 -1
  69. data/lib/datadog/di/probe_file_loader.rb +8 -3
  70. data/lib/datadog/di/probe_manager.rb +67 -9
  71. data/lib/datadog/di/probe_notification_builder.rb +52 -8
  72. data/lib/datadog/di/probe_notifier_worker.rb +52 -32
  73. data/lib/datadog/di/redactor.rb +16 -1
  74. data/lib/datadog/di/remote.rb +176 -15
  75. data/lib/datadog/di/serializer.rb +34 -15
  76. data/lib/datadog/di/transport/input.rb +8 -4
  77. data/lib/datadog/di.rb +63 -0
  78. data/lib/datadog/kit/appsec/events/v2.rb +60 -2
  79. data/lib/datadog/open_feature/component.rb +29 -6
  80. data/lib/datadog/open_feature/configuration.rb +8 -0
  81. data/lib/datadog/open_feature/ext.rb +2 -0
  82. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +286 -0
  83. data/lib/datadog/open_feature/flag_evaluation/writer.rb +433 -0
  84. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +101 -0
  85. data/lib/datadog/open_feature/hooks/{flag_eval_hook.rb → flag_eval_metrics_hook.rb} +1 -1
  86. data/lib/datadog/open_feature/native_evaluator.rb +22 -0
  87. data/lib/datadog/open_feature/provider.rb +59 -27
  88. data/lib/datadog/open_feature/transport.rb +55 -1
  89. data/lib/datadog/opentelemetry/sdk/logs_exporter.rb +5 -12
  90. data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +4 -11
  91. data/lib/datadog/opentelemetry/sdk.rb +19 -0
  92. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +6 -1
  93. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -3
  94. data/lib/datadog/profiling/collectors/thread_context.rb +9 -1
  95. data/lib/datadog/profiling/component.rb +17 -0
  96. data/lib/datadog/symbol_database/component.rb +164 -47
  97. data/lib/datadog/symbol_database/configuration.rb +12 -4
  98. data/lib/datadog/symbol_database/extensions.rb +19 -0
  99. data/lib/datadog/symbol_database/extractor.rb +124 -87
  100. data/lib/datadog/symbol_database/file_hash.rb +3 -1
  101. data/lib/datadog/symbol_database/remote.rb +8 -3
  102. data/lib/datadog/symbol_database/scope_batcher.rb +7 -3
  103. data/lib/datadog/symbol_database/uploader.rb +9 -2
  104. data/lib/datadog/symbol_database.rb +22 -0
  105. data/lib/datadog/tracing/contrib/active_job/data_streams.rb +62 -0
  106. data/lib/datadog/tracing/contrib/active_job/patcher.rb +12 -0
  107. data/lib/datadog/tracing/contrib/active_model_serializers/events/serialize.rb +2 -3
  108. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +12 -12
  109. data/lib/datadog/tracing/contrib/rack/patcher.rb +1 -1
  110. data/lib/datadog/tracing/contrib/sequel/utils.rb +2 -2
  111. data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +1 -1
  112. data/lib/datadog/tracing/contrib/sinatra/framework.rb +1 -1
  113. data/lib/datadog/tracing/distributed/baggage.rb +2 -1
  114. data/lib/datadog/tracing/ext.rb +9 -0
  115. data/lib/datadog/tracing/remote.rb +34 -3
  116. data/lib/datadog/tracing/transport/trace_formatter.rb +1 -1
  117. data/lib/datadog/version.rb +1 -1
  118. data/lib/datadog.rb +1 -0
  119. metadata +25 -8
@@ -34,11 +34,17 @@ module Datadog
34
34
  class Probe
35
35
  KNOWN_TYPES = %i[log].freeze
36
36
 
37
+ EVALUATE_AT_VALUES = %i[entry exit].freeze
38
+
37
39
  def initialize(id:, type:,
38
40
  file: nil, line_no: nil, type_name: nil, method_name: nil,
39
41
  template: nil, template_segments: nil,
40
42
  capture_snapshot: false, max_capture_depth: nil,
41
- max_capture_attribute_count: nil, condition: nil,
43
+ max_capture_attribute_count: nil,
44
+ max_capture_collection_size: nil, max_capture_string_length: nil,
45
+ capture_expressions: [],
46
+ evaluate_at: nil,
47
+ condition: nil,
42
48
  rate_limit: nil)
43
49
  # Perform some sanity checks here to detect unexpected attribute
44
50
  # combinations, in order to not do them in subsequent code.
@@ -81,9 +87,17 @@ module Datadog
81
87
  @capture_snapshot = !!capture_snapshot
82
88
  @max_capture_depth = max_capture_depth
83
89
  @max_capture_attribute_count = max_capture_attribute_count
90
+ @max_capture_collection_size = max_capture_collection_size
91
+ @max_capture_string_length = max_capture_string_length
92
+ @capture_expressions = capture_expressions || []
93
+ evaluate_at = :exit if evaluate_at.nil?
94
+ unless EVALUATE_AT_VALUES.include?(evaluate_at)
95
+ raise ArgumentError, "Unknown evaluate_at value: #{evaluate_at.inspect} (expected one of #{EVALUATE_AT_VALUES.inspect})"
96
+ end
97
+ @evaluate_at = evaluate_at
84
98
  @condition = condition
85
99
 
86
- @rate_limit = rate_limit || (@capture_snapshot ? 1 : 5000)
100
+ @rate_limit = rate_limit || ((@capture_snapshot || !@capture_expressions.empty?) ? 1 : 5000)
87
101
  @rate_limiter = Datadog::Core::TokenBucket.new(@rate_limit)
88
102
 
89
103
  # At most one report per second.
@@ -120,6 +134,14 @@ module Datadog
120
134
  # the global default will be used.
121
135
  attr_reader :max_capture_attribute_count
122
136
 
137
+ attr_reader :max_capture_collection_size
138
+
139
+ attr_reader :max_capture_string_length
140
+
141
+ attr_reader :capture_expressions
142
+
143
+ attr_reader :evaluate_at
144
+
123
145
  # Rate limit in effect, in invocations per second. Always present.
124
146
  attr_reader :rate_limit
125
147
 
@@ -140,6 +162,29 @@ module Datadog
140
162
  @capture_snapshot
141
163
  end
142
164
 
165
+ def capture_expressions?
166
+ !@capture_expressions.empty?
167
+ end
168
+
169
+ def evaluate_at_entry?
170
+ evaluate_at == :entry
171
+ end
172
+
173
+ # Capture-expression mode: the probe captures expressions but not a full
174
+ # snapshot. Snapshot capture serializes its own values, so expression
175
+ # capture only runs when a snapshot is not being taken.
176
+ def capture_expressions_only?
177
+ capture_expressions? && !capture_snapshot?
178
+ end
179
+
180
+ def capture_entry_expressions?
181
+ capture_expressions_only? && evaluate_at_entry?
182
+ end
183
+
184
+ def snapshot_serializer_limits(settings)
185
+ CaptureLimits.resolve(expr_limits: nil, probe: self, settings: settings)
186
+ end
187
+
143
188
  # Returns whether the probe is a line probe.
144
189
  #
145
190
  # Method probes may still specify a file name (to aid in locating the
@@ -171,6 +216,20 @@ module Datadog
171
216
  line_no
172
217
  end
173
218
 
219
+ # Returns the method name associated with the probe, raising
220
+ # Error::MissingMethodName if the probe does not have a method name
221
+ # associated with it.
222
+ #
223
+ # This method is used by instrumentation driver to ensure a method name
224
+ # that is passed into the instrumentation logic is actually a method name
225
+ # and not nil.
226
+ def method_name!
227
+ if method_name.nil?
228
+ raise Error::MissingMethodName, "Probe #{id} does not have a method name associated with it"
229
+ end
230
+ method_name
231
+ end
232
+
174
233
  # Source code location of the probe, for diagnostic reporting.
175
234
  def location
176
235
  if method?
@@ -3,6 +3,8 @@
3
3
  # rubocop:disable Lint/AssignmentInCondition
4
4
 
5
5
  require_relative "probe"
6
+ require_relative "capture_expression"
7
+ require_relative "capture_limits"
6
8
  require_relative 'el'
7
9
 
8
10
  module Datadog
@@ -27,7 +29,15 @@ module Datadog
27
29
 
28
30
  module_function
29
31
 
30
- def build_from_remote_config(config)
32
+ CAPTURE_EXPRESSION_NAME_PATTERN = /\A[a-zA-Z0-9_?]+\z/
33
+
34
+ EVALUATE_AT_STRINGS = {
35
+ "ENTRY" => :entry,
36
+ "EXIT" => :exit,
37
+ "DEFAULT" => :exit,
38
+ }.freeze
39
+
40
+ def build_from_remote_config(config, logger:)
31
41
  # The validations here are not yet comprehensive.
32
42
  type = config.fetch('type')
33
43
  type_symbol = PROBE_TYPES[type] or raise ArgumentError, "Unrecognized probe type: #{type}"
@@ -38,6 +48,13 @@ module Datadog
38
48
  compiled = EL::Compiler.new.compile(cond_spec['json'])
39
49
  EL::Expression.new(cond_spec['dsl'], compiled)
40
50
  end
51
+ capture_expressions = build_capture_expressions(config['captureExpressions'])
52
+ capture_expressions = dedup_capture_expressions(capture_expressions, config["id"], logger)
53
+ if !!config["captureSnapshot"] && !capture_expressions.empty?
54
+ logger.debug do
55
+ "di: probe #{config["id"]}: captureSnapshot=true wins over captureExpressions (n=#{capture_expressions.size})"
56
+ end
57
+ end
41
58
  Probe.new(
42
59
  id: config.fetch("id"),
43
60
  type: type_symbol,
@@ -54,6 +71,10 @@ module Datadog
54
71
  capture_snapshot: !!config["captureSnapshot"],
55
72
  max_capture_depth: config["capture"]&.[]("maxReferenceDepth"),
56
73
  max_capture_attribute_count: config["capture"]&.[]("maxFieldCount"),
74
+ max_capture_collection_size: config["capture"]&.[]("maxCollectionSize"),
75
+ max_capture_string_length: config["capture"]&.[]("maxLength"),
76
+ capture_expressions: capture_expressions,
77
+ evaluate_at: parse_evaluate_at(config["evaluateAt"], config["id"], logger),
57
78
  rate_limit: config["sampling"]&.[]("snapshotsPerSecond"),
58
79
  condition: cond,
59
80
  )
@@ -61,6 +82,67 @@ module Datadog
61
82
  raise ArgumentError, "Malformed remote configuration entry for probe: #{exc.class}: #{exc.message}: #{config}"
62
83
  end
63
84
 
85
+ def build_capture_expressions(raw)
86
+ return [] if raw.nil?
87
+ unless Array === raw
88
+ raise ArgumentError, "captureExpressions must be an array, got: #{raw.class}"
89
+ end
90
+ return [] if raw.empty?
91
+ raw.map do |entry|
92
+ unless Hash === entry
93
+ raise ArgumentError, "captureExpressions entry must be a hash, got: #{entry.class}"
94
+ end
95
+ name = entry['name']
96
+ unless String === name && CAPTURE_EXPRESSION_NAME_PATTERN.match?(name)
97
+ raise ArgumentError, "captureExpressions entry name missing or invalid (must match #{CAPTURE_EXPRESSION_NAME_PATTERN.inspect}): #{name.inspect}"
98
+ end
99
+ expr_spec = entry['expr']
100
+ unless Hash === expr_spec && expr_spec['dsl'] && expr_spec['json']
101
+ raise ArgumentError, "captureExpressions entry #{name}: missing or malformed expr"
102
+ end
103
+ compiled = EL::Compiler.new.compile(expr_spec['json'])
104
+ expr = EL::Expression.new(expr_spec['dsl'], compiled)
105
+ limits = build_capture_limits(entry['capture'])
106
+ CaptureExpression.new(name: name, expr: expr, limits: limits)
107
+ end
108
+ end
109
+
110
+ def dedup_capture_expressions(capture_expressions, probe_id, logger)
111
+ return capture_expressions if capture_expressions.size < 2
112
+ by_name = {}
113
+ capture_expressions.each do |capture_expression|
114
+ by_name[capture_expression.name] = capture_expression
115
+ end
116
+ return capture_expressions if by_name.size == capture_expressions.size
117
+ logger.debug do
118
+ "di: probe #{probe_id}: collapsed duplicate captureExpressions names, kept last per name (#{capture_expressions.size} -> #{by_name.size})"
119
+ end
120
+ by_name.values
121
+ end
122
+
123
+ def build_capture_limits(raw)
124
+ return nil if raw.nil?
125
+ unless Hash === raw
126
+ raise ArgumentError, "capture-expression entry capture must be a hash, got: #{raw.class}"
127
+ end
128
+ CaptureLimits.new(
129
+ max_reference_depth: raw['maxReferenceDepth'],
130
+ max_collection_size: raw['maxCollectionSize'],
131
+ max_length: raw['maxLength'],
132
+ max_field_count: raw['maxFieldCount'],
133
+ )
134
+ end
135
+
136
+ def parse_evaluate_at(raw, probe_id, logger)
137
+ return :exit if raw.nil?
138
+ EVALUATE_AT_STRINGS[raw] || begin
139
+ logger.debug do
140
+ "di: probe #{probe_id}: unrecognized evaluateAt value #{raw.inspect}, defaulting to :exit"
141
+ end
142
+ :exit
143
+ end
144
+ end
145
+
64
146
  def build_template_segments(segments)
65
147
  segments&.map do |segment|
66
148
  if Hash === segment
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'json'
4
4
 
5
+ require_relative 'fatal_exceptions'
6
+
5
7
  module Datadog
6
8
  module DI
7
9
  module ProbeFileLoader
@@ -47,7 +49,8 @@ module Datadog
47
49
 
48
50
  payload = component.probe_notification_builder.build_errored(probe, exc)
49
51
  component.probe_notifier_worker.add_status(payload, probe: probe)
50
- rescue => exc
52
+ rescue Exception => exc # standard:disable Lint/RescueException
53
+ Datadog::DI.reraise_if_fatal(exc)
51
54
  raise if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
52
55
 
53
56
  component.logger.debug { "di: unhandled exception adding #{probe.type} probe at #{probe.location} (#{probe.id}) in DI probe file loader: #{exc.class}: #{exc.message}" }
@@ -58,7 +61,8 @@ module Datadog
58
61
  component.probe_notifier_worker.add_status(payload, probe: probe)
59
62
  end
60
63
  end
61
- rescue => exc
64
+ rescue Exception => exc # standard:disable Lint/RescueException
65
+ Datadog::DI.reraise_if_fatal(exc)
62
66
  if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
63
67
  should_propagate = true
64
68
  raise
@@ -67,7 +71,8 @@ module Datadog
67
71
  component.logger.debug { "di: unhandled exception handling a probe in DI probe file loader: #{exc.class}: #{exc.message}" }
68
72
  component.telemetry&.report(exc, description: "Unhandled exception handling probe in DI probe file loader")
69
73
  end
70
- rescue
74
+ rescue Exception => exc # standard:disable Lint/RescueException
75
+ Datadog::DI.reraise_if_fatal(exc)
71
76
  # We should generally never get here, but if component tree
72
77
  # initialization fails for some unexpected reason, don't nuke
73
78
  # the customer application.
@@ -2,6 +2,8 @@
2
2
 
3
3
  # rubocop:disable Lint/AssignmentInCondition
4
4
 
5
+ require_relative 'fatal_exceptions'
6
+
5
7
  module Datadog
6
8
  module DI
7
9
  # Orchestrates probe lifecycle: installation, removal, and execution callbacks.
@@ -26,13 +28,13 @@ module Datadog
26
28
  @telemetry = telemetry
27
29
  @probe_repository = probe_repository
28
30
 
29
- @definition_trace_point = TracePoint.trace(:end) do |tp|
31
+ @definition_trace_point = TracePoint.new(:end) do |tp|
30
32
  install_pending_method_probes(tp.self)
31
- rescue => exc
33
+ rescue Exception => exc # standard:disable Lint/RescueException
34
+ Datadog::DI.reraise_if_fatal(exc)
32
35
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
33
36
  logger.debug { "di: unhandled exception in definition trace point: #{exc.class}: #{exc.message}" }
34
37
  telemetry&.report(exc, description: "Unhandled exception in definition trace point")
35
- # TODO test this path
36
38
  end
37
39
  end
38
40
 
@@ -40,15 +42,68 @@ module Datadog
40
42
  attr_reader :telemetry
41
43
  attr_reader :probe_repository
42
44
 
45
+ # Stops the probe manager without permanently releasing resources.
46
+ #
47
+ # Disables the class definition trace point and unhooks all installed
48
+ # probe instrumentation, but preserves the probe repository so that
49
+ # {#reopen} can re-install probes. Can be reversed by calling {#reopen}.
50
+ #
51
+ # Why preserve the repository: RC dispatch only delivers LIVE_DEBUGGING
52
+ # changes when the content hash changes. If a customer toggles DI off
53
+ # then back on in the UI with the same probe content, the receiver
54
+ # is not re-invoked, so probes wiped here would never be redelivered.
55
+ # Preserving the repo lets {#reopen} replay the probes locally.
56
+ #
57
+ # @return [void]
58
+ def stop
59
+ definition_trace_point.disable
60
+ probe_repository.synchronize do
61
+ probe_repository.installed_probes.each_value do |probe|
62
+ instrumenter.unhook(probe)
63
+ rescue => exc
64
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
65
+ logger.debug { "di: error unhooking #{probe.type} probe at #{probe.location} (#{probe.id}) on stop: #{exc.class}: #{exc.message}" }
66
+ telemetry&.report(exc, description: "Error unhooking probe on stop")
67
+ end
68
+ end
69
+ end
70
+
71
+ # Re-enables the probe manager after a {#stop}.
72
+ #
73
+ # Re-enables the class definition trace point so that pending
74
+ # method probes can be installed when their target classes are defined,
75
+ # and re-hooks all previously-installed probes from the repository.
76
+ # If a probe's target is no longer present (e.g. code was unloaded
77
+ # between stop and reopen), it is demoted to pending so the trace
78
+ # point or code tracker can re-install it if the target reappears.
79
+ #
80
+ # @return [void]
81
+ def reopen
82
+ definition_trace_point.enable
83
+ probe_repository.synchronize do
84
+ # values.each so concurrent mutation of @installed_probes (via
85
+ # remove_installed below) does not iterate over a stale key set.
86
+ probe_repository.installed_probes.values.each do |probe|
87
+ instrumenter.hook(probe, self)
88
+ rescue Error::DITargetNotDefined
89
+ probe_repository.remove_installed(probe.id)
90
+ probe_repository.add_pending(probe)
91
+ logger.trace { "di: target for #{probe.type} probe (#{probe.id}) no longer defined on reopen, demoting to pending" }
92
+ rescue => exc
93
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
94
+ logger.debug { "di: error re-hooking #{probe.type} probe at #{probe.location} (#{probe.id}) on reopen: #{exc.class}: #{exc.message}" }
95
+ telemetry&.report(exc, description: "Error re-hooking probe on reopen")
96
+ end
97
+ end
98
+ end
99
+
43
100
  # Shuts down the probe manager and releases all resources.
44
101
  #
45
102
  # Disables the class definition trace point and removes all installed
46
103
  # probe instrumentation. Called during component teardown.
104
+ # Unlike {#stop}, this is not reversible.
47
105
  #
48
106
  # @return [void]
49
- #
50
- # TODO test that close is called during component teardown and
51
- # the trace point is cleared
52
107
  def close
53
108
  definition_trace_point.disable
54
109
  clear_hooks
@@ -129,7 +184,8 @@ module Datadog
129
184
  false
130
185
  end
131
186
  end
132
- rescue => exc
187
+ rescue Exception => exc # standard:disable Lint/RescueException
188
+ Datadog::DI.reraise_if_fatal(exc)
133
189
  # In "propagate all exceptions" mode we will try to instrument again.
134
190
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
135
191
 
@@ -160,7 +216,8 @@ module Datadog
160
216
  begin
161
217
  instrumenter.unhook(probe)
162
218
  probe_repository.remove_installed(probe_id)
163
- rescue => exc
219
+ rescue Exception => exc # standard:disable Lint/RescueException
220
+ Datadog::DI.reraise_if_fatal(exc)
164
221
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
165
222
  # Silence all exceptions?
166
223
  # TODO should we propagate here and rescue upstream?
@@ -190,7 +247,8 @@ module Datadog
190
247
  break
191
248
  rescue Error::DITargetNotDefined
192
249
  # This should not happen... try installing again later?
193
- rescue => exc
250
+ rescue Exception => exc # standard:disable Lint/RescueException
251
+ Datadog::DI.reraise_if_fatal(exc)
194
252
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
195
253
 
196
254
  logger.debug { "di: error installing #{probe.type} probe at #{probe.location} (#{probe.id}) after class is defined: #{exc.class}: #{exc.message}" }
@@ -2,20 +2,35 @@
2
2
 
3
3
  # rubocop:disable Lint/AssignmentInCondition
4
4
 
5
+ require_relative 'fatal_exceptions'
6
+ require_relative 'capture_expression_evaluator'
7
+
5
8
  module Datadog
6
9
  module DI
7
10
  # Builds probe status notification and snapshot payloads.
8
11
  #
9
12
  # @api private
10
13
  class ProbeNotificationBuilder
11
- def initialize(settings, serializer)
14
+ def initialize(settings, serializer, logger, telemetry: nil)
12
15
  @settings = settings
13
16
  @serializer = serializer
17
+ @logger = logger
18
+ @telemetry = telemetry
19
+ @capture_expression_evaluator = CaptureExpressionEvaluator.new(
20
+ settings: settings, serializer: serializer, logger: logger, telemetry: telemetry,
21
+ )
14
22
  end
15
23
 
16
24
  attr_reader :settings
25
+
17
26
  attr_reader :serializer
18
27
 
28
+ attr_reader :logger
29
+
30
+ attr_reader :telemetry
31
+
32
+ attr_reader :capture_expression_evaluator
33
+
19
34
  def build_received(probe)
20
35
  build_status(probe,
21
36
  message: "Probe #{probe.id} has been received correctly",
@@ -65,13 +80,13 @@ module Datadog
65
80
 
66
81
  # TODO also verify that non-capturing probe does not pass
67
82
  # snapshot or vars/args into this method
83
+ capture_expression_evaluation_errors = []
68
84
  captures = if probe.capture_snapshot?
85
+ snapshot_limits = probe.snapshot_serializer_limits(settings)
69
86
  if probe.method?
70
87
  return_arguments = {
71
- "@return": serializer.serialize_value(context.return_value,
72
- depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
73
- attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count),
74
- self: serializer.serialize_value(context.target_self),
88
+ "@return": serializer.serialize_value(context.return_value, **snapshot_limits),
89
+ self: serializer.serialize_value(context.target_self, **snapshot_limits),
75
90
  }
76
91
  {
77
92
  entry: {
@@ -87,8 +102,35 @@ module Datadog
87
102
  lines: (locals = context.serialized_locals) && {
88
103
  probe.line_no => {
89
104
  locals: locals,
90
- arguments: {self: serializer.serialize_value(context.target_self)},
105
+ arguments: {self: serializer.serialize_value(context.target_self, **snapshot_limits)},
106
+ },
107
+ },
108
+ }
109
+ end
110
+ elsif probe.capture_expressions?
111
+ if probe.method?
112
+ if probe.evaluate_at_entry?
113
+ captured_block = context.entry_capture_expressions || {}
114
+ capture_expression_evaluation_errors = context.entry_capture_evaluation_errors || []
115
+ {
116
+ entry: {captureExpressions: captured_block},
117
+ }
118
+ else
119
+ captured_block, capture_expression_evaluation_errors =
120
+ capture_expression_evaluator.evaluate(probe, context)
121
+ {
122
+ return: {
123
+ captureExpressions: captured_block,
124
+ throwable: context.exception ? serialize_throwable(context.exception) : nil,
91
125
  },
126
+ }
127
+ end
128
+ elsif probe.line?
129
+ captured_block, capture_expression_evaluation_errors =
130
+ capture_expression_evaluator.evaluate(probe, context)
131
+ {
132
+ lines: {
133
+ probe.line_no => {captureExpressions: captured_block},
92
134
  },
93
135
  }
94
136
  end
@@ -99,6 +141,7 @@ module Datadog
99
141
  if segments = probe.template_segments
100
142
  message, evaluation_errors = evaluate_template(segments, context)
101
143
  end
144
+ evaluation_errors.concat(capture_expression_evaluation_errors)
102
145
  build_snapshot_base(context,
103
146
  evaluation_errors: evaluation_errors, message: message,
104
147
  captures: captures)
@@ -303,7 +346,7 @@ module Datadog
303
346
  # except there is currently no consensus on said heuristics.
304
347
  # .NET always sends ld, other languages send nothing at the moment.
305
348
  # Don't send anything for the time being.
306
- #product: 'di/ld',
349
+ # product: 'di/ld',
307
350
  snapshot: {
308
351
  id: SecureRandom.uuid,
309
352
  timestamp: timestamp,
@@ -366,7 +409,8 @@ module Datadog
366
409
  else
367
410
  raise ArgumentError, "Invalid template segment type: #{segment}"
368
411
  end
369
- rescue => exc
412
+ rescue Exception => exc # standard:disable Lint/RescueException
413
+ Datadog::DI.reraise_if_fatal(exc)
370
414
  evaluation_errors << {
371
415
  message: "#{exc.class}: #{exc.message}",
372
416
  expr: segment.dsl_expr, # steep:ignore NoMethod
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../core/semaphore'
4
+ require_relative 'fatal_exceptions'
4
5
 
5
6
  module Datadog
6
7
  module DI
@@ -62,6 +63,7 @@ module Datadog
62
63
  # @return [void]
63
64
  def start
64
65
  return if @thread && @pid == Process.pid
66
+ @stop_requested = false
65
67
  logger.trace { "di: starting probe notifier: pid #{$$}" }
66
68
  @thread = Thread.new do
67
69
  loop do
@@ -92,10 +94,11 @@ module Datadog
92
94
 
93
95
  begin
94
96
  more = maybe_send
95
- rescue => exc
97
+ rescue Exception => exc # standard:disable Lint/RescueException
98
+ Datadog::DI.reraise_if_fatal(exc)
96
99
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
97
100
 
98
- logger.debug { "di: error in probe notifier worker: #{exc.class}: #{exc.message} (at #{exc.backtrace.first})" }
101
+ logger.debug { "di: error in probe notifier worker: #{exc.class}: #{exc.message} (at #{exc.backtrace&.first})" }
99
102
  telemetry&.report(exc, description: "Error in probe notifier worker")
100
103
  end
101
104
  @lock.synchronize do
@@ -257,40 +260,56 @@ module Datadog
257
260
  # if it has been more than 1 second since the last send of the same
258
261
  # event type.
259
262
  define_method("add_#{event_type}") do |event, probe: nil|
260
- @lock.synchronize do
261
- queue = send("#{event_type}_queue")
262
- if queue.length > settings.dynamic_instrumentation.internal.snapshot_queue_capacity
263
- if event_type == :status && probe
264
- status = event.dig(:debugger, :diagnostics, :status)
265
- logger.debug { "di: dropping status for #{probe.type} probe at #{probe.location} (#{probe.id}): #{status} because queue is full" }
266
- else
267
- logger.debug { "di: #{self.class.name}: dropping #{event_type} event because queue is full" }
268
- end
269
- else
270
- if event_type == :status && probe
271
- status = event.dig(:debugger, :diagnostics, :status)
272
- logger.trace { "di: queueing status for #{probe.type} probe at #{probe.location} (#{probe.id}): #{status}" }
263
+ # Enqueueing reads DI configuration and writes debug/trace logs, both
264
+ # of which can call stdlib methods (e.g. Set#include? via the
265
+ # configuration layer's supported-configuration lookup). If a customer
266
+ # has a method probe on such a method, firing it here would re-enter
267
+ # the worker through probe_executed_callback and deadlock on @lock
268
+ # (recursive locking). Hold the re-entrancy guard for the duration so
269
+ # those DI-internal calls reach the original method without firing the
270
+ # probe. Restore the prior state afterwards so a caller that already
271
+ # holds the guard (e.g. run_method_probe post-processing calling
272
+ # add_snapshot) is not cleared early.
273
+ was_in_probe = Datadog::DI.in_probe?
274
+ Datadog::DI.enter_probe unless was_in_probe
275
+ begin
276
+ @lock.synchronize do
277
+ queue = send("#{event_type}_queue")
278
+ if queue.length > settings.dynamic_instrumentation.internal.snapshot_queue_capacity
279
+ if event_type == :status && probe
280
+ status = event.dig(:debugger, :diagnostics, :status)
281
+ logger.debug { "di: dropping status for #{probe.type} probe at #{probe.location} (#{probe.id}): #{status} because queue is full" }
282
+ else
283
+ logger.debug { "di: #{self.class.name}: dropping #{event_type} event because queue is full" }
284
+ end
273
285
  else
274
- logger.trace { "di: #{self.class.name}: queueing #{event_type} event" }
286
+ if event_type == :status && probe
287
+ status = event.dig(:debugger, :diagnostics, :status)
288
+ logger.trace { "di: queueing status for #{probe.type} probe at #{probe.location} (#{probe.id}): #{status}" }
289
+ else
290
+ logger.trace { "di: #{self.class.name}: queueing #{event_type} event" }
291
+ end
292
+ queue << event
275
293
  end
276
- queue << event
277
294
  end
278
- end
279
295
 
280
- # Figure out whether to wake up the worker thread.
281
- # If minimum send interval has elapsed since the last send,
282
- # wake up immediately.
283
- @lock.synchronize do
284
- unless @wake_scheduled
285
- @wake_scheduled = true
286
- set_sleep_remaining
287
- wake.signal
296
+ # Figure out whether to wake up the worker thread.
297
+ # If minimum send interval has elapsed since the last send,
298
+ # wake up immediately.
299
+ @lock.synchronize do
300
+ unless @wake_scheduled
301
+ @wake_scheduled = true
302
+ set_sleep_remaining
303
+ wake.signal
304
+ end
288
305
  end
289
- end
290
306
 
291
- # Worker could be not running if the process forked - check and
292
- # start it again in this case.
293
- start
307
+ # Worker could be not running if the process forked - check and
308
+ # start it again in this case.
309
+ start
310
+ ensure
311
+ Datadog::DI.leave_probe unless was_in_probe
312
+ end
294
313
  end
295
314
 
296
315
  public "add_#{event_type}"
@@ -318,9 +337,10 @@ module Datadog
318
337
  @lock.synchronize do
319
338
  @last_sent = time
320
339
  end
321
- rescue => exc
340
+ rescue Exception => exc # standard:disable Lint/RescueException
341
+ Datadog::DI.reraise_if_fatal(exc)
322
342
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
323
- logger.debug { "di: failed to send #{event_name}: #{exc.class}: #{exc.message} (at #{exc.backtrace.first})" }
343
+ logger.debug { "di: failed to send #{event_name}: #{exc.class}: #{exc.message} (at #{exc.backtrace&.first})" }
324
344
  telemetry&.report(exc, description: "Error sending #{event_type}")
325
345
  end
326
346
  end
@@ -186,8 +186,23 @@ module Datadog
186
186
  ]
187
187
 
188
188
  # Input can be a string or a symbol.
189
+ #
190
+ # Hash keys captured in snapshots often originate from a Rack environment
191
+ # (Rack/Rails/Sinatra middleware and controllers all keep `env` in scope).
192
+ # Rack rewrites incoming HTTP header names to the CGI form
193
+ # `HTTP_<HEADER>` with dashes converted to underscores — for example,
194
+ # `Authorization` becomes `HTTP_AUTHORIZATION`, `Cookie` becomes
195
+ # `HTTP_COOKIE`, `X-API-Key` becomes `HTTP_X_API_KEY`. Without stripping
196
+ # the prefix here, those keys normalize to `httpauthorization`,
197
+ # `httpcookie`, `httpxapikey`, etc., none of which appear in the default
198
+ # identifier list — so the values would not be matched against entries
199
+ # like `authorization`, `cookie`, or `xapikey`. The `\Ahttp_` anchor
200
+ # limits the strip to the literal CGI form, so identifiers that merely
201
+ # begin with the letters `http` (e.g. `httpinfo`) are unaffected. The
202
+ # strip must run before the punctuation gsub: the gsub removes the
203
+ # underscore, after which `\Ahttp_` could never match.
189
204
  def normalize(str)
190
- str.to_s.strip.downcase.gsub(/[-_$@]/, "")
205
+ str.to_s.strip.downcase.sub(/\Ahttp_/, "").tr("-_$@", "")
191
206
  end
192
207
  end
193
208
  end