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