datadog 2.4.0 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -1
  3. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +3 -3
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +57 -18
  5. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +93 -106
  6. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +8 -2
  7. data/ext/datadog_profiling_native_extension/extconf.rb +8 -8
  8. data/ext/datadog_profiling_native_extension/heap_recorder.c +174 -28
  9. data/ext/datadog_profiling_native_extension/heap_recorder.h +11 -0
  10. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +1 -1
  11. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +1 -1
  12. data/ext/datadog_profiling_native_extension/ruby_helpers.c +14 -11
  13. data/ext/datadog_profiling_native_extension/stack_recorder.c +58 -22
  14. data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -0
  15. data/ext/libdatadog_api/crashtracker.c +3 -5
  16. data/ext/libdatadog_extconf_helpers.rb +1 -1
  17. data/lib/datadog/appsec/configuration/settings.rb +8 -0
  18. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +1 -5
  19. data/lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb +7 -20
  20. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +9 -15
  21. data/lib/datadog/appsec/contrib/rack/reactive/request.rb +6 -18
  22. data/lib/datadog/appsec/contrib/rack/reactive/request_body.rb +7 -20
  23. data/lib/datadog/appsec/contrib/rack/reactive/response.rb +5 -18
  24. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +3 -1
  25. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +3 -5
  26. data/lib/datadog/appsec/contrib/rails/reactive/action.rb +5 -18
  27. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +6 -10
  28. data/lib/datadog/appsec/contrib/sinatra/reactive/routed.rb +7 -20
  29. data/lib/datadog/appsec/event.rb +24 -0
  30. data/lib/datadog/appsec/ext.rb +4 -0
  31. data/lib/datadog/appsec/monitor/gateway/watcher.rb +3 -5
  32. data/lib/datadog/appsec/monitor/reactive/set_user.rb +7 -20
  33. data/lib/datadog/appsec/processor/context.rb +107 -0
  34. data/lib/datadog/appsec/processor.rb +7 -71
  35. data/lib/datadog/appsec/scope.rb +1 -4
  36. data/lib/datadog/appsec/utils/trace_operation.rb +15 -0
  37. data/lib/datadog/appsec/utils.rb +2 -0
  38. data/lib/datadog/appsec.rb +1 -0
  39. data/lib/datadog/core/configuration/agent_settings_resolver.rb +26 -25
  40. data/lib/datadog/core/configuration/settings.rb +12 -0
  41. data/lib/datadog/core/configuration.rb +1 -3
  42. data/lib/datadog/core/crashtracking/component.rb +8 -5
  43. data/lib/datadog/core/environment/yjit.rb +5 -0
  44. data/lib/datadog/core/remote/transport/http.rb +5 -0
  45. data/lib/datadog/core/remote/worker.rb +1 -1
  46. data/lib/datadog/core/runtime/ext.rb +1 -0
  47. data/lib/datadog/core/runtime/metrics.rb +4 -0
  48. data/lib/datadog/core/semaphore.rb +35 -0
  49. data/lib/datadog/core/telemetry/logging.rb +10 -10
  50. data/lib/datadog/core/transport/ext.rb +1 -0
  51. data/lib/datadog/core/workers/async.rb +1 -1
  52. data/lib/datadog/di/code_tracker.rb +11 -13
  53. data/lib/datadog/di/instrumenter.rb +301 -0
  54. data/lib/datadog/di/probe.rb +29 -0
  55. data/lib/datadog/di/probe_builder.rb +7 -1
  56. data/lib/datadog/di/probe_notification_builder.rb +207 -0
  57. data/lib/datadog/di/probe_notifier_worker.rb +244 -0
  58. data/lib/datadog/di/serializer.rb +23 -1
  59. data/lib/datadog/di/transport.rb +67 -0
  60. data/lib/datadog/di/utils.rb +39 -0
  61. data/lib/datadog/di.rb +43 -0
  62. data/lib/datadog/profiling/collectors/thread_context.rb +9 -11
  63. data/lib/datadog/profiling/component.rb +1 -0
  64. data/lib/datadog/profiling/stack_recorder.rb +37 -9
  65. data/lib/datadog/tracing/component.rb +13 -0
  66. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +4 -0
  67. data/lib/datadog/tracing/contrib/excon/middleware.rb +3 -0
  68. data/lib/datadog/tracing/contrib/faraday/middleware.rb +3 -0
  69. data/lib/datadog/tracing/contrib/grape/endpoint.rb +5 -2
  70. data/lib/datadog/tracing/contrib/http/circuit_breaker.rb +9 -0
  71. data/lib/datadog/tracing/contrib/http/instrumentation.rb +4 -0
  72. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +4 -0
  73. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +4 -0
  74. data/lib/datadog/tracing/contrib/rails/runner.rb +1 -1
  75. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +3 -0
  76. data/lib/datadog/tracing/sampling/rule_sampler.rb +6 -4
  77. data/lib/datadog/tracing/tracer.rb +15 -10
  78. data/lib/datadog/tracing/transport/http.rb +4 -0
  79. data/lib/datadog/tracing/workers.rb +1 -1
  80. data/lib/datadog/tracing/writer.rb +26 -28
  81. data/lib/datadog/version.rb +1 -1
  82. metadata +22 -14
@@ -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 = Datadog::AppSec::Processor::Context.new(processor)
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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'utils/trace_operation'
4
+
3
5
  module Datadog
4
6
  module AppSec
5
7
  # Utilities for AppSec
@@ -4,6 +4,7 @@ require_relative 'appsec/configuration'
4
4
  require_relative 'appsec/extensions'
5
5
  require_relative 'appsec/scope'
6
6
  require_relative 'appsec/ext'
7
+ require_relative 'appsec/utils'
7
8
 
8
9
  module Datadog
9
10
  # Namespace for Datadog AppSec instrumentation
@@ -232,7 +232,32 @@ module Datadog
232
232
  end
233
233
 
234
234
  def should_use_uds?
235
- can_use_uds? && !mixed_http_and_uds?
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
- unless Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3')
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
- start_or_update_on_fork(action: :update_on_fork)
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} successfully")
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 unless Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3')
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
- stack_trace = +''
35
- backtrace.each do |line|
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
- stack_trace.chomp(',')
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 unless Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3')
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 iseqs_for_path(suffix)
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
- # Exact match is not possible here, meaning any matching path
120
- # has to be longer than the suffix. Require full component matches,
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