datadog 2.35.0 → 2.37.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 +85 -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 +71 -31
- data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
- data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +18 -4
- data/ext/datadog_profiling_native_extension/collectors_stack.c +37 -18
- data/ext/datadog_profiling_native_extension/collectors_stack.h +8 -2
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +596 -341
- data/ext/datadog_profiling_native_extension/collectors_thread_context.h +11 -7
- data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +7 -8
- data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -12
- data/ext/datadog_profiling_native_extension/extconf.rb +13 -2
- data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +4 -43
- data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +15 -47
- data/ext/datadog_profiling_native_extension/heap_recorder.c +44 -26
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +14 -35
- data/ext/datadog_profiling_native_extension/profiling.c +41 -4
- data/ext/datadog_profiling_native_extension/ruby_helpers.c +40 -34
- data/ext/datadog_profiling_native_extension/stack_recorder.c +24 -3
- data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -0
- data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.h +4 -2
- data/ext/libdatadog_api/datadog_ruby_common.c +7 -8
- data/ext/libdatadog_api/datadog_ruby_common.h +0 -12
- 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 +13 -4
- data/lib/datadog/appsec/component.rb +1 -1
- data/lib/datadog/appsec/configuration.rb +13 -0
- data/lib/datadog/appsec/context.rb +4 -2
- data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +37 -4
- 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 +64 -19
- data/lib/datadog/appsec/contrib/graphql/integration.rb +1 -0
- data/lib/datadog/appsec/contrib/rack/buffered_input.rb +83 -0
- data/lib/datadog/appsec/contrib/rack/gateway/request.rb +41 -3
- data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +24 -7
- data/lib/datadog/appsec/contrib/rack/input_peeker.rb +79 -0
- data/lib/datadog/appsec/contrib/rack/request_middleware.rb +21 -0
- data/lib/datadog/appsec/contrib/rails/gateway/request.rb +35 -1
- data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +17 -1
- data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +60 -7
- data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +24 -3
- data/lib/datadog/appsec/default_header_tags.rb +10 -6
- 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/components.rb +83 -2
- data/lib/datadog/core/configuration/components_state.rb +6 -1
- data/lib/datadog/core/configuration/settings.rb +1 -5
- data/lib/datadog/core/configuration/supported_configurations.rb +4 -0
- data/lib/datadog/core/evp.rb +11 -0
- data/lib/datadog/core/remote/client/capabilities.rb +34 -7
- data/lib/datadog/core/remote/component.rb +1 -1
- data/lib/datadog/core/telemetry/event/app_started.rb +8 -22
- data/lib/datadog/core/utils/at_fork_monkey_patch.rb +1 -1
- data/lib/datadog/core/utils/forking.rb +3 -1
- data/lib/datadog/core/utils/spawn_monkey_patch.rb +3 -1
- data/lib/datadog/core.rb +3 -0
- data/lib/datadog/di/base.rb +7 -2
- data/lib/datadog/di/code_tracker.rb +5 -2
- data/lib/datadog/di/component.rb +110 -36
- data/lib/datadog/di/error.rb +10 -0
- data/lib/datadog/di/fatal_exceptions.rb +26 -0
- data/lib/datadog/di/instrumenter.rb +381 -165
- data/lib/datadog/di/probe.rb +14 -0
- 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 +4 -1
- data/lib/datadog/di/probe_notifier_worker.rb +52 -32
- data/lib/datadog/di/redactor.rb +16 -1
- data/lib/datadog/di/remote.rb +175 -14
- data/lib/datadog/di/serializer.rb +12 -5
- data/lib/datadog/di/transport/input.rb +8 -4
- data/lib/datadog/di.rb +63 -0
- data/lib/datadog/error_tracking/collector.rb +2 -1
- data/lib/datadog/error_tracking/component.rb +2 -2
- data/lib/datadog/kit/appsec/events/v2.rb +60 -2
- data/lib/datadog/kit/tracing/method_tracer.rb +4 -1
- 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/propagator.rb +9 -3
- data/lib/datadog/opentelemetry/sdk/span_processor.rb +4 -1
- data/lib/datadog/opentelemetry/sdk.rb +19 -0
- data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +1 -1
- data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -3
- data/lib/datadog/profiling/collectors/thread_context.rb +1 -0
- data/lib/datadog/profiling/component.rb +13 -15
- data/lib/datadog/profiling/ext/dir_monkey_patches.rb +3 -3
- data/lib/datadog/ruby_version.rb +25 -0
- data/lib/datadog/symbol_database/component.rb +453 -128
- 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 +326 -150
- 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/configuration/ext.rb +13 -0
- data/lib/datadog/tracing/configuration/settings.rb +17 -0
- data/lib/datadog/tracing/contrib/configuration/resolver.rb +7 -0
- data/lib/datadog/tracing/contrib/grpc/distributed/propagation.rb +2 -0
- data/lib/datadog/tracing/contrib/grpc.rb +1 -0
- data/lib/datadog/tracing/contrib/http/distributed/propagation.rb +2 -0
- data/lib/datadog/tracing/contrib/http.rb +1 -0
- data/lib/datadog/tracing/contrib/karafka/distributed/propagation.rb +2 -0
- data/lib/datadog/tracing/contrib/karafka.rb +1 -0
- data/lib/datadog/tracing/contrib/rack/middlewares.rb +3 -1
- data/lib/datadog/tracing/contrib/rack/route_inference.rb +3 -1
- data/lib/datadog/tracing/contrib/sidekiq/distributed/propagation.rb +2 -0
- data/lib/datadog/tracing/contrib/sidekiq.rb +1 -0
- data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +2 -0
- data/lib/datadog/tracing/contrib/waterdrop.rb +1 -0
- data/lib/datadog/tracing/distributed/baggage.rb +2 -1
- data/lib/datadog/tracing/distributed/propagation.rb +33 -1
- data/lib/datadog/tracing/distributed/trace_context.rb +11 -2
- data/lib/datadog/tracing/ext.rb +9 -0
- data/lib/datadog/tracing/remote.rb +34 -3
- data/lib/datadog/tracing/trace_digest.rb +7 -0
- data/lib/datadog/tracing/trace_operation.rb +4 -1
- data/lib/datadog/tracing/tracer.rb +1 -0
- data/lib/datadog/version.rb +1 -1
- data/lib/datadog.rb +5 -1
- metadata +22 -6
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../symbol_database'
|
|
3
4
|
require_relative 'extractor'
|
|
4
5
|
require_relative 'logger'
|
|
5
6
|
require_relative 'scope_batcher'
|
|
6
7
|
require_relative 'uploader'
|
|
7
8
|
require_relative '../core/utils/time'
|
|
9
|
+
require_relative '../di/fatal_exceptions'
|
|
8
10
|
|
|
9
11
|
module Datadog
|
|
10
12
|
module SymbolDatabase
|
|
@@ -17,16 +19,24 @@ module Datadog
|
|
|
17
19
|
# - Debounce: extraction is deferred by EXTRACT_DEBOUNCE_INTERVAL seconds so
|
|
18
20
|
# reconfigurations during boot coalesce into a single extraction on the
|
|
19
21
|
# final Component instance.
|
|
22
|
+
# - Hot-load coverage: TracePoint :class hook captures classes loaded after
|
|
23
|
+
# initial extraction, enqueues them on a per-instance buffer; the scheduler
|
|
24
|
+
# drains the buffer on debounce and extracts each one via Extractor#extract,
|
|
25
|
+
# matching Java/Python/.NET continuous coverage.
|
|
20
26
|
#
|
|
21
27
|
# Upload flow:
|
|
22
28
|
# 1. Remote config sends upload_symbols: true (or force_upload mode)
|
|
23
29
|
# 2. start_upload called — schedules extraction EXTRACT_DEBOUNCE_INTERVAL
|
|
24
|
-
# seconds in the future on a per-instance scheduler thread
|
|
30
|
+
# seconds in the future on a per-instance scheduler thread, and lazily
|
|
31
|
+
# installs the TracePoint :class hook if not already installed.
|
|
25
32
|
# 3. When the timer fires (no further start_upload calls reset it),
|
|
26
|
-
# extract_and_upload runs: ObjectSpace iteration →
|
|
33
|
+
# extract_and_upload runs. On the first call: ObjectSpace iteration →
|
|
34
|
+
# Extractor#extract_all. On subsequent calls: drain the hot-load buffer →
|
|
35
|
+
# Extractor#extract per module.
|
|
27
36
|
# 4. ScopeBatcher batches and triggers Uploader.
|
|
28
|
-
# 5.
|
|
29
|
-
#
|
|
37
|
+
# 5. As new classes load throughout the process lifetime, the TracePoint hook
|
|
38
|
+
# fires and signals the scheduler — the next debounce window produces an
|
|
39
|
+
# incremental upload of just the new classes.
|
|
30
40
|
#
|
|
31
41
|
# Created by: Components#initialize (in Core::Configuration::Components)
|
|
32
42
|
# Accessed by: Remote config receiver via Datadog.send(:components).symbol_database
|
|
@@ -39,57 +49,31 @@ module Datadog
|
|
|
39
49
|
# Long enough to absorb reconfiguration cascades during Rails boot.
|
|
40
50
|
EXTRACT_DEBOUNCE_INTERVAL = 5 # seconds
|
|
41
51
|
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# Component rebuilds within a single Ruby process.
|
|
55
|
-
# @return [Boolean]
|
|
56
|
-
def uploaded_this_process?
|
|
57
|
-
@upload_done_mutex.synchronize { @uploaded_this_process }
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Mark the current process as having completed a symbol upload.
|
|
61
|
-
# Called by the Component instance that successfully completes an
|
|
62
|
-
# upload; subsequent start_upload calls on any instance short-circuit.
|
|
63
|
-
# @return [void]
|
|
64
|
-
def mark_uploaded
|
|
65
|
-
@upload_done_mutex.synchronize do
|
|
66
|
-
@uploaded_this_process = true
|
|
67
|
-
@upload_done_cv.broadcast
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Reset class-level upload state. Test-only.
|
|
72
|
-
# @api private
|
|
73
|
-
def reset_uploaded_this_process_for_tests!
|
|
74
|
-
@upload_done_mutex.synchronize { @uploaded_this_process = false }
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Build a new Component if feature is enabled and dependencies met.
|
|
52
|
+
# Cached unbound Module#singleton_class? — dispatched explicitly inside the
|
|
53
|
+
# hot-load TracePoint so user code that overrides `singleton_class?` (e.g.
|
|
54
|
+
# `def self.singleton_class?(arg)`) cannot raise inside the :class hook and
|
|
55
|
+
# abort the user's class definition. Mirrors the cache in Extractor.
|
|
56
|
+
MODULE_SINGLETON_CLASS_PRED = Module.instance_method(:singleton_class?)
|
|
57
|
+
private_constant :MODULE_SINGLETON_CLASS_PRED
|
|
58
|
+
|
|
59
|
+
# Build a new Component if the runtime supports it and dependencies are met.
|
|
60
|
+
# The caller (Core::Configuration::Components) decides whether the feature is
|
|
61
|
+
# enabled and only invokes build when it is, so a disabled component is never
|
|
62
|
+
# constructed. This method gates only on symbol database's own requirements
|
|
63
|
+
# (supported platform, remote config availability).
|
|
79
64
|
# @param settings [Configuration::Settings] Tracer settings
|
|
80
65
|
# @param agent_settings [Configuration::AgentSettings] Agent configuration
|
|
81
66
|
# @param logger [Logger] Logger instance
|
|
82
67
|
# @param telemetry [Core::Telemetry::Component, nil] Telemetry component for error reporting
|
|
83
|
-
# @
|
|
84
|
-
|
|
68
|
+
# @param di_active [Proc, nil] Predicate returning whether Dynamic
|
|
69
|
+
# Instrumentation is currently active (started). Gates remote-config
|
|
70
|
+
# uploads in the nil-default case; nil means "no gate" (standalone
|
|
71
|
+
# force_upload contexts).
|
|
72
|
+
# @return [Component, nil] Component instance or nil if requirements not met
|
|
73
|
+
def self.build(settings, agent_settings, logger, telemetry: nil, di_active: nil)
|
|
85
74
|
symdb_logger = SymbolDatabase::Logger.new(settings, logger)
|
|
86
75
|
|
|
87
|
-
|
|
88
|
-
symdb_logger.debug("symdb: symbol database upload not enabled, skipping")
|
|
89
|
-
return
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Symbol database requires MRI Ruby 2.6+.
|
|
76
|
+
# Symbol database requires MRI Ruby 2.7+.
|
|
93
77
|
# Configuration accessors (settings.symbol_database.*) remain available on all
|
|
94
78
|
# platforms — only the component (upload) is disabled on unsupported engines/versions.
|
|
95
79
|
# environment_supported? logs the specific reason (engine or version) internally.
|
|
@@ -101,7 +85,7 @@ module Datadog
|
|
|
101
85
|
return nil
|
|
102
86
|
end
|
|
103
87
|
|
|
104
|
-
new(settings, agent_settings, symdb_logger, telemetry: telemetry).tap do |component|
|
|
88
|
+
new(settings, agent_settings, symdb_logger, telemetry: telemetry, di_active: di_active).tap do |component|
|
|
105
89
|
# Defer extraction if force upload mode — wait for app boot to complete
|
|
106
90
|
component.schedule_deferred_upload if settings.symbol_database.internal.force_upload
|
|
107
91
|
end
|
|
@@ -114,11 +98,13 @@ module Datadog
|
|
|
114
98
|
# @param agent_settings [Configuration::AgentSettings] Agent configuration
|
|
115
99
|
# @param logger [Logger] Logger instance
|
|
116
100
|
# @param telemetry [Core::Telemetry::Component, nil] Telemetry component for error reporting
|
|
117
|
-
|
|
101
|
+
# @param di_active [Proc, nil] Predicate returning whether Dynamic Instrumentation is currently active
|
|
102
|
+
def initialize(settings, agent_settings, logger, telemetry: nil, di_active: nil)
|
|
118
103
|
@settings = settings
|
|
119
104
|
@agent_settings = agent_settings
|
|
120
105
|
@logger = logger
|
|
121
106
|
@telemetry = telemetry
|
|
107
|
+
@di_active = di_active
|
|
122
108
|
|
|
123
109
|
@extractor = Extractor.new(logger: logger, settings: settings)
|
|
124
110
|
@uploader = Uploader.new(settings: settings, agent_settings: agent_settings, logger: logger, telemetry: telemetry)
|
|
@@ -130,6 +116,16 @@ module Datadog
|
|
|
130
116
|
@upload_in_progress = false
|
|
131
117
|
@upload_in_progress_cv = ConditionVariable.new
|
|
132
118
|
@shutdown = false
|
|
119
|
+
# PID at construction time. Compared against Process.pid in shutdown!
|
|
120
|
+
# to detect forked-child callers, whose inherited @upload_in_progress
|
|
121
|
+
# snapshot is stale: the scheduler thread that would clear it lives
|
|
122
|
+
# only in the parent. See shutdown! for details.
|
|
123
|
+
@owner_pid = Process.pid
|
|
124
|
+
|
|
125
|
+
# Signalled when @last_upload_time advances. wait_for_idle blocks on this
|
|
126
|
+
# so short-lived scripts that trigger an upload can wait for an upload
|
|
127
|
+
# attempt to complete without depending on a one-shot flag.
|
|
128
|
+
@last_upload_time_cv = ConditionVariable.new
|
|
133
129
|
|
|
134
130
|
# Per-instance scheduler state. The scheduler thread is started lazily
|
|
135
131
|
# on the first start_upload call.
|
|
@@ -138,6 +134,25 @@ module Datadog
|
|
|
138
134
|
@scheduled_at = nil
|
|
139
135
|
@scheduler_signaled = false
|
|
140
136
|
@scheduler_thread = nil
|
|
137
|
+
|
|
138
|
+
# Hot-load coverage state. TracePoint :class hook is installed lazily on
|
|
139
|
+
# the first start_upload call; classes defined after that point are
|
|
140
|
+
# enqueued here and drained by the scheduler on debounce. Distinguishes
|
|
141
|
+
# initial extraction (extract_all) from incremental (per-module extract).
|
|
142
|
+
@hot_load_buffer = []
|
|
143
|
+
@hot_load_buffer_mutex = Mutex.new
|
|
144
|
+
@hot_load_tracepoint = nil
|
|
145
|
+
@initial_extraction_done = false
|
|
146
|
+
|
|
147
|
+
# Sticky record of "remote config (or force mode) wants symbols
|
|
148
|
+
# uploaded", independent of whether DI is currently active. Set when an
|
|
149
|
+
# upload is requested and either allowed or deferred by the DI gate;
|
|
150
|
+
# cleared only when RC explicitly disables uploads (stop_upload). It
|
|
151
|
+
# survives stop_for_di_disable's scheduler teardown, so
|
|
152
|
+
# resume_pending_upload can restart uploads after a DI disable->re-enable
|
|
153
|
+
# cycle even though RC does not re-dispatch the unchanged symbol-database
|
|
154
|
+
# config.
|
|
155
|
+
@upload_requested = false
|
|
141
156
|
end
|
|
142
157
|
|
|
143
158
|
# Schedule a deferred upload that waits for app boot to complete.
|
|
@@ -149,9 +164,10 @@ module Datadog
|
|
|
149
164
|
# In non-Rails: triggers start_upload immediately.
|
|
150
165
|
#
|
|
151
166
|
# Each Component registers its own callback. Old Components that have
|
|
152
|
-
# been shut down short-circuit in start_upload via @shutdown.
|
|
153
|
-
#
|
|
154
|
-
#
|
|
167
|
+
# been shut down short-circuit in start_upload via @shutdown. The hot-load
|
|
168
|
+
# hook handles classes loaded after this initial trigger, so under
|
|
169
|
+
# eager_load=false an under-extracted initial upload self-corrects as the
|
|
170
|
+
# app exercises code.
|
|
155
171
|
#
|
|
156
172
|
# @return [void]
|
|
157
173
|
def schedule_deferred_upload
|
|
@@ -160,18 +176,8 @@ module Datadog
|
|
|
160
176
|
# loaded object (Rails::Application), so a bare `start_upload`
|
|
161
177
|
# would resolve against it.
|
|
162
178
|
component = self
|
|
163
|
-
logger = @logger
|
|
164
179
|
::ActiveSupport.on_load(:after_initialize) do
|
|
165
|
-
|
|
166
|
-
# classes during initialization. In dev (eager_load=false)
|
|
167
|
-
# there is nothing complete to extract; the auto-deferred
|
|
168
|
-
# upload would race with explicit triggers and produce
|
|
169
|
-
# under-extracted uploads.
|
|
170
|
-
if defined?(::Rails) && ::Rails.application&.config&.eager_load # steep:ignore NoMethod
|
|
171
|
-
component.start_upload
|
|
172
|
-
else
|
|
173
|
-
logger.debug { "symdb: skipping auto-deferred upload (eager_load disabled)" }
|
|
174
|
-
end
|
|
180
|
+
component.start_upload
|
|
175
181
|
end
|
|
176
182
|
else
|
|
177
183
|
start_upload
|
|
@@ -190,60 +196,133 @@ module Datadog
|
|
|
190
196
|
# Thread-safe: can be called concurrently from multiple remote config updates.
|
|
191
197
|
# @return [void]
|
|
192
198
|
def start_upload
|
|
193
|
-
return if Component.uploaded_this_process?
|
|
194
|
-
|
|
195
199
|
@scheduler_mutex.synchronize do
|
|
196
200
|
return if @shutdown
|
|
197
201
|
|
|
202
|
+
unless upload_allowed?
|
|
203
|
+
if deferred_by_di_gate?
|
|
204
|
+
# nil-default case: Symbol Database mirrors Dynamic Instrumentation
|
|
205
|
+
# and DI is not active. Record the desire and defer; resume_pending_upload
|
|
206
|
+
# re-attempts when DI is enabled. Without this gate the tracer would
|
|
207
|
+
# extract and upload symbols for applications that never enabled DI.
|
|
208
|
+
@upload_requested = true
|
|
209
|
+
@logger.debug("symdb: upload requested but Dynamic Instrumentation is not active; deferring until DI is enabled")
|
|
210
|
+
else
|
|
211
|
+
# Explicit symbol_database.enabled = false: the feature is disabled,
|
|
212
|
+
# not merely waiting on DI. Clear the desire so resume_pending_upload
|
|
213
|
+
# does not retry a disabled feature.
|
|
214
|
+
@upload_requested = false
|
|
215
|
+
@logger.debug("symdb: upload requested but symbol database upload is disabled; skipping")
|
|
216
|
+
end
|
|
217
|
+
return
|
|
218
|
+
end
|
|
219
|
+
@upload_requested = true
|
|
220
|
+
|
|
221
|
+
if @owner_pid != Process.pid
|
|
222
|
+
# Forked child: claim ownership and clear inherited
|
|
223
|
+
# @upload_in_progress. The inherited flag was the parent's
|
|
224
|
+
# snapshot; the parent's scheduler thread does not exist in this
|
|
225
|
+
# process. Any upload starting now is child-owned and must be
|
|
226
|
+
# waited on in shutdown! via the PID-match branch.
|
|
227
|
+
@owner_pid = Process.pid
|
|
228
|
+
@mutex.synchronize { @upload_in_progress = false }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
install_hot_load_hook
|
|
198
232
|
@scheduled_at = Datadog::Core::Utils::Time.get_time + EXTRACT_DEBOUNCE_INTERVAL
|
|
199
233
|
@scheduler_signaled = true
|
|
200
234
|
@scheduler_cv.signal
|
|
201
235
|
ensure_scheduler_thread
|
|
202
236
|
end
|
|
203
|
-
rescue => e
|
|
237
|
+
rescue Exception => e # standard:disable Lint/RescueException
|
|
238
|
+
Datadog::DI.reraise_if_fatal(e)
|
|
204
239
|
@logger.debug { "symdb: error scheduling upload: #{e.class}: #{e.message}" }
|
|
205
240
|
@telemetry&.report(e, description: 'symdb: error scheduling upload')
|
|
206
241
|
end
|
|
207
242
|
|
|
208
|
-
# Stop symbol upload
|
|
243
|
+
# Stop symbol upload in response to remote config sending
|
|
244
|
+
# upload_symbols: false or deleting the config: the customer no longer wants
|
|
245
|
+
# uploads, so clear the sticky @upload_requested desire (a later
|
|
246
|
+
# resume_pending_upload must not restart it) and tear down the scheduler and
|
|
247
|
+
# hot-load hook.
|
|
209
248
|
# Thread-safe: can be called concurrently from multiple remote config updates.
|
|
210
249
|
# @return [void]
|
|
211
250
|
def stop_upload
|
|
212
|
-
@scheduler_mutex.synchronize
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
251
|
+
@scheduler_mutex.synchronize { @upload_requested = false }
|
|
252
|
+
suspend_scheduling
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Re-attempt a symbol upload that remote config requested but that is not
|
|
256
|
+
# currently running because Dynamic Instrumentation was inactive — either
|
|
257
|
+
# deferred at request time, or suspended by stop_for_di_disable when DI was
|
|
258
|
+
# turned off. Called from the orchestration layer (Tracing::Remote) when DI
|
|
259
|
+
# is enabled via remote configuration (implicit enablement). No-op unless an
|
|
260
|
+
# upload was requested and not since disabled. Mirrors DI's
|
|
261
|
+
# replay_current_probes: RC does not re-dispatch the unchanged
|
|
262
|
+
# symbol-database config on DI re-enable, so the tracer restarts the upload
|
|
263
|
+
# from its own retained desire.
|
|
264
|
+
# @return [void]
|
|
265
|
+
def resume_pending_upload
|
|
266
|
+
requested = @scheduler_mutex.synchronize { @upload_requested }
|
|
267
|
+
start_upload if requested
|
|
268
|
+
nil
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Stop uploading when Dynamic Instrumentation is disabled via remote
|
|
272
|
+
# configuration. Only the nil-default (follows-DI) case stops; an explicit
|
|
273
|
+
# symbol_database.enabled = true and force_upload are independent of DI and
|
|
274
|
+
# keep running. Called from the orchestration layer (Tracing::Remote) so
|
|
275
|
+
# Symbol Database's TracePoint and scheduler don't keep uploading after DI
|
|
276
|
+
# is turned off. No-op if uploads were never started.
|
|
277
|
+
# @return [void]
|
|
278
|
+
def stop_for_di_disable
|
|
279
|
+
return if @settings.symbol_database.internal.force_upload
|
|
280
|
+
return unless @settings.symbol_database.enabled.nil?
|
|
281
|
+
|
|
282
|
+
# Suspend, don't stop: preserve @upload_requested so resume_pending_upload
|
|
283
|
+
# restarts the upload when DI is re-enabled and RC never re-sends the
|
|
284
|
+
# (unchanged) symbol-database config.
|
|
285
|
+
suspend_scheduling
|
|
217
286
|
end
|
|
218
287
|
|
|
219
|
-
# Block until
|
|
288
|
+
# Block until this Component finishes an extract+upload after this call,
|
|
220
289
|
# or until the timeout elapses. Used by short-lived scripts that trigger
|
|
221
290
|
# an upload via force_upload and need to wait before exiting.
|
|
291
|
+
# Tracks @last_upload_time advance — returns true once any upload attempt
|
|
292
|
+
# completes (success or failure), false on timeout.
|
|
222
293
|
# @param timeout [Numeric] Maximum seconds to wait
|
|
223
294
|
# @return [Boolean] true if an upload completed; false on timeout
|
|
224
295
|
def wait_for_idle(timeout: 30)
|
|
225
296
|
deadline = Datadog::Core::Utils::Time.get_time + timeout
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
# would try to re-acquire it (non-reentrant), deadlocking.
|
|
230
|
-
until Component.instance_variable_get(:@uploaded_this_process)
|
|
297
|
+
@mutex.synchronize do
|
|
298
|
+
start_time = @last_upload_time
|
|
299
|
+
while @last_upload_time == start_time
|
|
231
300
|
remaining = deadline - Datadog::Core::Utils::Time.get_time
|
|
232
301
|
return false if remaining <= 0
|
|
233
|
-
|
|
302
|
+
@last_upload_time_cv.wait(@mutex, remaining)
|
|
234
303
|
end
|
|
235
304
|
end
|
|
236
305
|
true
|
|
237
306
|
end
|
|
238
307
|
|
|
239
308
|
# Shutdown component and cleanup resources.
|
|
240
|
-
#
|
|
241
|
-
#
|
|
242
|
-
#
|
|
243
|
-
#
|
|
309
|
+
# Disables the hot-load TracePoint so no events queue for a dead
|
|
310
|
+
# scheduler. Cancels the per-instance scheduler so any pending debounced
|
|
311
|
+
# extraction is dropped. Waits for an in-flight extraction to complete
|
|
312
|
+
# before returning. Does not touch any sibling Components, so a sibling
|
|
313
|
+
# Component built after shutdown can still upload.
|
|
314
|
+
# The TracePoint teardown sits inside the same @scheduler_mutex critical
|
|
315
|
+
# section as the @shutdown flag flip, so it is atomic against a concurrent
|
|
316
|
+
# start_upload (which installs the TracePoint under @scheduler_mutex). Without
|
|
317
|
+
# that, a shutdown interleaved with a start could leave an enabled TracePoint
|
|
318
|
+
# rooted by the VM — class loads would keep growing @hot_load_buffer for the
|
|
319
|
+
# rest of the process lifetime (enqueue_hot_load's @shutdown check skips
|
|
320
|
+
# re-scheduling but only after the buffer push).
|
|
244
321
|
# @return [void]
|
|
245
322
|
def shutdown!
|
|
246
323
|
@scheduler_mutex.synchronize do
|
|
324
|
+
@hot_load_tracepoint&.disable
|
|
325
|
+
@hot_load_tracepoint = nil
|
|
247
326
|
@shutdown = true
|
|
248
327
|
@scheduler_signaled = true
|
|
249
328
|
@scheduler_cv.signal
|
|
@@ -253,32 +332,178 @@ module Datadog
|
|
|
253
332
|
|
|
254
333
|
@mutex.synchronize do
|
|
255
334
|
if @upload_in_progress
|
|
256
|
-
|
|
335
|
+
if Process.pid == @owner_pid
|
|
336
|
+
@upload_in_progress_cv.wait(@mutex, 5)
|
|
337
|
+
else
|
|
338
|
+
# We are in a forked child that inherited this Component but
|
|
339
|
+
# never called start_upload here. The scheduler thread (the
|
|
340
|
+
# only writer that clears @upload_in_progress and signals the
|
|
341
|
+
# cv) lives only in the parent — fork carries only the calling
|
|
342
|
+
# thread, so nothing in this process can ever signal us.
|
|
343
|
+
# Waiting would burn the full 5s timeout for no benefit. Treat
|
|
344
|
+
# the inherited @upload_in_progress as a stale snapshot and
|
|
345
|
+
# proceed; the parent's shutdown! (running in the parent) is
|
|
346
|
+
# authoritative. Child-owned uploads (where start_upload was
|
|
347
|
+
# called in this process) take the PID-match branch above,
|
|
348
|
+
# because start_upload claims @owner_pid for the current
|
|
349
|
+
# process.
|
|
350
|
+
@upload_in_progress = false
|
|
351
|
+
end
|
|
257
352
|
end
|
|
258
353
|
end
|
|
259
354
|
|
|
260
355
|
@scope_batcher.shutdown
|
|
261
356
|
end
|
|
262
357
|
|
|
358
|
+
# Reinitialize per-instance state in a forked child process.
|
|
359
|
+
#
|
|
360
|
+
# `Process.fork` copies the parent's memory but only the forking thread
|
|
361
|
+
# survives in the child. Background threads (`@scheduler_thread`) are
|
|
362
|
+
# dead, mutexes and condition variables are copied without owner
|
|
363
|
+
# tracking (orphan-lock risk if the parent held a mutex at the fork
|
|
364
|
+
# instant), and the TracePoint hook is bound to the dead scheduler.
|
|
365
|
+
#
|
|
366
|
+
# State reset (the child does its own initial extraction, then hot-load
|
|
367
|
+
# continues from there):
|
|
368
|
+
# - Hot-load buffer cleared — the child will rediscover via extract_all.
|
|
369
|
+
# - `@initial_extraction_done = false` — child has not extracted yet.
|
|
370
|
+
# - `@hot_load_tracepoint = nil` — `start_upload` reinstalls a fresh one
|
|
371
|
+
# bound to the child's component.
|
|
372
|
+
# - `@scheduler_thread = nil`, `@scheduled_at = nil`,
|
|
373
|
+
# `@scheduler_signaled = false` — scheduler restarts on next
|
|
374
|
+
# `start_upload`.
|
|
375
|
+
# - `@upload_in_progress = false` — parent may have been mid-upload at
|
|
376
|
+
# the fork instant; the child has no upload in flight.
|
|
377
|
+
# - `@scope_batcher` replaced with a fresh instance. The inherited batcher
|
|
378
|
+
# carries the parent's `@uploaded_modules` set, which `add_scope` uses
|
|
379
|
+
# to dedup by scope name. Without a fresh batcher, the child's
|
|
380
|
+
# re-extraction silently drops every scope whose name the parent
|
|
381
|
+
# already uploaded — under `preload_app!` that's most of the app.
|
|
382
|
+
#
|
|
383
|
+
# Mutex/CV reinit (orphan-lock guard):
|
|
384
|
+
# - `@scheduler_mutex`, `@scheduler_cv`, `@mutex`,
|
|
385
|
+
# `@upload_in_progress_cv`, `@last_upload_time_cv`,
|
|
386
|
+
# `@hot_load_buffer_mutex`.
|
|
387
|
+
#
|
|
388
|
+
# Force-upload mode: the parent's scheduled extraction is dead in the
|
|
389
|
+
# child, so re-register the deferred-upload callback. In Rails the
|
|
390
|
+
# `:after_initialize` hook has already fired (initialization happened
|
|
391
|
+
# in the parent), so the on_load block runs immediately and the child
|
|
392
|
+
# schedules its own upload. In non-Rails, this calls `start_upload`
|
|
393
|
+
# directly.
|
|
394
|
+
#
|
|
395
|
+
# Cross-process upload deduplication is intentionally not handled here.
|
|
396
|
+
# Each forked Component does its own initial extraction. Workers in
|
|
397
|
+
# `preload_app! + eager_load=true` deployments hold identical code to
|
|
398
|
+
# the parent — backend dedup of identical-content uploads is the
|
|
399
|
+
# backend's responsibility, not the tracer's.
|
|
400
|
+
#
|
|
401
|
+
# @return [void]
|
|
402
|
+
def after_fork!
|
|
403
|
+
# Disable the inherited TracePoint before dropping the reference: fork
|
|
404
|
+
# copies the enabled TP into the child, where it remains rooted by the
|
|
405
|
+
# VM. Without an explicit disable, every subsequent class load in the
|
|
406
|
+
# child would enqueue through the inherited hook in addition to the
|
|
407
|
+
# fresh hook that start_upload installs.
|
|
408
|
+
@hot_load_tracepoint&.disable
|
|
409
|
+
@hot_load_buffer = []
|
|
410
|
+
@hot_load_buffer_mutex = Mutex.new
|
|
411
|
+
@hot_load_tracepoint = nil
|
|
412
|
+
@initial_extraction_done = false
|
|
413
|
+
|
|
414
|
+
@scheduler_mutex = Mutex.new
|
|
415
|
+
@scheduler_cv = ConditionVariable.new
|
|
416
|
+
@scheduled_at = nil
|
|
417
|
+
@scheduler_signaled = false
|
|
418
|
+
@scheduler_thread = nil
|
|
419
|
+
|
|
420
|
+
@mutex = Mutex.new
|
|
421
|
+
@upload_in_progress = false
|
|
422
|
+
@upload_in_progress_cv = ConditionVariable.new
|
|
423
|
+
@last_upload_time_cv = ConditionVariable.new
|
|
424
|
+
|
|
425
|
+
# Fresh ScopeBatcher: the inherited one carries the parent's
|
|
426
|
+
# @uploaded_modules set, against which add_scope dedups by name.
|
|
427
|
+
@scope_batcher = ScopeBatcher.new(@uploader, logger: @logger)
|
|
428
|
+
|
|
429
|
+
schedule_deferred_upload if @settings.symbol_database.internal.force_upload
|
|
430
|
+
end
|
|
431
|
+
|
|
263
432
|
private
|
|
264
433
|
|
|
265
|
-
#
|
|
266
|
-
#
|
|
267
|
-
#
|
|
268
|
-
#
|
|
269
|
-
#
|
|
434
|
+
# Tear down the scheduler and hot-load hook without clearing the sticky
|
|
435
|
+
# @upload_requested desire, so resume_pending_upload can restart uploads
|
|
436
|
+
# that RC still wants. Disables the TracePoint :class hook so post-stop
|
|
437
|
+
# class loads don't re-arm the scheduler, clears the hot-load buffer, and
|
|
438
|
+
# resets @initial_extraction_done so a future resume performs a fresh
|
|
439
|
+
# extract_all instead of draining an empty buffer.
|
|
440
|
+
# The TracePoint teardown sits inside the same @scheduler_mutex critical
|
|
441
|
+
# section as the @scheduled_at reset, so it is atomic against a concurrent
|
|
442
|
+
# start_upload (which installs the TracePoint under @scheduler_mutex). Without
|
|
443
|
+
# that, a stop interleaved with a start could leave an enabled TracePoint
|
|
444
|
+
# rooted by the VM after this returned.
|
|
445
|
+
# @return [void]
|
|
446
|
+
def suspend_scheduling
|
|
447
|
+
@scheduler_mutex.synchronize do
|
|
448
|
+
@hot_load_tracepoint&.disable
|
|
449
|
+
@hot_load_tracepoint = nil
|
|
450
|
+
@scheduled_at = nil
|
|
451
|
+
@scheduler_signaled = true
|
|
452
|
+
@scheduler_cv.signal
|
|
453
|
+
end
|
|
454
|
+
@hot_load_buffer_mutex.synchronize { @hot_load_buffer.clear }
|
|
455
|
+
@initial_extraction_done = false
|
|
456
|
+
nil
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Whether a remote-config-triggered upload may proceed now.
|
|
460
|
+
#
|
|
461
|
+
# force_upload and an explicit `symbol_database.enabled = true` both mean
|
|
462
|
+
# "upload regardless of Dynamic Instrumentation". Only the nil-default case
|
|
463
|
+
# — where Symbol Database mirrors DI — is gated on DI actually being active,
|
|
464
|
+
# so the tracer never extracts symbols for an application that never
|
|
465
|
+
# enabled Dynamic Instrumentation.
|
|
466
|
+
# @return [bool]
|
|
467
|
+
def upload_allowed?
|
|
468
|
+
return true if @settings.symbol_database.internal.force_upload
|
|
469
|
+
|
|
470
|
+
case @settings.symbol_database.enabled
|
|
471
|
+
when true then true
|
|
472
|
+
when false then false
|
|
473
|
+
# steep:ignore NoMethod — Steep does not narrow @di_active to non-nil after the .nil? check
|
|
474
|
+
else @di_active.nil? || @di_active.call # steep:ignore NoMethod
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Whether upload is currently blocked specifically by the nil-default
|
|
479
|
+
# DI-active gate — the only case that defers and retries via
|
|
480
|
+
# resume_pending_upload. An explicit symbol_database.enabled = false is a
|
|
481
|
+
# disabled feature, not a deferral, so it clears @upload_requested and is
|
|
482
|
+
# not retried. force_upload and explicit true are never gated, so they are
|
|
483
|
+
# never deferred.
|
|
484
|
+
# @return [bool]
|
|
485
|
+
def deferred_by_di_gate?
|
|
486
|
+
return false if @settings.symbol_database.internal.force_upload
|
|
487
|
+
return false unless @settings.symbol_database.enabled.nil?
|
|
488
|
+
|
|
489
|
+
# After the guards above, upload_allowed? reduces to the DI-active gate,
|
|
490
|
+
# so a disallowed upload here is precisely a DI-gate deferral.
|
|
491
|
+
!upload_allowed?
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Check whether the runtime environment supports symbol database upload,
|
|
495
|
+
# logging the reason when it does not.
|
|
270
496
|
# @param logger [Logger]
|
|
271
497
|
# @return [Boolean]
|
|
272
498
|
def self.environment_supported?(logger)
|
|
499
|
+
return true if SymbolDatabase.supported_runtime?
|
|
500
|
+
|
|
273
501
|
if RUBY_ENGINE != 'ruby'
|
|
274
502
|
logger.debug { "symdb: not supported on #{RUBY_ENGINE}, skipping" }
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if RUBY_VERSION < '2.6'
|
|
278
|
-
logger.debug { "symdb: requires Ruby 2.6+, running #{RUBY_VERSION}, skipping" }
|
|
279
|
-
return false
|
|
503
|
+
else
|
|
504
|
+
logger.debug { "symdb: requires Ruby 2.7+, running #{RUBY_VERSION}, skipping" }
|
|
280
505
|
end
|
|
281
|
-
|
|
506
|
+
false
|
|
282
507
|
end
|
|
283
508
|
private_class_method :environment_supported?
|
|
284
509
|
|
|
@@ -291,7 +516,9 @@ module Datadog
|
|
|
291
516
|
end
|
|
292
517
|
|
|
293
518
|
# Scheduler thread main loop. Waits for the debounce window to elapse,
|
|
294
|
-
# then runs extract_and_upload
|
|
519
|
+
# then runs extract_and_upload. Loops indefinitely so that hot-load
|
|
520
|
+
# signals fired after the initial upload trigger subsequent incremental
|
|
521
|
+
# uploads.
|
|
295
522
|
# @return [void]
|
|
296
523
|
def scheduler_loop
|
|
297
524
|
loop do
|
|
@@ -301,27 +528,31 @@ module Datadog
|
|
|
301
528
|
|
|
302
529
|
@scheduler_mutex.synchronize do
|
|
303
530
|
return if @shutdown
|
|
304
|
-
return if Component.uploaded_this_process?
|
|
305
531
|
|
|
306
532
|
# Copy to local so Steep narrows `Float?` to `Float` in the else branch.
|
|
307
533
|
# Steep does not track narrowing on instance variables across nil checks.
|
|
308
534
|
scheduled_at = @scheduled_at
|
|
309
535
|
if scheduled_at.nil?
|
|
310
|
-
# Nothing scheduled (e.g. stop_upload cleared it
|
|
311
|
-
# indefinitely for a signal,
|
|
536
|
+
# Nothing scheduled (e.g. stop_upload cleared it, or no hot-load
|
|
537
|
+
# events since the last upload). Wait indefinitely for a signal,
|
|
538
|
+
# then re-evaluate on next loop.
|
|
312
539
|
@scheduler_signaled = false
|
|
313
540
|
@scheduler_cv.wait(@scheduler_mutex)
|
|
314
541
|
else
|
|
315
542
|
remaining = scheduled_at - Datadog::Core::Utils::Time.get_time
|
|
316
543
|
if remaining > 0
|
|
317
544
|
# Wait until the debounce deadline. Any signal (start_upload,
|
|
318
|
-
# stop_upload, shutdown
|
|
319
|
-
# and recompute rather than firing immediately
|
|
545
|
+
# stop_upload, shutdown!, hot-load event) wakes us early; we
|
|
546
|
+
# always re-loop and recompute rather than firing immediately
|
|
547
|
+
# on wake.
|
|
320
548
|
@scheduler_signaled = false
|
|
321
549
|
@scheduler_cv.wait(@scheduler_mutex, remaining)
|
|
322
550
|
else
|
|
323
|
-
# Deadline elapsed without further signal — fire after releasing
|
|
551
|
+
# Deadline elapsed without further signal — fire after releasing
|
|
552
|
+
# the mutex. Clear @scheduled_at so the next loop iteration
|
|
553
|
+
# waits for the next start_upload or hot-load signal.
|
|
324
554
|
should_fire = true
|
|
555
|
+
@scheduled_at = nil
|
|
325
556
|
end
|
|
326
557
|
end
|
|
327
558
|
end
|
|
@@ -333,20 +564,18 @@ module Datadog
|
|
|
333
564
|
|
|
334
565
|
# Outside the mutex.
|
|
335
566
|
return if @shutdown
|
|
336
|
-
if Component.uploaded_this_process?
|
|
337
|
-
return
|
|
338
|
-
end
|
|
339
567
|
|
|
340
568
|
extract_and_upload
|
|
341
|
-
Component.mark_uploaded
|
|
342
|
-
return
|
|
343
569
|
end
|
|
344
|
-
rescue => e
|
|
570
|
+
rescue Exception => e # standard:disable Lint/RescueException
|
|
571
|
+
Datadog::DI.reraise_if_fatal(e)
|
|
345
572
|
@logger.debug { "symdb: scheduler error: #{e.class}: #{e.message}" }
|
|
346
573
|
@telemetry&.report(e, description: 'symdb: scheduler error')
|
|
347
574
|
end
|
|
348
575
|
|
|
349
|
-
# Extract symbols
|
|
576
|
+
# Extract symbols and upload. First call runs extract_all (full ObjectSpace
|
|
577
|
+
# walk); subsequent calls drain the hot-load buffer and extract just the
|
|
578
|
+
# newly-loaded modules via Extractor#extract.
|
|
350
579
|
# @return [void]
|
|
351
580
|
def extract_and_upload
|
|
352
581
|
@mutex.synchronize { @upload_in_progress = true }
|
|
@@ -355,30 +584,51 @@ module Datadog
|
|
|
355
584
|
@logger.trace { "symdb: starting extraction and upload" }
|
|
356
585
|
start_time = Datadog::Core::Utils::Time.get_time
|
|
357
586
|
|
|
358
|
-
# Extract symbols from all loaded modules grouped by source file.
|
|
359
|
-
# extract_all handles ObjectSpace iteration, filtering, and FQN-based nesting.
|
|
360
|
-
file_scopes = @extractor.extract_all
|
|
361
587
|
extracted_count = 0
|
|
362
|
-
|
|
588
|
+
targetable_count = 0
|
|
589
|
+
consume = lambda do |scope|
|
|
363
590
|
@scope_batcher.add_scope(scope)
|
|
364
591
|
extracted_count += 1
|
|
592
|
+
targetable_count += count_targetable_methods_in_scope(scope)
|
|
365
593
|
log_scope_tree(scope, 0)
|
|
366
594
|
end
|
|
367
595
|
|
|
368
|
-
@
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
596
|
+
if @initial_extraction_done
|
|
597
|
+
extract_hot_load_buffer.each(&consume)
|
|
598
|
+
mode_label = "hot-load"
|
|
599
|
+
else
|
|
600
|
+
# Discard any TracePoint events captured between hook install and
|
|
601
|
+
# this initial scan — extract_all walks ObjectSpace which already
|
|
602
|
+
# covers everything loaded at this moment. Anything loaded during
|
|
603
|
+
# or after extract_all stays buffered for the next drain.
|
|
604
|
+
@hot_load_buffer_mutex.synchronize { @hot_load_buffer.clear }
|
|
605
|
+
# Stream form of extract_all yields one FILE scope at a time and frees
|
|
606
|
+
# the per-file intermediate tree as it goes — the full Array<Scope> is
|
|
607
|
+
# never materialized, keeping peak memory bounded for large workspaces.
|
|
608
|
+
@extractor.extract_all(&consume)
|
|
609
|
+
@initial_extraction_done = true
|
|
610
|
+
mode_label = "initial"
|
|
372
611
|
end
|
|
373
612
|
|
|
613
|
+
extraction_duration = Datadog::Core::Utils::Time.get_time - start_time
|
|
614
|
+
@logger.debug { "symdb: #{mode_label} extracted #{extracted_count} scopes (#{targetable_count} methods with targetable lines) in #{'%.2f' % extraction_duration}s" }
|
|
615
|
+
|
|
374
616
|
# Flush any remaining scopes (triggers upload)
|
|
375
617
|
@scope_batcher.flush
|
|
376
618
|
|
|
377
|
-
@
|
|
378
|
-
|
|
379
|
-
|
|
619
|
+
@mutex.synchronize do
|
|
620
|
+
@last_upload_time = Datadog::Core::Utils::Time.now
|
|
621
|
+
@last_upload_scope_count = extracted_count
|
|
622
|
+
@last_upload_time_cv.broadcast
|
|
623
|
+
end
|
|
624
|
+
rescue Exception => e # standard:disable Lint/RescueException
|
|
625
|
+
Datadog::DI.reraise_if_fatal(e)
|
|
380
626
|
@logger.debug { "symdb: extraction error: #{e.class}: #{e.message}" }
|
|
381
627
|
@telemetry&.report(e, description: 'symdb: extraction error')
|
|
628
|
+
@mutex.synchronize do
|
|
629
|
+
@last_upload_time = Datadog::Core::Utils::Time.now
|
|
630
|
+
@last_upload_time_cv.broadcast
|
|
631
|
+
end
|
|
382
632
|
ensure
|
|
383
633
|
@mutex.synchronize do
|
|
384
634
|
@upload_in_progress = false
|
|
@@ -387,19 +637,94 @@ module Datadog
|
|
|
387
637
|
end
|
|
388
638
|
end
|
|
389
639
|
|
|
640
|
+
# Drain the hot-load buffer, dedup by object_id, return the array of
|
|
641
|
+
# FILE scopes from per-module extraction.
|
|
642
|
+
# @return [Array<Scope>]
|
|
643
|
+
def extract_hot_load_buffer
|
|
644
|
+
modules = @hot_load_buffer_mutex.synchronize { @hot_load_buffer.shift(@hot_load_buffer.length) }
|
|
645
|
+
return [] if modules.empty?
|
|
646
|
+
|
|
647
|
+
seen = {}
|
|
648
|
+
modules.each { |mod| seen[mod.object_id] = mod }
|
|
649
|
+
seen.values.map { |mod| @extractor.extract(mod) }.compact
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
# Install the TracePoint :class hook (lazy — only on first start_upload).
|
|
653
|
+
# Hook fires for every class/module body open including reopens; pushes
|
|
654
|
+
# the module onto @hot_load_buffer and signals the scheduler. Singleton
|
|
655
|
+
# classes are filtered for the same reason as in Extractor#extract_all.
|
|
656
|
+
# Must be called from within @scheduler_mutex.synchronize.
|
|
657
|
+
# @return [void]
|
|
658
|
+
def install_hot_load_hook
|
|
659
|
+
return if @hot_load_tracepoint
|
|
660
|
+
component = self
|
|
661
|
+
logger = @logger
|
|
662
|
+
telemetry = @telemetry
|
|
663
|
+
@hot_load_tracepoint = TracePoint.new(:class) do |tp|
|
|
664
|
+
# The :class TracePoint fires inside the customer's class body —
|
|
665
|
+
# any exception that escapes this block surfaces at the customer's
|
|
666
|
+
# `class Foo; ... end` line and breaks their class load. The
|
|
667
|
+
# MODULE_SINGLETON_CLASS_PRED dispatch defends against one specific
|
|
668
|
+
# raise source (user-overridden singleton_class?); this rescue
|
|
669
|
+
# closes the general case. Verified: a raise inside the callback
|
|
670
|
+
# backtraces through `<class:CustomerClass>` in Ruby 3.x.
|
|
671
|
+
|
|
672
|
+
mod = tp.self
|
|
673
|
+
next if MODULE_SINGLETON_CLASS_PRED.bind(mod).call
|
|
674
|
+
component.send(:enqueue_hot_load, mod)
|
|
675
|
+
rescue Exception => e # standard:disable Lint/RescueException
|
|
676
|
+
Datadog::DI.reraise_if_fatal(e)
|
|
677
|
+
# Logger or telemetry can themselves raise (custom logger
|
|
678
|
+
# implementation, telemetry worker in an unexpected state). The
|
|
679
|
+
# :class TracePoint fires inside customer class bodies, so the
|
|
680
|
+
# error boundary must hold even when error reporting fails;
|
|
681
|
+
# nothing useful to do if logging is broken.
|
|
682
|
+
begin
|
|
683
|
+
logger.debug { "symdb: hot-load hook error: #{e.class}: #{e.message}" }
|
|
684
|
+
telemetry&.report(e, description: 'symdb: hot-load hook error')
|
|
685
|
+
rescue Exception => report_exc # standard:disable Lint/RescueException
|
|
686
|
+
Datadog::DI.reraise_if_fatal(report_exc)
|
|
687
|
+
nil
|
|
688
|
+
end
|
|
689
|
+
end
|
|
690
|
+
@hot_load_tracepoint.enable # steep:ignore NoMethod
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
# Enqueue a hot-loaded module and signal the scheduler.
|
|
694
|
+
# Called from the TracePoint :class block — must be cheap.
|
|
695
|
+
# @param mod [Module]
|
|
696
|
+
# @return [void]
|
|
697
|
+
def enqueue_hot_load(mod)
|
|
698
|
+
@hot_load_buffer_mutex.synchronize { @hot_load_buffer << mod }
|
|
699
|
+
@scheduler_mutex.synchronize do
|
|
700
|
+
return if @shutdown
|
|
701
|
+
# TracePoint#disable does not wait for in-flight callbacks: a :class
|
|
702
|
+
# event firing concurrently with stop_upload can reach here after the
|
|
703
|
+
# hook has been torn down. Without this guard the stale event would
|
|
704
|
+
# re-arm the scheduler, contradicting stop_upload's contract. The
|
|
705
|
+
# buffer push above is harmless — the next start_upload runs
|
|
706
|
+
# extract_all, which clears the buffer before extracting.
|
|
707
|
+
return unless @hot_load_tracepoint
|
|
708
|
+
@scheduled_at = Datadog::Core::Utils::Time.get_time + EXTRACT_DEBOUNCE_INTERVAL
|
|
709
|
+
@scheduler_signaled = true
|
|
710
|
+
@scheduler_cv.signal
|
|
711
|
+
end
|
|
712
|
+
end
|
|
713
|
+
|
|
390
714
|
def log_scope_tree(scope, depth)
|
|
391
715
|
indent = ' ' * depth
|
|
392
716
|
@logger.trace { "symdb: #{indent}#{scope.scope_type} #{scope.name}" }
|
|
393
717
|
scope.scopes&.each { |child| log_scope_tree(child, depth + 1) }
|
|
394
718
|
end
|
|
395
719
|
|
|
396
|
-
|
|
720
|
+
# Count METHOD scopes with targetable lines inside one FILE scope. Used by
|
|
721
|
+
# extract_and_upload to accumulate the count while streaming, without
|
|
722
|
+
# retaining the Array<Scope> just to compute the total at the end.
|
|
723
|
+
def count_targetable_methods_in_scope(file_scope)
|
|
397
724
|
count = 0
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
count += 1 if method_scope.scope_type == 'METHOD' && method_scope.targetable_lines?
|
|
402
|
-
end
|
|
725
|
+
file_scope.scopes&.each do |class_or_module|
|
|
726
|
+
class_or_module.scopes&.each do |method_scope|
|
|
727
|
+
count += 1 if method_scope.scope_type == 'METHOD' && method_scope.targetable_lines?
|
|
403
728
|
end
|
|
404
729
|
end
|
|
405
730
|
count
|