datadog 2.7.1 → 2.8.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -1
  3. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +47 -17
  4. data/ext/datadog_profiling_native_extension/extconf.rb +0 -8
  5. data/ext/datadog_profiling_native_extension/heap_recorder.c +11 -89
  6. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +1 -1
  7. data/ext/datadog_profiling_native_extension/stack_recorder.c +0 -34
  8. data/ext/libdatadog_extconf_helpers.rb +1 -1
  9. data/lib/datadog/appsec/component.rb +1 -8
  10. data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +73 -0
  11. data/lib/datadog/appsec/contrib/active_record/integration.rb +41 -0
  12. data/lib/datadog/appsec/contrib/active_record/patcher.rb +53 -0
  13. data/lib/datadog/appsec/event.rb +1 -1
  14. data/lib/datadog/appsec/processor/context.rb +2 -2
  15. data/lib/datadog/appsec/remote.rb +1 -3
  16. data/lib/datadog/appsec/response.rb +7 -11
  17. data/lib/datadog/appsec.rb +3 -2
  18. data/lib/datadog/core/configuration/components.rb +17 -1
  19. data/lib/datadog/core/configuration/settings.rb +10 -0
  20. data/lib/datadog/core/configuration.rb +9 -1
  21. data/lib/datadog/core/remote/client/capabilities.rb +6 -0
  22. data/lib/datadog/core/remote/client.rb +65 -59
  23. data/lib/datadog/core/telemetry/component.rb +9 -3
  24. data/lib/datadog/core/telemetry/ext.rb +1 -0
  25. data/lib/datadog/di/code_tracker.rb +5 -4
  26. data/lib/datadog/di/component.rb +5 -1
  27. data/lib/datadog/di/contrib/active_record.rb +1 -0
  28. data/lib/datadog/di/init.rb +20 -0
  29. data/lib/datadog/di/instrumenter.rb +81 -11
  30. data/lib/datadog/di/probe.rb +11 -1
  31. data/lib/datadog/di/probe_builder.rb +1 -0
  32. data/lib/datadog/di/probe_manager.rb +4 -1
  33. data/lib/datadog/di/probe_notification_builder.rb +13 -7
  34. data/lib/datadog/di/remote.rb +124 -0
  35. data/lib/datadog/di/serializer.rb +14 -7
  36. data/lib/datadog/di/transport.rb +1 -1
  37. data/lib/datadog/di/utils.rb +7 -0
  38. data/lib/datadog/di.rb +84 -20
  39. data/lib/datadog/profiling/component.rb +4 -16
  40. data/lib/datadog/tracing/configuration/settings.rb +4 -8
  41. data/lib/datadog/tracing/contrib/active_support/cache/redis.rb +16 -4
  42. data/lib/datadog/tracing/contrib/elasticsearch/configuration/settings.rb +4 -0
  43. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +6 -1
  44. data/lib/datadog/version.rb +2 -2
  45. data/lib/datadog.rb +3 -0
  46. metadata +17 -13
  47. data/lib/datadog/appsec/processor/actions.rb +0 -49
@@ -92,34 +92,89 @@ module Datadog
92
92
  cls = symbolize_class_name(probe.type_name)
93
93
  serializer = self.serializer
94
94
  method_name = probe.method_name
95
- target_method = cls.instance_method(method_name)
96
- loc = target_method.source_location
95
+ loc = begin
96
+ cls.instance_method(method_name).source_location
97
+ rescue NameError
98
+ # The target method is not defined.
99
+ # This could be because it will be explicitly defined later
100
+ # (since classes can be reopened in Ruby)
101
+ # or the method is virtual (provided by a method_missing handler).
102
+ # In these cases we do not have a source location for the
103
+ # target method here.
104
+ end
97
105
  rate_limiter = probe.rate_limiter
106
+ settings = self.settings
98
107
 
99
108
  mod = Module.new do
100
- define_method(method_name) do |*args, **kwargs| # steep:ignore
109
+ define_method(method_name) do |*args, **kwargs, &target_block| # steep:ignore
101
110
  if rate_limiter.nil? || rate_limiter.allow?
102
111
  # Arguments may be mutated by the method, therefore
103
112
  # they need to be serialized prior to method invocation.
104
113
  entry_args = if probe.capture_snapshot?
105
- serializer.serialize_args(args, kwargs)
114
+ serializer.serialize_args(args, kwargs,
115
+ depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
116
+ attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count)
106
117
  end
107
118
  rv = nil
119
+ # Under Ruby 2.6 we cannot just call super(*args, **kwargs)
120
+ # for methods defined via method_missing.
108
121
  duration = Benchmark.realtime do # steep:ignore
109
- rv = super(*args, **kwargs)
122
+ rv = if args.any?
123
+ if kwargs.any?
124
+ super(*args, **kwargs, &target_block)
125
+ else
126
+ super(*args, &target_block)
127
+ end
128
+ elsif kwargs.any?
129
+ super(**kwargs, &target_block)
130
+ else
131
+ super(&target_block)
132
+ end
110
133
  end
111
134
  # The method itself is not part of the stack trace because
112
135
  # we are getting the stack trace from outside of the method.
113
136
  # Add the method in manually as the top frame.
114
- method_frame = Location.new(loc.first, loc.last, method_name)
115
- caller_locs = [method_frame] + caller_locations # steep:ignore
137
+ method_frame = if loc
138
+ [Location.new(loc.first, loc.last, method_name)]
139
+ else
140
+ # For virtual and lazily-defined methods, we do not have
141
+ # the original source location here, and they won't be
142
+ # included in the stack trace currently.
143
+ # TODO when begin/end trace points are added for local
144
+ # variable capture in method probes, we should be able
145
+ # to obtain actual method execution location and use
146
+ # that location here.
147
+ []
148
+ end
149
+ caller_locs = method_frame + caller_locations # steep:ignore
116
150
  # TODO capture arguments at exit
117
151
  # & is to stop steep complaints, block is always present here.
118
152
  block&.call(probe: probe, rv: rv, duration: duration, caller_locations: caller_locs,
119
153
  serialized_entry_args: entry_args)
120
154
  rv
121
155
  else
122
- super(*args, **kwargs)
156
+ # stop standard from trying to mess up my code
157
+ _ = 42
158
+
159
+ # The necessity to invoke super in each of these specific
160
+ # ways is very difficult to test.
161
+ # Existing tests, even though I wrote many, still don't
162
+ # cause a failure if I replace all of the below with a
163
+ # simple super(*args, **kwargs, &target_block).
164
+ # But, let's be safe and go through the motions in case
165
+ # there is actually a legitimate need for the breakdown.
166
+ # TODO figure out how to test this properly.
167
+ if args.any?
168
+ if kwargs.any?
169
+ super(*args, **kwargs, &target_block)
170
+ else
171
+ super(*args, &target_block)
172
+ end
173
+ elsif kwargs.any?
174
+ super(**kwargs, &target_block)
175
+ else
176
+ super(&target_block)
177
+ end
123
178
  end
124
179
  end
125
180
  end
@@ -222,12 +277,25 @@ module Datadog
222
277
  # overhead of targeted trace points is minimal, don't worry about
223
278
  # this optimization just yet and create a trace point for each probe.
224
279
 
225
- tp = TracePoint.new(:line) do |tp|
280
+ types = if iseq
281
+ # When targeting trace points we can target the 'end' line of a method.
282
+ # However, by adding the :return trace point we lose diagnostics
283
+ # for lines that contain no executable code (e.g. comments only)
284
+ # and thus cannot actually be instrumented.
285
+ [:line, :return, :b_return]
286
+ else
287
+ [:line]
288
+ end
289
+ tp = TracePoint.new(*types) do |tp|
226
290
  begin
227
291
  # If trace point is not targeted, we must verify that the invocation
228
292
  # is the file & line that we want, because untargeted trace points
229
293
  # are invoked for *each* line of Ruby executed.
230
- if iseq || tp.lineno == probe.line_no && probe.file_matches?(tp.path)
294
+ # TODO find out exactly when the path in trace point is relative.
295
+ # Looks like this is the case when line trace point is not targeted?
296
+ if iseq || tp.lineno == probe.line_no && (
297
+ probe.file == tp.path || probe.file_matches?(tp.path)
298
+ )
231
299
  if rate_limiter.nil? || rate_limiter.allow?
232
300
  # & is to stop steep complaints, block is always present here.
233
301
  block&.call(probe: probe, trace_point: tp, caller_locations: caller_locations)
@@ -240,7 +308,7 @@ module Datadog
240
308
  # TODO test this path
241
309
  end
242
310
  rescue => exc
243
- raise if settings.dynamic_instrumentation.propagate_all_exceptions
311
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
244
312
  logger.warn("Unhandled exception in line trace point: #{exc.class}: #{exc}")
245
313
  telemetry&.report(exc, description: "Unhandled exception in line trace point")
246
314
  # TODO test this path
@@ -266,7 +334,9 @@ module Datadog
266
334
  else
267
335
  tp.enable
268
336
  end
337
+ # TracePoint#enable returns false when it succeeds.
269
338
  end
339
+ true
270
340
  end
271
341
 
272
342
  def unhook_line(probe)
@@ -36,7 +36,9 @@ module Datadog
36
36
 
37
37
  def initialize(id:, type:,
38
38
  file: nil, line_no: nil, type_name: nil, method_name: nil,
39
- template: nil, capture_snapshot: false, max_capture_depth: nil, rate_limit: nil)
39
+ template: nil, capture_snapshot: false, max_capture_depth: nil,
40
+ max_capture_attribute_count: nil,
41
+ rate_limit: nil)
40
42
  # Perform some sanity checks here to detect unexpected attribute
41
43
  # combinations, in order to not do them in subsequent code.
42
44
  unless KNOWN_TYPES.include?(type)
@@ -64,6 +66,7 @@ module Datadog
64
66
  @template = template
65
67
  @capture_snapshot = !!capture_snapshot
66
68
  @max_capture_depth = max_capture_depth
69
+ @max_capture_attribute_count = max_capture_attribute_count
67
70
 
68
71
  # These checks use instance methods that have more complex logic
69
72
  # than checking a single argument value. To avoid duplicating
@@ -91,6 +94,10 @@ module Datadog
91
94
  # the global default will be used.
92
95
  attr_reader :max_capture_depth
93
96
 
97
+ # Configured maximum capture attribute count. Can be nil in which case
98
+ # the global default will be used.
99
+ attr_reader :max_capture_attribute_count
100
+
94
101
  # Rate limit in effect, in invocations per second. Always present.
95
102
  attr_reader :rate_limit
96
103
 
@@ -154,6 +161,9 @@ module Datadog
154
161
  # If file is not an absolute path, the path matches if the file is its suffix,
155
162
  # at a path component boundary.
156
163
  def file_matches?(path)
164
+ if path.nil?
165
+ raise ArgumentError, "Cannot match against a nil path"
166
+ end
157
167
  unless file
158
168
  raise ArgumentError, "Probe does not have a file to match against"
159
169
  end
@@ -37,6 +37,7 @@ module Datadog
37
37
  template: config["template"],
38
38
  capture_snapshot: !!config["captureSnapshot"],
39
39
  max_capture_depth: config["capture"]&.[]("maxReferenceDepth"),
40
+ max_capture_attribute_count: config["capture"]&.[]("maxFieldCount"),
40
41
  rate_limit: config["sampling"]&.[]("snapshotsPerSecond"),
41
42
  )
42
43
  rescue KeyError => exc
@@ -161,7 +161,7 @@ module Datadog
161
161
  # Silence all exceptions?
162
162
  # TODO should we propagate here and rescue upstream?
163
163
  logger.warn("Error removing probe #{probe.id}: #{exc.class}: #{exc}")
164
- telemetry&.report(exc, description: "Error removing probe #{probe.id}")
164
+ telemetry&.report(exc, description: "Error removing probe")
165
165
  end
166
166
  end
167
167
  end
@@ -206,6 +206,9 @@ module Datadog
206
206
  # point, which is invoked for each required or loaded file
207
207
  # (and also for eval'd code, but those invocations are filtered out).
208
208
  def install_pending_line_probes(path)
209
+ if path.nil?
210
+ raise ArgumentError, "path must not be nil"
211
+ end
209
212
  @lock.synchronize do
210
213
  @pending_probes.values.each do |probe|
211
214
  if probe.line?
@@ -65,14 +65,18 @@ module Datadog
65
65
  arguments: if serialized_entry_args
66
66
  serialized_entry_args
67
67
  else
68
- (args || kwargs) && serializer.serialize_args(args, kwargs)
68
+ (args || kwargs) && serializer.serialize_args(args, kwargs,
69
+ depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
70
+ attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count)
69
71
  end,
70
72
  throwable: nil,
71
73
  # standard:enable all
72
74
  },
73
75
  return: {
74
76
  arguments: {
75
- "@return": serializer.serialize_value(rv),
77
+ "@return": serializer.serialize_value(rv,
78
+ depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
79
+ attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count),
76
80
  },
77
81
  throwable: nil,
78
82
  },
@@ -80,7 +84,9 @@ module Datadog
80
84
  elsif probe.line?
81
85
  {
82
86
  lines: snapshot && {
83
- probe.line_no => {locals: serializer.serialize_vars(snapshot)},
87
+ probe.line_no => {locals: serializer.serialize_vars(snapshot,
88
+ depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
89
+ attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count,)},
84
90
  },
85
91
  }
86
92
  end
@@ -121,7 +127,7 @@ module Datadog
121
127
  },
122
128
  # In python tracer duration is under debugger.snapshot,
123
129
  # but UI appears to expect it here at top level.
124
- duration: duration ? (duration * 10**9).to_i : nil,
130
+ duration: duration ? (duration * 10**9).to_i : 0,
125
131
  host: nil,
126
132
  logger: {
127
133
  name: probe.file,
@@ -135,11 +141,11 @@ module Datadog
135
141
  version: 2,
136
142
  },
137
143
  # TODO add tests that the trace/span id is correctly propagated
138
- "dd.trace_id": Datadog::Tracing.active_trace&.id,
139
- "dd.span_id": Datadog::Tracing.active_span&.id,
144
+ "dd.trace_id": Datadog::Tracing.active_trace&.id&.to_s,
145
+ "dd.span_id": Datadog::Tracing.active_span&.id&.to_s,
140
146
  ddsource: 'dd_debugger',
141
147
  message: probe.template && evaluate_template(probe.template,
142
- duration: duration ? duration * 1000 : nil),
148
+ duration: duration ? duration * 1000 : 0),
143
149
  timestamp: timestamp,
144
150
  }
145
151
  end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ # Provides an interface expected by the core Remote subsystem to
6
+ # receive DI-specific remote configuration.
7
+ #
8
+ # In order to apply (i.e., act on) the configuration, we need the
9
+ # state stored under DI Component. Thus, this module forwards actual
10
+ # configuration application to the ProbeManager associated with the
11
+ # global DI Component.
12
+ #
13
+ # @api private
14
+ module Remote
15
+ class ReadError < StandardError; end
16
+
17
+ class << self
18
+ PRODUCT = 'LIVE_DEBUGGING'
19
+
20
+ def products
21
+ [PRODUCT]
22
+ end
23
+
24
+ def capabilities
25
+ []
26
+ end
27
+
28
+ def receivers(telemetry)
29
+ receiver do |repository, _changes|
30
+ # DEV: Filter our by product. Given it will be very common
31
+ # DEV: we can filter this out before we receive the data in this method.
32
+ # DEV: Apply this refactor to AppSec as well if implemented.
33
+
34
+ component = DI.component
35
+ # We should always have a non-nil DI component here, because we
36
+ # only add DI product to remote config request if DI is enabled.
37
+ # Ideally, we should be injected with the DI component here
38
+ # rather than having to retrieve it from global state.
39
+ # If the component is nil for some reason, we also don't have a
40
+ # logger instance to report the issue.
41
+ if component
42
+
43
+ probe_manager = component.probe_manager
44
+
45
+ current_probe_ids = {}
46
+ repository.contents.each do |content|
47
+ case content.path.product
48
+ when PRODUCT
49
+ begin
50
+ probe_spec = parse_content(content)
51
+ probe = ProbeBuilder.build_from_remote_config(probe_spec)
52
+ payload = component.probe_notification_builder.build_received(probe)
53
+ component.probe_notifier_worker.add_status(payload)
54
+ component.logger.info("Received probe from RC: #{probe.type} #{probe.location}")
55
+
56
+ begin
57
+ # TODO test exception capture
58
+ probe_manager.add_probe(probe)
59
+ content.applied
60
+ rescue => exc
61
+ raise if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
62
+
63
+ component.logger.warn("Unhandled exception adding probe in DI remote receiver: #{exc.class}: #{exc}")
64
+ component.telemetry&.report(exc, description: "Unhandled exception adding probe in DI remote receiver")
65
+
66
+ # If a probe fails to install, we will mark the content
67
+ # as errored. On subsequent remote configuration application
68
+ # attemps, probe manager will raise the "previously errored"
69
+ # exception and we'll rescue it here, again marking the
70
+ # content as errored but with a somewhat different exception
71
+ # message.
72
+ # TODO stack trace must be redacted or not sent at all
73
+ content.errored("Error applying dynamic instrumentation configuration: #{exc.class.name} #{exc.message}: #{Array(exc.backtrace).join("\n")}")
74
+ end
75
+
76
+ # Important: even if processing fails for this probe config,
77
+ # we need to note it as being current so that we do not
78
+ # try to remove instrumentation that is still supposed to be
79
+ # active.
80
+ current_probe_ids[probe_spec.fetch('id')] = true
81
+ rescue => exc
82
+ raise if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
83
+
84
+ component.logger.warn("Unhandled exception handling probe in DI remote receiver: #{exc.class}: #{exc}")
85
+ component.telemetry&.report(exc, description: "Unhandled exception handling probe in DI remote receiver")
86
+
87
+ content.errored("Error applying dynamic instrumentation configuration: #{exc.class.name} #{exc.message}: #{Array(exc.backtrace).join("\n")}")
88
+ end
89
+ end
90
+ end
91
+
92
+ begin
93
+ # TODO test exception capture
94
+ probe_manager.remove_other_probes(current_probe_ids.keys)
95
+ rescue => exc
96
+ raise if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
97
+
98
+ component.logger.warn("Unhandled exception removing probes in DI remote receiver: #{exc.class}: #{exc}")
99
+ component.telemetry&.report(exc, description: "Unhandled exception removing probes in DI remote receiver")
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ def receiver(products = [PRODUCT], &block)
106
+ matcher = Core::Remote::Dispatcher::Matcher::Product.new(products)
107
+ [Core::Remote::Dispatcher::Receiver.new(matcher, &block)]
108
+ end
109
+
110
+ private
111
+
112
+ def parse_content(content)
113
+ data = content.data.read
114
+
115
+ content.data.rewind
116
+
117
+ raise ReadError, 'EOF reached' if data.nil?
118
+
119
+ JSON.parse(data)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -82,7 +82,9 @@ module Datadog
82
82
  # between positional and keyword arguments. We convert positional
83
83
  # arguments to keyword arguments ("arg1", "arg2", ...) and ensure
84
84
  # the positional arguments are listed first.
85
- def serialize_args(args, kwargs)
85
+ def serialize_args(args, kwargs,
86
+ depth: settings.dynamic_instrumentation.max_capture_depth,
87
+ attribute_count: settings.dynamic_instrumentation.max_capture_attribute_count)
86
88
  counter = 0
87
89
  combined = args.each_with_object({}) do |value, c|
88
90
  counter += 1
@@ -90,16 +92,18 @@ module Datadog
90
92
  # kwargs when they are merged below.
91
93
  c[:"arg#{counter}"] = value
92
94
  end.update(kwargs)
93
- serialize_vars(combined)
95
+ serialize_vars(combined, depth: depth, attribute_count: attribute_count)
94
96
  end
95
97
 
96
98
  # Serializes variables captured by a line probe.
97
99
  #
98
100
  # These are normally local variables that exist on a particular line
99
101
  # of executed code.
100
- def serialize_vars(vars)
102
+ def serialize_vars(vars,
103
+ depth: settings.dynamic_instrumentation.max_capture_depth,
104
+ attribute_count: settings.dynamic_instrumentation.max_capture_attribute_count)
101
105
  vars.each_with_object({}) do |(k, v), agg|
102
- agg[k] = serialize_value(v, name: k)
106
+ agg[k] = serialize_value(v, name: k, depth: depth, attribute_count: attribute_count)
103
107
  end
104
108
  end
105
109
 
@@ -115,7 +119,11 @@ module Datadog
115
119
  # (integers, strings, arrays, hashes).
116
120
  #
117
121
  # Respects string length, collection size and traversal depth limits.
118
- def serialize_value(value, name: nil, depth: settings.dynamic_instrumentation.max_capture_depth, type: nil)
122
+ def serialize_value(value, name: nil,
123
+ depth: settings.dynamic_instrumentation.max_capture_depth,
124
+ attribute_count: nil,
125
+ type: nil)
126
+ attribute_count ||= settings.dynamic_instrumentation.max_capture_attribute_count
119
127
  cls = type || value.class
120
128
  begin
121
129
  if redactor.redact_type?(value)
@@ -203,7 +211,6 @@ module Datadog
203
211
  serialized.update(notCapturedReason: "depth")
204
212
  else
205
213
  fields = {}
206
- max = settings.dynamic_instrumentation.max_capture_attribute_count
207
214
  cur = 0
208
215
 
209
216
  # MRI and JRuby 9.4.5+ preserve instance variable definition
@@ -229,7 +236,7 @@ module Datadog
229
236
  ivars = value.instance_variables
230
237
 
231
238
  ivars.each do |ivar|
232
- if cur >= max
239
+ if cur >= attribute_count
233
240
  serialized.update(notCapturedReason: "fieldCount", fields: fields)
234
241
  break
235
242
  end
@@ -45,7 +45,7 @@ module Datadog
45
45
 
46
46
  def send_input(payload)
47
47
  send_request('Probe snapshot submission',
48
- path: INPUT_PATH, body: payload.to_s,
48
+ path: INPUT_PATH, body: payload.to_json,
49
49
  headers: {'content-type' => 'application/json'},)
50
50
  end
51
51
 
@@ -12,6 +12,13 @@ module Datadog
12
12
  # If suffix is not an absolute path, the path matches if its suffix is
13
13
  # the provided suffix, at a path component boundary.
14
14
  module_function def path_matches_suffix?(path, suffix)
15
+ if path.nil?
16
+ raise ArgumentError, "nil path passed"
17
+ end
18
+ if suffix.nil?
19
+ raise ArgumentError, "nil suffix passed"
20
+ end
21
+
15
22
  if suffix.start_with?('/')
16
23
  path == suffix
17
24
  else
data/lib/datadog/di.rb CHANGED
@@ -12,6 +12,7 @@ require_relative 'di/probe_manager'
12
12
  require_relative 'di/probe_notification_builder'
13
13
  require_relative 'di/probe_notifier_worker'
14
14
  require_relative 'di/redactor'
15
+ require_relative 'di/remote'
15
16
  require_relative 'di/serializer'
16
17
  require_relative 'di/transport'
17
18
  require_relative 'di/utils'
@@ -25,6 +26,9 @@ if defined?(ActiveRecord::Base)
25
26
  # and AR should be loaded before any application code is loaded, being
26
27
  # part of Rails, therefore for now we should be OK to just require the
27
28
  # AR integration from here.
29
+ #
30
+ # TODO this require might need to be delayed via Rails post-initialization
31
+ # logic?
28
32
  require_relative 'di/contrib/active_record'
29
33
  end
30
34
 
@@ -42,6 +46,8 @@ module Datadog
42
46
  # Expose DI to global shared objects
43
47
  Extensions.activate!
44
48
 
49
+ LOCK = Mutex.new
50
+
45
51
  class << self
46
52
  attr_reader :code_tracker
47
53
 
@@ -58,6 +64,32 @@ module Datadog
58
64
  (@code_tracker ||= CodeTracker.new).start
59
65
  end
60
66
 
67
+ # Activates code tracking if possible.
68
+ #
69
+ # This method does nothing if invoked in an environment that does not
70
+ # implement required trace points for code tracking (MRI Ruby < 2.6,
71
+ # JRuby) and rescues any exceptions that may be raised by downstream
72
+ # DI code.
73
+ def activate_tracking
74
+ # :script_compiled trace point was added in Ruby 2.6.
75
+ return unless RUBY_VERSION >= '2.6'
76
+
77
+ begin
78
+ # Activate code tracking by default because line trace points will not work
79
+ # without it.
80
+ Datadog::DI.activate_tracking!
81
+ rescue => exc
82
+ if defined?(Datadog.logger)
83
+ Datadog.logger.warn("Failed to activate code tracking for DI: #{exc.class}: #{exc}")
84
+ else
85
+ # We do not have Datadog logger potentially because DI code tracker is
86
+ # being loaded early in application boot process and the rest of datadog
87
+ # wasn't loaded yet. Output to standard error.
88
+ warn("Failed to activate code tracking for DI: #{exc.class}: #{exc}")
89
+ end
90
+ end
91
+ end
92
+
61
93
  # Deactivates code tracking. In normal usage of DI this method should
62
94
  # never be called, however it is used by DI's test suite to reset
63
95
  # state for individual tests.
@@ -76,30 +108,62 @@ module Datadog
76
108
  code_tracker&.active? || false
77
109
  end
78
110
 
111
+ # This method is called from DI Remote handler to issue DI operations
112
+ # to the probe manager (add or remove probes).
113
+ #
114
+ # When DI Remote is executing, Datadog.components should be initialized
115
+ # and we should be able to reference it to get to the DI component.
116
+ #
117
+ # Given that we need the current_component anyway for code tracker,
118
+ # perhaps we should delete the +component+ method and just use
119
+ # +current_component+ in all cases.
79
120
  def component
80
- # TODO uncomment when remote is merged
81
- #Datadog.send(:components).dynamic_instrumentation
121
+ Datadog.send(:components).dynamic_instrumentation
122
+ end
123
+
124
+ # DI code tracker is instantiated globally before the regular set of
125
+ # components is created, but the code tracker needs to call out to the
126
+ # "current" DI component to perform instrumentation when application
127
+ # code is loaded. Because this call may happen prior to Datadog
128
+ # components having been initialized, we maintain the "current component"
129
+ # which contains a reference to the most recently instantiated
130
+ # DI::Component. This way, if a DI component hasn't been instantiated,
131
+ # we do not try to reference Datadog.components.
132
+ def current_component
133
+ LOCK.synchronize do
134
+ @current_components&.last
135
+ end
136
+ end
137
+
138
+ # To avoid potential races with DI::Component being added and removed,
139
+ # we maintain a list of the components. Normally the list should contain
140
+ # either zero or one component depending on whether DI is enabled in
141
+ # Datadog configuration. However, if a new instance of DI::Component
142
+ # is created while the previous instance is still running, we are
143
+ # guaranteed to not end up with no component when one is running.
144
+ def add_current_component(component)
145
+ LOCK.synchronize do
146
+ @current_components ||= []
147
+ @current_components << component
148
+ end
149
+ end
150
+
151
+ def remove_current_component(component)
152
+ LOCK.synchronize do
153
+ @current_components&.delete(component)
154
+ end
82
155
  end
83
156
  end
84
157
  end
85
158
  end
86
159
 
87
- =begin not yet enabled
88
- # :script_compiled trace point was added in Ruby 2.6.
89
- if RUBY_VERSION >= '2.6'
90
- begin
91
- # Activate code tracking by default because line trace points will not work
92
- # without it.
93
- Datadog::DI.activate_tracking!
94
- rescue => exc
95
- if defined?(Datadog.logger)
96
- Datadog.logger.warn("Failed to activate code tracking for DI: #{exc.class}: #{exc}")
97
- else
98
- # We do not have Datadog logger potentially because DI code tracker is
99
- # being loaded early in application boot process and the rest of datadog
100
- # wasn't loaded yet. Output to standard error.
101
- warn("Failed to activate code tracking for DI: #{exc.class}: #{exc}")
102
- end
103
- end
160
+ if %w(1 true).include?(ENV['DD_DYNAMIC_INSTRUMENTATION_ENABLED']) # steep:ignore
161
+ # For initial release of Dynamic Instrumentation, activate code tracking
162
+ # only if DI is explicitly requested in the environment.
163
+ # Code tracking is required for line probes to work; see the comments
164
+ # above for the implementation of the method.
165
+ #
166
+ # If DI is enabled programmatically, the application can (and must,
167
+ # for line probes to work) activate tracking in an initializer.
168
+ Datadog::DI.activate_tracking
104
169
  end
105
- =end
@@ -207,28 +207,16 @@ module Datadog
207
207
 
208
208
  return false unless heap_profiling_enabled
209
209
 
210
- if RUBY_VERSION.start_with?("2.") && RUBY_VERSION < "2.7"
210
+ if RUBY_VERSION < "3.1"
211
211
  Datadog.logger.warn(
212
- "Heap profiling currently relies on features introduced in Ruby 2.7 and will be forcibly disabled. " \
213
- "Please upgrade to Ruby >= 2.7 in order to use this feature."
212
+ "Current Ruby version (#{RUBY_VERSION}) cannot support heap profiling due to VM limitations. " \
213
+ "Please upgrade to Ruby >= 3.1 in order to use this feature. Heap profiling has been disabled."
214
214
  )
215
215
  return false
216
216
  end
217
217
 
218
- if RUBY_VERSION < "3.1"
219
- Datadog.logger.debug(
220
- "Current Ruby version (#{RUBY_VERSION}) supports forced object recycling which has a bug that the " \
221
- "heap profiler is forced to work around to remain accurate. This workaround requires force-setting " \
222
- "the SEEN_OBJ_ID flag on objects that should have it but don't. Full details can be found in " \
223
- "https://github.com/DataDog/dd-trace-rb/pull/3360. This workaround should be safe but can be " \
224
- "bypassed by disabling the heap profiler or upgrading to Ruby >= 3.1 where forced object recycling " \
225
- "was completely removed (https://bugs.ruby-lang.org/issues/18290)."
226
- )
227
- end
228
-
229
218
  unless allocation_profiling_enabled
230
- raise ArgumentError,
231
- "Heap profiling requires allocation profiling to be enabled"
219
+ raise ArgumentError, "Heap profiling requires allocation profiling to be enabled"
232
220
  end
233
221
 
234
222
  Datadog.logger.warn(