datadog 2.7.1 → 2.8.0

Sign up to get free protection for your applications and to get access to all the features.
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(