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/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
|