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