datadog 2.7.0 → 2.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -1
  3. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +47 -17
  4. data/ext/datadog_profiling_native_extension/extconf.rb +3 -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 +3 -9
  7. data/ext/datadog_profiling_native_extension/profiling.c +6 -0
  8. data/ext/datadog_profiling_native_extension/ruby_helpers.c +14 -4
  9. data/ext/datadog_profiling_native_extension/ruby_helpers.h +4 -0
  10. data/ext/datadog_profiling_native_extension/stack_recorder.c +0 -34
  11. data/ext/libdatadog_extconf_helpers.rb +1 -1
  12. data/lib/datadog/appsec/component.rb +1 -8
  13. data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +73 -0
  14. data/lib/datadog/appsec/contrib/active_record/integration.rb +41 -0
  15. data/lib/datadog/appsec/contrib/active_record/patcher.rb +53 -0
  16. data/lib/datadog/appsec/event.rb +1 -1
  17. data/lib/datadog/appsec/processor/context.rb +2 -2
  18. data/lib/datadog/appsec/remote.rb +1 -3
  19. data/lib/datadog/appsec/response.rb +7 -11
  20. data/lib/datadog/appsec.rb +3 -2
  21. data/lib/datadog/core/configuration/components.rb +17 -1
  22. data/lib/datadog/core/configuration/settings.rb +10 -0
  23. data/lib/datadog/core/configuration.rb +9 -1
  24. data/lib/datadog/core/remote/client/capabilities.rb +6 -0
  25. data/lib/datadog/core/remote/client.rb +65 -59
  26. data/lib/datadog/core/telemetry/component.rb +9 -3
  27. data/lib/datadog/core/telemetry/ext.rb +1 -0
  28. data/lib/datadog/di/code_tracker.rb +5 -4
  29. data/lib/datadog/di/component.rb +5 -1
  30. data/lib/datadog/di/contrib/active_record.rb +1 -0
  31. data/lib/datadog/di/init.rb +20 -0
  32. data/lib/datadog/di/instrumenter.rb +81 -11
  33. data/lib/datadog/di/probe.rb +11 -1
  34. data/lib/datadog/di/probe_builder.rb +1 -0
  35. data/lib/datadog/di/probe_manager.rb +4 -1
  36. data/lib/datadog/di/probe_notification_builder.rb +13 -7
  37. data/lib/datadog/di/remote.rb +124 -0
  38. data/lib/datadog/di/serializer.rb +14 -7
  39. data/lib/datadog/di/transport.rb +2 -1
  40. data/lib/datadog/di/utils.rb +7 -0
  41. data/lib/datadog/di.rb +84 -20
  42. data/lib/datadog/profiling/component.rb +4 -16
  43. data/lib/datadog/tracing/configuration/settings.rb +4 -8
  44. data/lib/datadog/tracing/contrib/active_support/cache/redis.rb +16 -4
  45. data/lib/datadog/tracing/contrib/elasticsearch/configuration/settings.rb +4 -0
  46. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +6 -1
  47. data/lib/datadog/tracing/tracer.rb +1 -1
  48. data/lib/datadog/version.rb +1 -1
  49. data/lib/datadog.rb +3 -0
  50. metadata +17 -13
  51. data/lib/datadog/appsec/processor/actions.rb +0 -49
@@ -24,7 +24,9 @@ module Datadog
24
24
 
25
25
  return unless environment_supported?(settings)
26
26
 
27
- new(settings, agent_settings, Datadog.logger, code_tracker: DI.code_tracker, telemetry: telemetry)
27
+ new(settings, agent_settings, Datadog.logger, code_tracker: DI.code_tracker, telemetry: telemetry).tap do |component|
28
+ DI.add_current_component(component)
29
+ end
28
30
  end
29
31
 
30
32
  def build!(settings, agent_settings, telemetry: nil)
@@ -99,6 +101,8 @@ module Datadog
99
101
  # was replaced by a new instance, the new instance of it wouldn't have
100
102
  # any of the already loaded code tracked.
101
103
  def shutdown!(replacement = nil)
104
+ DI.remove_current_component(self)
105
+
102
106
  probe_manager.clear_hooks
103
107
  probe_manager.close
104
108
  probe_notifier_worker.stop
@@ -5,6 +5,7 @@ Datadog::DI::Serializer.register(condition: lambda { |value| ActiveRecord::Base
5
5
  # steep:ignore:start
6
6
  value_to_serialize = {
7
7
  attributes: value.attributes,
8
+ new_record: value.new_record?,
8
9
  }
9
10
  serializer.serialize_value(value_to_serialize, depth: depth ? depth - 1 : nil, type: value.class)
10
11
  # steep:ignore:end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Require 'datadog/di/init' early in the application boot process to
4
+ # enable dynamic instrumentation for third-party libraries used by the
5
+ # application.
6
+
7
+ require_relative '../tracing'
8
+ require_relative '../tracing/contrib'
9
+ require_relative '../di'
10
+
11
+ # Code tracking is required for line probes to work; see the comments
12
+ # on the activate_tracking methods in di.rb for further details.
13
+ #
14
+ # Unlike di.rb which conditionally activates tracking only if the
15
+ # DD_DYNAMIC_INSTRUMENTATION_ENABLED environment variable is set, this file
16
+ # always activates tracking. This is because this file is explicitly loaded
17
+ # by customer applications for the purpose of enabling code tracking
18
+ # early in application boot process (i.e., before datadog library itself
19
+ # is loaded).
20
+ Datadog::DI.activate_tracking
@@ -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
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'ostruct'
3
4
  require_relative 'error'
4
5
 
5
6
  module Datadog
@@ -44,7 +45,7 @@ module Datadog
44
45
 
45
46
  def send_input(payload)
46
47
  send_request('Probe snapshot submission',
47
- path: INPUT_PATH, body: payload.to_s,
48
+ path: INPUT_PATH, body: payload.to_json,
48
49
  headers: {'content-type' => 'application/json'},)
49
50
  end
50
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