hyperprobe-agent 1.2.27.pre.3-java
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 +7 -0
- data/LICENSE +13 -0
- data/README.md +386 -0
- data/lib/hyperprobe/agent.rb +537 -0
- data/lib/hyperprobe/core/broker.rb +183 -0
- data/lib/hyperprobe/core/evaluator.rb +418 -0
- data/lib/hyperprobe/core/lexical_scope.rb +222 -0
- data/lib/hyperprobe/core/logger.rb +198 -0
- data/lib/hyperprobe/core/monitoring_engine.rb +857 -0
- data/lib/hyperprobe/core/quota.rb +111 -0
- data/lib/hyperprobe/core/safe_ast_validator.rb +170 -0
- data/lib/hyperprobe/core/safety.rb +256 -0
- data/lib/hyperprobe/core/serializer.rb +437 -0
- data/lib/hyperprobe/core/trace_extractor.rb +158 -0
- data/lib/hyperprobe/core/transports/java_grpc.rb +57 -0
- data/lib/hyperprobe/jars/hyperprobe-grpc.jar +0 -0
- data/lib/hyperprobe/lambda.rb +92 -0
- data/lib/hyperprobe/protos/agent_descriptor.rb +7 -0
- data/lib/hyperprobe/protos/agent_pb.rb +33 -0
- data/lib/hyperprobe/protos/agent_services_pb.rb +31 -0
- data/lib/hyperprobe/protos/java_messages.rb +199 -0
- data/lib/hyperprobe/protos.rb +14 -0
- data/lib/hyperprobe/railtie.rb +22 -0
- data/lib/hyperprobe/version.rb +5 -0
- data/lib/hyperprobe-agent.rb +3 -0
- data/lib/hyperprobe.rb +282 -0
- data/transport/README.md +98 -0
- data/transport/THIRD_PARTY_NOTICES.md +57 -0
- data/transport/pom.xml +78 -0
- data/transport/src/main/java/co/hyperprobe/transport/GrpcTransport.java +118 -0
- data/transport/src/main/java/co/hyperprobe/transport/ProtoCodec.java +43 -0
- metadata +117 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'thread'
|
|
6
|
+
require 'monitor'
|
|
7
|
+
require_relative 'version'
|
|
8
|
+
require_relative 'core/quota'
|
|
9
|
+
require_relative 'core/safety'
|
|
10
|
+
require_relative 'core/serializer'
|
|
11
|
+
require_relative 'core/evaluator'
|
|
12
|
+
require_relative 'core/trace_extractor'
|
|
13
|
+
require_relative 'core/monitoring_engine'
|
|
14
|
+
require_relative 'core/broker'
|
|
15
|
+
|
|
16
|
+
module HyperProbe
|
|
17
|
+
UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.freeze
|
|
18
|
+
|
|
19
|
+
class Agent
|
|
20
|
+
attr_reader :options, :agent_id, :owner_pid, :is_shutdown, :active_probes, :global_config, :quota_manager, :safety_monitor, :broker_client, :engine
|
|
21
|
+
|
|
22
|
+
def initialize(options = {})
|
|
23
|
+
@options = (options || {}).dup
|
|
24
|
+
@owner_pid = Process.pid
|
|
25
|
+
@agent_id = SecureRandom.uuid
|
|
26
|
+
@is_shutdown = false
|
|
27
|
+
@is_agent_disabled = false
|
|
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 = {}
|
|
39
|
+
|
|
40
|
+
@log = Core::Logger.get_logger('hyperprobe:agent')
|
|
41
|
+
@log_broker = Core::Logger.get_logger('hyperprobe:broker')
|
|
42
|
+
@log_safety = Core::Logger.get_logger('hyperprobe:safety')
|
|
43
|
+
@log_stats = Core::Logger.get_logger('hyperprobe:stats')
|
|
44
|
+
|
|
45
|
+
parse_configuration
|
|
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
|
+
|
|
51
|
+
@quota_manager = Core::QuotaManager.new(@hits_per_sec, @bandwidth_kb_per_sec * 1024)
|
|
52
|
+
@safety_monitor = Core::SafetyMonitor.new(
|
|
53
|
+
method(:handle_health_change),
|
|
54
|
+
max_lag_ms: @max_lag_ms,
|
|
55
|
+
pause_budget_ms: @pause_budget_ms,
|
|
56
|
+
is_ephemeral_lambda: @is_ephemeral_lambda
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@broker_client = Core::BrokerClient.new(
|
|
60
|
+
broker_url: @broker_url,
|
|
61
|
+
service_id: @service_id,
|
|
62
|
+
environment: @environment,
|
|
63
|
+
commit_sha: @commit_sha,
|
|
64
|
+
agent_id: @agent_id,
|
|
65
|
+
agent_version: VERSION,
|
|
66
|
+
rpc_timeout_sec: @rpc_timeout_sec,
|
|
67
|
+
enable_keep_alive: @enable_keep_alive
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@cooldown_timer = nil
|
|
71
|
+
@cooldown_until = nil
|
|
72
|
+
|
|
73
|
+
@engine = Core::MonitoringEngine.new(
|
|
74
|
+
@quota_manager,
|
|
75
|
+
@safety_monitor,
|
|
76
|
+
method(:handle_capture),
|
|
77
|
+
@options[:set_trace_id]
|
|
78
|
+
)
|
|
79
|
+
@engine.set_global_config(@global_config)
|
|
80
|
+
|
|
81
|
+
@sync_thread = nil
|
|
82
|
+
@flush_thread = nil
|
|
83
|
+
@stats_thread = nil
|
|
84
|
+
@apply_thread = Thread.new { apply_loop }
|
|
85
|
+
@apply_thread.name = 'hyperprobe-apply'
|
|
86
|
+
|
|
87
|
+
if @is_ephemeral_lambda
|
|
88
|
+
@log.debug 'Running in Ephemeral AWS Lambda Mode.'
|
|
89
|
+
else
|
|
90
|
+
@safety_monitor.start
|
|
91
|
+
start_background_loops
|
|
92
|
+
end
|
|
93
|
+
|
|
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
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def agent_disabled?
|
|
103
|
+
@is_agent_disabled
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def telemetry_queue_length
|
|
107
|
+
@telemetry_queue&.size || 0
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def force_sync(timeout_sec = nil)
|
|
111
|
+
return if @owner_pid != Process.pid || @is_shutdown || @is_agent_disabled
|
|
112
|
+
|
|
113
|
+
sync_with_broker(timeout_sec)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def force_flush(timeout_sec = nil)
|
|
117
|
+
return if @owner_pid != Process.pid || @is_shutdown || @is_agent_disabled
|
|
118
|
+
|
|
119
|
+
flush_telemetry(timeout_sec)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def shutdown
|
|
123
|
+
return after_fork if @owner_pid && @owner_pid != Process.pid
|
|
124
|
+
@shutdown_mutex ||= Mutex.new
|
|
125
|
+
return unless @shutdown_mutex.try_lock
|
|
126
|
+
|
|
127
|
+
shutdown_locked = true
|
|
128
|
+
return if @is_shutdown
|
|
129
|
+
|
|
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 }
|
|
134
|
+
|
|
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.
|
|
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
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def after_fork
|
|
183
|
+
return if @owner_pid == Process.pid
|
|
184
|
+
|
|
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
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
private
|
|
197
|
+
|
|
198
|
+
def parse_configuration
|
|
199
|
+
@service_id = get_opt(:service_id, :serviceId) || ENV['HYPERPROBE_SERVICE_ID']
|
|
200
|
+
@environment = get_opt(:environment) || ENV['HYPERPROBE_ENVIRONMENT']
|
|
201
|
+
@broker_url = get_opt(:broker_url, :brokerUrl) || ENV['HYPERPROBE_BROKER_URL']
|
|
202
|
+
@commit_sha = get_opt(:commit_sha, :commitSha) || ENV['GIT_COMMIT'] || ENV['HYPERPROBE_COMMIT_SHA']
|
|
203
|
+
|
|
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
|
|
209
|
+
@max_queue_size = get_opt(:max_queue_size, :maxQueueSize) || parse_env_int('HYPERPROBE_MAX_QUEUE_SIZE', 100)
|
|
210
|
+
@cooldown_sec = get_opt(:cooldown_sec, :cooldownSec) || parse_env_int('HYPERPROBE_COOLDOWN_SEC', 10)
|
|
211
|
+
|
|
212
|
+
@hits_per_sec = get_opt(:hits_per_sec, :hitsPerSec) || parse_env_int('HYPERPROBE_HITS_PER_SEC', 10)
|
|
213
|
+
@bandwidth_kb_per_sec = get_opt(:bandwidth_kb_per_sec, :bandwidthKbPerSec) || parse_env_int('HYPERPROBE_BANDWIDTH_KB_PER_SEC', 1024)
|
|
214
|
+
@max_lag_ms = (get_opt(:max_lag_ms, :maxLagMs) || parse_env_int('HYPERPROBE_MAX_LAG_MS', 50)).to_f
|
|
215
|
+
@pause_budget_ms = (get_opt(:pause_budget_ms, :pauseBudgetMs) || parse_env_int('HYPERPROBE_PAUSE_BUDGET_MS', 15)).to_f
|
|
216
|
+
@rpc_timeout_sec = (get_opt(:rpc_timeout_sec, :rpcTimeoutSec) || parse_env_int('HYPERPROBE_RPC_TIMEOUT_SEC', 10)).to_f
|
|
217
|
+
|
|
218
|
+
is_aws_lambda = !ENV['AWS_LAMBDA_FUNCTION_NAME'].nil?
|
|
219
|
+
is_local_emulator = ENV['IS_OFFLINE'] == 'true' || ENV['AWS_SAM_LOCAL'] == 'true'
|
|
220
|
+
lambda_opt = get_opt(:is_lambda, :isLambda)
|
|
221
|
+
@is_ephemeral_lambda = (lambda_opt.nil? ? is_aws_lambda : lambda_opt) && !is_local_emulator
|
|
222
|
+
enable_keep_alive_opt = get_opt(:enable_keep_alive, :enableKeepAlive)
|
|
223
|
+
@enable_keep_alive = enable_keep_alive_opt.nil? ? !@is_ephemeral_lambda : enable_keep_alive_opt
|
|
224
|
+
|
|
225
|
+
redact_keys_raw = get_opt(:redact_keys, :redactKeys) || (ENV['HYPERPROBE_REDACT_KEYS'] ? ENV['HYPERPROBE_REDACT_KEYS'].split(',') : %w[password secret token authorization cookie key signature])
|
|
226
|
+
redact_values_raw = get_opt(:redact_values, :redactValues) || (ENV['HYPERPROBE_REDACT_VALUES'] ? ENV['HYPERPROBE_REDACT_VALUES'].split(',') : [])
|
|
227
|
+
|
|
228
|
+
@global_config = {
|
|
229
|
+
redact_keys: Array(redact_keys_raw).map(&:to_s).map(&:strip).reject(&:empty?),
|
|
230
|
+
redact_values: Array(redact_values_raw).map(&:to_s).map(&:strip).reject(&:empty?),
|
|
231
|
+
max_object_depth: get_opt(:max_object_depth, :maxObjectDepth) || parse_env_int('HYPERPROBE_MAX_OBJECT_DEPTH', 3),
|
|
232
|
+
max_array_length: get_opt(:max_array_length, :maxArrayLength) || parse_env_int('HYPERPROBE_MAX_ARRAY_LENGTH', 3),
|
|
233
|
+
stack_frame_depth: get_opt(:stack_frame_depth, :stackFrameDepth) || parse_env_int('HYPERPROBE_STACK_FRAME_DEPTH', 3),
|
|
234
|
+
max_object_properties: get_opt(:max_object_properties, :maxObjectProperties) || parse_env_int('HYPERPROBE_MAX_OBJECT_PROPERTIES', 50),
|
|
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))
|
|
238
|
+
}
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def get_opt(*keys)
|
|
242
|
+
keys.each do |k|
|
|
243
|
+
return @options[k] if @options.key?(k)
|
|
244
|
+
return @options[k.to_s] if @options.key?(k.to_s)
|
|
245
|
+
end
|
|
246
|
+
nil
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def parse_env_int(key, default_val)
|
|
250
|
+
val = ENV[key]
|
|
251
|
+
return default_val if val.nil? || val.strip.empty?
|
|
252
|
+
|
|
253
|
+
val.to_i rescue default_val
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def start_background_loops
|
|
257
|
+
@sync_thread = Thread.new { sync_loop }
|
|
258
|
+
@sync_thread.name = 'hyperprobe-sync' if @sync_thread.respond_to?(:name=)
|
|
259
|
+
|
|
260
|
+
@flush_thread = Thread.new { flush_loop }
|
|
261
|
+
@flush_thread.name = 'hyperprobe-flush' if @flush_thread.respond_to?(:name=)
|
|
262
|
+
|
|
263
|
+
@stats_thread = Thread.new { stats_loop }
|
|
264
|
+
@stats_thread.name = 'hyperprobe-stats' if @stats_thread.respond_to?(:name=)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def sync_loop
|
|
268
|
+
until @is_shutdown
|
|
269
|
+
begin
|
|
270
|
+
sync_with_broker
|
|
271
|
+
sleep @sync_interval_sec
|
|
272
|
+
rescue Exception
|
|
273
|
+
# Silently handle transient sync error
|
|
274
|
+
break
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def sync_with_broker(timeout_sec = nil)
|
|
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
|
|
284
|
+
|
|
285
|
+
@log_broker.debug "sync_with_broker started #{timeout_sec ? "with timeout #{timeout_sec}" : ''}"
|
|
286
|
+
response = @broker_client.get_probes(timeout_sec)
|
|
287
|
+
return unless response
|
|
288
|
+
|
|
289
|
+
@log_broker.debug "sync_with_broker: got #{response.probes.length} probes from broker"
|
|
290
|
+
|
|
291
|
+
server_probes = response.probes.to_a
|
|
292
|
+
server_probe_ids = server_probes.map(&:id)
|
|
293
|
+
now_ms = (Time.now.to_f * 1000).to_i
|
|
294
|
+
config = nil
|
|
295
|
+
snapshot = @mutex.synchronize do
|
|
296
|
+
return false if @is_shutdown
|
|
297
|
+
|
|
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
|
|
307
|
+
# Clean stale local probes
|
|
308
|
+
@active_probes.delete_if { |pid, _| !server_probe_ids.include?(pid) }
|
|
309
|
+
@local_hits.delete_if { |pid, _| !server_probe_ids.include?(pid) }
|
|
310
|
+
@finished_probes.delete_if { |pid, _| !server_probe_ids.include?(pid) }
|
|
311
|
+
|
|
312
|
+
server_probes.each do |probe|
|
|
313
|
+
hits = @local_hits[probe.id] || 0
|
|
314
|
+
is_expired = probe.expiry_time.positive? && probe.expiry_time <= now_ms
|
|
315
|
+
|
|
316
|
+
if hits < probe.hit_limit && !is_expired && !@finished_probes[probe.id]
|
|
317
|
+
@active_probes[probe.id] = probe
|
|
318
|
+
else
|
|
319
|
+
@active_probes.delete(probe.id)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
probe_snapshot_locked
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
@engine.set_global_config(config) if config
|
|
326
|
+
@engine.set_probes(snapshot[0], generation: snapshot[1])
|
|
327
|
+
true
|
|
328
|
+
rescue SignalException, SystemExit
|
|
329
|
+
raise
|
|
330
|
+
rescue Exception
|
|
331
|
+
false
|
|
332
|
+
ensure
|
|
333
|
+
@sync_mutex.unlock if sync_locked
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def flush_loop
|
|
337
|
+
until @is_shutdown
|
|
338
|
+
begin
|
|
339
|
+
sleep @flush_interval_sec
|
|
340
|
+
break if @is_shutdown
|
|
341
|
+
|
|
342
|
+
flush_telemetry
|
|
343
|
+
rescue Exception
|
|
344
|
+
# Silently handle flush loop error
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def flush_telemetry(timeout_sec = nil)
|
|
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
|
|
354
|
+
|
|
355
|
+
batch = []
|
|
356
|
+
@mutex.synchronize do
|
|
357
|
+
batch << @telemetry_queue.pop(true) until @telemetry_queue.empty?
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
return if batch.empty?
|
|
361
|
+
|
|
362
|
+
begin
|
|
363
|
+
events = batch.map { |item| item[:event] }
|
|
364
|
+
finished_probe_ids = @broker_client.report_telemetry(events, timeout_sec)
|
|
365
|
+
delivered = true
|
|
366
|
+
|
|
367
|
+
# Commit bandwidth reservations
|
|
368
|
+
batch.each do |item|
|
|
369
|
+
settle_reservation(item[:reservation], :commit)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Handle globally finished probes
|
|
373
|
+
if finished_probe_ids && !finished_probe_ids.empty?
|
|
374
|
+
snapshot = @mutex.synchronize do
|
|
375
|
+
return if @is_shutdown
|
|
376
|
+
|
|
377
|
+
finished_probe_ids.each do |pid|
|
|
378
|
+
@finished_probes[pid] = true
|
|
379
|
+
@active_probes.delete(pid)
|
|
380
|
+
end
|
|
381
|
+
probe_snapshot_locked
|
|
382
|
+
end
|
|
383
|
+
@engine.set_probes(snapshot[0], generation: snapshot[1])
|
|
384
|
+
end
|
|
385
|
+
rescue SignalException, SystemExit
|
|
386
|
+
raise
|
|
387
|
+
rescue Exception
|
|
388
|
+
return false if delivered
|
|
389
|
+
|
|
390
|
+
# On flush failure, re-queue events up to max capacity
|
|
391
|
+
batch.each do |item|
|
|
392
|
+
retained = @mutex.synchronize do
|
|
393
|
+
if !@is_shutdown && @telemetry_queue.size < @max_queue_size
|
|
394
|
+
@telemetry_queue.push(item)
|
|
395
|
+
true
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
settle_reservation(item[:reservation], :release) unless retained
|
|
399
|
+
end
|
|
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
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def stats_loop
|
|
413
|
+
until @is_shutdown
|
|
414
|
+
begin
|
|
415
|
+
sleep 5.0
|
|
416
|
+
break if @is_shutdown
|
|
417
|
+
|
|
418
|
+
stats = @engine.get_stats
|
|
419
|
+
if stats[:hits].positive? || stats[:skips].positive?
|
|
420
|
+
@log_stats.info "Probes Hit: #{stats[:hits]}, Probes Skipped: #{stats[:skips]}"
|
|
421
|
+
end
|
|
422
|
+
rescue Exception
|
|
423
|
+
# Ignore stats error
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
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
|
|
433
|
+
return if @is_shutdown || @is_agent_disabled
|
|
434
|
+
|
|
435
|
+
probe_id = event[:probe_id]
|
|
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
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
return if @telemetry_queue.size >= @max_queue_size
|
|
453
|
+
|
|
454
|
+
event_size = @broker_client.estimate_event_size(event)
|
|
455
|
+
reservation = @quota_manager.reserve_bandwidth(event_size)
|
|
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
|
|
469
|
+
|
|
470
|
+
def settle_reservation(reservation, action)
|
|
471
|
+
reservation&.public_send(action)
|
|
472
|
+
rescue Exception
|
|
473
|
+
nil
|
|
474
|
+
end
|
|
475
|
+
|
|
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
|
|
490
|
+
end
|
|
491
|
+
break unless snapshot
|
|
492
|
+
|
|
493
|
+
@engine.set_probes(snapshot[0], generation: snapshot[1])
|
|
494
|
+
end
|
|
495
|
+
rescue Exception
|
|
496
|
+
shutdown
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def handle_health_change(health, reason = nil)
|
|
500
|
+
return if @owner_pid != Process.pid || @is_shutdown
|
|
501
|
+
|
|
502
|
+
case health
|
|
503
|
+
when Core::AgentHealth::RED
|
|
504
|
+
@log_safety.error "Safety Shield Triggered: RED Status. #{reason || ''}"
|
|
505
|
+
@engine.suspend
|
|
506
|
+
|
|
507
|
+
@cooldown_timer&.exit if @cooldown_timer&.alive?
|
|
508
|
+
cooldown_duration = @cooldown_sec
|
|
509
|
+
@cooldown_until = Process.clock_gettime(Process::CLOCK_MONOTONIC) + cooldown_duration
|
|
510
|
+
@cooldown_timer = Thread.new do
|
|
511
|
+
@log_safety.info "Cooldown period started (#{cooldown_duration}s)..."
|
|
512
|
+
sleep cooldown_duration
|
|
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
|
|
518
|
+
end
|
|
519
|
+
rescue Exception
|
|
520
|
+
# Agent-owned cooldown work must never abort the application.
|
|
521
|
+
end
|
|
522
|
+
@cooldown_timer.kill if @is_shutdown
|
|
523
|
+
when Core::AgentHealth::YELLOW
|
|
524
|
+
@log_safety.warn "Safety Warning: YELLOW Status (Moderate Overhead). #{reason || ''}"
|
|
525
|
+
when Core::AgentHealth::GREEN
|
|
526
|
+
@log_safety.info "Status back to GREEN. #{reason || ''}"
|
|
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)
|
|
530
|
+
@engine.resume
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
rescue Exception
|
|
534
|
+
@is_agent_disabled = true
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
end
|