datadog 2.7.1 → 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -1
  3. data/ext/datadog_profiling_native_extension/clock_id.h +2 -2
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +64 -54
  5. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
  6. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.h +1 -1
  7. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +16 -16
  8. data/ext/datadog_profiling_native_extension/collectors_stack.c +7 -7
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +259 -132
  10. data/ext/datadog_profiling_native_extension/extconf.rb +0 -8
  11. data/ext/datadog_profiling_native_extension/heap_recorder.c +11 -89
  12. data/ext/datadog_profiling_native_extension/heap_recorder.h +1 -1
  13. data/ext/datadog_profiling_native_extension/http_transport.c +4 -4
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +4 -1
  15. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +3 -1
  16. data/ext/datadog_profiling_native_extension/profiling.c +10 -8
  17. data/ext/datadog_profiling_native_extension/ruby_helpers.c +8 -8
  18. data/ext/datadog_profiling_native_extension/stack_recorder.c +54 -88
  19. data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -1
  20. data/ext/datadog_profiling_native_extension/time_helpers.h +1 -1
  21. data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.c +47 -0
  22. data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.h +31 -0
  23. data/ext/libdatadog_api/crashtracker.c +3 -0
  24. data/ext/libdatadog_extconf_helpers.rb +1 -1
  25. data/lib/datadog/appsec/assets/waf_rules/recommended.json +355 -157
  26. data/lib/datadog/appsec/assets/waf_rules/strict.json +62 -32
  27. data/lib/datadog/appsec/component.rb +1 -8
  28. data/lib/datadog/appsec/context.rb +54 -0
  29. data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +73 -0
  30. data/lib/datadog/appsec/contrib/active_record/integration.rb +41 -0
  31. data/lib/datadog/appsec/contrib/active_record/patcher.rb +53 -0
  32. data/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb +6 -6
  33. data/lib/datadog/appsec/contrib/devise/patcher/registration_controller_patch.rb +4 -4
  34. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +19 -28
  35. data/lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb +5 -5
  36. data/lib/datadog/appsec/contrib/rack/gateway/response.rb +3 -3
  37. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +64 -96
  38. data/lib/datadog/appsec/contrib/rack/reactive/request.rb +10 -10
  39. data/lib/datadog/appsec/contrib/rack/reactive/request_body.rb +5 -5
  40. data/lib/datadog/appsec/contrib/rack/reactive/response.rb +6 -6
  41. data/lib/datadog/appsec/contrib/rack/request_body_middleware.rb +10 -11
  42. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +43 -49
  43. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +21 -32
  44. data/lib/datadog/appsec/contrib/rails/patcher.rb +1 -1
  45. data/lib/datadog/appsec/contrib/rails/reactive/action.rb +6 -6
  46. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +41 -63
  47. data/lib/datadog/appsec/contrib/sinatra/patcher.rb +2 -2
  48. data/lib/datadog/appsec/contrib/sinatra/reactive/routed.rb +5 -5
  49. data/lib/datadog/appsec/event.rb +6 -6
  50. data/lib/datadog/appsec/ext.rb +3 -1
  51. data/lib/datadog/appsec/monitor/gateway/watcher.rb +22 -32
  52. data/lib/datadog/appsec/monitor/reactive/set_user.rb +5 -5
  53. data/lib/datadog/appsec/processor/context.rb +2 -2
  54. data/lib/datadog/appsec/processor/rule_loader.rb +0 -3
  55. data/lib/datadog/appsec/remote.rb +1 -3
  56. data/lib/datadog/appsec/response.rb +7 -11
  57. data/lib/datadog/appsec.rb +6 -5
  58. data/lib/datadog/auto_instrument.rb +3 -0
  59. data/lib/datadog/core/configuration/agent_settings_resolver.rb +39 -11
  60. data/lib/datadog/core/configuration/components.rb +20 -2
  61. data/lib/datadog/core/configuration/settings.rb +10 -0
  62. data/lib/datadog/core/configuration.rb +10 -2
  63. data/lib/datadog/{tracing → core}/contrib/rails/utils.rb +1 -3
  64. data/lib/datadog/core/crashtracking/component.rb +1 -3
  65. data/lib/datadog/core/remote/client/capabilities.rb +6 -0
  66. data/lib/datadog/core/remote/client.rb +65 -59
  67. data/lib/datadog/core/telemetry/component.rb +9 -3
  68. data/lib/datadog/core/telemetry/event.rb +87 -3
  69. data/lib/datadog/core/telemetry/ext.rb +1 -0
  70. data/lib/datadog/core/telemetry/logging.rb +2 -2
  71. data/lib/datadog/core/telemetry/metric.rb +22 -0
  72. data/lib/datadog/core/telemetry/worker.rb +33 -0
  73. data/lib/datadog/di/base.rb +115 -0
  74. data/lib/datadog/di/code_tracker.rb +11 -7
  75. data/lib/datadog/di/component.rb +21 -11
  76. data/lib/datadog/di/configuration/settings.rb +11 -1
  77. data/lib/datadog/di/contrib/active_record.rb +1 -0
  78. data/lib/datadog/di/contrib/railtie.rb +15 -0
  79. data/lib/datadog/di/contrib.rb +26 -0
  80. data/lib/datadog/di/error.rb +5 -0
  81. data/lib/datadog/di/instrumenter.rb +111 -20
  82. data/lib/datadog/di/preload.rb +18 -0
  83. data/lib/datadog/di/probe.rb +11 -1
  84. data/lib/datadog/di/probe_builder.rb +1 -0
  85. data/lib/datadog/di/probe_manager.rb +8 -5
  86. data/lib/datadog/di/probe_notification_builder.rb +27 -7
  87. data/lib/datadog/di/probe_notifier_worker.rb +5 -6
  88. data/lib/datadog/di/remote.rb +124 -0
  89. data/lib/datadog/di/serializer.rb +14 -7
  90. data/lib/datadog/di/transport.rb +3 -5
  91. data/lib/datadog/di/utils.rb +7 -0
  92. data/lib/datadog/di.rb +23 -62
  93. data/lib/datadog/kit/appsec/events.rb +3 -3
  94. data/lib/datadog/kit/identity.rb +4 -4
  95. data/lib/datadog/profiling/component.rb +59 -69
  96. data/lib/datadog/profiling/http_transport.rb +1 -26
  97. data/lib/datadog/tracing/configuration/settings.rb +4 -8
  98. data/lib/datadog/tracing/contrib/action_cable/integration.rb +5 -2
  99. data/lib/datadog/tracing/contrib/action_mailer/integration.rb +6 -2
  100. data/lib/datadog/tracing/contrib/action_pack/integration.rb +5 -2
  101. data/lib/datadog/tracing/contrib/action_view/integration.rb +5 -2
  102. data/lib/datadog/tracing/contrib/active_job/integration.rb +5 -2
  103. data/lib/datadog/tracing/contrib/active_record/integration.rb +6 -2
  104. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +3 -1
  105. data/lib/datadog/tracing/contrib/active_support/cache/instrumentation.rb +3 -1
  106. data/lib/datadog/tracing/contrib/active_support/cache/redis.rb +16 -4
  107. data/lib/datadog/tracing/contrib/active_support/configuration/settings.rb +10 -0
  108. data/lib/datadog/tracing/contrib/active_support/integration.rb +5 -2
  109. data/lib/datadog/tracing/contrib/auto_instrument.rb +2 -2
  110. data/lib/datadog/tracing/contrib/aws/integration.rb +3 -0
  111. data/lib/datadog/tracing/contrib/concurrent_ruby/integration.rb +3 -0
  112. data/lib/datadog/tracing/contrib/elasticsearch/configuration/settings.rb +4 -0
  113. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +6 -1
  114. data/lib/datadog/tracing/contrib/httprb/integration.rb +3 -0
  115. data/lib/datadog/tracing/contrib/kafka/integration.rb +3 -0
  116. data/lib/datadog/tracing/contrib/mongodb/integration.rb +3 -0
  117. data/lib/datadog/tracing/contrib/opensearch/integration.rb +3 -0
  118. data/lib/datadog/tracing/contrib/presto/integration.rb +3 -0
  119. data/lib/datadog/tracing/contrib/rack/integration.rb +2 -2
  120. data/lib/datadog/tracing/contrib/rails/framework.rb +2 -2
  121. data/lib/datadog/tracing/contrib/rails/patcher.rb +1 -1
  122. data/lib/datadog/tracing/contrib/rest_client/integration.rb +3 -0
  123. data/lib/datadog/tracing/span.rb +12 -4
  124. data/lib/datadog/tracing/span_event.rb +123 -3
  125. data/lib/datadog/tracing/span_operation.rb +6 -0
  126. data/lib/datadog/tracing/transport/serializable_trace.rb +24 -6
  127. data/lib/datadog/version.rb +2 -2
  128. data/lib/datadog.rb +3 -0
  129. metadata +30 -17
  130. data/lib/datadog/appsec/processor/actions.rb +0 -49
  131. data/lib/datadog/appsec/reactive/operation.rb +0 -68
  132. data/lib/datadog/appsec/scope.rb +0 -58
  133. data/lib/datadog/core/crashtracking/agent_base_url.rb +0 -21
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is loaded by datadog/di/preload.rb.
4
+ # It contains just the global DI reference to the (normally one and only)
5
+ # code tracker for the current process.
6
+ # This file should not require the rest of DI, specifically none of the
7
+ # contrib code that is meant to be loaded after third-party libraries
8
+ # are loaded, and also none of the rest of datadog library which also
9
+ # has contrib code in other products.
10
+
11
+ require_relative 'code_tracker'
12
+
13
+ module Datadog
14
+ # Namespace for Datadog dynamic instrumentation.
15
+ #
16
+ # @api private
17
+ module DI
18
+ LOCK = Mutex.new
19
+
20
+ class << self
21
+ attr_reader :code_tracker
22
+
23
+ # Activates code tracking. Normally this method should be called
24
+ # when the application starts. If instrumenting third-party code,
25
+ # code tracking needs to be enabled before the third-party libraries
26
+ # are loaded. Any third-party code loaded before code tracking is
27
+ # activated will NOT be instrumentable using dynamic instrumentation.
28
+ #
29
+ # TODO test that activating tracker multiple times preserves
30
+ # existing mappings in the registry
31
+ def activate_tracking!
32
+ (@code_tracker ||= CodeTracker.new).start
33
+ end
34
+
35
+ # Activates code tracking if possible.
36
+ #
37
+ # This method does nothing if invoked in an environment that does not
38
+ # implement required trace points for code tracking (MRI Ruby < 2.6,
39
+ # JRuby) and rescues any exceptions that may be raised by downstream
40
+ # DI code.
41
+ def activate_tracking
42
+ # :script_compiled trace point was added in Ruby 2.6.
43
+ return unless RUBY_VERSION >= '2.6'
44
+
45
+ begin
46
+ # Activate code tracking by default because line trace points will not work
47
+ # without it.
48
+ Datadog::DI.activate_tracking!
49
+ rescue => exc
50
+ if defined?(Datadog.logger)
51
+ Datadog.logger.warn { "di: Failed to activate code tracking for DI: #{exc.class}: #{exc}" }
52
+ else
53
+ # We do not have Datadog logger potentially because DI code tracker is
54
+ # being loaded early in application boot process and the rest of datadog
55
+ # wasn't loaded yet. Output to standard error.
56
+ warn("datadog: di: Failed to activate code tracking for DI: #{exc.class}: #{exc}")
57
+ end
58
+ end
59
+ end
60
+
61
+ # Deactivates code tracking. In normal usage of DI this method should
62
+ # never be called, however it is used by DI's test suite to reset
63
+ # state for individual tests.
64
+ #
65
+ # Note that deactivating tracking clears out the registry, losing
66
+ # the ability to look up files that have been loaded into the process
67
+ # already.
68
+ def deactivate_tracking!
69
+ code_tracker&.stop
70
+ end
71
+
72
+ # Returns whether code tracking is available.
73
+ # This method should be used instead of querying #code_tracker
74
+ # because the latter one may be nil.
75
+ def code_tracking_active?
76
+ code_tracker&.active? || false
77
+ end
78
+
79
+ # DI code tracker is instantiated globally before the regular set of
80
+ # components is created, but the code tracker needs to call out to the
81
+ # "current" DI component to perform instrumentation when application
82
+ # code is loaded. Because this call may happen prior to Datadog
83
+ # components having been initialized, we maintain the "current component"
84
+ # which contains a reference to the most recently instantiated
85
+ # DI::Component. This way, if a DI component hasn't been instantiated,
86
+ # we do not try to reference Datadog.components.
87
+ # In other words, this method exists so that we never attempt to call
88
+ # Datadog.components from the code tracker.
89
+ def current_component
90
+ LOCK.synchronize do
91
+ @current_components&.last
92
+ end
93
+ end
94
+
95
+ # To avoid potential races with DI::Component being added and removed,
96
+ # we maintain a list of the components. Normally the list should contain
97
+ # either zero or one component depending on whether DI is enabled in
98
+ # Datadog configuration. However, if a new instance of DI::Component
99
+ # is created while the previous instance is still running, we are
100
+ # guaranteed to not end up with no component when one is running.
101
+ def add_current_component(component)
102
+ LOCK.synchronize do
103
+ @current_components ||= []
104
+ @current_components << component
105
+ end
106
+ end
107
+
108
+ def remove_current_component(component)
109
+ LOCK.synchronize do
110
+ @current_components&.delete(component)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  # rubocop:disable Lint/AssignmentInCondition
4
4
 
5
+ require_relative 'error'
6
+
5
7
  module Datadog
6
8
  module DI
7
9
  # Tracks loaded Ruby code by source file and maintains a map from
@@ -78,19 +80,21 @@ module Datadog
78
80
  registry_lock.synchronize do
79
81
  registry[path] = tp.instruction_sequence
80
82
  end
81
- end
82
-
83
- DI.component&.probe_manager&.install_pending_line_probes(path)
84
83
 
84
+ # Also, pending line probes should only be installed for
85
+ # non-eval'd code.
86
+ DI.current_component&.probe_manager&.install_pending_line_probes(path)
87
+ end
85
88
  # Since this method normally is called from customer applications,
86
89
  # rescue any exceptions that might not be handled to not break said
87
90
  # customer applications.
88
91
  rescue => exc
89
- # TODO we do not have DI.component defined yet, remove steep:ignore
90
- # before release.
91
- if component = DI.component # steep:ignore
92
+ # Code tracker may be loaded without the rest of DI,
93
+ # in which case DI.component will not yet be defined,
94
+ # but we will have DI.current_component (set to nil).
95
+ if component = DI.current_component
92
96
  raise if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
93
- component.logger.warn("Unhandled exception in script_compiled trace point: #{exc.class}: #{exc}")
97
+ component.logger.debug { "di: unhandled exception in script_compiled trace point: #{exc.class}: #{exc}" }
94
98
  component.telemetry&.report(exc, description: "Unhandled exception in script_compiled trace point")
95
99
  # TODO test this path
96
100
  else
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../core'
4
+
3
5
  module Datadog
4
6
  module DI
5
7
  # Component for dynamic instrumentation.
@@ -14,20 +16,22 @@ module Datadog
14
16
  # resources and installed tracepoints upon shutdown.
15
17
  class Component
16
18
  class << self
17
- def build(settings, agent_settings, telemetry: nil)
19
+ def build(settings, agent_settings, logger, telemetry: nil)
18
20
  return unless settings.respond_to?(:dynamic_instrumentation) && settings.dynamic_instrumentation.enabled
19
21
 
20
22
  unless settings.respond_to?(:remote) && settings.remote.enabled
21
- Datadog.logger.debug("Dynamic Instrumentation could not be enabled because Remote Configuration Management is not available. To enable Remote Configuration, see https://docs.datadoghq.com/agent/remote_config")
23
+ logger.warn("di: dynamic instrumentation could not be enabled because Remote Configuration Management is not available. To enable Remote Configuration, see https://docs.datadoghq.com/agent/remote_config")
22
24
  return
23
25
  end
24
26
 
25
- return unless environment_supported?(settings)
27
+ return unless environment_supported?(settings, logger)
26
28
 
27
- new(settings, agent_settings, Datadog.logger, code_tracker: DI.code_tracker, telemetry: telemetry)
29
+ new(settings, agent_settings, logger, code_tracker: DI.code_tracker, telemetry: telemetry).tap do |component|
30
+ DI.add_current_component(component)
31
+ end
28
32
  end
29
33
 
30
- def build!(settings, agent_settings, telemetry: nil)
34
+ def build!(settings, agent_settings, logger, telemetry: nil)
31
35
  unless settings.respond_to?(:dynamic_instrumentation) && settings.dynamic_instrumentation.enabled
32
36
  raise "Requested DI component but DI is not enabled in settings"
33
37
  end
@@ -36,27 +40,31 @@ module Datadog
36
40
  raise "Requested DI component but remote config is not enabled in settings"
37
41
  end
38
42
 
39
- unless environment_supported?(settings)
43
+ unless environment_supported?(settings, logger)
40
44
  raise "DI does not support the environment (development or Ruby version too low or not MRI)"
41
45
  end
42
46
 
43
- new(settings, agent_settings, Datadog.logger, code_tracker: DI.code_tracker, telemetry: telemetry)
47
+ new(settings, agent_settings, logger, code_tracker: DI.code_tracker, telemetry: telemetry)
44
48
  end
45
49
 
46
50
  # Checks whether the runtime environment is supported by
47
51
  # dynamic instrumentation. Currently we only require that, if Rails
48
52
  # is used, that Rails environment is not development because
49
53
  # DI does not currently support code unloading and reloading.
50
- def environment_supported?(settings)
54
+ def environment_supported?(settings, logger)
51
55
  # TODO add tests?
52
56
  unless settings.dynamic_instrumentation.internal.development
53
57
  if Datadog::Core::Environment::Execution.development?
54
- Datadog.logger.debug("Not enabling dynamic instrumentation because we are in development environment")
58
+ logger.warn("di: development environment detected; not enabling dynamic instrumentation")
55
59
  return false
56
60
  end
57
61
  end
58
- if RUBY_ENGINE != 'ruby' || RUBY_VERSION < '2.6'
59
- Datadog.logger.debug("Not enabling dynamic instrumentation because of unsupported Ruby version")
62
+ if RUBY_ENGINE != 'ruby'
63
+ logger.warn("di: cannot enable dynamic instrumentation: MRI is required, but running on #{RUBY_ENGINE}")
64
+ return false
65
+ end
66
+ if RUBY_VERSION < '2.6'
67
+ logger.warn("di: cannot enable dynamic instrumentation: Ruby 2.6+ is required, but running on #{RUBY_VERSION}")
60
68
  return false
61
69
  end
62
70
  true
@@ -99,6 +107,8 @@ module Datadog
99
107
  # was replaced by a new instance, the new instance of it wouldn't have
100
108
  # any of the already loaded code tracked.
101
109
  def shutdown!(replacement = nil)
110
+ DI.remove_current_component(self)
111
+
102
112
  probe_manager.clear_hooks
103
113
  probe_manager.close
104
114
  probe_notifier_worker.stop
@@ -166,10 +166,20 @@ module Datadog
166
166
  # being sent out by the probe notifier worker) and creates a
167
167
  # possibility of dropping payloads if the queue gets too long.
168
168
  option :min_send_interval do |o|
169
- o.type :int
169
+ o.type :float
170
170
  o.default 3
171
171
  end
172
172
 
173
+ # Number of snapshots that can be stored in the probe
174
+ # notifier worker queue. Larger capacity runs the risk of
175
+ # creating snapshots that exceed the agent's request size
176
+ # limit. Smaller capacity increases the risk of dropping
177
+ # snapshots.
178
+ option :snapshot_queue_capacity do |o|
179
+ o.type :int
180
+ o.default 100
181
+ end
182
+
173
183
  # Enable dynamic instrumentation in development environments.
174
184
  # Currently DI does not fully implement support for code
175
185
  # unloading and reloading, and is not supported in
@@ -5,6 +5,7 @@ Datadog::DI::Serializer.register(condition: lambda { |value| ActiveRecord::Base
5
5
  # steep:ignore:start
6
6
  value_to_serialize = {
7
7
  attributes: value.attributes,
8
+ new_record: value.new_record?,
8
9
  }
9
10
  serializer.serialize_value(value_to_serialize, depth: depth ? depth - 1 : nil, type: value.class)
10
11
  # steep:ignore:end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ module Contrib
6
+ # Railtie class initializes dynamic instrumentation contrib code
7
+ # in Rails environments.
8
+ class Railtie < Rails::Railtie
9
+ initializer 'datadog.dynamic_instrumentation.initialize' do |app|
10
+ Contrib.load_now
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../core/contrib/rails/utils'
4
+
5
+ module Datadog
6
+ module DI
7
+ module Contrib
8
+ module_function def load_now_or_later
9
+ if Datadog::Core::Contrib::Rails::Utils.railtie_supported?
10
+ require_relative 'contrib/railtie'
11
+ else
12
+ load_now
13
+ end
14
+ end
15
+
16
+ # This method can be called more than once, to attempt to load
17
+ # DI components that depend on third-party libraries after additional
18
+ # dependencies are loaded (or potentially loaded).
19
+ module_function def load_now
20
+ if defined?(ActiveRecord::Base)
21
+ require_relative 'contrib/active_record'
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -27,6 +27,11 @@ module Datadog
27
27
  class DITargetNotDefined < Error
28
28
  end
29
29
 
30
+ # Attempting to instrument a line and the file containing the line
31
+ # was loaded prior to code tracking being enabled.
32
+ class DITargetNotInRegistry < Error
33
+ end
34
+
30
35
  # Raised when trying to install a probe whose installation failed
31
36
  # earlier in the same process. This exception should contain the
32
37
  # original exception report from initial installation attempt.
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Lint/AssignmentInCondition
3
+ require_relative '../core/utils/time'
4
4
 
5
- require 'benchmark'
5
+ # rubocop:disable Lint/AssignmentInCondition
6
6
 
7
7
  module Datadog
8
8
  module DI
@@ -92,34 +92,88 @@ module Datadog
92
92
  cls = symbolize_class_name(probe.type_name)
93
93
  serializer = self.serializer
94
94
  method_name = probe.method_name
95
- target_method = cls.instance_method(method_name)
96
- loc = target_method.source_location
95
+ loc = begin
96
+ cls.instance_method(method_name).source_location
97
+ rescue NameError
98
+ # The target method is not defined.
99
+ # This could be because it will be explicitly defined later
100
+ # (since classes can be reopened in Ruby)
101
+ # or the method is virtual (provided by a method_missing handler).
102
+ # In these cases we do not have a source location for the
103
+ # target method here.
104
+ end
97
105
  rate_limiter = probe.rate_limiter
106
+ settings = self.settings
98
107
 
99
108
  mod = Module.new do
100
- define_method(method_name) do |*args, **kwargs| # steep:ignore
109
+ define_method(method_name) do |*args, **kwargs, &target_block| # steep:ignore
101
110
  if rate_limiter.nil? || rate_limiter.allow?
102
111
  # Arguments may be mutated by the method, therefore
103
112
  # they need to be serialized prior to method invocation.
104
113
  entry_args = if probe.capture_snapshot?
105
- serializer.serialize_args(args, kwargs)
114
+ serializer.serialize_args(args, kwargs,
115
+ depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
116
+ attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count)
106
117
  end
107
- rv = nil
108
- duration = Benchmark.realtime do # steep:ignore
109
- rv = super(*args, **kwargs)
118
+ start_time = Core::Utils::Time.get_time
119
+ # Under Ruby 2.6 we cannot just call super(*args, **kwargs)
120
+ # for methods defined via method_missing.
121
+ rv = if args.any?
122
+ if kwargs.any?
123
+ super(*args, **kwargs, &target_block)
124
+ else
125
+ super(*args, &target_block)
126
+ end
127
+ elsif kwargs.any?
128
+ super(**kwargs, &target_block)
129
+ else
130
+ super(&target_block)
110
131
  end
132
+ duration = Core::Utils::Time.get_time - start_time
111
133
  # The method itself is not part of the stack trace because
112
134
  # we are getting the stack trace from outside of the method.
113
135
  # Add the method in manually as the top frame.
114
- method_frame = Location.new(loc.first, loc.last, method_name)
115
- caller_locs = [method_frame] + caller_locations # steep:ignore
136
+ method_frame = if loc
137
+ [Location.new(loc.first, loc.last, method_name)]
138
+ else
139
+ # For virtual and lazily-defined methods, we do not have
140
+ # the original source location here, and they won't be
141
+ # included in the stack trace currently.
142
+ # TODO when begin/end trace points are added for local
143
+ # variable capture in method probes, we should be able
144
+ # to obtain actual method execution location and use
145
+ # that location here.
146
+ []
147
+ end
148
+ caller_locs = method_frame + caller_locations # steep:ignore
116
149
  # TODO capture arguments at exit
117
150
  # & is to stop steep complaints, block is always present here.
118
151
  block&.call(probe: probe, rv: rv, duration: duration, caller_locations: caller_locs,
119
152
  serialized_entry_args: entry_args)
120
153
  rv
121
154
  else
122
- super(*args, **kwargs)
155
+ # stop standard from trying to mess up my code
156
+ _ = 42
157
+
158
+ # The necessity to invoke super in each of these specific
159
+ # ways is very difficult to test.
160
+ # Existing tests, even though I wrote many, still don't
161
+ # cause a failure if I replace all of the below with a
162
+ # simple super(*args, **kwargs, &target_block).
163
+ # But, let's be safe and go through the motions in case
164
+ # there is actually a legitimate need for the breakdown.
165
+ # TODO figure out how to test this properly.
166
+ if args.any?
167
+ if kwargs.any?
168
+ super(*args, **kwargs, &target_block)
169
+ else
170
+ super(*args, &target_block)
171
+ end
172
+ elsif kwargs.any?
173
+ super(**kwargs, &target_block)
174
+ else
175
+ super(&target_block)
176
+ end
123
177
  end
124
178
  end
125
179
  end
@@ -191,11 +245,12 @@ module Datadog
191
245
  #
192
246
  # If the requested file is not in code tracker's registry,
193
247
  # or the code tracker does not exist at all,
194
- # do not attempt to instrumnet now.
248
+ # do not attempt to instrument now.
195
249
  # The caller should add the line to the list of pending lines
196
250
  # to instrument and install the hook when the file in
197
251
  # question is loaded (and hopefully, by then code tracking
198
252
  # is active, otherwise the line will never be instrumented.)
253
+ raise_if_probe_in_loaded_features(probe)
199
254
  raise Error::DITargetNotDefined, "File not in code tracker registry: #{probe.file}"
200
255
  end
201
256
  end
@@ -203,6 +258,7 @@ module Datadog
203
258
  # Same as previous comment, if untargeted trace points are not
204
259
  # explicitly defined, and we do not have code tracking, do not
205
260
  # instrument the method.
261
+ raise_if_probe_in_loaded_features(probe)
206
262
  raise Error::DITargetNotDefined, "File not in code tracker registry: #{probe.file}"
207
263
  end
208
264
 
@@ -222,12 +278,25 @@ module Datadog
222
278
  # overhead of targeted trace points is minimal, don't worry about
223
279
  # this optimization just yet and create a trace point for each probe.
224
280
 
225
- tp = TracePoint.new(:line) do |tp|
281
+ types = if iseq
282
+ # When targeting trace points we can target the 'end' line of a method.
283
+ # However, by adding the :return trace point we lose diagnostics
284
+ # for lines that contain no executable code (e.g. comments only)
285
+ # and thus cannot actually be instrumented.
286
+ [:line, :return, :b_return]
287
+ else
288
+ [:line]
289
+ end
290
+ tp = TracePoint.new(*types) do |tp|
226
291
  begin
227
292
  # If trace point is not targeted, we must verify that the invocation
228
293
  # is the file & line that we want, because untargeted trace points
229
294
  # are invoked for *each* line of Ruby executed.
230
- if iseq || tp.lineno == probe.line_no && probe.file_matches?(tp.path)
295
+ # TODO find out exactly when the path in trace point is relative.
296
+ # Looks like this is the case when line trace point is not targeted?
297
+ if iseq || tp.lineno == probe.line_no && (
298
+ probe.file == tp.path || probe.file_matches?(tp.path)
299
+ )
231
300
  if rate_limiter.nil? || rate_limiter.allow?
232
301
  # & is to stop steep complaints, block is always present here.
233
302
  block&.call(probe: probe, trace_point: tp, caller_locations: caller_locations)
@@ -235,13 +304,13 @@ module Datadog
235
304
  end
236
305
  rescue => exc
237
306
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
238
- logger.warn("Unhandled exception in line trace point: #{exc.class}: #{exc}")
307
+ logger.debug { "di: unhandled exception in line trace point: #{exc.class}: #{exc}" }
239
308
  telemetry&.report(exc, description: "Unhandled exception in line trace point")
240
309
  # TODO test this path
241
310
  end
242
311
  rescue => exc
243
- raise if settings.dynamic_instrumentation.propagate_all_exceptions
244
- logger.warn("Unhandled exception in line trace point: #{exc.class}: #{exc}")
312
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
313
+ logger.debug { "di: unhandled exception in line trace point: #{exc.class}: #{exc}" }
245
314
  telemetry&.report(exc, description: "Unhandled exception in line trace point")
246
315
  # TODO test this path
247
316
  end
@@ -266,7 +335,9 @@ module Datadog
266
335
  else
267
336
  tp.enable
268
337
  end
338
+ # TracePoint#enable returns false when it succeeds.
269
339
  end
340
+ true
270
341
  end
271
342
 
272
343
  def unhook_line(probe)
@@ -285,7 +356,7 @@ module Datadog
285
356
  hook_line(probe, &block)
286
357
  else
287
358
  # TODO add test coverage for this path
288
- logger.warn("Unknown probe type to hook: #{probe}")
359
+ logger.debug { "di: unknown probe type to hook: #{probe}" }
289
360
  end
290
361
  end
291
362
 
@@ -296,7 +367,7 @@ module Datadog
296
367
  unhook_line(probe)
297
368
  else
298
369
  # TODO add test coverage for this path
299
- logger.warn("Unknown probe type to unhook: #{probe}")
370
+ logger.debug { "di: unknown probe type to unhook: #{probe}" }
300
371
  end
301
372
  end
302
373
 
@@ -304,6 +375,26 @@ module Datadog
304
375
 
305
376
  attr_reader :lock
306
377
 
378
+ def raise_if_probe_in_loaded_features(probe)
379
+ return unless probe.file
380
+
381
+ # If the probe file is in the list of loaded files
382
+ # (as per $LOADED_FEATURES, using either exact or suffix match),
383
+ # raise an error indicating that
384
+ # code tracker is missing the loaded file because the file
385
+ # won't be loaded again (DI only works in production environments
386
+ # that do not normally reload code).
387
+ if $LOADED_FEATURES.include?(probe.file)
388
+ raise Error::DITargetNotInRegistry, "File loaded but is not in code tracker registry: #{probe.file}"
389
+ end
390
+ # Ths is an expensive check
391
+ $LOADED_FEATURES.each do |path|
392
+ if Utils.path_matches_suffix?(path, probe.file)
393
+ raise Error::DITargetNotInRegistry, "File matching probe path (#{probe.file}) was loaded and is not in code tracker registry: #{path}"
394
+ end
395
+ end
396
+ end
397
+
307
398
  # TODO test that this resolves qualified names e.g. A::B
308
399
  def symbolize_class_name(cls_name)
309
400
  Object.const_get(cls_name)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Require 'datadog/di/preload' early in the application boot process to
4
+ # enable dynamic instrumentation for third-party libraries used by the
5
+ # application.
6
+
7
+ require_relative 'base'
8
+
9
+ # Code tracking is required for line probes to work; see the comments
10
+ # on the activate_tracking methods in di.rb for further details.
11
+ #
12
+ # Unlike di.rb which conditionally activates tracking only if the
13
+ # DD_DYNAMIC_INSTRUMENTATION_ENABLED environment variable is set, this file
14
+ # always activates tracking. This is because this file is explicitly loaded
15
+ # by customer applications for the purpose of enabling code tracking
16
+ # early in application boot process (i.e., before datadog library itself
17
+ # is loaded).
18
+ Datadog::DI.activate_tracking
@@ -36,7 +36,9 @@ module Datadog
36
36
 
37
37
  def initialize(id:, type:,
38
38
  file: nil, line_no: nil, type_name: nil, method_name: nil,
39
- template: nil, capture_snapshot: false, max_capture_depth: nil, rate_limit: nil)
39
+ template: nil, capture_snapshot: false, max_capture_depth: nil,
40
+ max_capture_attribute_count: nil,
41
+ rate_limit: nil)
40
42
  # Perform some sanity checks here to detect unexpected attribute
41
43
  # combinations, in order to not do them in subsequent code.
42
44
  unless KNOWN_TYPES.include?(type)
@@ -64,6 +66,7 @@ module Datadog
64
66
  @template = template
65
67
  @capture_snapshot = !!capture_snapshot
66
68
  @max_capture_depth = max_capture_depth
69
+ @max_capture_attribute_count = max_capture_attribute_count
67
70
 
68
71
  # These checks use instance methods that have more complex logic
69
72
  # than checking a single argument value. To avoid duplicating
@@ -91,6 +94,10 @@ module Datadog
91
94
  # the global default will be used.
92
95
  attr_reader :max_capture_depth
93
96
 
97
+ # Configured maximum capture attribute count. Can be nil in which case
98
+ # the global default will be used.
99
+ attr_reader :max_capture_attribute_count
100
+
94
101
  # Rate limit in effect, in invocations per second. Always present.
95
102
  attr_reader :rate_limit
96
103
 
@@ -154,6 +161,9 @@ module Datadog
154
161
  # If file is not an absolute path, the path matches if the file is its suffix,
155
162
  # at a path component boundary.
156
163
  def file_matches?(path)
164
+ if path.nil?
165
+ raise ArgumentError, "Cannot match against a nil path"
166
+ end
157
167
  unless file
158
168
  raise ArgumentError, "Probe does not have a file to match against"
159
169
  end
@@ -37,6 +37,7 @@ module Datadog
37
37
  template: config["template"],
38
38
  capture_snapshot: !!config["captureSnapshot"],
39
39
  max_capture_depth: config["capture"]&.[]("maxReferenceDepth"),
40
+ max_capture_attribute_count: config["capture"]&.[]("maxFieldCount"),
40
41
  rate_limit: config["sampling"]&.[]("snapshotsPerSecond"),
41
42
  )
42
43
  rescue KeyError => exc