datadog 2.36.0 → 2.38.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +74 -1
- data/ext/datadog_profiling_native_extension/clock_id_from_mach.c +5 -12
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +29 -18
- data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +17 -3
- data/ext/datadog_profiling_native_extension/collectors_stack.c +19 -24
- data/ext/datadog_profiling_native_extension/collectors_stack.h +0 -1
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +348 -213
- data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -0
- data/ext/datadog_profiling_native_extension/extconf.rb +11 -3
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +5 -0
- data/ext/datadog_profiling_native_extension/private_vm_api_access.h +2 -0
- data/ext/datadog_profiling_native_extension/ruby_helpers.c +7 -0
- data/ext/datadog_profiling_native_extension/time_helpers.h +12 -7
- data/ext/libdatadog_api/di.c +76 -0
- data/ext/libdatadog_api/extconf.rb +6 -0
- data/ext/libdatadog_extconf_helpers.rb +1 -1
- data/lib/datadog/ai_guard/api_client.rb +6 -4
- data/lib/datadog/appsec/api_security/route_extractor.rb +7 -4
- data/lib/datadog/appsec/configuration.rb +6 -0
- data/lib/datadog/appsec/context.rb +4 -2
- data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +59 -4
- data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +59 -5
- data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +1 -1
- data/lib/datadog/appsec/contrib/rack/gateway/request.rb +5 -0
- data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +4 -0
- data/lib/datadog/appsec/contrib/rack/input_peeker.rb +4 -17
- data/lib/datadog/appsec/contrib/rack/request_middleware.rb +21 -0
- data/lib/datadog/appsec/contrib/rails/gateway/request.rb +7 -1
- data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +60 -7
- data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +4 -0
- data/lib/datadog/appsec/ext.rb +1 -0
- data/lib/datadog/appsec/metrics/collector.rb +18 -1
- data/lib/datadog/appsec/metrics/exporter.rb +9 -3
- data/lib/datadog/appsec/remote.rb +9 -3
- data/lib/datadog/appsec/route_normalizer/rails_route_pattern.rb +176 -0
- data/lib/datadog/appsec/route_normalizer/route_pattern.rb +378 -0
- data/lib/datadog/appsec/route_normalizer/route_text.rb +57 -0
- data/lib/datadog/appsec/route_normalizer.rb +80 -0
- data/lib/datadog/appsec/utils/http/body_reader.rb +61 -0
- data/lib/datadog/core/configuration/agentless_settings_resolver.rb +2 -2
- data/lib/datadog/core/configuration/components.rb +82 -2
- data/lib/datadog/core/configuration/components_state.rb +6 -1
- data/lib/datadog/core/configuration/settings.rb +4 -7
- data/lib/datadog/core/configuration/supported_configurations.rb +3 -0
- data/lib/datadog/core/evp.rb +11 -0
- data/lib/datadog/core/knuth_sampler.rb +3 -1
- data/lib/datadog/core/metrics/client.rb +1 -1
- data/lib/datadog/core/remote/client/capabilities.rb +34 -7
- data/lib/datadog/core/telemetry/event/app_started.rb +8 -1
- data/lib/datadog/core/transport/http/builder.rb +1 -1
- data/lib/datadog/core/utils/at_fork_monkey_patch.rb +1 -1
- data/lib/datadog/core/utils/time.rb +5 -1
- data/lib/datadog/di/base.rb +3 -1
- data/lib/datadog/di/boot.rb +3 -0
- data/lib/datadog/di/capture_expression.rb +21 -0
- data/lib/datadog/di/capture_expression_evaluator.rb +71 -0
- data/lib/datadog/di/capture_limits.rb +41 -0
- data/lib/datadog/di/code_tracker.rb +5 -2
- data/lib/datadog/di/component.rb +112 -38
- data/lib/datadog/di/configuration.rb +6 -0
- data/lib/datadog/di/context.rb +9 -3
- data/lib/datadog/di/el/compiler.rb +1 -1
- data/lib/datadog/di/error.rb +10 -0
- data/lib/datadog/di/fatal_exceptions.rb +26 -0
- data/lib/datadog/di/instrumenter.rb +412 -165
- data/lib/datadog/di/probe.rb +61 -2
- data/lib/datadog/di/probe_builder.rb +83 -1
- data/lib/datadog/di/probe_file_loader.rb +8 -3
- data/lib/datadog/di/probe_manager.rb +67 -9
- data/lib/datadog/di/probe_notification_builder.rb +52 -8
- data/lib/datadog/di/probe_notifier_worker.rb +52 -32
- data/lib/datadog/di/redactor.rb +16 -1
- data/lib/datadog/di/remote.rb +176 -15
- data/lib/datadog/di/serializer.rb +34 -15
- data/lib/datadog/di/transport/input.rb +8 -4
- data/lib/datadog/di.rb +63 -0
- data/lib/datadog/kit/appsec/events/v2.rb +60 -2
- data/lib/datadog/open_feature/component.rb +29 -6
- data/lib/datadog/open_feature/configuration.rb +8 -0
- data/lib/datadog/open_feature/ext.rb +2 -0
- data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +286 -0
- data/lib/datadog/open_feature/flag_evaluation/writer.rb +433 -0
- data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +101 -0
- data/lib/datadog/open_feature/hooks/{flag_eval_hook.rb → flag_eval_metrics_hook.rb} +1 -1
- data/lib/datadog/open_feature/native_evaluator.rb +22 -0
- data/lib/datadog/open_feature/provider.rb +59 -27
- data/lib/datadog/open_feature/transport.rb +55 -1
- data/lib/datadog/opentelemetry/sdk/logs_exporter.rb +5 -12
- data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +4 -11
- data/lib/datadog/opentelemetry/sdk.rb +19 -0
- data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +6 -1
- data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -3
- data/lib/datadog/profiling/collectors/thread_context.rb +9 -1
- data/lib/datadog/profiling/component.rb +17 -0
- data/lib/datadog/symbol_database/component.rb +164 -47
- data/lib/datadog/symbol_database/configuration.rb +12 -4
- data/lib/datadog/symbol_database/extensions.rb +19 -0
- data/lib/datadog/symbol_database/extractor.rb +124 -87
- data/lib/datadog/symbol_database/file_hash.rb +3 -1
- data/lib/datadog/symbol_database/remote.rb +8 -3
- data/lib/datadog/symbol_database/scope_batcher.rb +7 -3
- data/lib/datadog/symbol_database/uploader.rb +9 -2
- data/lib/datadog/symbol_database.rb +22 -0
- data/lib/datadog/tracing/contrib/active_job/data_streams.rb +62 -0
- data/lib/datadog/tracing/contrib/active_job/patcher.rb +12 -0
- data/lib/datadog/tracing/contrib/active_model_serializers/events/serialize.rb +2 -3
- data/lib/datadog/tracing/contrib/pg/instrumentation.rb +12 -12
- data/lib/datadog/tracing/contrib/rack/patcher.rb +1 -1
- data/lib/datadog/tracing/contrib/sequel/utils.rb +2 -2
- data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +1 -1
- data/lib/datadog/tracing/contrib/sinatra/framework.rb +1 -1
- data/lib/datadog/tracing/distributed/baggage.rb +2 -1
- data/lib/datadog/tracing/ext.rb +9 -0
- data/lib/datadog/tracing/remote.rb +34 -3
- data/lib/datadog/tracing/transport/trace_formatter.rb +1 -1
- data/lib/datadog/version.rb +1 -1
- data/lib/datadog.rb +1 -0
- metadata +25 -8
data/lib/datadog/di/probe.rb
CHANGED
|
@@ -34,11 +34,17 @@ module Datadog
|
|
|
34
34
|
class Probe
|
|
35
35
|
KNOWN_TYPES = %i[log].freeze
|
|
36
36
|
|
|
37
|
+
EVALUATE_AT_VALUES = %i[entry exit].freeze
|
|
38
|
+
|
|
37
39
|
def initialize(id:, type:,
|
|
38
40
|
file: nil, line_no: nil, type_name: nil, method_name: nil,
|
|
39
41
|
template: nil, template_segments: nil,
|
|
40
42
|
capture_snapshot: false, max_capture_depth: nil,
|
|
41
|
-
max_capture_attribute_count: nil,
|
|
43
|
+
max_capture_attribute_count: nil,
|
|
44
|
+
max_capture_collection_size: nil, max_capture_string_length: nil,
|
|
45
|
+
capture_expressions: [],
|
|
46
|
+
evaluate_at: nil,
|
|
47
|
+
condition: nil,
|
|
42
48
|
rate_limit: nil)
|
|
43
49
|
# Perform some sanity checks here to detect unexpected attribute
|
|
44
50
|
# combinations, in order to not do them in subsequent code.
|
|
@@ -81,9 +87,17 @@ module Datadog
|
|
|
81
87
|
@capture_snapshot = !!capture_snapshot
|
|
82
88
|
@max_capture_depth = max_capture_depth
|
|
83
89
|
@max_capture_attribute_count = max_capture_attribute_count
|
|
90
|
+
@max_capture_collection_size = max_capture_collection_size
|
|
91
|
+
@max_capture_string_length = max_capture_string_length
|
|
92
|
+
@capture_expressions = capture_expressions || []
|
|
93
|
+
evaluate_at = :exit if evaluate_at.nil?
|
|
94
|
+
unless EVALUATE_AT_VALUES.include?(evaluate_at)
|
|
95
|
+
raise ArgumentError, "Unknown evaluate_at value: #{evaluate_at.inspect} (expected one of #{EVALUATE_AT_VALUES.inspect})"
|
|
96
|
+
end
|
|
97
|
+
@evaluate_at = evaluate_at
|
|
84
98
|
@condition = condition
|
|
85
99
|
|
|
86
|
-
@rate_limit = rate_limit || (@capture_snapshot ? 1 : 5000)
|
|
100
|
+
@rate_limit = rate_limit || ((@capture_snapshot || !@capture_expressions.empty?) ? 1 : 5000)
|
|
87
101
|
@rate_limiter = Datadog::Core::TokenBucket.new(@rate_limit)
|
|
88
102
|
|
|
89
103
|
# At most one report per second.
|
|
@@ -120,6 +134,14 @@ module Datadog
|
|
|
120
134
|
# the global default will be used.
|
|
121
135
|
attr_reader :max_capture_attribute_count
|
|
122
136
|
|
|
137
|
+
attr_reader :max_capture_collection_size
|
|
138
|
+
|
|
139
|
+
attr_reader :max_capture_string_length
|
|
140
|
+
|
|
141
|
+
attr_reader :capture_expressions
|
|
142
|
+
|
|
143
|
+
attr_reader :evaluate_at
|
|
144
|
+
|
|
123
145
|
# Rate limit in effect, in invocations per second. Always present.
|
|
124
146
|
attr_reader :rate_limit
|
|
125
147
|
|
|
@@ -140,6 +162,29 @@ module Datadog
|
|
|
140
162
|
@capture_snapshot
|
|
141
163
|
end
|
|
142
164
|
|
|
165
|
+
def capture_expressions?
|
|
166
|
+
!@capture_expressions.empty?
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def evaluate_at_entry?
|
|
170
|
+
evaluate_at == :entry
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Capture-expression mode: the probe captures expressions but not a full
|
|
174
|
+
# snapshot. Snapshot capture serializes its own values, so expression
|
|
175
|
+
# capture only runs when a snapshot is not being taken.
|
|
176
|
+
def capture_expressions_only?
|
|
177
|
+
capture_expressions? && !capture_snapshot?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def capture_entry_expressions?
|
|
181
|
+
capture_expressions_only? && evaluate_at_entry?
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def snapshot_serializer_limits(settings)
|
|
185
|
+
CaptureLimits.resolve(expr_limits: nil, probe: self, settings: settings)
|
|
186
|
+
end
|
|
187
|
+
|
|
143
188
|
# Returns whether the probe is a line probe.
|
|
144
189
|
#
|
|
145
190
|
# Method probes may still specify a file name (to aid in locating the
|
|
@@ -171,6 +216,20 @@ module Datadog
|
|
|
171
216
|
line_no
|
|
172
217
|
end
|
|
173
218
|
|
|
219
|
+
# Returns the method name associated with the probe, raising
|
|
220
|
+
# Error::MissingMethodName if the probe does not have a method name
|
|
221
|
+
# associated with it.
|
|
222
|
+
#
|
|
223
|
+
# This method is used by instrumentation driver to ensure a method name
|
|
224
|
+
# that is passed into the instrumentation logic is actually a method name
|
|
225
|
+
# and not nil.
|
|
226
|
+
def method_name!
|
|
227
|
+
if method_name.nil?
|
|
228
|
+
raise Error::MissingMethodName, "Probe #{id} does not have a method name associated with it"
|
|
229
|
+
end
|
|
230
|
+
method_name
|
|
231
|
+
end
|
|
232
|
+
|
|
174
233
|
# Source code location of the probe, for diagnostic reporting.
|
|
175
234
|
def location
|
|
176
235
|
if method?
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# rubocop:disable Lint/AssignmentInCondition
|
|
4
4
|
|
|
5
5
|
require_relative "probe"
|
|
6
|
+
require_relative "capture_expression"
|
|
7
|
+
require_relative "capture_limits"
|
|
6
8
|
require_relative 'el'
|
|
7
9
|
|
|
8
10
|
module Datadog
|
|
@@ -27,7 +29,15 @@ module Datadog
|
|
|
27
29
|
|
|
28
30
|
module_function
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
CAPTURE_EXPRESSION_NAME_PATTERN = /\A[a-zA-Z0-9_?]+\z/
|
|
33
|
+
|
|
34
|
+
EVALUATE_AT_STRINGS = {
|
|
35
|
+
"ENTRY" => :entry,
|
|
36
|
+
"EXIT" => :exit,
|
|
37
|
+
"DEFAULT" => :exit,
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
def build_from_remote_config(config, logger:)
|
|
31
41
|
# The validations here are not yet comprehensive.
|
|
32
42
|
type = config.fetch('type')
|
|
33
43
|
type_symbol = PROBE_TYPES[type] or raise ArgumentError, "Unrecognized probe type: #{type}"
|
|
@@ -38,6 +48,13 @@ module Datadog
|
|
|
38
48
|
compiled = EL::Compiler.new.compile(cond_spec['json'])
|
|
39
49
|
EL::Expression.new(cond_spec['dsl'], compiled)
|
|
40
50
|
end
|
|
51
|
+
capture_expressions = build_capture_expressions(config['captureExpressions'])
|
|
52
|
+
capture_expressions = dedup_capture_expressions(capture_expressions, config["id"], logger)
|
|
53
|
+
if !!config["captureSnapshot"] && !capture_expressions.empty?
|
|
54
|
+
logger.debug do
|
|
55
|
+
"di: probe #{config["id"]}: captureSnapshot=true wins over captureExpressions (n=#{capture_expressions.size})"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
41
58
|
Probe.new(
|
|
42
59
|
id: config.fetch("id"),
|
|
43
60
|
type: type_symbol,
|
|
@@ -54,6 +71,10 @@ module Datadog
|
|
|
54
71
|
capture_snapshot: !!config["captureSnapshot"],
|
|
55
72
|
max_capture_depth: config["capture"]&.[]("maxReferenceDepth"),
|
|
56
73
|
max_capture_attribute_count: config["capture"]&.[]("maxFieldCount"),
|
|
74
|
+
max_capture_collection_size: config["capture"]&.[]("maxCollectionSize"),
|
|
75
|
+
max_capture_string_length: config["capture"]&.[]("maxLength"),
|
|
76
|
+
capture_expressions: capture_expressions,
|
|
77
|
+
evaluate_at: parse_evaluate_at(config["evaluateAt"], config["id"], logger),
|
|
57
78
|
rate_limit: config["sampling"]&.[]("snapshotsPerSecond"),
|
|
58
79
|
condition: cond,
|
|
59
80
|
)
|
|
@@ -61,6 +82,67 @@ module Datadog
|
|
|
61
82
|
raise ArgumentError, "Malformed remote configuration entry for probe: #{exc.class}: #{exc.message}: #{config}"
|
|
62
83
|
end
|
|
63
84
|
|
|
85
|
+
def build_capture_expressions(raw)
|
|
86
|
+
return [] if raw.nil?
|
|
87
|
+
unless Array === raw
|
|
88
|
+
raise ArgumentError, "captureExpressions must be an array, got: #{raw.class}"
|
|
89
|
+
end
|
|
90
|
+
return [] if raw.empty?
|
|
91
|
+
raw.map do |entry|
|
|
92
|
+
unless Hash === entry
|
|
93
|
+
raise ArgumentError, "captureExpressions entry must be a hash, got: #{entry.class}"
|
|
94
|
+
end
|
|
95
|
+
name = entry['name']
|
|
96
|
+
unless String === name && CAPTURE_EXPRESSION_NAME_PATTERN.match?(name)
|
|
97
|
+
raise ArgumentError, "captureExpressions entry name missing or invalid (must match #{CAPTURE_EXPRESSION_NAME_PATTERN.inspect}): #{name.inspect}"
|
|
98
|
+
end
|
|
99
|
+
expr_spec = entry['expr']
|
|
100
|
+
unless Hash === expr_spec && expr_spec['dsl'] && expr_spec['json']
|
|
101
|
+
raise ArgumentError, "captureExpressions entry #{name}: missing or malformed expr"
|
|
102
|
+
end
|
|
103
|
+
compiled = EL::Compiler.new.compile(expr_spec['json'])
|
|
104
|
+
expr = EL::Expression.new(expr_spec['dsl'], compiled)
|
|
105
|
+
limits = build_capture_limits(entry['capture'])
|
|
106
|
+
CaptureExpression.new(name: name, expr: expr, limits: limits)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def dedup_capture_expressions(capture_expressions, probe_id, logger)
|
|
111
|
+
return capture_expressions if capture_expressions.size < 2
|
|
112
|
+
by_name = {}
|
|
113
|
+
capture_expressions.each do |capture_expression|
|
|
114
|
+
by_name[capture_expression.name] = capture_expression
|
|
115
|
+
end
|
|
116
|
+
return capture_expressions if by_name.size == capture_expressions.size
|
|
117
|
+
logger.debug do
|
|
118
|
+
"di: probe #{probe_id}: collapsed duplicate captureExpressions names, kept last per name (#{capture_expressions.size} -> #{by_name.size})"
|
|
119
|
+
end
|
|
120
|
+
by_name.values
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def build_capture_limits(raw)
|
|
124
|
+
return nil if raw.nil?
|
|
125
|
+
unless Hash === raw
|
|
126
|
+
raise ArgumentError, "capture-expression entry capture must be a hash, got: #{raw.class}"
|
|
127
|
+
end
|
|
128
|
+
CaptureLimits.new(
|
|
129
|
+
max_reference_depth: raw['maxReferenceDepth'],
|
|
130
|
+
max_collection_size: raw['maxCollectionSize'],
|
|
131
|
+
max_length: raw['maxLength'],
|
|
132
|
+
max_field_count: raw['maxFieldCount'],
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def parse_evaluate_at(raw, probe_id, logger)
|
|
137
|
+
return :exit if raw.nil?
|
|
138
|
+
EVALUATE_AT_STRINGS[raw] || begin
|
|
139
|
+
logger.debug do
|
|
140
|
+
"di: probe #{probe_id}: unrecognized evaluateAt value #{raw.inspect}, defaulting to :exit"
|
|
141
|
+
end
|
|
142
|
+
:exit
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
64
146
|
def build_template_segments(segments)
|
|
65
147
|
segments&.map do |segment|
|
|
66
148
|
if Hash === segment
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
|
|
5
|
+
require_relative 'fatal_exceptions'
|
|
6
|
+
|
|
5
7
|
module Datadog
|
|
6
8
|
module DI
|
|
7
9
|
module ProbeFileLoader
|
|
@@ -47,7 +49,8 @@ module Datadog
|
|
|
47
49
|
|
|
48
50
|
payload = component.probe_notification_builder.build_errored(probe, exc)
|
|
49
51
|
component.probe_notifier_worker.add_status(payload, probe: probe)
|
|
50
|
-
rescue => exc
|
|
52
|
+
rescue Exception => exc # standard:disable Lint/RescueException
|
|
53
|
+
Datadog::DI.reraise_if_fatal(exc)
|
|
51
54
|
raise if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
|
|
52
55
|
|
|
53
56
|
component.logger.debug { "di: unhandled exception adding #{probe.type} probe at #{probe.location} (#{probe.id}) in DI probe file loader: #{exc.class}: #{exc.message}" }
|
|
@@ -58,7 +61,8 @@ module Datadog
|
|
|
58
61
|
component.probe_notifier_worker.add_status(payload, probe: probe)
|
|
59
62
|
end
|
|
60
63
|
end
|
|
61
|
-
rescue => exc
|
|
64
|
+
rescue Exception => exc # standard:disable Lint/RescueException
|
|
65
|
+
Datadog::DI.reraise_if_fatal(exc)
|
|
62
66
|
if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
|
|
63
67
|
should_propagate = true
|
|
64
68
|
raise
|
|
@@ -67,7 +71,8 @@ module Datadog
|
|
|
67
71
|
component.logger.debug { "di: unhandled exception handling a probe in DI probe file loader: #{exc.class}: #{exc.message}" }
|
|
68
72
|
component.telemetry&.report(exc, description: "Unhandled exception handling probe in DI probe file loader")
|
|
69
73
|
end
|
|
70
|
-
rescue
|
|
74
|
+
rescue Exception => exc # standard:disable Lint/RescueException
|
|
75
|
+
Datadog::DI.reraise_if_fatal(exc)
|
|
71
76
|
# We should generally never get here, but if component tree
|
|
72
77
|
# initialization fails for some unexpected reason, don't nuke
|
|
73
78
|
# the customer application.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
# rubocop:disable Lint/AssignmentInCondition
|
|
4
4
|
|
|
5
|
+
require_relative 'fatal_exceptions'
|
|
6
|
+
|
|
5
7
|
module Datadog
|
|
6
8
|
module DI
|
|
7
9
|
# Orchestrates probe lifecycle: installation, removal, and execution callbacks.
|
|
@@ -26,13 +28,13 @@ module Datadog
|
|
|
26
28
|
@telemetry = telemetry
|
|
27
29
|
@probe_repository = probe_repository
|
|
28
30
|
|
|
29
|
-
@definition_trace_point = TracePoint.
|
|
31
|
+
@definition_trace_point = TracePoint.new(:end) do |tp|
|
|
30
32
|
install_pending_method_probes(tp.self)
|
|
31
|
-
rescue => exc
|
|
33
|
+
rescue Exception => exc # standard:disable Lint/RescueException
|
|
34
|
+
Datadog::DI.reraise_if_fatal(exc)
|
|
32
35
|
raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
|
|
33
36
|
logger.debug { "di: unhandled exception in definition trace point: #{exc.class}: #{exc.message}" }
|
|
34
37
|
telemetry&.report(exc, description: "Unhandled exception in definition trace point")
|
|
35
|
-
# TODO test this path
|
|
36
38
|
end
|
|
37
39
|
end
|
|
38
40
|
|
|
@@ -40,15 +42,68 @@ module Datadog
|
|
|
40
42
|
attr_reader :telemetry
|
|
41
43
|
attr_reader :probe_repository
|
|
42
44
|
|
|
45
|
+
# Stops the probe manager without permanently releasing resources.
|
|
46
|
+
#
|
|
47
|
+
# Disables the class definition trace point and unhooks all installed
|
|
48
|
+
# probe instrumentation, but preserves the probe repository so that
|
|
49
|
+
# {#reopen} can re-install probes. Can be reversed by calling {#reopen}.
|
|
50
|
+
#
|
|
51
|
+
# Why preserve the repository: RC dispatch only delivers LIVE_DEBUGGING
|
|
52
|
+
# changes when the content hash changes. If a customer toggles DI off
|
|
53
|
+
# then back on in the UI with the same probe content, the receiver
|
|
54
|
+
# is not re-invoked, so probes wiped here would never be redelivered.
|
|
55
|
+
# Preserving the repo lets {#reopen} replay the probes locally.
|
|
56
|
+
#
|
|
57
|
+
# @return [void]
|
|
58
|
+
def stop
|
|
59
|
+
definition_trace_point.disable
|
|
60
|
+
probe_repository.synchronize do
|
|
61
|
+
probe_repository.installed_probes.each_value do |probe|
|
|
62
|
+
instrumenter.unhook(probe)
|
|
63
|
+
rescue => exc
|
|
64
|
+
raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
|
|
65
|
+
logger.debug { "di: error unhooking #{probe.type} probe at #{probe.location} (#{probe.id}) on stop: #{exc.class}: #{exc.message}" }
|
|
66
|
+
telemetry&.report(exc, description: "Error unhooking probe on stop")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Re-enables the probe manager after a {#stop}.
|
|
72
|
+
#
|
|
73
|
+
# Re-enables the class definition trace point so that pending
|
|
74
|
+
# method probes can be installed when their target classes are defined,
|
|
75
|
+
# and re-hooks all previously-installed probes from the repository.
|
|
76
|
+
# If a probe's target is no longer present (e.g. code was unloaded
|
|
77
|
+
# between stop and reopen), it is demoted to pending so the trace
|
|
78
|
+
# point or code tracker can re-install it if the target reappears.
|
|
79
|
+
#
|
|
80
|
+
# @return [void]
|
|
81
|
+
def reopen
|
|
82
|
+
definition_trace_point.enable
|
|
83
|
+
probe_repository.synchronize do
|
|
84
|
+
# values.each so concurrent mutation of @installed_probes (via
|
|
85
|
+
# remove_installed below) does not iterate over a stale key set.
|
|
86
|
+
probe_repository.installed_probes.values.each do |probe|
|
|
87
|
+
instrumenter.hook(probe, self)
|
|
88
|
+
rescue Error::DITargetNotDefined
|
|
89
|
+
probe_repository.remove_installed(probe.id)
|
|
90
|
+
probe_repository.add_pending(probe)
|
|
91
|
+
logger.trace { "di: target for #{probe.type} probe (#{probe.id}) no longer defined on reopen, demoting to pending" }
|
|
92
|
+
rescue => exc
|
|
93
|
+
raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
|
|
94
|
+
logger.debug { "di: error re-hooking #{probe.type} probe at #{probe.location} (#{probe.id}) on reopen: #{exc.class}: #{exc.message}" }
|
|
95
|
+
telemetry&.report(exc, description: "Error re-hooking probe on reopen")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
43
100
|
# Shuts down the probe manager and releases all resources.
|
|
44
101
|
#
|
|
45
102
|
# Disables the class definition trace point and removes all installed
|
|
46
103
|
# probe instrumentation. Called during component teardown.
|
|
104
|
+
# Unlike {#stop}, this is not reversible.
|
|
47
105
|
#
|
|
48
106
|
# @return [void]
|
|
49
|
-
#
|
|
50
|
-
# TODO test that close is called during component teardown and
|
|
51
|
-
# the trace point is cleared
|
|
52
107
|
def close
|
|
53
108
|
definition_trace_point.disable
|
|
54
109
|
clear_hooks
|
|
@@ -129,7 +184,8 @@ module Datadog
|
|
|
129
184
|
false
|
|
130
185
|
end
|
|
131
186
|
end
|
|
132
|
-
rescue => exc
|
|
187
|
+
rescue Exception => exc # standard:disable Lint/RescueException
|
|
188
|
+
Datadog::DI.reraise_if_fatal(exc)
|
|
133
189
|
# In "propagate all exceptions" mode we will try to instrument again.
|
|
134
190
|
raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
|
|
135
191
|
|
|
@@ -160,7 +216,8 @@ module Datadog
|
|
|
160
216
|
begin
|
|
161
217
|
instrumenter.unhook(probe)
|
|
162
218
|
probe_repository.remove_installed(probe_id)
|
|
163
|
-
rescue => exc
|
|
219
|
+
rescue Exception => exc # standard:disable Lint/RescueException
|
|
220
|
+
Datadog::DI.reraise_if_fatal(exc)
|
|
164
221
|
raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
|
|
165
222
|
# Silence all exceptions?
|
|
166
223
|
# TODO should we propagate here and rescue upstream?
|
|
@@ -190,7 +247,8 @@ module Datadog
|
|
|
190
247
|
break
|
|
191
248
|
rescue Error::DITargetNotDefined
|
|
192
249
|
# This should not happen... try installing again later?
|
|
193
|
-
rescue => exc
|
|
250
|
+
rescue Exception => exc # standard:disable Lint/RescueException
|
|
251
|
+
Datadog::DI.reraise_if_fatal(exc)
|
|
194
252
|
raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
|
|
195
253
|
|
|
196
254
|
logger.debug { "di: error installing #{probe.type} probe at #{probe.location} (#{probe.id}) after class is defined: #{exc.class}: #{exc.message}" }
|
|
@@ -2,20 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
# rubocop:disable Lint/AssignmentInCondition
|
|
4
4
|
|
|
5
|
+
require_relative 'fatal_exceptions'
|
|
6
|
+
require_relative 'capture_expression_evaluator'
|
|
7
|
+
|
|
5
8
|
module Datadog
|
|
6
9
|
module DI
|
|
7
10
|
# Builds probe status notification and snapshot payloads.
|
|
8
11
|
#
|
|
9
12
|
# @api private
|
|
10
13
|
class ProbeNotificationBuilder
|
|
11
|
-
def initialize(settings, serializer)
|
|
14
|
+
def initialize(settings, serializer, logger, telemetry: nil)
|
|
12
15
|
@settings = settings
|
|
13
16
|
@serializer = serializer
|
|
17
|
+
@logger = logger
|
|
18
|
+
@telemetry = telemetry
|
|
19
|
+
@capture_expression_evaluator = CaptureExpressionEvaluator.new(
|
|
20
|
+
settings: settings, serializer: serializer, logger: logger, telemetry: telemetry,
|
|
21
|
+
)
|
|
14
22
|
end
|
|
15
23
|
|
|
16
24
|
attr_reader :settings
|
|
25
|
+
|
|
17
26
|
attr_reader :serializer
|
|
18
27
|
|
|
28
|
+
attr_reader :logger
|
|
29
|
+
|
|
30
|
+
attr_reader :telemetry
|
|
31
|
+
|
|
32
|
+
attr_reader :capture_expression_evaluator
|
|
33
|
+
|
|
19
34
|
def build_received(probe)
|
|
20
35
|
build_status(probe,
|
|
21
36
|
message: "Probe #{probe.id} has been received correctly",
|
|
@@ -65,13 +80,13 @@ module Datadog
|
|
|
65
80
|
|
|
66
81
|
# TODO also verify that non-capturing probe does not pass
|
|
67
82
|
# snapshot or vars/args into this method
|
|
83
|
+
capture_expression_evaluation_errors = []
|
|
68
84
|
captures = if probe.capture_snapshot?
|
|
85
|
+
snapshot_limits = probe.snapshot_serializer_limits(settings)
|
|
69
86
|
if probe.method?
|
|
70
87
|
return_arguments = {
|
|
71
|
-
"@return": serializer.serialize_value(context.return_value,
|
|
72
|
-
|
|
73
|
-
attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count),
|
|
74
|
-
self: serializer.serialize_value(context.target_self),
|
|
88
|
+
"@return": serializer.serialize_value(context.return_value, **snapshot_limits),
|
|
89
|
+
self: serializer.serialize_value(context.target_self, **snapshot_limits),
|
|
75
90
|
}
|
|
76
91
|
{
|
|
77
92
|
entry: {
|
|
@@ -87,8 +102,35 @@ module Datadog
|
|
|
87
102
|
lines: (locals = context.serialized_locals) && {
|
|
88
103
|
probe.line_no => {
|
|
89
104
|
locals: locals,
|
|
90
|
-
arguments: {self: serializer.serialize_value(context.target_self)},
|
|
105
|
+
arguments: {self: serializer.serialize_value(context.target_self, **snapshot_limits)},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
elsif probe.capture_expressions?
|
|
111
|
+
if probe.method?
|
|
112
|
+
if probe.evaluate_at_entry?
|
|
113
|
+
captured_block = context.entry_capture_expressions || {}
|
|
114
|
+
capture_expression_evaluation_errors = context.entry_capture_evaluation_errors || []
|
|
115
|
+
{
|
|
116
|
+
entry: {captureExpressions: captured_block},
|
|
117
|
+
}
|
|
118
|
+
else
|
|
119
|
+
captured_block, capture_expression_evaluation_errors =
|
|
120
|
+
capture_expression_evaluator.evaluate(probe, context)
|
|
121
|
+
{
|
|
122
|
+
return: {
|
|
123
|
+
captureExpressions: captured_block,
|
|
124
|
+
throwable: context.exception ? serialize_throwable(context.exception) : nil,
|
|
91
125
|
},
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
elsif probe.line?
|
|
129
|
+
captured_block, capture_expression_evaluation_errors =
|
|
130
|
+
capture_expression_evaluator.evaluate(probe, context)
|
|
131
|
+
{
|
|
132
|
+
lines: {
|
|
133
|
+
probe.line_no => {captureExpressions: captured_block},
|
|
92
134
|
},
|
|
93
135
|
}
|
|
94
136
|
end
|
|
@@ -99,6 +141,7 @@ module Datadog
|
|
|
99
141
|
if segments = probe.template_segments
|
|
100
142
|
message, evaluation_errors = evaluate_template(segments, context)
|
|
101
143
|
end
|
|
144
|
+
evaluation_errors.concat(capture_expression_evaluation_errors)
|
|
102
145
|
build_snapshot_base(context,
|
|
103
146
|
evaluation_errors: evaluation_errors, message: message,
|
|
104
147
|
captures: captures)
|
|
@@ -303,7 +346,7 @@ module Datadog
|
|
|
303
346
|
# except there is currently no consensus on said heuristics.
|
|
304
347
|
# .NET always sends ld, other languages send nothing at the moment.
|
|
305
348
|
# Don't send anything for the time being.
|
|
306
|
-
#product: 'di/ld',
|
|
349
|
+
# product: 'di/ld',
|
|
307
350
|
snapshot: {
|
|
308
351
|
id: SecureRandom.uuid,
|
|
309
352
|
timestamp: timestamp,
|
|
@@ -366,7 +409,8 @@ module Datadog
|
|
|
366
409
|
else
|
|
367
410
|
raise ArgumentError, "Invalid template segment type: #{segment}"
|
|
368
411
|
end
|
|
369
|
-
rescue => exc
|
|
412
|
+
rescue Exception => exc # standard:disable Lint/RescueException
|
|
413
|
+
Datadog::DI.reraise_if_fatal(exc)
|
|
370
414
|
evaluation_errors << {
|
|
371
415
|
message: "#{exc.class}: #{exc.message}",
|
|
372
416
|
expr: segment.dsl_expr, # steep:ignore NoMethod
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../core/semaphore'
|
|
4
|
+
require_relative 'fatal_exceptions'
|
|
4
5
|
|
|
5
6
|
module Datadog
|
|
6
7
|
module DI
|
|
@@ -62,6 +63,7 @@ module Datadog
|
|
|
62
63
|
# @return [void]
|
|
63
64
|
def start
|
|
64
65
|
return if @thread && @pid == Process.pid
|
|
66
|
+
@stop_requested = false
|
|
65
67
|
logger.trace { "di: starting probe notifier: pid #{$$}" }
|
|
66
68
|
@thread = Thread.new do
|
|
67
69
|
loop do
|
|
@@ -92,10 +94,11 @@ module Datadog
|
|
|
92
94
|
|
|
93
95
|
begin
|
|
94
96
|
more = maybe_send
|
|
95
|
-
rescue => exc
|
|
97
|
+
rescue Exception => exc # standard:disable Lint/RescueException
|
|
98
|
+
Datadog::DI.reraise_if_fatal(exc)
|
|
96
99
|
raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
|
|
97
100
|
|
|
98
|
-
logger.debug { "di: error in probe notifier worker: #{exc.class}: #{exc.message} (at #{exc.backtrace
|
|
101
|
+
logger.debug { "di: error in probe notifier worker: #{exc.class}: #{exc.message} (at #{exc.backtrace&.first})" }
|
|
99
102
|
telemetry&.report(exc, description: "Error in probe notifier worker")
|
|
100
103
|
end
|
|
101
104
|
@lock.synchronize do
|
|
@@ -257,40 +260,56 @@ module Datadog
|
|
|
257
260
|
# if it has been more than 1 second since the last send of the same
|
|
258
261
|
# event type.
|
|
259
262
|
define_method("add_#{event_type}") do |event, probe: nil|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
263
|
+
# Enqueueing reads DI configuration and writes debug/trace logs, both
|
|
264
|
+
# of which can call stdlib methods (e.g. Set#include? via the
|
|
265
|
+
# configuration layer's supported-configuration lookup). If a customer
|
|
266
|
+
# has a method probe on such a method, firing it here would re-enter
|
|
267
|
+
# the worker through probe_executed_callback and deadlock on @lock
|
|
268
|
+
# (recursive locking). Hold the re-entrancy guard for the duration so
|
|
269
|
+
# those DI-internal calls reach the original method without firing the
|
|
270
|
+
# probe. Restore the prior state afterwards so a caller that already
|
|
271
|
+
# holds the guard (e.g. run_method_probe post-processing calling
|
|
272
|
+
# add_snapshot) is not cleared early.
|
|
273
|
+
was_in_probe = Datadog::DI.in_probe?
|
|
274
|
+
Datadog::DI.enter_probe unless was_in_probe
|
|
275
|
+
begin
|
|
276
|
+
@lock.synchronize do
|
|
277
|
+
queue = send("#{event_type}_queue")
|
|
278
|
+
if queue.length > settings.dynamic_instrumentation.internal.snapshot_queue_capacity
|
|
279
|
+
if event_type == :status && probe
|
|
280
|
+
status = event.dig(:debugger, :diagnostics, :status)
|
|
281
|
+
logger.debug { "di: dropping status for #{probe.type} probe at #{probe.location} (#{probe.id}): #{status} because queue is full" }
|
|
282
|
+
else
|
|
283
|
+
logger.debug { "di: #{self.class.name}: dropping #{event_type} event because queue is full" }
|
|
284
|
+
end
|
|
273
285
|
else
|
|
274
|
-
|
|
286
|
+
if event_type == :status && probe
|
|
287
|
+
status = event.dig(:debugger, :diagnostics, :status)
|
|
288
|
+
logger.trace { "di: queueing status for #{probe.type} probe at #{probe.location} (#{probe.id}): #{status}" }
|
|
289
|
+
else
|
|
290
|
+
logger.trace { "di: #{self.class.name}: queueing #{event_type} event" }
|
|
291
|
+
end
|
|
292
|
+
queue << event
|
|
275
293
|
end
|
|
276
|
-
queue << event
|
|
277
294
|
end
|
|
278
|
-
end
|
|
279
295
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
296
|
+
# Figure out whether to wake up the worker thread.
|
|
297
|
+
# If minimum send interval has elapsed since the last send,
|
|
298
|
+
# wake up immediately.
|
|
299
|
+
@lock.synchronize do
|
|
300
|
+
unless @wake_scheduled
|
|
301
|
+
@wake_scheduled = true
|
|
302
|
+
set_sleep_remaining
|
|
303
|
+
wake.signal
|
|
304
|
+
end
|
|
288
305
|
end
|
|
289
|
-
end
|
|
290
306
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
307
|
+
# Worker could be not running if the process forked - check and
|
|
308
|
+
# start it again in this case.
|
|
309
|
+
start
|
|
310
|
+
ensure
|
|
311
|
+
Datadog::DI.leave_probe unless was_in_probe
|
|
312
|
+
end
|
|
294
313
|
end
|
|
295
314
|
|
|
296
315
|
public "add_#{event_type}"
|
|
@@ -318,9 +337,10 @@ module Datadog
|
|
|
318
337
|
@lock.synchronize do
|
|
319
338
|
@last_sent = time
|
|
320
339
|
end
|
|
321
|
-
rescue => exc
|
|
340
|
+
rescue Exception => exc # standard:disable Lint/RescueException
|
|
341
|
+
Datadog::DI.reraise_if_fatal(exc)
|
|
322
342
|
raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
|
|
323
|
-
logger.debug { "di: failed to send #{event_name}: #{exc.class}: #{exc.message} (at #{exc.backtrace
|
|
343
|
+
logger.debug { "di: failed to send #{event_name}: #{exc.class}: #{exc.message} (at #{exc.backtrace&.first})" }
|
|
324
344
|
telemetry&.report(exc, description: "Error sending #{event_type}")
|
|
325
345
|
end
|
|
326
346
|
end
|
data/lib/datadog/di/redactor.rb
CHANGED
|
@@ -186,8 +186,23 @@ module Datadog
|
|
|
186
186
|
]
|
|
187
187
|
|
|
188
188
|
# Input can be a string or a symbol.
|
|
189
|
+
#
|
|
190
|
+
# Hash keys captured in snapshots often originate from a Rack environment
|
|
191
|
+
# (Rack/Rails/Sinatra middleware and controllers all keep `env` in scope).
|
|
192
|
+
# Rack rewrites incoming HTTP header names to the CGI form
|
|
193
|
+
# `HTTP_<HEADER>` with dashes converted to underscores — for example,
|
|
194
|
+
# `Authorization` becomes `HTTP_AUTHORIZATION`, `Cookie` becomes
|
|
195
|
+
# `HTTP_COOKIE`, `X-API-Key` becomes `HTTP_X_API_KEY`. Without stripping
|
|
196
|
+
# the prefix here, those keys normalize to `httpauthorization`,
|
|
197
|
+
# `httpcookie`, `httpxapikey`, etc., none of which appear in the default
|
|
198
|
+
# identifier list — so the values would not be matched against entries
|
|
199
|
+
# like `authorization`, `cookie`, or `xapikey`. The `\Ahttp_` anchor
|
|
200
|
+
# limits the strip to the literal CGI form, so identifiers that merely
|
|
201
|
+
# begin with the letters `http` (e.g. `httpinfo`) are unaffected. The
|
|
202
|
+
# strip must run before the punctuation gsub: the gsub removes the
|
|
203
|
+
# underscore, after which `\Ahttp_` could never match.
|
|
189
204
|
def normalize(str)
|
|
190
|
-
str.to_s.strip.downcase.
|
|
205
|
+
str.to_s.strip.downcase.sub(/\Ahttp_/, "").tr("-_$@", "")
|
|
191
206
|
end
|
|
192
207
|
end
|
|
193
208
|
end
|