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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -1
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +47 -17
- data/ext/datadog_profiling_native_extension/extconf.rb +0 -8
- data/ext/datadog_profiling_native_extension/heap_recorder.c +11 -89
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +1 -1
- data/ext/datadog_profiling_native_extension/stack_recorder.c +0 -34
- data/ext/libdatadog_extconf_helpers.rb +1 -1
- data/lib/datadog/appsec/component.rb +1 -8
- data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +73 -0
- data/lib/datadog/appsec/contrib/active_record/integration.rb +41 -0
- data/lib/datadog/appsec/contrib/active_record/patcher.rb +53 -0
- data/lib/datadog/appsec/event.rb +1 -1
- data/lib/datadog/appsec/processor/context.rb +2 -2
- data/lib/datadog/appsec/remote.rb +1 -3
- data/lib/datadog/appsec/response.rb +7 -11
- data/lib/datadog/appsec.rb +3 -2
- data/lib/datadog/core/configuration/components.rb +17 -1
- data/lib/datadog/core/configuration/settings.rb +10 -0
- data/lib/datadog/core/configuration.rb +9 -1
- data/lib/datadog/core/remote/client/capabilities.rb +6 -0
- data/lib/datadog/core/remote/client.rb +65 -59
- data/lib/datadog/core/telemetry/component.rb +9 -3
- data/lib/datadog/core/telemetry/ext.rb +1 -0
- data/lib/datadog/di/code_tracker.rb +5 -4
- data/lib/datadog/di/component.rb +5 -1
- data/lib/datadog/di/contrib/active_record.rb +1 -0
- data/lib/datadog/di/init.rb +20 -0
- data/lib/datadog/di/instrumenter.rb +81 -11
- data/lib/datadog/di/probe.rb +11 -1
- data/lib/datadog/di/probe_builder.rb +1 -0
- data/lib/datadog/di/probe_manager.rb +4 -1
- data/lib/datadog/di/probe_notification_builder.rb +13 -7
- data/lib/datadog/di/remote.rb +124 -0
- data/lib/datadog/di/serializer.rb +14 -7
- data/lib/datadog/di/transport.rb +1 -1
- data/lib/datadog/di/utils.rb +7 -0
- data/lib/datadog/di.rb +84 -20
- data/lib/datadog/profiling/component.rb +4 -16
- data/lib/datadog/tracing/configuration/settings.rb +4 -8
- data/lib/datadog/tracing/contrib/active_support/cache/redis.rb +16 -4
- data/lib/datadog/tracing/contrib/elasticsearch/configuration/settings.rb +4 -0
- data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +6 -1
- data/lib/datadog/version.rb +2 -2
- data/lib/datadog.rb +3 -0
- metadata +17 -13
- 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
|
-
|
96
|
-
|
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 =
|
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 =
|
115
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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)
|
data/lib/datadog/di/probe.rb
CHANGED
@@ -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,
|
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
|
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 :
|
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 :
|
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,
|
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 >=
|
239
|
+
if cur >= attribute_count
|
233
240
|
serialized.update(notCapturedReason: "fieldCount", fields: fields)
|
234
241
|
break
|
235
242
|
end
|
data/lib/datadog/di/transport.rb
CHANGED
data/lib/datadog/di/utils.rb
CHANGED
@@ -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
|
-
|
81
|
-
|
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
|
-
|
88
|
-
#
|
89
|
-
if
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
210
|
+
if RUBY_VERSION < "3.1"
|
211
211
|
Datadog.logger.warn(
|
212
|
-
"
|
213
|
-
"Please upgrade to Ruby >=
|
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(
|