datadog 2.7.0 → 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 (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