datadog 2.3.0 → 2.5.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 (173) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -2
  3. data/ext/datadog_profiling_loader/datadog_profiling_loader.c +9 -1
  4. data/ext/datadog_profiling_loader/extconf.rb +10 -22
  5. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +3 -3
  6. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +198 -41
  7. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +4 -2
  8. data/ext/datadog_profiling_native_extension/collectors_stack.c +89 -46
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +645 -107
  10. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +15 -1
  11. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +0 -27
  12. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -4
  13. data/ext/datadog_profiling_native_extension/extconf.rb +42 -25
  14. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +50 -0
  15. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +75 -0
  16. data/ext/datadog_profiling_native_extension/heap_recorder.c +194 -34
  17. data/ext/datadog_profiling_native_extension/heap_recorder.h +11 -0
  18. data/ext/datadog_profiling_native_extension/http_transport.c +38 -6
  19. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +1 -1
  20. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +53 -2
  21. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +3 -0
  22. data/ext/datadog_profiling_native_extension/profiling.c +1 -1
  23. data/ext/datadog_profiling_native_extension/ruby_helpers.c +14 -11
  24. data/ext/datadog_profiling_native_extension/stack_recorder.c +58 -22
  25. data/ext/datadog_profiling_native_extension/stack_recorder.h +2 -0
  26. data/ext/libdatadog_api/crashtracker.c +20 -18
  27. data/ext/libdatadog_api/datadog_ruby_common.c +0 -27
  28. data/ext/libdatadog_api/datadog_ruby_common.h +0 -4
  29. data/ext/libdatadog_extconf_helpers.rb +1 -1
  30. data/lib/datadog/appsec/assets/waf_rules/recommended.json +2184 -108
  31. data/lib/datadog/appsec/assets/waf_rules/strict.json +1430 -2
  32. data/lib/datadog/appsec/component.rb +29 -8
  33. data/lib/datadog/appsec/configuration/settings.rb +10 -2
  34. data/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb +1 -0
  35. data/lib/datadog/appsec/contrib/devise/patcher/rememberable_patch.rb +21 -0
  36. data/lib/datadog/appsec/contrib/devise/patcher.rb +12 -2
  37. data/lib/datadog/appsec/contrib/graphql/appsec_trace.rb +0 -14
  38. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +67 -31
  39. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +14 -15
  40. data/lib/datadog/appsec/contrib/graphql/integration.rb +14 -1
  41. data/lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb +7 -20
  42. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +2 -5
  43. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +9 -15
  44. data/lib/datadog/appsec/contrib/rack/reactive/request.rb +6 -18
  45. data/lib/datadog/appsec/contrib/rack/reactive/request_body.rb +7 -20
  46. data/lib/datadog/appsec/contrib/rack/reactive/response.rb +5 -18
  47. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +3 -1
  48. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +3 -5
  49. data/lib/datadog/appsec/contrib/rails/reactive/action.rb +5 -18
  50. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +6 -10
  51. data/lib/datadog/appsec/contrib/sinatra/reactive/routed.rb +7 -20
  52. data/lib/datadog/appsec/event.rb +25 -1
  53. data/lib/datadog/appsec/ext.rb +4 -0
  54. data/lib/datadog/appsec/monitor/gateway/watcher.rb +3 -5
  55. data/lib/datadog/appsec/monitor/reactive/set_user.rb +7 -20
  56. data/lib/datadog/appsec/processor/context.rb +109 -0
  57. data/lib/datadog/appsec/processor/rule_loader.rb +3 -1
  58. data/lib/datadog/appsec/processor/rule_merger.rb +33 -15
  59. data/lib/datadog/appsec/processor.rb +42 -107
  60. data/lib/datadog/appsec/rate_limiter.rb +25 -40
  61. data/lib/datadog/appsec/remote.rb +7 -3
  62. data/lib/datadog/appsec/scope.rb +1 -4
  63. data/lib/datadog/appsec/utils/trace_operation.rb +15 -0
  64. data/lib/datadog/appsec/utils.rb +2 -0
  65. data/lib/datadog/appsec.rb +3 -2
  66. data/lib/datadog/core/configuration/agent_settings_resolver.rb +26 -25
  67. data/lib/datadog/core/configuration/components.rb +4 -3
  68. data/lib/datadog/core/configuration/settings.rb +96 -5
  69. data/lib/datadog/core/configuration.rb +1 -3
  70. data/lib/datadog/core/crashtracking/component.rb +9 -6
  71. data/lib/datadog/core/environment/execution.rb +5 -5
  72. data/lib/datadog/core/environment/yjit.rb +5 -0
  73. data/lib/datadog/core/metrics/client.rb +7 -0
  74. data/lib/datadog/core/rate_limiter.rb +183 -0
  75. data/lib/datadog/core/remote/client/capabilities.rb +4 -3
  76. data/lib/datadog/core/remote/component.rb +4 -2
  77. data/lib/datadog/core/remote/negotiation.rb +4 -4
  78. data/lib/datadog/core/remote/tie.rb +2 -0
  79. data/lib/datadog/core/remote/transport/http.rb +5 -0
  80. data/lib/datadog/core/remote/worker.rb +1 -1
  81. data/lib/datadog/core/runtime/ext.rb +1 -0
  82. data/lib/datadog/core/runtime/metrics.rb +5 -1
  83. data/lib/datadog/core/semaphore.rb +35 -0
  84. data/lib/datadog/core/telemetry/component.rb +2 -0
  85. data/lib/datadog/core/telemetry/event.rb +12 -7
  86. data/lib/datadog/core/telemetry/logger.rb +51 -0
  87. data/lib/datadog/core/telemetry/logging.rb +50 -14
  88. data/lib/datadog/core/telemetry/request.rb +13 -1
  89. data/lib/datadog/core/transport/ext.rb +1 -0
  90. data/lib/datadog/core/utils/time.rb +12 -0
  91. data/lib/datadog/core/workers/async.rb +1 -1
  92. data/lib/datadog/di/code_tracker.rb +166 -0
  93. data/lib/datadog/di/configuration/settings.rb +163 -0
  94. data/lib/datadog/di/configuration.rb +11 -0
  95. data/lib/datadog/di/error.rb +31 -0
  96. data/lib/datadog/di/extensions.rb +16 -0
  97. data/lib/datadog/di/instrumenter.rb +301 -0
  98. data/lib/datadog/di/probe.rb +162 -0
  99. data/lib/datadog/di/probe_builder.rb +47 -0
  100. data/lib/datadog/di/probe_notification_builder.rb +207 -0
  101. data/lib/datadog/di/probe_notifier_worker.rb +244 -0
  102. data/lib/datadog/di/redactor.rb +188 -0
  103. data/lib/datadog/di/serializer.rb +215 -0
  104. data/lib/datadog/di/transport.rb +67 -0
  105. data/lib/datadog/di/utils.rb +39 -0
  106. data/lib/datadog/di.rb +57 -0
  107. data/lib/datadog/opentelemetry/sdk/propagator.rb +2 -0
  108. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +12 -10
  109. data/lib/datadog/profiling/collectors/info.rb +12 -3
  110. data/lib/datadog/profiling/collectors/thread_context.rb +32 -8
  111. data/lib/datadog/profiling/component.rb +21 -4
  112. data/lib/datadog/profiling/http_transport.rb +6 -1
  113. data/lib/datadog/profiling/scheduler.rb +2 -0
  114. data/lib/datadog/profiling/stack_recorder.rb +40 -9
  115. data/lib/datadog/single_step_instrument.rb +12 -0
  116. data/lib/datadog/tracing/component.rb +13 -0
  117. data/lib/datadog/tracing/contrib/action_cable/instrumentation.rb +8 -12
  118. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +5 -0
  119. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/instrumentation.rb +78 -0
  120. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/patcher.rb +33 -0
  121. data/lib/datadog/tracing/contrib/action_pack/patcher.rb +2 -0
  122. data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +4 -0
  123. data/lib/datadog/tracing/contrib/active_record/events/instantiation.rb +3 -1
  124. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +3 -1
  125. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +5 -1
  126. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +5 -0
  127. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +6 -1
  128. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +4 -0
  129. data/lib/datadog/tracing/contrib/excon/middleware.rb +3 -0
  130. data/lib/datadog/tracing/contrib/faraday/middleware.rb +12 -0
  131. data/lib/datadog/tracing/contrib/grape/endpoint.rb +24 -2
  132. data/lib/datadog/tracing/contrib/graphql/patcher.rb +9 -12
  133. data/lib/datadog/tracing/contrib/graphql/trace_patcher.rb +3 -3
  134. data/lib/datadog/tracing/contrib/graphql/tracing_patcher.rb +3 -3
  135. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +13 -9
  136. data/lib/datadog/tracing/contrib/graphql/unified_trace_patcher.rb +6 -3
  137. data/lib/datadog/tracing/contrib/http/circuit_breaker.rb +9 -0
  138. data/lib/datadog/tracing/contrib/http/instrumentation.rb +22 -15
  139. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +10 -5
  140. data/lib/datadog/tracing/contrib/httpclient/patcher.rb +1 -14
  141. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +9 -0
  142. data/lib/datadog/tracing/contrib/httprb/patcher.rb +1 -14
  143. data/lib/datadog/tracing/contrib/lograge/patcher.rb +1 -2
  144. data/lib/datadog/tracing/contrib/mongodb/subscribers.rb +2 -0
  145. data/lib/datadog/tracing/contrib/opensearch/patcher.rb +13 -6
  146. data/lib/datadog/tracing/contrib/patcher.rb +2 -1
  147. data/lib/datadog/tracing/contrib/presto/patcher.rb +1 -13
  148. data/lib/datadog/tracing/contrib/rack/middlewares.rb +27 -0
  149. data/lib/datadog/tracing/contrib/rails/runner.rb +1 -1
  150. data/lib/datadog/tracing/contrib/redis/tags.rb +4 -0
  151. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +3 -0
  152. data/lib/datadog/tracing/contrib/sinatra/tracer.rb +4 -0
  153. data/lib/datadog/tracing/contrib/stripe/request.rb +3 -2
  154. data/lib/datadog/tracing/distributed/propagation.rb +7 -0
  155. data/lib/datadog/tracing/metadata/ext.rb +2 -0
  156. data/lib/datadog/tracing/remote.rb +5 -2
  157. data/lib/datadog/tracing/sampling/matcher.rb +6 -1
  158. data/lib/datadog/tracing/sampling/rate_sampler.rb +1 -1
  159. data/lib/datadog/tracing/sampling/rule.rb +2 -0
  160. data/lib/datadog/tracing/sampling/rule_sampler.rb +15 -9
  161. data/lib/datadog/tracing/sampling/span/ext.rb +1 -1
  162. data/lib/datadog/tracing/sampling/span/rule.rb +2 -2
  163. data/lib/datadog/tracing/trace_operation.rb +26 -2
  164. data/lib/datadog/tracing/tracer.rb +29 -22
  165. data/lib/datadog/tracing/transport/http/client.rb +1 -0
  166. data/lib/datadog/tracing/transport/http.rb +4 -0
  167. data/lib/datadog/tracing/transport/io/client.rb +1 -0
  168. data/lib/datadog/tracing/workers/trace_writer.rb +1 -1
  169. data/lib/datadog/tracing/workers.rb +2 -2
  170. data/lib/datadog/tracing/writer.rb +26 -28
  171. data/lib/datadog/version.rb +1 -1
  172. metadata +40 -15
  173. data/lib/datadog/tracing/sampling/rate_limiter.rb +0 -185
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ # Tracks loaded Ruby code by source file and maintains a map from
6
+ # source file to the loaded code (instruction sequences).
7
+ # Also arranges for code in the loaded files to be instrumented by
8
+ # line probes that have already been received by the library.
9
+ #
10
+ # The loaded code is used to target line trace points when installing
11
+ # line probes which dramatically improves efficiency of line trace points.
12
+ #
13
+ # Note that, since most files will only be loaded one time (via the
14
+ # "require" mechanism), the code tracker needs to be global and not be
15
+ # recreated when the DI component is created.
16
+ #
17
+ # @api private
18
+ class CodeTracker
19
+ def initialize
20
+ @registry = {}
21
+ @trace_point_lock = Mutex.new
22
+ @registry_lock = Mutex.new
23
+ @compiled_trace_point = nil
24
+ end
25
+
26
+ # Starts tracking loaded code.
27
+ #
28
+ # This method should generally be called early in application boot
29
+ # process, because any code loaded before code tracking is enabled
30
+ # will not be instrumentable via line probes.
31
+ #
32
+ # Normally tracking should remain active for the lifetime of the
33
+ # process and would not be ever stopped.
34
+ def start
35
+ trace_point_lock.synchronize do
36
+ # If this code tracker is already running, we can do nothing or
37
+ # restart it (by disabling the trace point and recreating it).
38
+ # It is likely that some applications will attempt to activate
39
+ # DI more than once where the intention is to just activate DI;
40
+ # do not break such applications by clearing out the registry.
41
+ # For now, until there is a use case for recreating the trace point,
42
+ # do nothing if the code tracker has already started.
43
+ return if @compiled_trace_point
44
+
45
+ # Note: .trace enables the trace point.
46
+ @compiled_trace_point = TracePoint.trace(:script_compiled) do |tp|
47
+ # Useful attributes of the trace point object here:
48
+ # .instruction_sequence
49
+ # .instruction_sequence.path (either absolute file path for
50
+ # loaded or required code, or for eval'd code, if filename
51
+ # is specified as argument to eval, then this is the provided
52
+ # filename, otherwise this is a synthesized
53
+ # "(eval at <definition-file>:<line>)" string)
54
+ # .instruction_sequence.absolute_path (absolute file path when
55
+ # load or require are used to load code, nil for eval'd code
56
+ # regardless of whether filename was specified as an argument
57
+ # to eval on ruby 3.1+, same as path for eval'd code on ruby 3.0
58
+ # and lower)
59
+ # .method_id
60
+ # .path (refers to the code location that called the require/eval/etc.,
61
+ # not where the loaded code is; use .path on the instruction sequence
62
+ # to obtain the location of the compiled code)
63
+ # .eval_script
64
+ #
65
+ # For now just map the path to the instruction sequence.
66
+ path = tp.instruction_sequence.absolute_path
67
+ # Do not store mapping for eval'd code, since there is no way
68
+ # to target such code from dynamic instrumentation UI.
69
+ # eval'd code always sets tp.eval_script.
70
+ # When tp.eval_script is nil, code is either 'load'ed or 'require'd.
71
+ # steep, of course, complains about indexing with +path+
72
+ # without checking that it is not nil, so here, maybe there is
73
+ # some situation where path would in fact be nil and
74
+ # steep would end up saving the day.
75
+ if path && !tp.eval_script
76
+ registry_lock.synchronize do
77
+ registry[path] = tp.instruction_sequence
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ # Returns whether this code tracker has been activated and is
85
+ # tracking.
86
+ def active?
87
+ trace_point_lock.synchronize do
88
+ !!@compiled_trace_point
89
+ end
90
+ end
91
+
92
+ # Returns an array of RubVM::InstructionSequence (i.e. the compiled code)
93
+ # for the provided path.
94
+ #
95
+ # The argument can be a full path to a Ruby source code file or a
96
+ # suffix (basename + one or more directories preceding the basename).
97
+ # The idea with suffix matches is that file paths are likely to
98
+ # be different between development and production environments and
99
+ # the source control system uses relative paths and doesn't have
100
+ # absolute paths at all.
101
+ #
102
+ # Suffix matches are not guaranteed to be correct, meaning there may
103
+ # be multiple files with the same basename and they may all match a
104
+ # given suffix. In such cases, this method will return all matching
105
+ # paths (and all of these paths will be attempted to be instrumented
106
+ # by upstream code).
107
+ #
108
+ # If the suffix matches one of the paths completely (which requires it
109
+ # to be an absolute path), only the exactly matching path is returned.
110
+ # Otherwise all known paths that end in the suffix are returned.
111
+ # If no paths match, an empty array is returned.
112
+ def iseqs_for_path_suffix(suffix)
113
+ registry_lock.synchronize do
114
+ exact = registry[suffix]
115
+ return [exact] if exact
116
+
117
+ inexact = []
118
+ registry.each do |path, iseq|
119
+ if Utils.path_matches_suffix?(path, suffix)
120
+ inexact << iseq
121
+ end
122
+ end
123
+ inexact
124
+ end
125
+ end
126
+
127
+ # Stops tracking code that is being loaded.
128
+ #
129
+ # This method should ordinarily never be called - if a file is loaded
130
+ # when code tracking is not active, this file will not be instrumentable
131
+ # by line probes.
132
+ #
133
+ # This method is intended for test suite use only, where multiple
134
+ # code tracker instances are created, to fully clean up the old instances.
135
+ def stop
136
+ # Permit multiple stop calls.
137
+ trace_point_lock.synchronize do
138
+ @compiled_trace_point&.disable
139
+ # Clear the instance variable so that the trace point may be
140
+ # reinstated in the future.
141
+ @compiled_trace_point = nil
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
151
+ registry_lock.synchronize do
152
+ registry.clear
153
+ end
154
+ end
155
+
156
+ private
157
+
158
+ # Mapping from paths of loaded files to RubyVM::InstructionSequence
159
+ # objects representing compiled code of those files.
160
+ attr_reader :registry
161
+
162
+ attr_reader :trace_point_lock
163
+ attr_reader :registry_lock
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ module Configuration
6
+ # Settings
7
+ module Settings
8
+ def self.extended(base)
9
+ base = base.singleton_class unless base.is_a?(Class)
10
+ add_settings!(base)
11
+ end
12
+
13
+ def self.add_settings!(base)
14
+ base.class_eval do
15
+ # The setting has "internal" prefix to prevent it from being
16
+ # prematurely turned on by customers.
17
+ settings :dynamic_instrumentation do
18
+ option :enabled do |o|
19
+ o.type :bool
20
+ # The environment variable has an "internal" prefix so that
21
+ # any customers that have the "proper" environment variable
22
+ # turned on (i.e. DD_DYNAMIC_INSTRUMENTATION_ENABLED)
23
+ # do not enable Ruby DI until the latter is ready for
24
+ # customer testing.
25
+ o.env "DD_DYNAMIC_INSTRUMENTATION_ENABLED"
26
+ o.default false
27
+ end
28
+
29
+ # This option instructs dynamic instrumentation to use
30
+ # untargeted trace points when installing line probes and
31
+ # code tracking is not active.
32
+ # WARNING: untargeted trace points carry a massive performance
33
+ # penalty for the entire file in which a line probe is placed.
34
+ #
35
+ # If this option is set to false, which is the default,
36
+ # dynamic instrumentation will add probes that reference
37
+ # unknown files to the list of pending probes, and when
38
+ # the respective files are loaded, the line probes will be
39
+ # installed using targeted trace points. If the file in
40
+ # question is already loaded when the probe is received
41
+ # (for example, it is in a third-party library loaded during
42
+ # application boot), and code tracking was not active when
43
+ # the file was loaded, such files will not be instrumentable
44
+ # via line probes.
45
+ #
46
+ # If this option is set to true
47
+ #
48
+ # activated, DI will in
49
+ # activated or because the files being targeted have beenIf true and code tracking is not enabled, dynamic instrumentation
50
+ # will use untargeted trace points.
51
+ # If false and code tracking is not enabled, dynamic
52
+ # instrumentation will not instrument any files loaded
53
+ # WARNING: these trace points will greatly degrade performance
54
+ # of all code in the instrumented files.
55
+ option :untargeted_trace_points do |o|
56
+ o.type :bool
57
+ o.default false
58
+ end
59
+
60
+ # If true, all of the catch-all rescue blocks in DI
61
+ # will propagate the exceptions onward.
62
+ # WARNING: for internal Datadog use only - this will break
63
+ # the DI product and potentially the library in general in
64
+ # a multitude of ways, cause resource leakage, permanent
65
+ # performance decreases, etc.
66
+ option :propagate_all_exceptions do |o|
67
+ o.type :bool
68
+ o.default false
69
+ end
70
+
71
+ # An array of variable and key names to redact in addition to
72
+ # the built-in list of identifiers.
73
+ #
74
+ # The names will be normalized by removing the following
75
+ # symbols: _, -, @, $, and then matched to the complete
76
+ # variable or key name while ignoring the case.
77
+ # For example, specifying pass_word will match password and
78
+ # PASSWORD, and specifying PASSWORD will match pass_word.
79
+ # Note that, while the at sign (@) is used in Ruby to refer
80
+ # to instance variables, it does not have any significance
81
+ # for this setting (and is removed before matching identifiers).
82
+ option :redacted_identifiers do |o|
83
+ o.env "DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS"
84
+ o.env_parser do |value|
85
+ value&.split(",")&.map(&:strip)
86
+ end
87
+
88
+ o.type :array
89
+ o.default []
90
+ end
91
+
92
+ # An array of class names, values of which will be redacted from
93
+ # dynamic instrumentation snapshots. Example: FooClass.
94
+ # If a name is suffixed by '*', it becomes a wildcard and
95
+ # instances of any class whose name begins with the specified
96
+ # prefix will be redacted (example: Foo*).
97
+ #
98
+ # The names must all be fully-qualified, if any prefix of a
99
+ # class name is configured to be redacted, the value will be
100
+ # subject to redaction. For example, if Foo* is in the
101
+ # redacted class name list, instances of Foo, FooBar,
102
+ # Foo::Bar are all subject to redaction, but Bar::Foo will
103
+ # not be subject to redaction.
104
+ #
105
+ # Leading double-colon is permitted but has no effect,
106
+ # because the names are always considered to be fully-qualified.
107
+ # For example, adding ::Foo to the list will redact instances
108
+ # of Foo.
109
+ #
110
+ # Trailing colons should not be used because they will trigger
111
+ # exact match behavior but Ruby class names do not have
112
+ # trailing colons. For example, Foo:: will not cause anything
113
+ # to be redacted. Use Foo::* to redact all classes under
114
+ # the Foo module.
115
+ option :redacted_type_names do |o|
116
+ o.env "DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES"
117
+ o.env_parser do |value|
118
+ value&.split(",")&.map(&:strip)
119
+ end
120
+
121
+ o.type :array
122
+ o.default []
123
+ end
124
+
125
+ # Maximum number of object or collection traversals that
126
+ # will be permitted when serializing captured values.
127
+ option :max_capture_depth do |o|
128
+ o.type :int
129
+ o.default 3
130
+ end
131
+
132
+ # Maximum number of collection (Array and Hash) elements
133
+ # that will be captured. Arrays and hashes that have more
134
+ # elements will be truncated to this many elements.
135
+ option :max_capture_collection_size do |o|
136
+ o.type :int
137
+ o.default 100
138
+ end
139
+
140
+ # Strings longer than this length will be truncated to this
141
+ # length in dynamic instrumentation snapshots.
142
+ #
143
+ # Note that while all values are stringified during
144
+ # serialization, only values which are originally instances
145
+ # of the String class are subject to this length limit.
146
+ option :max_capture_string_length do |o|
147
+ o.type :int
148
+ o.default 255
149
+ end
150
+
151
+ # Maximim number of attributes that will be captured for
152
+ # a single non-primitive value.
153
+ option :max_capture_attribute_count do |o|
154
+ o.type :int
155
+ o.default 20
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ # Configuration for DI
6
+ module Configuration
7
+ end
8
+ end
9
+ end
10
+
11
+ require_relative "configuration/settings"
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ # Base class for Dynamic Instrumentation exceptions.
6
+ #
7
+ # None of these exceptions should be propagated out of DI to user
8
+ # applications, therefore these exceptions are not considered to be
9
+ # part of the public API of the library.
10
+ #
11
+ # @api private
12
+ class Error < StandardError
13
+ # Probe does not contain a line number (i.e., is not a line probe).
14
+ class MissingLineNumber < Error
15
+ end
16
+
17
+ # Failed to communicate to the local Datadog agent (e.g. to send
18
+ # probe status or a snapshot).
19
+ class AgentCommunicationError < Error
20
+ end
21
+
22
+ # Attempting to instrument a method or file which does not exist.
23
+ #
24
+ # This could be due to the code that is referenced in the probe
25
+ # having not been loaded yet, or due to the probe referencing code
26
+ # that does not in fact exist anywhere (e.g. due to a misspelling).
27
+ class DITargetNotDefined < Error
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../core/configuration"
4
+ require_relative "configuration"
5
+
6
+ module Datadog
7
+ module DI
8
+ # Extends Datadog tracing with DI features
9
+ module Extensions
10
+ # Inject DI into global objects.
11
+ def self.activate!
12
+ Core::Configuration::Settings.extend(Configuration::Settings)
13
+ end
14
+ end
15
+ end
16
+ 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