datadog 2.4.0 → 2.6.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 +40 -1
- data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +3 -3
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +57 -18
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +93 -106
- data/ext/datadog_profiling_native_extension/collectors_thread_context.h +8 -2
- data/ext/datadog_profiling_native_extension/extconf.rb +8 -8
- data/ext/datadog_profiling_native_extension/heap_recorder.c +174 -28
- data/ext/datadog_profiling_native_extension/heap_recorder.h +11 -0
- data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +1 -1
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +1 -1
- data/ext/datadog_profiling_native_extension/ruby_helpers.c +14 -11
- data/ext/datadog_profiling_native_extension/stack_recorder.c +58 -22
- data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -0
- data/ext/libdatadog_api/crashtracker.c +3 -5
- data/ext/libdatadog_extconf_helpers.rb +1 -1
- data/lib/datadog/appsec/configuration/settings.rb +8 -0
- data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +1 -5
- data/lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb +7 -20
- data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +9 -15
- data/lib/datadog/appsec/contrib/rack/reactive/request.rb +6 -18
- data/lib/datadog/appsec/contrib/rack/reactive/request_body.rb +7 -20
- data/lib/datadog/appsec/contrib/rack/reactive/response.rb +5 -18
- data/lib/datadog/appsec/contrib/rack/request_middleware.rb +3 -1
- data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +3 -5
- data/lib/datadog/appsec/contrib/rails/reactive/action.rb +5 -18
- data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +6 -10
- data/lib/datadog/appsec/contrib/sinatra/reactive/routed.rb +7 -20
- data/lib/datadog/appsec/event.rb +24 -0
- data/lib/datadog/appsec/ext.rb +4 -0
- data/lib/datadog/appsec/monitor/gateway/watcher.rb +3 -5
- data/lib/datadog/appsec/monitor/reactive/set_user.rb +7 -20
- data/lib/datadog/appsec/processor/context.rb +107 -0
- data/lib/datadog/appsec/processor.rb +7 -71
- data/lib/datadog/appsec/scope.rb +1 -4
- data/lib/datadog/appsec/utils/trace_operation.rb +15 -0
- data/lib/datadog/appsec/utils.rb +2 -0
- data/lib/datadog/appsec.rb +1 -0
- data/lib/datadog/core/configuration/agent_settings_resolver.rb +26 -25
- data/lib/datadog/core/configuration/settings.rb +12 -0
- data/lib/datadog/core/configuration.rb +1 -3
- data/lib/datadog/core/crashtracking/component.rb +8 -5
- data/lib/datadog/core/environment/yjit.rb +5 -0
- data/lib/datadog/core/remote/transport/http.rb +5 -0
- data/lib/datadog/core/remote/worker.rb +1 -1
- data/lib/datadog/core/runtime/ext.rb +1 -0
- data/lib/datadog/core/runtime/metrics.rb +4 -0
- data/lib/datadog/core/semaphore.rb +35 -0
- data/lib/datadog/core/telemetry/logging.rb +10 -10
- data/lib/datadog/core/transport/ext.rb +1 -0
- data/lib/datadog/core/workers/async.rb +1 -1
- data/lib/datadog/di/code_tracker.rb +11 -13
- data/lib/datadog/di/instrumenter.rb +301 -0
- data/lib/datadog/di/probe.rb +29 -0
- data/lib/datadog/di/probe_builder.rb +7 -1
- data/lib/datadog/di/probe_notification_builder.rb +207 -0
- data/lib/datadog/di/probe_notifier_worker.rb +244 -0
- data/lib/datadog/di/serializer.rb +23 -1
- data/lib/datadog/di/transport.rb +67 -0
- data/lib/datadog/di/utils.rb +39 -0
- data/lib/datadog/di.rb +43 -0
- data/lib/datadog/profiling/collectors/thread_context.rb +9 -11
- data/lib/datadog/profiling/component.rb +1 -0
- data/lib/datadog/profiling/stack_recorder.rb +37 -9
- data/lib/datadog/tracing/component.rb +13 -0
- data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +4 -0
- data/lib/datadog/tracing/contrib/excon/middleware.rb +3 -0
- data/lib/datadog/tracing/contrib/faraday/middleware.rb +3 -0
- data/lib/datadog/tracing/contrib/grape/endpoint.rb +5 -2
- data/lib/datadog/tracing/contrib/http/circuit_breaker.rb +9 -0
- data/lib/datadog/tracing/contrib/http/instrumentation.rb +4 -0
- data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +4 -0
- data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +4 -0
- data/lib/datadog/tracing/contrib/rails/runner.rb +1 -1
- data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +3 -0
- data/lib/datadog/tracing/sampling/rule_sampler.rb +6 -4
- data/lib/datadog/tracing/tracer.rb +15 -10
- data/lib/datadog/tracing/transport/http.rb +4 -0
- data/lib/datadog/tracing/workers.rb +1 -1
- data/lib/datadog/tracing/writer.rb +26 -28
- data/lib/datadog/version.rb +1 -1
- metadata +22 -14
data/lib/datadog/di/probe.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "error"
|
4
|
+
require_relative "utils"
|
4
5
|
require_relative "../core/rate_limiter"
|
5
6
|
|
6
7
|
module Datadog
|
@@ -31,11 +32,17 @@ module Datadog
|
|
31
32
|
#
|
32
33
|
# @api private
|
33
34
|
class Probe
|
35
|
+
KNOWN_TYPES = %i[log].freeze
|
36
|
+
|
34
37
|
def initialize(id:, type:,
|
35
38
|
file: nil, line_no: nil, type_name: nil, method_name: nil,
|
36
39
|
template: nil, capture_snapshot: false, max_capture_depth: nil, rate_limit: nil)
|
37
40
|
# Perform some sanity checks here to detect unexpected attribute
|
38
41
|
# combinations, in order to not do them in subsequent code.
|
42
|
+
unless KNOWN_TYPES.include?(type)
|
43
|
+
raise ArgumentError, "Unknown probe type: #{type}"
|
44
|
+
end
|
45
|
+
|
39
46
|
if line_no && method_name
|
40
47
|
raise ArgumentError, "Probe contains both line number and method name: #{id}"
|
41
48
|
end
|
@@ -128,6 +135,28 @@ module Datadog
|
|
128
135
|
raise NotImplementedError
|
129
136
|
end
|
130
137
|
end
|
138
|
+
|
139
|
+
# Returns whether the provided +path+ matches the user-designated
|
140
|
+
# file (of a line probe).
|
141
|
+
#
|
142
|
+
# If file is an absolute path (i.e., it starts with a slash), the file
|
143
|
+
# must be identical to path to match.
|
144
|
+
#
|
145
|
+
# If file is not an absolute path, the path matches if the file is its suffix,
|
146
|
+
# at a path component boundary.
|
147
|
+
def file_matches?(path)
|
148
|
+
unless file
|
149
|
+
raise ArgumentError, "Probe does not have a file to match against"
|
150
|
+
end
|
151
|
+
Utils.path_matches_suffix?(path, file)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Instrumentation module for method probes.
|
155
|
+
attr_accessor :instrumentation_module
|
156
|
+
|
157
|
+
# Line trace point for line probes. Normally this would be a targeted
|
158
|
+
# trace point.
|
159
|
+
attr_accessor :instrumentation_trace_point
|
131
160
|
end
|
132
161
|
end
|
133
162
|
end
|
@@ -17,11 +17,17 @@ module Datadog
|
|
17
17
|
#
|
18
18
|
# @api private
|
19
19
|
module ProbeBuilder
|
20
|
+
PROBE_TYPES = {
|
21
|
+
'LOG_PROBE' => :log,
|
22
|
+
}.freeze
|
23
|
+
|
20
24
|
module_function def build_from_remote_config(config)
|
21
25
|
# The validations here are not yet comprehensive.
|
26
|
+
type = config.fetch('type')
|
27
|
+
type_symbol = PROBE_TYPES[type] or raise ArgumentError, "Unrecognized probe type: #{type}"
|
22
28
|
Probe.new(
|
23
29
|
id: config.fetch("id"),
|
24
|
-
type:
|
30
|
+
type: type_symbol,
|
25
31
|
file: config["where"]&.[]("sourceFile"),
|
26
32
|
# Sometimes lines are sometimes received as an array of nil
|
27
33
|
# for some reason.
|
@@ -0,0 +1,207 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
module DI
|
5
|
+
# Builds probe status notification and snapshot payloads.
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class ProbeNotificationBuilder
|
9
|
+
def initialize(settings, serializer)
|
10
|
+
@settings = settings
|
11
|
+
@serializer = serializer
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :settings
|
15
|
+
attr_reader :serializer
|
16
|
+
|
17
|
+
def build_received(probe)
|
18
|
+
build_status(probe,
|
19
|
+
message: "Probe #{probe.id} has been received correctly",
|
20
|
+
status: 'RECEIVED',)
|
21
|
+
end
|
22
|
+
|
23
|
+
def build_installed(probe)
|
24
|
+
build_status(probe,
|
25
|
+
message: "Probe #{probe.id} has been instrumented correctly",
|
26
|
+
status: 'INSTALLED',)
|
27
|
+
end
|
28
|
+
|
29
|
+
def build_emitting(probe)
|
30
|
+
build_status(probe,
|
31
|
+
message: "Probe #{probe.id} is emitting",
|
32
|
+
status: 'EMITTING',)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Duration is in seconds.
|
36
|
+
def build_executed(probe,
|
37
|
+
trace_point: nil, rv: nil, duration: nil, caller_locations: nil,
|
38
|
+
args: nil, kwargs: nil, serialized_entry_args: nil)
|
39
|
+
snapshot = if probe.line? && probe.capture_snapshot?
|
40
|
+
if trace_point.nil?
|
41
|
+
raise "Cannot create snapshot because there is no trace point"
|
42
|
+
end
|
43
|
+
get_local_variables(trace_point)
|
44
|
+
end
|
45
|
+
# TODO check how many stack frames we should be keeping/sending,
|
46
|
+
# this should be all frames for enriched probes and no frames for
|
47
|
+
# non-enriched probes?
|
48
|
+
build_snapshot(probe, rv: rv, snapshot: snapshot,
|
49
|
+
duration: duration, caller_locations: caller_locations, args: args, kwargs: kwargs,
|
50
|
+
serialized_entry_args: serialized_entry_args)
|
51
|
+
end
|
52
|
+
|
53
|
+
def build_snapshot(probe, rv: nil, snapshot: nil,
|
54
|
+
duration: nil, caller_locations: nil, args: nil, kwargs: nil,
|
55
|
+
serialized_entry_args: nil)
|
56
|
+
# TODO also verify that non-capturing probe does not pass
|
57
|
+
# snapshot or vars/args into this method
|
58
|
+
captures = if probe.capture_snapshot?
|
59
|
+
if probe.method?
|
60
|
+
{
|
61
|
+
entry: {
|
62
|
+
# standard:disable all
|
63
|
+
arguments: if serialized_entry_args
|
64
|
+
serialized_entry_args
|
65
|
+
else
|
66
|
+
(args || kwargs) && serializer.serialize_args(args, kwargs)
|
67
|
+
end,
|
68
|
+
throwable: nil,
|
69
|
+
# standard:enable all
|
70
|
+
},
|
71
|
+
return: {
|
72
|
+
arguments: {
|
73
|
+
"@return": serializer.serialize_value(rv),
|
74
|
+
},
|
75
|
+
throwable: nil,
|
76
|
+
},
|
77
|
+
}
|
78
|
+
elsif probe.line?
|
79
|
+
{
|
80
|
+
lines: snapshot && {
|
81
|
+
probe.line_no => {locals: serializer.serialize_vars(snapshot)},
|
82
|
+
},
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
location = if probe.line?
|
88
|
+
actual_file = if probe.file
|
89
|
+
# Normally caller_locations should always be filled for a line probe
|
90
|
+
# but in the test suite we don't always provide all arguments.
|
91
|
+
actual_file_basename = File.basename(probe.file)
|
92
|
+
caller_locations&.detect do |loc|
|
93
|
+
# TODO record actual path that probe was installed into,
|
94
|
+
# perform exact match here against that path.
|
95
|
+
File.basename(loc.path) == actual_file_basename
|
96
|
+
end&.path || probe.file
|
97
|
+
end
|
98
|
+
{
|
99
|
+
file: actual_file,
|
100
|
+
lines: [probe.line_no],
|
101
|
+
}
|
102
|
+
elsif probe.method?
|
103
|
+
{
|
104
|
+
method: probe.method_name,
|
105
|
+
type: probe.type_name,
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
stack = if caller_locations
|
110
|
+
format_caller_locations(caller_locations)
|
111
|
+
end
|
112
|
+
|
113
|
+
timestamp = timestamp_now
|
114
|
+
{
|
115
|
+
service: settings.service,
|
116
|
+
"debugger.snapshot": {
|
117
|
+
id: SecureRandom.uuid,
|
118
|
+
timestamp: timestamp,
|
119
|
+
evaluationErrors: [],
|
120
|
+
probe: {
|
121
|
+
id: probe.id,
|
122
|
+
version: 0,
|
123
|
+
location: location,
|
124
|
+
},
|
125
|
+
language: 'ruby',
|
126
|
+
# TODO add test coverage for callers being nil
|
127
|
+
stack: stack,
|
128
|
+
captures: captures,
|
129
|
+
},
|
130
|
+
# In python tracer duration is under debugger.snapshot,
|
131
|
+
# but UI appears to expect it here at top level.
|
132
|
+
duration: duration ? (duration * 10**9).to_i : nil,
|
133
|
+
host: nil,
|
134
|
+
logger: {
|
135
|
+
name: probe.file,
|
136
|
+
method: probe.method_name || 'no_method',
|
137
|
+
thread_name: Thread.current.name,
|
138
|
+
# Dynamic instrumentation currently does not need thread_id for
|
139
|
+
# anything. It can be sent if a customer requests it at which point
|
140
|
+
# we can also determine which thread identifier to send
|
141
|
+
# (Thread#native_thread_id or something else).
|
142
|
+
thread_id: nil,
|
143
|
+
version: 2,
|
144
|
+
},
|
145
|
+
# TODO add tests that the trace/span id is correctly propagated
|
146
|
+
"dd.trace_id": Datadog::Tracing.active_trace&.id,
|
147
|
+
"dd.span_id": Datadog::Tracing.active_span&.id,
|
148
|
+
ddsource: 'dd_debugger',
|
149
|
+
message: probe.template && evaluate_template(probe.template,
|
150
|
+
duration: duration ? duration * 1000 : nil),
|
151
|
+
timestamp: timestamp,
|
152
|
+
}
|
153
|
+
end
|
154
|
+
|
155
|
+
def build_status(probe, message:, status:)
|
156
|
+
{
|
157
|
+
service: settings.service,
|
158
|
+
timestamp: timestamp_now,
|
159
|
+
message: message,
|
160
|
+
ddsource: 'dd_debugger',
|
161
|
+
debugger: {
|
162
|
+
diagnostics: {
|
163
|
+
probeId: probe.id,
|
164
|
+
probeVersion: 0,
|
165
|
+
runtimeId: Core::Environment::Identity.id,
|
166
|
+
parentId: nil,
|
167
|
+
status: status,
|
168
|
+
},
|
169
|
+
},
|
170
|
+
}
|
171
|
+
end
|
172
|
+
|
173
|
+
def format_caller_locations(caller_locations)
|
174
|
+
caller_locations.map do |loc|
|
175
|
+
{fileName: loc.path, function: loc.label, lineNumber: loc.lineno}
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def evaluate_template(template, **vars)
|
180
|
+
message = template.dup
|
181
|
+
vars.each do |key, value|
|
182
|
+
message.gsub!("{@#{key}}", value.to_s)
|
183
|
+
end
|
184
|
+
message
|
185
|
+
end
|
186
|
+
|
187
|
+
def timestamp_now
|
188
|
+
(Time.now.to_f * 1000).to_i
|
189
|
+
end
|
190
|
+
|
191
|
+
def get_local_variables(trace_point)
|
192
|
+
# binding appears to be constructed on access, therefore
|
193
|
+
# 1) we should attempt to cache it and
|
194
|
+
# 2) we should not call +binding+ until we actually need variable values.
|
195
|
+
binding = trace_point.binding
|
196
|
+
|
197
|
+
# steep hack - should never happen
|
198
|
+
return {} unless binding
|
199
|
+
|
200
|
+
binding.local_variables.each_with_object({}) do |name, map|
|
201
|
+
value = binding.local_variable_get(name)
|
202
|
+
map[name] = value
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../core/semaphore'
|
4
|
+
|
5
|
+
module Datadog
|
6
|
+
module DI
|
7
|
+
# Background worker thread for sending probe statuses and snapshots
|
8
|
+
# to the backend (via the agent).
|
9
|
+
#
|
10
|
+
# The loop inside the worker rescues all exceptions to prevent termination
|
11
|
+
# due to unhandled exceptions raised by any downstream code.
|
12
|
+
# This includes communication and protocol errors when sending the
|
13
|
+
# payloads to the agent.
|
14
|
+
#
|
15
|
+
# The worker groups the data to send into batches. The goal is to perform
|
16
|
+
# no more than one network operation per event type per second.
|
17
|
+
# There is also a limit on the length of the sending queue to prevent
|
18
|
+
# it from growing without bounds if upstream code generates an enormous
|
19
|
+
# number of events for some reason.
|
20
|
+
#
|
21
|
+
# Wake-up events are used (via ConditionVariable) to keep the thread
|
22
|
+
# asleep if there is no work to be done.
|
23
|
+
#
|
24
|
+
# @api private
|
25
|
+
class ProbeNotifierWorker
|
26
|
+
# Minimum interval between submissions.
|
27
|
+
# TODO make this into an internal setting and increase default to 2 or 3.
|
28
|
+
MIN_SEND_INTERVAL = 1
|
29
|
+
|
30
|
+
def initialize(settings, transport, logger)
|
31
|
+
@settings = settings
|
32
|
+
@status_queue = []
|
33
|
+
@snapshot_queue = []
|
34
|
+
@transport = transport
|
35
|
+
@logger = logger
|
36
|
+
@lock = Mutex.new
|
37
|
+
@wake = Core::Semaphore.new
|
38
|
+
@io_in_progress = false
|
39
|
+
@sleep_remaining = nil
|
40
|
+
@wake_scheduled = false
|
41
|
+
@thread = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :settings
|
45
|
+
attr_reader :logger
|
46
|
+
|
47
|
+
def start
|
48
|
+
return if @thread
|
49
|
+
@thread = Thread.new do
|
50
|
+
loop do
|
51
|
+
# TODO If stop is requested, we stop immediately without
|
52
|
+
# flushing the submissions. Should we send pending submissions
|
53
|
+
# and then quit?
|
54
|
+
break if @stop_requested
|
55
|
+
|
56
|
+
sleep_remaining = @lock.synchronize do
|
57
|
+
if sleep_remaining && sleep_remaining > 0
|
58
|
+
# Recalculate how much sleep time is remaining, then sleep that long.
|
59
|
+
set_sleep_remaining
|
60
|
+
else
|
61
|
+
0
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if sleep_remaining > 0
|
66
|
+
# Do not need to update @wake_scheduled here because
|
67
|
+
# wake-up is already scheduled for the earliest possible time.
|
68
|
+
wake.wait(sleep_remaining)
|
69
|
+
next
|
70
|
+
end
|
71
|
+
|
72
|
+
begin
|
73
|
+
more = maybe_send
|
74
|
+
rescue => exc
|
75
|
+
raise if settings.dynamic_instrumentation.propagate_all_exceptions
|
76
|
+
|
77
|
+
logger.warn("Error in probe notifier worker: #{exc.class}: #{exc} (at #{exc.backtrace.first})")
|
78
|
+
end
|
79
|
+
@lock.synchronize do
|
80
|
+
@wake_scheduled = more
|
81
|
+
end
|
82
|
+
wake.wait(more ? MIN_SEND_INTERVAL : nil)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Stops the background thread.
|
88
|
+
#
|
89
|
+
# Attempts a graceful stop with the specified timeout, then falls back
|
90
|
+
# to killing the thread using Thread#kill.
|
91
|
+
def stop(timeout = 1)
|
92
|
+
@stop_requested = true
|
93
|
+
wake.signal
|
94
|
+
if thread
|
95
|
+
unless thread.join(timeout)
|
96
|
+
thread.kill
|
97
|
+
end
|
98
|
+
@thread = nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Waits for background thread to send pending notifications.
|
103
|
+
#
|
104
|
+
# This method waits for the notification queue to become empty
|
105
|
+
# rather than for a particular set of notifications to be sent out,
|
106
|
+
# therefore, it should only be called when there is no parallel
|
107
|
+
# activity (in another thread) that causes more notifications
|
108
|
+
# to be generated.
|
109
|
+
def flush
|
110
|
+
loop do
|
111
|
+
if @thread.nil? || !@thread.alive?
|
112
|
+
return
|
113
|
+
end
|
114
|
+
|
115
|
+
io_in_progress, queues_empty = @lock.synchronize do
|
116
|
+
[io_in_progress?, status_queue.empty? && snapshot_queue.empty?]
|
117
|
+
end
|
118
|
+
|
119
|
+
if io_in_progress
|
120
|
+
# If we just call Thread.pass we could be in a busy loop -
|
121
|
+
# add a sleep.
|
122
|
+
sleep 0.25
|
123
|
+
next
|
124
|
+
elsif queues_empty
|
125
|
+
break
|
126
|
+
else
|
127
|
+
sleep 0.25
|
128
|
+
next
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
attr_reader :transport
|
136
|
+
attr_reader :wake
|
137
|
+
attr_reader :thread
|
138
|
+
|
139
|
+
# This method should be called while @lock is held.
|
140
|
+
def io_in_progress?
|
141
|
+
@io_in_progress
|
142
|
+
end
|
143
|
+
|
144
|
+
attr_reader :last_sent
|
145
|
+
|
146
|
+
[
|
147
|
+
[:status, 'probe status'],
|
148
|
+
[:snapshot, 'snapshot'],
|
149
|
+
].each do |(event_type, event_name)|
|
150
|
+
attr_reader "#{event_type}_queue"
|
151
|
+
|
152
|
+
# Adds a status or a snapshot to the queue to be sent to the agent
|
153
|
+
# at the next opportunity.
|
154
|
+
#
|
155
|
+
# If the queue is too large, the event will not be added.
|
156
|
+
#
|
157
|
+
# Signals the background thread to wake up (and do the sending)
|
158
|
+
# if it has been more than 1 second since the last send of the same
|
159
|
+
# event type.
|
160
|
+
define_method("add_#{event_type}") do |event|
|
161
|
+
@lock.synchronize do
|
162
|
+
queue = send("#{event_type}_queue")
|
163
|
+
# TODO determine a suitable limit via testing/benchmarking
|
164
|
+
if queue.length > 100
|
165
|
+
logger.warn("#{self.class.name}: dropping #{event_type} because queue is full")
|
166
|
+
else
|
167
|
+
queue << event
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Figure out whether to wake up the worker thread.
|
172
|
+
# If minimum send interval has elapsed since the last send,
|
173
|
+
# wake up immediately.
|
174
|
+
@lock.synchronize do
|
175
|
+
unless @wake_scheduled
|
176
|
+
@wake_scheduled = true
|
177
|
+
set_sleep_remaining
|
178
|
+
wake.signal
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Determine how much longer the worker thread should sleep
|
184
|
+
# so as not to send in less than MIN_SEND_INTERVAL since the last send.
|
185
|
+
# Important: this method must be called when @lock is held.
|
186
|
+
#
|
187
|
+
# Returns the time remaining to sleep.
|
188
|
+
def set_sleep_remaining
|
189
|
+
now = Core::Utils::Time.get_time
|
190
|
+
@sleep_remaining = if last_sent
|
191
|
+
[last_sent + MIN_SEND_INTERVAL - now, 0].max
|
192
|
+
else
|
193
|
+
0
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
public "add_#{event_type}"
|
198
|
+
|
199
|
+
# Sends pending probe statuses or snapshots.
|
200
|
+
#
|
201
|
+
# This method should ideally only be called when there are actually
|
202
|
+
# events to send, but it can be called when there is nothing to do.
|
203
|
+
# Currently we only have one wake-up signaling object and two
|
204
|
+
# types of events. Therefore on most wake-ups we expect to only
|
205
|
+
# send one type of events.
|
206
|
+
define_method("maybe_send_#{event_type}") do
|
207
|
+
batch = nil
|
208
|
+
@lock.synchronize do
|
209
|
+
batch = instance_variable_get("@#{event_type}_queue")
|
210
|
+
instance_variable_set("@#{event_type}_queue", [])
|
211
|
+
@io_in_progress = batch.any? # steep:ignore
|
212
|
+
end
|
213
|
+
if batch.any? # steep:ignore
|
214
|
+
begin
|
215
|
+
transport.public_send("send_#{event_type}", batch)
|
216
|
+
time = Core::Utils::Time.get_time
|
217
|
+
@lock.synchronize do
|
218
|
+
@last_sent = time
|
219
|
+
end
|
220
|
+
rescue => exc
|
221
|
+
raise if settings.dynamic_instrumentation.propagate_all_exceptions
|
222
|
+
logger.warn("failed to send #{event_name}: #{exc.class}: #{exc} (at #{exc.backtrace.first})")
|
223
|
+
end
|
224
|
+
end
|
225
|
+
batch.any? # steep:ignore
|
226
|
+
rescue ThreadError
|
227
|
+
# Normally the queue should only be consumed in this method,
|
228
|
+
# however if anyone consumes it elsewhere we don't want to block
|
229
|
+
# while consuming it here. Rescue ThreadError and return.
|
230
|
+
logger.warn("unexpected #{event_name} queue underflow - consumed elsewhere?")
|
231
|
+
ensure
|
232
|
+
@lock.synchronize do
|
233
|
+
@io_in_progress = false
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def maybe_send
|
239
|
+
rv = maybe_send_status
|
240
|
+
rv || maybe_send_snapshot
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
@@ -26,6 +26,16 @@ module Datadog
|
|
26
26
|
# All serialization methods take the names of the variables being
|
27
27
|
# serialized in order to be able to redact values.
|
28
28
|
#
|
29
|
+
# The result of serialization should not reference parameter values when
|
30
|
+
# the values are mutable (currently, this only applies to string values).
|
31
|
+
# Serializer will duplicate such mutable values, so that if method
|
32
|
+
# arguments are captured at entry and then modified during method execution,
|
33
|
+
# the serialized values from entry are correctly preserved.
|
34
|
+
# Alternatively, we could pass a parameter to the serialization methods
|
35
|
+
# which would control whether values are duplicated. This may be more
|
36
|
+
# efficient but there would be additional overhead from passing this
|
37
|
+
# parameter all the time and the API would get more complex.
|
38
|
+
#
|
29
39
|
# @api private
|
30
40
|
class Serializer
|
31
41
|
def initialize(settings, redactor)
|
@@ -92,12 +102,24 @@ module Datadog
|
|
92
102
|
when Integer, Float, TrueClass, FalseClass
|
93
103
|
serialized.update(value: value.to_s)
|
94
104
|
when String, Symbol
|
95
|
-
|
105
|
+
need_dup = false
|
106
|
+
value = if String === value
|
107
|
+
# This is the only place where we duplicate the value, currently.
|
108
|
+
# All other values are immutable primitives (e.g. numbers).
|
109
|
+
# However, do not duplicate if the string is frozen, or if
|
110
|
+
# it is later truncated.
|
111
|
+
need_dup = !value.frozen?
|
112
|
+
value
|
113
|
+
else
|
114
|
+
value.to_s
|
115
|
+
end
|
96
116
|
max = settings.dynamic_instrumentation.max_capture_string_length
|
97
117
|
if value.length > max
|
98
118
|
serialized.update(truncated: true, size: value.length)
|
99
119
|
value = value[0...max]
|
120
|
+
need_dup = false
|
100
121
|
end
|
122
|
+
value = value.dup if need_dup
|
101
123
|
serialized.update(value: value)
|
102
124
|
when Array
|
103
125
|
if depth < 0
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'error'
|
4
|
+
|
5
|
+
module Datadog
|
6
|
+
module DI
|
7
|
+
# Transport for sending probe statuses and snapshots to local agent.
|
8
|
+
#
|
9
|
+
# Handles encoding of the payloads into multipart posts if necessary,
|
10
|
+
# body formatting/encoding, setting correct headers, etc.
|
11
|
+
#
|
12
|
+
# The transport does not handle batching of statuses or snapshots -
|
13
|
+
# the batching should be implemented upstream of this class.
|
14
|
+
#
|
15
|
+
# Timeout settings are forwarded from agent settings to the Net adapter.
|
16
|
+
#
|
17
|
+
# The send_* methods raise Error::AgentCommunicationError on errors
|
18
|
+
# (network errors and HTTP protocol errors). It is the responsibility
|
19
|
+
# of upstream code to rescue these exceptions appropriately to prevent them
|
20
|
+
# from being propagated to the application.
|
21
|
+
#
|
22
|
+
# @api private
|
23
|
+
class Transport
|
24
|
+
DIAGNOSTICS_PATH = '/debugger/v1/diagnostics'
|
25
|
+
INPUT_PATH = '/debugger/v1/input'
|
26
|
+
|
27
|
+
def initialize(agent_settings)
|
28
|
+
# Note that this uses host, port, timeout and TLS flag from
|
29
|
+
# agent settings.
|
30
|
+
@client = Core::Transport::HTTP::Adapters::Net.new(agent_settings)
|
31
|
+
end
|
32
|
+
|
33
|
+
def send_diagnostics(payload)
|
34
|
+
event_payload = Core::Vendor::Multipart::Post::UploadIO.new(
|
35
|
+
StringIO.new(JSON.dump(payload)), 'application/json', 'event.json'
|
36
|
+
)
|
37
|
+
payload = {'event' => event_payload}
|
38
|
+
send_request('Probe status submission', DIAGNOSTICS_PATH, payload)
|
39
|
+
end
|
40
|
+
|
41
|
+
def send_input(payload)
|
42
|
+
send_request('Probe snapshot submission', INPUT_PATH, payload,
|
43
|
+
headers: {'content-type' => 'application/json'},)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
attr_reader :client
|
49
|
+
|
50
|
+
def send_request(desc, path, payload, headers: {})
|
51
|
+
# steep:ignore:start
|
52
|
+
env = OpenStruct.new(
|
53
|
+
path: path,
|
54
|
+
form: payload,
|
55
|
+
headers: headers,
|
56
|
+
)
|
57
|
+
# steep:ignore:end
|
58
|
+
response = client.post(env)
|
59
|
+
unless response.ok?
|
60
|
+
raise Error::AgentCommunicationError, "#{desc} failed: #{response.code}: #{response.payload}"
|
61
|
+
end
|
62
|
+
rescue IOError, SystemCallError => exc
|
63
|
+
raise Error::AgentCommunicationError, "#{desc} failed: #{exc.class}: #{exc}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
module DI
|
5
|
+
module Utils
|
6
|
+
# Returns whether the provided +path+ matches the user-designated
|
7
|
+
# file suffix (of a line probe).
|
8
|
+
#
|
9
|
+
# If suffix is an absolute path (i.e., it starts with a slash), the path
|
10
|
+
# must be identical for it to match.
|
11
|
+
#
|
12
|
+
# If suffix is not an absolute path, the path matches if its suffix is
|
13
|
+
# the provided suffix, at a path component boundary.
|
14
|
+
module_function def path_matches_suffix?(path, suffix)
|
15
|
+
if suffix.start_with?('/')
|
16
|
+
path == suffix
|
17
|
+
else
|
18
|
+
# Exact match is not possible here, meaning any matching path
|
19
|
+
# has to be longer than the suffix. Require full component matches,
|
20
|
+
# meaning either the first character of the suffix is a slash
|
21
|
+
# or the previous character in the path is a slash.
|
22
|
+
# For now only check for forward slashes for Unix-like OSes;
|
23
|
+
# backslash is a legitimate character of a file name in Unix
|
24
|
+
# therefore simply permitting forward or back slash is not
|
25
|
+
# sufficient, we need to perform an OS check to know which
|
26
|
+
# path separator to use.
|
27
|
+
!!
|
28
|
+
if path.length > suffix.length && path.end_with?(suffix)
|
29
|
+
previous_char = path[path.length - suffix.length - 1]
|
30
|
+
previous_char == "/" || suffix[0] == "/"
|
31
|
+
end
|
32
|
+
|
33
|
+
# Alternative implementation using a regular expression:
|
34
|
+
# !!(path =~ %r,(/|\A)#{Regexp.quote(suffix)}\z,)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|