datadog 2.7.1 → 2.9.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 (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