datadog 2.4.0 → 2.6.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 +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/appsec/scope.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative 'processor'
|
4
|
-
|
5
3
|
module Datadog
|
6
4
|
module AppSec
|
7
5
|
# Capture context essential to consistently call processor and report via traces
|
@@ -22,8 +20,7 @@ module Datadog
|
|
22
20
|
def activate_scope(trace, service_entry_span, processor)
|
23
21
|
raise ActiveScopeError, 'another scope is active, nested scopes are not supported' if active_scope
|
24
22
|
|
25
|
-
context =
|
26
|
-
|
23
|
+
context = processor.new_context
|
27
24
|
self.active_scope = new(trace, service_entry_span, context)
|
28
25
|
end
|
29
26
|
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
module AppSec
|
5
|
+
module Utils
|
6
|
+
# Utility class to to AppSec-specific trace operations
|
7
|
+
class TraceOperation
|
8
|
+
def self.appsec_standalone_reject?(trace)
|
9
|
+
Datadog.configuration.appsec.standalone.enabled &&
|
10
|
+
(trace.nil? || trace.get_tag(Datadog::AppSec::Ext::TAG_DISTRIBUTED_APPSEC_EVENT) != '1')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/datadog/appsec/utils.rb
CHANGED
data/lib/datadog/appsec.rb
CHANGED
@@ -232,7 +232,32 @@ module Datadog
|
|
232
232
|
end
|
233
233
|
|
234
234
|
def should_use_uds?
|
235
|
-
|
235
|
+
# When we have mixed settings for http/https and uds, we print a warning
|
236
|
+
# and use the uds settings.
|
237
|
+
mixed_http_and_uds
|
238
|
+
can_use_uds?
|
239
|
+
end
|
240
|
+
|
241
|
+
def mixed_http_and_uds
|
242
|
+
return @mixed_http_and_uds if defined?(@mixed_http_and_uds)
|
243
|
+
|
244
|
+
@mixed_http_and_uds = (configured_hostname || configured_port) && can_use_uds?
|
245
|
+
if @mixed_http_and_uds
|
246
|
+
warn_if_configuration_mismatch(
|
247
|
+
[
|
248
|
+
DetectedConfiguration.new(
|
249
|
+
friendly_name: 'configuration for unix domain socket',
|
250
|
+
value: parsed_url.to_s,
|
251
|
+
),
|
252
|
+
DetectedConfiguration.new(
|
253
|
+
friendly_name: 'configuration of hostname/port for http/https use',
|
254
|
+
value: "hostname: '#{hostname}', port: '#{port}'",
|
255
|
+
),
|
256
|
+
]
|
257
|
+
)
|
258
|
+
end
|
259
|
+
|
260
|
+
@mixed_http_and_uds
|
236
261
|
end
|
237
262
|
|
238
263
|
def can_use_uds?
|
@@ -307,30 +332,6 @@ module Datadog
|
|
307
332
|
uri.scheme == 'unix'
|
308
333
|
end
|
309
334
|
|
310
|
-
# When we have mixed settings for http/https and uds, we print a warning and ignore the uds settings
|
311
|
-
def mixed_http_and_uds?
|
312
|
-
return @mixed_http_and_uds if defined?(@mixed_http_and_uds)
|
313
|
-
|
314
|
-
@mixed_http_and_uds = (configured_hostname || configured_port) && can_use_uds?
|
315
|
-
|
316
|
-
if @mixed_http_and_uds
|
317
|
-
warn_if_configuration_mismatch(
|
318
|
-
[
|
319
|
-
DetectedConfiguration.new(
|
320
|
-
friendly_name: 'configuration of hostname/port for http/https use',
|
321
|
-
value: "hostname: '#{hostname}', port: '#{port}'",
|
322
|
-
),
|
323
|
-
DetectedConfiguration.new(
|
324
|
-
friendly_name: 'configuration for unix domain socket',
|
325
|
-
value: parsed_url.to_s,
|
326
|
-
),
|
327
|
-
]
|
328
|
-
)
|
329
|
-
end
|
330
|
-
|
331
|
-
@mixed_http_and_uds
|
332
|
-
end
|
333
|
-
|
334
335
|
# Represents a given configuration value and where we got it from
|
335
336
|
class DetectedConfiguration
|
336
337
|
attr_reader :friendly_name, :value
|
@@ -514,6 +514,18 @@ module Datadog
|
|
514
514
|
end
|
515
515
|
end
|
516
516
|
end
|
517
|
+
|
518
|
+
# Controls if the heap profiler should attempt to clean young objects after GC, rather than just at
|
519
|
+
# serialization time. This lowers memory usage and high percentile latency.
|
520
|
+
#
|
521
|
+
# Only takes effect when used together with `gc_enabled: true` and `experimental_heap_enabled: true`.
|
522
|
+
#
|
523
|
+
# @default false
|
524
|
+
option :heap_clean_after_gc_enabled do |o|
|
525
|
+
o.type :bool
|
526
|
+
o.env 'DD_PROFILING_HEAP_CLEAN_AFTER_GC_ENABLED'
|
527
|
+
o.default false
|
528
|
+
end
|
517
529
|
end
|
518
530
|
|
519
531
|
# @public_api
|
@@ -278,9 +278,7 @@ module Datadog
|
|
278
278
|
def handle_interrupt_shutdown!
|
279
279
|
logger = Datadog.logger
|
280
280
|
shutdown_thread = Thread.new { shutdown! }
|
281
|
-
|
282
|
-
shutdown_thread.name = Datadog::Core::Configuration.name
|
283
|
-
end
|
281
|
+
shutdown_thread.name = Datadog::Core::Configuration.name
|
284
282
|
|
285
283
|
print_message_treshold_seconds = 0.2
|
286
284
|
|
@@ -66,7 +66,8 @@ module Datadog
|
|
66
66
|
def start
|
67
67
|
Utils::AtForkMonkeyPatch.apply!
|
68
68
|
|
69
|
-
start_or_update_on_fork(action: :start)
|
69
|
+
start_or_update_on_fork(action: :start, tags: tags)
|
70
|
+
|
70
71
|
ONLY_ONCE.run do
|
71
72
|
Utils::AtForkMonkeyPatch.at_fork(:child) do
|
72
73
|
# Must NOT reference `self` here, as only the first instance will
|
@@ -77,8 +78,10 @@ module Datadog
|
|
77
78
|
end
|
78
79
|
end
|
79
80
|
|
80
|
-
def update_on_fork
|
81
|
-
|
81
|
+
def update_on_fork(settings: Datadog.configuration)
|
82
|
+
# Here we pick up the latest settings, so that we pick up any tags that change after forking
|
83
|
+
# such as the pid or runtime-id
|
84
|
+
start_or_update_on_fork(action: :update_on_fork, tags: TagBuilder.call(settings))
|
82
85
|
end
|
83
86
|
|
84
87
|
def stop
|
@@ -92,7 +95,7 @@ module Datadog
|
|
92
95
|
|
93
96
|
attr_reader :tags, :agent_base_url, :ld_library_path, :path_to_crashtracking_receiver_binary, :logger
|
94
97
|
|
95
|
-
def start_or_update_on_fork(action:)
|
98
|
+
def start_or_update_on_fork(action:, tags:)
|
96
99
|
self.class._native_start_or_update_on_fork(
|
97
100
|
action: action,
|
98
101
|
agent_base_url: agent_base_url,
|
@@ -101,7 +104,7 @@ module Datadog
|
|
101
104
|
tags_as_array: tags.to_a,
|
102
105
|
upload_timeout_seconds: 1
|
103
106
|
)
|
104
|
-
logger.debug("Crash tracking #{action}
|
107
|
+
logger.debug("Crash tracking action: #{action} successful")
|
105
108
|
rescue => e
|
106
109
|
logger.error("Failed to #{action} crash tracking: #{e.message}")
|
107
110
|
end
|
@@ -52,6 +52,11 @@ module Datadog
|
|
52
52
|
::RubyVM::YJIT.runtime_stats[:yjit_alloc_size]
|
53
53
|
end
|
54
54
|
|
55
|
+
# Ratio of YJIT-executed instructions
|
56
|
+
def ratio_in_yjit
|
57
|
+
::RubyVM::YJIT.runtime_stats[:ratio_in_yjit]
|
58
|
+
end
|
59
|
+
|
55
60
|
def available?
|
56
61
|
defined?(::RubyVM::YJIT) \
|
57
62
|
&& ::RubyVM::YJIT.enabled? \
|
@@ -120,6 +120,11 @@ module Datadog
|
|
120
120
|
# Add container ID, if present.
|
121
121
|
container_id = Datadog::Core::Environment::Container.container_id
|
122
122
|
headers[Datadog::Core::Transport::Ext::HTTP::HEADER_CONTAINER_ID] = container_id unless container_id.nil?
|
123
|
+
# Sending this header to the agent will disable metrics computation (and billing) on the agent side
|
124
|
+
# by pretending it has already been done on the library side.
|
125
|
+
if Datadog.configuration.appsec.standalone.enabled
|
126
|
+
headers[Datadog::Core::Transport::Ext::HTTP::HEADER_CLIENT_COMPUTED_STATS] = 'yes'
|
127
|
+
end
|
123
128
|
end
|
124
129
|
end
|
125
130
|
|
@@ -34,7 +34,7 @@ module Datadog
|
|
34
34
|
@starting = true
|
35
35
|
|
36
36
|
thread = Thread.new { poll(@interval) }
|
37
|
-
thread.name = self.class.name
|
37
|
+
thread.name = self.class.name
|
38
38
|
thread.thread_variable_set(:fork_safe, true)
|
39
39
|
@thr = thread
|
40
40
|
|
@@ -31,6 +31,7 @@ module Datadog
|
|
31
31
|
METRIC_YJIT_OBJECT_SHAPE_COUNT = 'runtime.ruby.yjit.object_shape_count'
|
32
32
|
METRIC_YJIT_OUTLINED_CODE_SIZE = 'runtime.ruby.yjit.outlined_code_size'
|
33
33
|
METRIC_YJIT_YJIT_ALLOC_SIZE = 'runtime.ruby.yjit.yjit_alloc_size'
|
34
|
+
METRIC_YJIT_RATIO_IN_YJIT = 'runtime.ruby.yjit.ratio_in_yjit'
|
34
35
|
|
35
36
|
TAG_SERVICE = 'service'
|
36
37
|
end
|
@@ -181,6 +181,10 @@ module Datadog
|
|
181
181
|
Core::Runtime::Ext::Metrics::METRIC_YJIT_YJIT_ALLOC_SIZE,
|
182
182
|
Core::Environment::YJIT.yjit_alloc_size
|
183
183
|
)
|
184
|
+
gauge_if_not_nil(
|
185
|
+
Core::Runtime::Ext::Metrics::METRIC_YJIT_RATIO_IN_YJIT,
|
186
|
+
Core::Environment::YJIT.ratio_in_yjit
|
187
|
+
)
|
184
188
|
end
|
185
189
|
end
|
186
190
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
module Core
|
5
|
+
# Semaphore pattern implementation, as described in documentation for
|
6
|
+
# ConditionVariable.
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
class Semaphore
|
10
|
+
def initialize
|
11
|
+
@wake_lock = Mutex.new
|
12
|
+
@wake = ConditionVariable.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def signal
|
16
|
+
wake_lock.synchronize do
|
17
|
+
wake.signal
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def wait(timeout = nil)
|
22
|
+
wake_lock.synchronize do
|
23
|
+
# steep specifies that the second argument to wait is of type
|
24
|
+
# ::Time::_Timeout which for some reason is not Numeric and is not
|
25
|
+
# castable from Numeric.
|
26
|
+
wake.wait(wake_lock, timeout) # steep:ignore
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :wake_lock, :wake
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -31,17 +31,17 @@ module Datadog
|
|
31
31
|
return unless backtrace
|
32
32
|
return if backtrace.empty?
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
stack_trace << if line.start_with?(GEM_ROOT)
|
37
|
-
line[GEM_ROOT.length..-1] || ''
|
38
|
-
else
|
39
|
-
'REDACTED'
|
40
|
-
end
|
41
|
-
stack_trace << ','
|
42
|
-
end
|
34
|
+
# vendored deps
|
35
|
+
vendored_deps = Gem.path.any? { |p| p.start_with?(GEM_ROOT) }
|
43
36
|
|
44
|
-
|
37
|
+
backtrace.map do |line|
|
38
|
+
if !vendored_deps && line.start_with?(GEM_ROOT) ||
|
39
|
+
vendored_deps && line.start_with?(GEM_ROOT) && Gem.path.none? { |p| line.start_with?(p) }
|
40
|
+
line[GEM_ROOT.length..-1] || ''
|
41
|
+
else
|
42
|
+
'REDACTED'
|
43
|
+
end
|
44
|
+
end.join(',')
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
@@ -16,6 +16,7 @@ module Datadog
|
|
16
16
|
#
|
17
17
|
# Setting this header to any non-empty value enables this feature.
|
18
18
|
HEADER_CLIENT_COMPUTED_TOP_LEVEL = 'Datadog-Client-Computed-Top-Level'
|
19
|
+
HEADER_CLIENT_COMPUTED_STATS = 'Datadog-Client-Computed-Stats'
|
19
20
|
HEADER_META_LANG = 'Datadog-Meta-Lang'
|
20
21
|
HEADER_META_LANG_VERSION = 'Datadog-Meta-Lang-Version'
|
21
22
|
HEADER_META_LANG_INTERPRETER = 'Datadog-Meta-Lang-Interpreter'
|
@@ -148,7 +148,7 @@ module Datadog
|
|
148
148
|
end
|
149
149
|
# rubocop:enable Lint/RescueException
|
150
150
|
end
|
151
|
-
@worker.name = self.class.name
|
151
|
+
@worker.name = self.class.name
|
152
152
|
@worker.thread_variable_set(:fork_safe, true)
|
153
153
|
|
154
154
|
nil
|
@@ -109,25 +109,15 @@ module Datadog
|
|
109
109
|
# to be an absolute path), only the exactly matching path is returned.
|
110
110
|
# Otherwise all known paths that end in the suffix are returned.
|
111
111
|
# If no paths match, an empty array is returned.
|
112
|
-
def
|
112
|
+
def iseqs_for_path_suffix(suffix)
|
113
113
|
registry_lock.synchronize do
|
114
114
|
exact = registry[suffix]
|
115
115
|
return [exact] if exact
|
116
116
|
|
117
117
|
inexact = []
|
118
118
|
registry.each do |path, iseq|
|
119
|
-
|
120
|
-
|
121
|
-
# meaning either the first character of the suffix is a slash
|
122
|
-
# or the previous character in the path is a slash.
|
123
|
-
# For now only check for forward slashes for Unix-like OSes;
|
124
|
-
# backslash is a legitimate character of a file name in Unix
|
125
|
-
# therefore simply permitting forward or back slash is not
|
126
|
-
# sufficient, we need to perform an OS check to know which
|
127
|
-
# path separator to use.
|
128
|
-
if path.length > suffix.length && path.end_with?(suffix)
|
129
|
-
previous_char = path[path.length - suffix.length - 1]
|
130
|
-
inexact << iseq if previous_char == "/" || suffix[0] == "/"
|
119
|
+
if Utils.path_matches_suffix?(path, suffix)
|
120
|
+
inexact << iseq
|
131
121
|
end
|
132
122
|
end
|
133
123
|
inexact
|
@@ -150,6 +140,14 @@ module Datadog
|
|
150
140
|
# reinstated in the future.
|
151
141
|
@compiled_trace_point = nil
|
152
142
|
end
|
143
|
+
clear
|
144
|
+
end
|
145
|
+
|
146
|
+
# Clears the stored mapping from paths to compiled code.
|
147
|
+
#
|
148
|
+
# This method should normally never be called. It is meant to be
|
149
|
+
# used only by the test suite.
|
150
|
+
def clear
|
153
151
|
registry_lock.synchronize do
|
154
152
|
registry.clear
|
155
153
|
end
|
@@ -0,0 +1,301 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Lint/AssignmentInCondition
|
4
|
+
|
5
|
+
require 'benchmark'
|
6
|
+
|
7
|
+
module Datadog
|
8
|
+
module DI
|
9
|
+
# Arranges to invoke a callback when a particular Ruby method or
|
10
|
+
# line of code is executed.
|
11
|
+
#
|
12
|
+
# Method instrumentation is accomplished via module prepending.
|
13
|
+
# Unlike the alias_method_chain pattern, module prepending permits
|
14
|
+
# removing instrumentation with no virtually performance side-effects
|
15
|
+
# (the target class retains an empty included module, but no additional
|
16
|
+
# code is executed as part of target method).
|
17
|
+
#
|
18
|
+
# Method hooking works with explicitly defined methods and "virtual"
|
19
|
+
# methods defined via method_missing.
|
20
|
+
#
|
21
|
+
# Line instrumentation is normally accomplished with a targeted line
|
22
|
+
# trace point. This requires MRI and at least Ruby 2.6.
|
23
|
+
# For testing purposes, it is also possible to use untargeted trace
|
24
|
+
# points, but they have a huge performance penalty and should generally
|
25
|
+
# not be used in production.
|
26
|
+
#
|
27
|
+
# Targeted line trace points require tracking of loaded code; see
|
28
|
+
# the CodeTracker class for more details.
|
29
|
+
#
|
30
|
+
# Instrumentation state (i.e., the module or trace point used for
|
31
|
+
# instrumentation) is stored in the Probe instance. Thus, Instrumenter
|
32
|
+
# mutates attributes of Probes it is asked to install or remove.
|
33
|
+
# A previous version of the code attempted to maintain the instrumentation
|
34
|
+
# state within Instrumenter but this was very messy and hard to
|
35
|
+
# guarantee correctness of. With the state stored in Probes, it is
|
36
|
+
# straightforward to determine if a Probe has been successfully instrumented,
|
37
|
+
# and thus requires cleanup, and to properly clean it up.
|
38
|
+
#
|
39
|
+
# Note that the upstream code is responsible for generally storing Probes.
|
40
|
+
# This is normally accomplished by ProbeManager. ProbeManager stores all
|
41
|
+
# known probes, instrumented or not, and is responsible for calling
|
42
|
+
# +unhook+ of Instrumenter to clean up instrumentation when a user
|
43
|
+
# deletes a probe in UI or when DI is shut down.
|
44
|
+
#
|
45
|
+
# Given the need to store state, and also that there are several Probe
|
46
|
+
# attributes that affect how instrumentation is set up and that must be
|
47
|
+
# consulted very early in the callback invocation (e.g., to perform
|
48
|
+
# rate limiting correctly), Instrumenter takes Probe instances as
|
49
|
+
# arguments rather than e.g. file + line number or class + method name.
|
50
|
+
# As a result, Instrumenter is rather coupled to DI the product and is
|
51
|
+
# not trivially usable as a general-purpose Ruby instrumentation tool
|
52
|
+
# (however, Probe instances can be replaced by OpenStruct instances
|
53
|
+
# providing the same interface with not much effort).
|
54
|
+
#
|
55
|
+
# @api private
|
56
|
+
class Instrumenter
|
57
|
+
def initialize(settings, serializer, logger, code_tracker: nil)
|
58
|
+
@settings = settings
|
59
|
+
@serializer = serializer
|
60
|
+
@logger = logger
|
61
|
+
@code_tracker = code_tracker
|
62
|
+
|
63
|
+
@lock = Mutex.new
|
64
|
+
end
|
65
|
+
|
66
|
+
attr_reader :settings
|
67
|
+
attr_reader :serializer
|
68
|
+
attr_reader :logger
|
69
|
+
attr_reader :code_tracker
|
70
|
+
|
71
|
+
# This is a substitute for Thread::Backtrace::Location
|
72
|
+
# which does not have a public constructor.
|
73
|
+
# Used for the fabricated stack frame for the method itself
|
74
|
+
# for method probes (which use Module#prepend and thus aren't called
|
75
|
+
# from the method but from outside of the method).
|
76
|
+
Location = Struct.new(:path, :lineno, :label)
|
77
|
+
|
78
|
+
def hook_method(probe, &block)
|
79
|
+
unless block
|
80
|
+
raise ArgumentError, 'block is required'
|
81
|
+
end
|
82
|
+
|
83
|
+
lock.synchronize do
|
84
|
+
if probe.instrumentation_module
|
85
|
+
# Already instrumented, warn?
|
86
|
+
return
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
cls = symbolize_class_name(probe.type_name)
|
91
|
+
serializer = self.serializer
|
92
|
+
method_name = probe.method_name
|
93
|
+
target_method = cls.instance_method(method_name)
|
94
|
+
loc = target_method.source_location
|
95
|
+
rate_limiter = probe.rate_limiter
|
96
|
+
|
97
|
+
mod = Module.new do
|
98
|
+
define_method(method_name) do |*args, **kwargs| # steep:ignore
|
99
|
+
if rate_limiter.nil? || rate_limiter.allow?
|
100
|
+
# Arguments may be mutated by the method, therefore
|
101
|
+
# they need to be serialized prior to method invocation.
|
102
|
+
entry_args = if probe.capture_snapshot?
|
103
|
+
serializer.serialize_args(args, kwargs)
|
104
|
+
end
|
105
|
+
rv = nil
|
106
|
+
duration = Benchmark.realtime do # steep:ignore
|
107
|
+
rv = super(*args, **kwargs)
|
108
|
+
end
|
109
|
+
# The method itself is not part of the stack trace because
|
110
|
+
# we are getting the stack trace from outside of the method.
|
111
|
+
# Add the method in manually as the top frame.
|
112
|
+
method_frame = Location.new(loc.first, loc.last, method_name)
|
113
|
+
caller_locs = [method_frame] + caller_locations # steep:ignore
|
114
|
+
# TODO capture arguments at exit
|
115
|
+
# & is to stop steep complaints, block is always present here.
|
116
|
+
block&.call(probe: probe, rv: rv, duration: duration, caller_locations: caller_locs,
|
117
|
+
serialized_entry_args: entry_args)
|
118
|
+
rv
|
119
|
+
else
|
120
|
+
super(*args, **kwargs)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
lock.synchronize do
|
126
|
+
if probe.instrumentation_module
|
127
|
+
# Already instrumented from another thread
|
128
|
+
return
|
129
|
+
end
|
130
|
+
|
131
|
+
probe.instrumentation_module = mod
|
132
|
+
cls.send(:prepend, mod)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def unhook_method(probe)
|
137
|
+
# Ruby does not permit removing modules from classes.
|
138
|
+
# We can, however, remove method definitions from modules.
|
139
|
+
# After this the modules remain in memory and stay included
|
140
|
+
# in the classes but are empty (have no methods).
|
141
|
+
lock.synchronize do
|
142
|
+
if mod = probe.instrumentation_module
|
143
|
+
mod.send(:remove_method, probe.method_name)
|
144
|
+
probe.instrumentation_module = nil
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Instruments a particluar line in a source file.
|
150
|
+
# Note that this method only works for physical files,
|
151
|
+
# not for eval'd code, unless the eval'd code is associated with
|
152
|
+
# a file name and client invokes this method with the correct
|
153
|
+
# file name for the eval'd code.
|
154
|
+
def hook_line(probe, &block)
|
155
|
+
unless block
|
156
|
+
raise ArgumentError, 'No block given to hook_line'
|
157
|
+
end
|
158
|
+
|
159
|
+
lock.synchronize do
|
160
|
+
if probe.instrumentation_trace_point
|
161
|
+
# Already instrumented, warn?
|
162
|
+
return
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
line_no = probe.line_no!
|
167
|
+
rate_limiter = probe.rate_limiter
|
168
|
+
|
169
|
+
# Memoize the value to ensure this method always uses the same
|
170
|
+
# value for the setting.
|
171
|
+
# Normally none of the settings should change, but in the test suite
|
172
|
+
# we use mock objects and the methods may be mocked with
|
173
|
+
# individual invocations, yielding different return values on
|
174
|
+
# different calls to the same method.
|
175
|
+
permit_untargeted_trace_points = settings.dynamic_instrumentation.untargeted_trace_points
|
176
|
+
|
177
|
+
iseq = nil
|
178
|
+
if code_tracker
|
179
|
+
iseq = code_tracker.iseqs_for_path_suffix(probe.file).first # steep:ignore
|
180
|
+
unless iseq
|
181
|
+
if permit_untargeted_trace_points
|
182
|
+
# Continue withoout targeting the trace point.
|
183
|
+
# This is going to cause a serious performance penalty for
|
184
|
+
# the entire file containing the line to be instrumented.
|
185
|
+
else
|
186
|
+
# Do not use untargeted trace points unless they have been
|
187
|
+
# explicitly requested by the user, since they cause a
|
188
|
+
# serious performance penalty.
|
189
|
+
#
|
190
|
+
# If the requested file is not in code tracker's registry,
|
191
|
+
# or the code tracker does not exist at all,
|
192
|
+
# do not attempt to instrumnet now.
|
193
|
+
# The caller should add the line to the list of pending lines
|
194
|
+
# to instrument and install the hook when the file in
|
195
|
+
# question is loaded (and hopefully, by then code tracking
|
196
|
+
# is active, otherwise the line will never be instrumented.)
|
197
|
+
raise Error::DITargetNotDefined, "File not in code tracker registry: #{probe.file}"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
elsif !permit_untargeted_trace_points
|
201
|
+
# Same as previous comment, if untargeted trace points are not
|
202
|
+
# explicitly defined, and we do not have code tracking, do not
|
203
|
+
# instrument the method.
|
204
|
+
raise Error::DITargetNotDefined, "File not in code tracker registry: #{probe.file}"
|
205
|
+
end
|
206
|
+
|
207
|
+
# If trace point is not targeted, we only need one trace point per file.
|
208
|
+
# Creating a trace point for each probe does work but the performance
|
209
|
+
# penalty will be taken for each trace point defined in the file.
|
210
|
+
# Since untargeted trace points are only (currently) used internally
|
211
|
+
# for benchmarking, and shouldn't be used in customer applications,
|
212
|
+
# we always create a trace point here to reduce complexity.
|
213
|
+
#
|
214
|
+
# For targeted trace points, if multiple probes target the same
|
215
|
+
# file and line, we also only need one trace point, but since the
|
216
|
+
# overhead of targeted trace points is minimal, don't worry about
|
217
|
+
# this optimization just yet and create a trace point for each probe.
|
218
|
+
|
219
|
+
tp = TracePoint.new(:line) do |tp|
|
220
|
+
# If trace point is not targeted, we must verify that the invocation
|
221
|
+
# is the file & line that we want, because untargeted trace points
|
222
|
+
# are invoked for *each* line of Ruby executed.
|
223
|
+
if iseq || tp.lineno == probe.line_no && probe.file_matches?(tp.path)
|
224
|
+
if rate_limiter.nil? || rate_limiter.allow?
|
225
|
+
# & is to stop steep complaints, block is always present here.
|
226
|
+
block&.call(probe: probe, trace_point: tp, caller_locations: caller_locations)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
rescue => exc
|
230
|
+
raise if settings.dynamic_instrumentation.propagate_all_exceptions
|
231
|
+
logger.warn("Unhandled exception in line trace point: #{exc.class}: #{exc}")
|
232
|
+
# TODO test this path
|
233
|
+
end
|
234
|
+
|
235
|
+
# TODO internal check - remove or use a proper exception
|
236
|
+
if !iseq && !permit_untargeted_trace_points
|
237
|
+
raise "Trying to use an untargeted trace point when user did not permit it"
|
238
|
+
end
|
239
|
+
|
240
|
+
lock.synchronize do
|
241
|
+
if probe.instrumentation_trace_point
|
242
|
+
# Already instrumented in another thread, warn?
|
243
|
+
return
|
244
|
+
end
|
245
|
+
|
246
|
+
probe.instrumentation_trace_point = tp
|
247
|
+
|
248
|
+
if iseq
|
249
|
+
tp.enable(target: iseq, target_line: line_no)
|
250
|
+
else
|
251
|
+
tp.enable
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def unhook_line(probe)
|
257
|
+
lock.synchronize do
|
258
|
+
if tp = probe.instrumentation_trace_point
|
259
|
+
tp.disable
|
260
|
+
probe.instrumentation_trace_point = nil
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def hook(probe, &block)
|
266
|
+
if probe.method?
|
267
|
+
hook_method(probe, &block)
|
268
|
+
elsif probe.line?
|
269
|
+
hook_line(probe, &block)
|
270
|
+
else
|
271
|
+
# TODO add test coverage for this path
|
272
|
+
logger.warn("Unknown probe type to hook: #{probe}")
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def unhook(probe)
|
277
|
+
if probe.method?
|
278
|
+
unhook_method(probe)
|
279
|
+
elsif probe.line?
|
280
|
+
unhook_line(probe)
|
281
|
+
else
|
282
|
+
# TODO add test coverage for this path
|
283
|
+
logger.warn("Unknown probe type to unhook: #{probe}")
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
private
|
288
|
+
|
289
|
+
attr_reader :lock
|
290
|
+
|
291
|
+
# TODO test that this resolves qualified names e.g. A::B
|
292
|
+
def symbolize_class_name(cls_name)
|
293
|
+
Object.const_get(cls_name)
|
294
|
+
rescue NameError => exc
|
295
|
+
raise Error::DITargetNotDefined, "Class not defined: #{cls_name}: #{exc.class}: #{exc}"
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# rubocop:enable Lint/AssignmentInCondition
|