hyperprobe-agent 1.2.27.pre.1 → 1.2.27.pre.3
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/README.md +316 -87
- data/lib/hyperprobe/agent.rb +243 -144
- data/lib/hyperprobe/core/broker.rb +58 -88
- data/lib/hyperprobe/core/evaluator.rb +358 -68
- data/lib/hyperprobe/core/lexical_scope.rb +222 -0
- data/lib/hyperprobe/core/logger.rb +33 -12
- data/lib/hyperprobe/core/monitoring_engine.rb +294 -120
- data/lib/hyperprobe/core/safe_ast_validator.rb +140 -61
- data/lib/hyperprobe/core/safety.rb +92 -15
- data/lib/hyperprobe/core/serializer.rb +354 -342
- data/lib/hyperprobe/core/transports/java_grpc.rb +57 -0
- data/lib/hyperprobe/lambda.rb +44 -42
- data/lib/hyperprobe/protos/agent_descriptor.rb +7 -0
- data/lib/hyperprobe/protos/java_messages.rb +199 -0
- data/lib/hyperprobe/protos.rb +7 -7
- data/lib/hyperprobe/railtie.rb +6 -0
- data/lib/hyperprobe/version.rb +1 -1
- data/lib/hyperprobe.rb +149 -20
- metadata +25 -9
data/lib/hyperprobe/agent.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'securerandom'
|
|
4
4
|
require 'uri'
|
|
5
5
|
require 'thread'
|
|
6
|
+
require 'monitor'
|
|
6
7
|
require_relative 'version'
|
|
7
8
|
require_relative 'core/quota'
|
|
8
9
|
require_relative 'core/safety'
|
|
@@ -24,8 +25,17 @@ module HyperProbe
|
|
|
24
25
|
@agent_id = SecureRandom.uuid
|
|
25
26
|
@is_shutdown = false
|
|
26
27
|
@is_agent_disabled = false
|
|
27
|
-
@stop_event = ConditionVariable.new
|
|
28
28
|
@mutex = Monitor.new # Reentrant mutex in Ruby
|
|
29
|
+
@stop_event = @mutex.new_cond
|
|
30
|
+
@sync_mutex = Mutex.new
|
|
31
|
+
@flush_mutex = Mutex.new
|
|
32
|
+
@shutdown_mutex = Mutex.new
|
|
33
|
+
@probe_generation = 0
|
|
34
|
+
@pending_probe_update = nil
|
|
35
|
+
@finished_probes = {}
|
|
36
|
+
@telemetry_queue = Queue.new
|
|
37
|
+
@active_probes = {}
|
|
38
|
+
@local_hits = {}
|
|
29
39
|
|
|
30
40
|
@log = Core::Logger.get_logger('hyperprobe:agent')
|
|
31
41
|
@log_broker = Core::Logger.get_logger('hyperprobe:broker')
|
|
@@ -34,6 +44,10 @@ module HyperProbe
|
|
|
34
44
|
|
|
35
45
|
parse_configuration
|
|
36
46
|
|
|
47
|
+
if @global_config[:disable_safe_evaluation]
|
|
48
|
+
@log.warn 'Safe evaluation is DISABLED. Probe expressions can execute arbitrary Ruby code, mutate application state, or block execution.'
|
|
49
|
+
end
|
|
50
|
+
|
|
37
51
|
@quota_manager = Core::QuotaManager.new(@hits_per_sec, @bandwidth_kb_per_sec * 1024)
|
|
38
52
|
@safety_monitor = Core::SafetyMonitor.new(
|
|
39
53
|
method(:handle_health_change),
|
|
@@ -53,10 +67,8 @@ module HyperProbe
|
|
|
53
67
|
enable_keep_alive: @enable_keep_alive
|
|
54
68
|
)
|
|
55
69
|
|
|
56
|
-
@telemetry_queue = Queue.new
|
|
57
|
-
@active_probes = {}
|
|
58
|
-
@local_hits = {}
|
|
59
70
|
@cooldown_timer = nil
|
|
71
|
+
@cooldown_until = nil
|
|
60
72
|
|
|
61
73
|
@engine = Core::MonitoringEngine.new(
|
|
62
74
|
@quota_manager,
|
|
@@ -69,16 +81,22 @@ module HyperProbe
|
|
|
69
81
|
@sync_thread = nil
|
|
70
82
|
@flush_thread = nil
|
|
71
83
|
@stats_thread = nil
|
|
84
|
+
@apply_thread = Thread.new { apply_loop }
|
|
85
|
+
@apply_thread.name = 'hyperprobe-apply'
|
|
72
86
|
|
|
73
87
|
if @is_ephemeral_lambda
|
|
74
88
|
@log.debug 'Running in Ephemeral AWS Lambda Mode.'
|
|
75
89
|
else
|
|
76
90
|
@safety_monitor.start
|
|
77
91
|
start_background_loops
|
|
78
|
-
sync_with_broker
|
|
79
92
|
end
|
|
80
93
|
|
|
81
94
|
@log.info "Agent started for #{@service_id} in #{@environment} (v#{VERSION})"
|
|
95
|
+
rescue SignalException, SystemExit
|
|
96
|
+
shutdown
|
|
97
|
+
raise
|
|
98
|
+
rescue Exception # Agent-owned startup failures must not escape into the host.
|
|
99
|
+
shutdown
|
|
82
100
|
end
|
|
83
101
|
|
|
84
102
|
def agent_disabled?
|
|
@@ -86,65 +104,93 @@ module HyperProbe
|
|
|
86
104
|
end
|
|
87
105
|
|
|
88
106
|
def telemetry_queue_length
|
|
89
|
-
@telemetry_queue
|
|
107
|
+
@telemetry_queue&.size || 0
|
|
90
108
|
end
|
|
91
109
|
|
|
92
110
|
def force_sync(timeout_sec = nil)
|
|
93
|
-
return if @is_shutdown || @is_agent_disabled
|
|
111
|
+
return if @owner_pid != Process.pid || @is_shutdown || @is_agent_disabled
|
|
94
112
|
|
|
95
113
|
sync_with_broker(timeout_sec)
|
|
96
114
|
end
|
|
97
115
|
|
|
98
116
|
def force_flush(timeout_sec = nil)
|
|
99
|
-
return if @is_shutdown || @is_agent_disabled
|
|
117
|
+
return if @owner_pid != Process.pid || @is_shutdown || @is_agent_disabled
|
|
100
118
|
|
|
101
119
|
flush_telemetry(timeout_sec)
|
|
102
120
|
end
|
|
103
121
|
|
|
104
122
|
def shutdown
|
|
105
|
-
@
|
|
106
|
-
|
|
123
|
+
return after_fork if @owner_pid && @owner_pid != Process.pid
|
|
124
|
+
@shutdown_mutex ||= Mutex.new
|
|
125
|
+
return unless @shutdown_mutex.try_lock
|
|
107
126
|
|
|
108
|
-
|
|
109
|
-
|
|
127
|
+
shutdown_locked = true
|
|
128
|
+
return if @is_shutdown
|
|
110
129
|
|
|
111
|
-
@
|
|
112
|
-
@
|
|
113
|
-
@
|
|
114
|
-
|
|
130
|
+
@is_shutdown = true
|
|
131
|
+
@is_agent_disabled = true
|
|
132
|
+
threads = [@sync_thread, @flush_thread, @stats_thread, @apply_thread, @cooldown_timer].compact
|
|
133
|
+
threads.each { |thread| thread.kill unless thread == Thread.current }
|
|
115
134
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
135
|
+
# Cleanup independently: a broken or stuck resource cannot prevent the rest.
|
|
136
|
+
cleanup = [[@engine, :close], [@safety_monitor, :stop], [@broker_client, :shutdown]].each_with_object([]) do |(resource, method), workers|
|
|
137
|
+
next unless resource
|
|
138
|
+
|
|
139
|
+
workers << Thread.new do
|
|
140
|
+
resource.public_send(method)
|
|
141
|
+
rescue Exception
|
|
142
|
+
# Only agent-owned cleanup runs in this thread.
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
cleanup << Thread.new do
|
|
146
|
+
@mutex.synchronize do
|
|
147
|
+
until @telemetry_queue.empty?
|
|
148
|
+
settle_reservation(@telemetry_queue.pop(true)[:reservation], :release)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
rescue Exception
|
|
152
|
+
# Reservations belong only to the stopped agent.
|
|
153
|
+
end
|
|
154
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 0.5
|
|
155
|
+
(threads + cleanup).each do |thread|
|
|
156
|
+
next if thread == Thread.current
|
|
157
|
+
|
|
158
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
159
|
+
thread.join(remaining) if remaining.positive?
|
|
160
|
+
rescue SignalException, SystemExit
|
|
161
|
+
raise
|
|
162
|
+
rescue Exception
|
|
163
|
+
# Continue closing the other resources, including from a signal trap.
|
|
119
164
|
end
|
|
165
|
+
cleanup.each do |thread|
|
|
166
|
+
thread.kill if thread.alive?
|
|
167
|
+
thread.join(0.01)
|
|
168
|
+
rescue SignalException, SystemExit
|
|
169
|
+
raise
|
|
170
|
+
rescue Exception
|
|
171
|
+
# Never wait indefinitely for a broken cleanup thread.
|
|
172
|
+
end
|
|
173
|
+
rescue SignalException, SystemExit
|
|
174
|
+
raise
|
|
175
|
+
rescue Exception
|
|
176
|
+
@is_shutdown = true
|
|
177
|
+
@is_agent_disabled = true
|
|
178
|
+
ensure
|
|
179
|
+
@shutdown_mutex.unlock if shutdown_locked
|
|
120
180
|
end
|
|
121
181
|
|
|
122
182
|
def after_fork
|
|
123
|
-
return if
|
|
124
|
-
|
|
125
|
-
@mutex.synchronize do
|
|
126
|
-
return if @is_shutdown
|
|
127
|
-
|
|
128
|
-
@owner_pid = Process.pid
|
|
129
|
-
@agent_id = SecureRandom.uuid
|
|
130
|
-
|
|
131
|
-
# Reset queues and client for new process
|
|
132
|
-
@telemetry_queue = Queue.new
|
|
133
|
-
@broker_client = Core::BrokerClient.new(
|
|
134
|
-
broker_url: @broker_url,
|
|
135
|
-
service_id: @service_id,
|
|
136
|
-
environment: @environment,
|
|
137
|
-
commit_sha: @commit_sha,
|
|
138
|
-
agent_id: @agent_id,
|
|
139
|
-
agent_version: VERSION,
|
|
140
|
-
rpc_timeout_sec: @rpc_timeout_sec,
|
|
141
|
-
enable_keep_alive: @enable_keep_alive
|
|
142
|
-
)
|
|
183
|
+
return if @owner_pid == Process.pid
|
|
143
184
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
185
|
+
# Never call, close, or replace an inherited native gRPC client here.
|
|
186
|
+
@is_shutdown = true
|
|
187
|
+
@is_agent_disabled = true
|
|
188
|
+
@engine&.after_fork
|
|
189
|
+
false
|
|
190
|
+
rescue SignalException, SystemExit
|
|
191
|
+
raise
|
|
192
|
+
rescue Exception
|
|
193
|
+
false
|
|
148
194
|
end
|
|
149
195
|
|
|
150
196
|
private
|
|
@@ -155,8 +201,11 @@ module HyperProbe
|
|
|
155
201
|
@broker_url = get_opt(:broker_url, :brokerUrl) || ENV['HYPERPROBE_BROKER_URL']
|
|
156
202
|
@commit_sha = get_opt(:commit_sha, :commitSha) || ENV['GIT_COMMIT'] || ENV['HYPERPROBE_COMMIT_SHA']
|
|
157
203
|
|
|
158
|
-
|
|
159
|
-
@
|
|
204
|
+
raw_sync = get_opt(:sync_interval_ms, :syncIntervalMs) || parse_env_int('HYPERPROBE_SYNC_INTERVAL_MS', 60_000)
|
|
205
|
+
@sync_interval_sec = [raw_sync.to_f / 1000.0, 0.1].max
|
|
206
|
+
|
|
207
|
+
raw_flush = get_opt(:flush_interval_ms, :flushIntervalMs) || parse_env_int('HYPERPROBE_FLUSH_INTERVAL_MS', 1000)
|
|
208
|
+
@flush_interval_sec = [raw_flush.to_f / 1000.0, 0.05].max
|
|
160
209
|
@max_queue_size = get_opt(:max_queue_size, :maxQueueSize) || parse_env_int('HYPERPROBE_MAX_QUEUE_SIZE', 100)
|
|
161
210
|
@cooldown_sec = get_opt(:cooldown_sec, :cooldownSec) || parse_env_int('HYPERPROBE_COOLDOWN_SEC', 10)
|
|
162
211
|
|
|
@@ -168,7 +217,8 @@ module HyperProbe
|
|
|
168
217
|
|
|
169
218
|
is_aws_lambda = !ENV['AWS_LAMBDA_FUNCTION_NAME'].nil?
|
|
170
219
|
is_local_emulator = ENV['IS_OFFLINE'] == 'true' || ENV['AWS_SAM_LOCAL'] == 'true'
|
|
171
|
-
|
|
220
|
+
lambda_opt = get_opt(:is_lambda, :isLambda)
|
|
221
|
+
@is_ephemeral_lambda = (lambda_opt.nil? ? is_aws_lambda : lambda_opt) && !is_local_emulator
|
|
172
222
|
enable_keep_alive_opt = get_opt(:enable_keep_alive, :enableKeepAlive)
|
|
173
223
|
@enable_keep_alive = enable_keep_alive_opt.nil? ? !@is_ephemeral_lambda : enable_keep_alive_opt
|
|
174
224
|
|
|
@@ -182,7 +232,9 @@ module HyperProbe
|
|
|
182
232
|
max_array_length: get_opt(:max_array_length, :maxArrayLength) || parse_env_int('HYPERPROBE_MAX_ARRAY_LENGTH', 3),
|
|
183
233
|
stack_frame_depth: get_opt(:stack_frame_depth, :stackFrameDepth) || parse_env_int('HYPERPROBE_STACK_FRAME_DEPTH', 3),
|
|
184
234
|
max_object_properties: get_opt(:max_object_properties, :maxObjectProperties) || parse_env_int('HYPERPROBE_MAX_OBJECT_PROPERTIES', 50),
|
|
185
|
-
max_string_length: get_opt(:max_string_length, :maxStringLength) || parse_env_int('HYPERPROBE_MAX_STRING_LENGTH', 1024)
|
|
235
|
+
max_string_length: get_opt(:max_string_length, :maxStringLength) || parse_env_int('HYPERPROBE_MAX_STRING_LENGTH', 1024),
|
|
236
|
+
capture_closures: get_opt(:capture_closures, :captureClosures) == true,
|
|
237
|
+
disable_safe_evaluation: !Core::Evaluator.safe_evaluation_enabled?(get_opt(:disable_safe_evaluation, :disableSafeEvaluation))
|
|
186
238
|
}
|
|
187
239
|
end
|
|
188
240
|
|
|
@@ -214,19 +266,21 @@ module HyperProbe
|
|
|
214
266
|
|
|
215
267
|
def sync_loop
|
|
216
268
|
until @is_shutdown
|
|
217
|
-
sleep @sync_interval_sec
|
|
218
|
-
break if @is_shutdown
|
|
219
|
-
|
|
220
269
|
begin
|
|
221
270
|
sync_with_broker
|
|
222
|
-
|
|
271
|
+
sleep @sync_interval_sec
|
|
272
|
+
rescue Exception
|
|
223
273
|
# Silently handle transient sync error
|
|
274
|
+
break
|
|
224
275
|
end
|
|
225
276
|
end
|
|
226
277
|
end
|
|
227
278
|
|
|
228
279
|
def sync_with_broker(timeout_sec = nil)
|
|
229
|
-
return if @is_shutdown
|
|
280
|
+
return if @owner_pid != Process.pid || @is_shutdown || @is_agent_disabled
|
|
281
|
+
return false unless @sync_mutex.try_lock
|
|
282
|
+
|
|
283
|
+
sync_locked = true
|
|
230
284
|
|
|
231
285
|
@log_broker.debug "sync_with_broker started #{timeout_sec ? "with timeout #{timeout_sec}" : ''}"
|
|
232
286
|
response = @broker_client.get_probes(timeout_sec)
|
|
@@ -234,181 +288,216 @@ module HyperProbe
|
|
|
234
288
|
|
|
235
289
|
@log_broker.debug "sync_with_broker: got #{response.probes.length} probes from broker"
|
|
236
290
|
|
|
237
|
-
# Dynamic global config updates from broker
|
|
238
|
-
if response.global_config
|
|
239
|
-
gc = response.global_config
|
|
240
|
-
@mutex.synchronize do
|
|
241
|
-
@global_config[:redact_keys] = gc.redact_keys.to_a unless gc.redact_keys.empty?
|
|
242
|
-
@global_config[:redact_values] = gc.redact_values.to_a unless gc.redact_values.empty?
|
|
243
|
-
@global_config[:max_object_depth] = gc.max_object_depth if gc.max_object_depth.positive?
|
|
244
|
-
@global_config[:max_array_length] = gc.max_array_length if gc.max_array_length.positive?
|
|
245
|
-
@global_config[:stack_frame_depth] = gc.stack_frame_depth if gc.stack_frame_depth.positive?
|
|
246
|
-
@global_config[:max_object_properties] = gc.max_object_properties if gc.max_object_properties.positive?
|
|
247
|
-
@global_config[:max_string_length] = gc.max_string_length if gc.max_string_length.positive?
|
|
248
|
-
end
|
|
249
|
-
@engine.set_global_config(@global_config)
|
|
250
|
-
end
|
|
251
|
-
|
|
252
291
|
server_probes = response.probes.to_a
|
|
253
292
|
server_probe_ids = server_probes.map(&:id)
|
|
254
|
-
|
|
255
|
-
to_apply = []
|
|
256
293
|
now_ms = (Time.now.to_f * 1000).to_i
|
|
294
|
+
config = nil
|
|
295
|
+
snapshot = @mutex.synchronize do
|
|
296
|
+
return false if @is_shutdown
|
|
257
297
|
|
|
258
|
-
|
|
298
|
+
if response.global_config
|
|
299
|
+
gc = response.global_config
|
|
300
|
+
@global_config[:redact_keys] = gc.redact_keys.to_a unless gc.redact_keys.empty?
|
|
301
|
+
@global_config[:redact_values] = gc.redact_values.to_a unless gc.redact_values.empty?
|
|
302
|
+
%i[max_object_depth max_array_length stack_frame_depth max_object_properties max_string_length capture_closures].each do |field|
|
|
303
|
+
@global_config[field] = gc.public_send(field) if gc.public_send("has_#{field}?")
|
|
304
|
+
end
|
|
305
|
+
config = @global_config.dup
|
|
306
|
+
end
|
|
259
307
|
# Clean stale local probes
|
|
260
308
|
@active_probes.delete_if { |pid, _| !server_probe_ids.include?(pid) }
|
|
261
309
|
@local_hits.delete_if { |pid, _| !server_probe_ids.include?(pid) }
|
|
310
|
+
@finished_probes.delete_if { |pid, _| !server_probe_ids.include?(pid) }
|
|
262
311
|
|
|
263
312
|
server_probes.each do |probe|
|
|
264
313
|
hits = @local_hits[probe.id] || 0
|
|
265
314
|
is_expired = probe.expiry_time.positive? && probe.expiry_time <= now_ms
|
|
266
315
|
|
|
267
|
-
if hits < probe.hit_limit && !is_expired
|
|
268
|
-
to_apply << probe
|
|
316
|
+
if hits < probe.hit_limit && !is_expired && !@finished_probes[probe.id]
|
|
269
317
|
@active_probes[probe.id] = probe
|
|
270
318
|
else
|
|
271
319
|
@active_probes.delete(probe.id)
|
|
272
320
|
end
|
|
273
321
|
end
|
|
322
|
+
probe_snapshot_locked
|
|
274
323
|
end
|
|
275
324
|
|
|
276
|
-
@
|
|
277
|
-
@engine.set_probes(
|
|
325
|
+
@engine.set_global_config(config) if config
|
|
326
|
+
@engine.set_probes(snapshot[0], generation: snapshot[1])
|
|
278
327
|
true
|
|
279
|
-
rescue
|
|
280
|
-
|
|
328
|
+
rescue SignalException, SystemExit
|
|
329
|
+
raise
|
|
330
|
+
rescue Exception
|
|
281
331
|
false
|
|
332
|
+
ensure
|
|
333
|
+
@sync_mutex.unlock if sync_locked
|
|
282
334
|
end
|
|
283
335
|
|
|
284
336
|
def flush_loop
|
|
285
337
|
until @is_shutdown
|
|
286
|
-
sleep @flush_interval_sec
|
|
287
|
-
break if @is_shutdown
|
|
288
|
-
|
|
289
338
|
begin
|
|
339
|
+
sleep @flush_interval_sec
|
|
340
|
+
break if @is_shutdown
|
|
341
|
+
|
|
290
342
|
flush_telemetry
|
|
291
|
-
rescue
|
|
343
|
+
rescue Exception
|
|
292
344
|
# Silently handle flush loop error
|
|
293
345
|
end
|
|
294
346
|
end
|
|
295
347
|
end
|
|
296
348
|
|
|
297
349
|
def flush_telemetry(timeout_sec = nil)
|
|
298
|
-
return if @is_shutdown || @
|
|
350
|
+
return if @owner_pid != Process.pid || @is_shutdown || @is_agent_disabled
|
|
351
|
+
return false unless @flush_mutex.try_lock
|
|
352
|
+
|
|
353
|
+
flush_locked = true
|
|
299
354
|
|
|
300
355
|
batch = []
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
batch << @telemetry_queue.pop(true)
|
|
304
|
-
rescue ThreadError
|
|
305
|
-
break
|
|
306
|
-
end
|
|
356
|
+
@mutex.synchronize do
|
|
357
|
+
batch << @telemetry_queue.pop(true) until @telemetry_queue.empty?
|
|
307
358
|
end
|
|
308
359
|
|
|
309
360
|
return if batch.empty?
|
|
310
361
|
|
|
311
|
-
@log_broker.info "Flushing #{batch.length} telemetry events to broker..."
|
|
312
|
-
|
|
313
362
|
begin
|
|
314
363
|
events = batch.map { |item| item[:event] }
|
|
315
364
|
finished_probe_ids = @broker_client.report_telemetry(events, timeout_sec)
|
|
316
|
-
|
|
317
|
-
@log_broker.info "Successfully flushed #{batch.length} telemetry events to broker"
|
|
365
|
+
delivered = true
|
|
318
366
|
|
|
319
367
|
# Commit bandwidth reservations
|
|
320
368
|
batch.each do |item|
|
|
321
|
-
item[:reservation]
|
|
369
|
+
settle_reservation(item[:reservation], :commit)
|
|
322
370
|
end
|
|
323
371
|
|
|
324
372
|
# Handle globally finished probes
|
|
325
373
|
if finished_probe_ids && !finished_probe_ids.empty?
|
|
326
|
-
|
|
327
|
-
|
|
374
|
+
snapshot = @mutex.synchronize do
|
|
375
|
+
return if @is_shutdown
|
|
376
|
+
|
|
328
377
|
finished_probe_ids.each do |pid|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
changed = true
|
|
332
|
-
end
|
|
378
|
+
@finished_probes[pid] = true
|
|
379
|
+
@active_probes.delete(pid)
|
|
333
380
|
end
|
|
381
|
+
probe_snapshot_locked
|
|
334
382
|
end
|
|
335
|
-
@engine.set_probes(
|
|
383
|
+
@engine.set_probes(snapshot[0], generation: snapshot[1])
|
|
336
384
|
end
|
|
337
|
-
rescue
|
|
338
|
-
|
|
385
|
+
rescue SignalException, SystemExit
|
|
386
|
+
raise
|
|
387
|
+
rescue Exception
|
|
388
|
+
return false if delivered
|
|
389
|
+
|
|
339
390
|
# On flush failure, re-queue events up to max capacity
|
|
340
391
|
batch.each do |item|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
392
|
+
retained = @mutex.synchronize do
|
|
393
|
+
if !@is_shutdown && @telemetry_queue.size < @max_queue_size
|
|
394
|
+
@telemetry_queue.push(item)
|
|
395
|
+
true
|
|
396
|
+
end
|
|
344
397
|
end
|
|
398
|
+
settle_reservation(item[:reservation], :release) unless retained
|
|
345
399
|
end
|
|
346
400
|
end
|
|
401
|
+
rescue SignalException, SystemExit
|
|
402
|
+
raise
|
|
403
|
+
rescue Exception
|
|
404
|
+
false
|
|
405
|
+
ensure
|
|
406
|
+
if @is_shutdown && batch && !delivered
|
|
407
|
+
batch.each { |item| settle_reservation(item[:reservation], :release) }
|
|
408
|
+
end
|
|
409
|
+
@flush_mutex.unlock if flush_locked
|
|
347
410
|
end
|
|
348
411
|
|
|
349
412
|
def stats_loop
|
|
350
413
|
until @is_shutdown
|
|
351
|
-
sleep 5.0
|
|
352
|
-
break if @is_shutdown
|
|
353
|
-
|
|
354
414
|
begin
|
|
415
|
+
sleep 5.0
|
|
416
|
+
break if @is_shutdown
|
|
417
|
+
|
|
355
418
|
stats = @engine.get_stats
|
|
356
419
|
if stats[:hits].positive? || stats[:skips].positive?
|
|
357
420
|
@log_stats.info "Probes Hit: #{stats[:hits]}, Probes Skipped: #{stats[:skips]}"
|
|
358
421
|
end
|
|
359
|
-
rescue
|
|
422
|
+
rescue Exception
|
|
360
423
|
# Ignore stats error
|
|
361
424
|
end
|
|
362
425
|
end
|
|
363
426
|
end
|
|
364
427
|
|
|
365
428
|
def handle_capture(event)
|
|
429
|
+
return if @owner_pid != Process.pid || @is_shutdown || @is_agent_disabled
|
|
430
|
+
return unless @mutex.try_enter
|
|
431
|
+
|
|
432
|
+
locked = true
|
|
366
433
|
return if @is_shutdown || @is_agent_disabled
|
|
367
434
|
|
|
368
435
|
probe_id = event[:probe_id]
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
436
|
+
probe = @active_probes[probe_id]
|
|
437
|
+
return unless probe
|
|
438
|
+
|
|
439
|
+
raw_limit = probe.respond_to?(:hit_limit) ? probe.hit_limit : probe[:hit_limit]
|
|
440
|
+
hit_limit = raw_limit.to_i
|
|
441
|
+
hits = @local_hits[probe_id] || 0
|
|
442
|
+
return if hit_limit.positive? && hits >= hit_limit
|
|
443
|
+
|
|
444
|
+
# Attempted captures consume the safety fuse even when delivery is dropped.
|
|
445
|
+
@local_hits[probe_id] = hits + 1
|
|
446
|
+
if hit_limit.positive? && hits + 1 >= hit_limit
|
|
447
|
+
@active_probes.delete(probe_id)
|
|
448
|
+
@pending_probe_update = probe_snapshot_locked
|
|
449
|
+
@stop_event.broadcast
|
|
379
450
|
end
|
|
380
451
|
|
|
381
|
-
|
|
452
|
+
return if @telemetry_queue.size >= @max_queue_size
|
|
453
|
+
|
|
382
454
|
event_size = @broker_client.estimate_event_size(event)
|
|
383
455
|
reservation = @quota_manager.reserve_bandwidth(event_size)
|
|
384
|
-
unless reservation
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
456
|
+
return unless reservation
|
|
457
|
+
|
|
458
|
+
@telemetry_queue.push(event: event, reservation: reservation)
|
|
459
|
+
admitted = true
|
|
460
|
+
rescue SignalException, SystemExit
|
|
461
|
+
raise
|
|
462
|
+
rescue Exception
|
|
463
|
+
# This callback is agent code running on an application thread.
|
|
464
|
+
nil
|
|
465
|
+
ensure
|
|
466
|
+
@mutex.exit if locked
|
|
467
|
+
settle_reservation(reservation, :release) if reservation && !admitted
|
|
468
|
+
end
|
|
388
469
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
@log.debug "Telemetry queue full. Dropping event for probe #{probe_id}"
|
|
395
|
-
reservation.release
|
|
396
|
-
end
|
|
470
|
+
def settle_reservation(reservation, action)
|
|
471
|
+
reservation&.public_send(action)
|
|
472
|
+
rescue Exception
|
|
473
|
+
nil
|
|
474
|
+
end
|
|
397
475
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
476
|
+
def probe_snapshot_locked
|
|
477
|
+
@probe_generation += 1
|
|
478
|
+
[@active_probes.values, @probe_generation]
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def apply_loop
|
|
482
|
+
loop do
|
|
483
|
+
snapshot = @mutex.synchronize do
|
|
484
|
+
@stop_event.wait until @is_shutdown || @pending_probe_update
|
|
485
|
+
break if @is_shutdown
|
|
486
|
+
|
|
487
|
+
update = @pending_probe_update
|
|
488
|
+
@pending_probe_update = nil
|
|
489
|
+
update
|
|
405
490
|
end
|
|
406
|
-
|
|
491
|
+
break unless snapshot
|
|
492
|
+
|
|
493
|
+
@engine.set_probes(snapshot[0], generation: snapshot[1])
|
|
407
494
|
end
|
|
495
|
+
rescue Exception
|
|
496
|
+
shutdown
|
|
408
497
|
end
|
|
409
498
|
|
|
410
499
|
def handle_health_change(health, reason = nil)
|
|
411
|
-
return if @is_shutdown
|
|
500
|
+
return if @owner_pid != Process.pid || @is_shutdown
|
|
412
501
|
|
|
413
502
|
case health
|
|
414
503
|
when Core::AgentHealth::RED
|
|
@@ -417,22 +506,32 @@ module HyperProbe
|
|
|
417
506
|
|
|
418
507
|
@cooldown_timer&.exit if @cooldown_timer&.alive?
|
|
419
508
|
cooldown_duration = @cooldown_sec
|
|
509
|
+
@cooldown_until = Process.clock_gettime(Process::CLOCK_MONOTONIC) + cooldown_duration
|
|
420
510
|
@cooldown_timer = Thread.new do
|
|
421
511
|
@log_safety.info "Cooldown period started (#{cooldown_duration}s)..."
|
|
422
512
|
sleep cooldown_duration
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
513
|
+
@safety_monitor.with_health(Core::AgentHealth::GREEN) do
|
|
514
|
+
if !@is_shutdown && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= @cooldown_until
|
|
515
|
+
@log_safety.info "Cooldown period ended. Resuming instrumentation..."
|
|
516
|
+
@engine.resume
|
|
517
|
+
end
|
|
426
518
|
end
|
|
519
|
+
rescue Exception
|
|
520
|
+
# Agent-owned cooldown work must never abort the application.
|
|
427
521
|
end
|
|
522
|
+
@cooldown_timer.kill if @is_shutdown
|
|
428
523
|
when Core::AgentHealth::YELLOW
|
|
429
524
|
@log_safety.warn "Safety Warning: YELLOW Status (Moderate Overhead). #{reason || ''}"
|
|
430
525
|
when Core::AgentHealth::GREEN
|
|
431
526
|
@log_safety.info "Status back to GREEN. #{reason || ''}"
|
|
432
|
-
|
|
527
|
+
# A timer may be alive briefly after its final health check. Use the
|
|
528
|
+
# deadline, not thread liveness, so a concurrent GREEN is never lost.
|
|
529
|
+
if @engine.is_suspended && (!@cooldown_until || Process.clock_gettime(Process::CLOCK_MONOTONIC) >= @cooldown_until)
|
|
433
530
|
@engine.resume
|
|
434
531
|
end
|
|
435
532
|
end
|
|
533
|
+
rescue Exception
|
|
534
|
+
@is_agent_disabled = true
|
|
436
535
|
end
|
|
437
536
|
end
|
|
438
537
|
end
|