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,857 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'monitor'
|
|
6
|
+
require 'debug_inspector' if RUBY_ENGINE == 'ruby'
|
|
7
|
+
require 'objspace' if RUBY_ENGINE == 'ruby'
|
|
8
|
+
require_relative 'logger'
|
|
9
|
+
require_relative 'evaluator'
|
|
10
|
+
require_relative 'lexical_scope'
|
|
11
|
+
|
|
12
|
+
module HyperProbe
|
|
13
|
+
module Core
|
|
14
|
+
class MonitoringEngine
|
|
15
|
+
DURATION_TTL_SECONDS = 60.0
|
|
16
|
+
MAX_DURATION_ENTRIES = 10_000
|
|
17
|
+
MAX_PROBES = 100
|
|
18
|
+
MAX_STACK_FRAMES = 16
|
|
19
|
+
MAX_CALLER_SCOPES = 256
|
|
20
|
+
MAX_INSPECTOR_STACK_DEPTH = 128
|
|
21
|
+
MAX_CALLER_ISEQS = 32
|
|
22
|
+
MAX_CALLER_ISEQ_BYTES = 16_384
|
|
23
|
+
ISEQ_TO_A = RubyVM::InstructionSequence.instance_method(:to_a) if RUBY_ENGINE == 'ruby'
|
|
24
|
+
ISEQ_EACH_CHILD = RubyVM::InstructionSequence.instance_method(:each_child) if RUBY_ENGINE == 'ruby'
|
|
25
|
+
ISEQ_MEMORY_SIZE = ObjectSpace.method(:memsize_of) if RUBY_ENGINE == 'ruby'
|
|
26
|
+
LOCAL_VARIABLES = Binding.instance_method(:local_variables)
|
|
27
|
+
LOCAL_GET = Binding.instance_method(:local_variable_get)
|
|
28
|
+
MODULE_NAME = Module.instance_method(:name)
|
|
29
|
+
SINGLETON_CLASS = Module.instance_method(:singleton_class?)
|
|
30
|
+
INSTANCE_METHODS = Module.instance_method(:instance_methods)
|
|
31
|
+
INSTANCE_METHOD = Module.instance_method(:instance_method)
|
|
32
|
+
METHODS = Kernel.instance_method(:methods)
|
|
33
|
+
METHOD = Kernel.instance_method(:method)
|
|
34
|
+
|
|
35
|
+
LEVEL_COLORS = {
|
|
36
|
+
'ERROR' => "\e[31m",
|
|
37
|
+
'WARN' => "\e[33m",
|
|
38
|
+
'INFO' => "\e[32m",
|
|
39
|
+
'DEBUG' => "\e[36m"
|
|
40
|
+
}.freeze
|
|
41
|
+
COLOR_RESET = "\e[0m"
|
|
42
|
+
COLOR_BOLD = "\e[1m"
|
|
43
|
+
COLOR_GRAY = "\e[90m"
|
|
44
|
+
COLOR_MAGENTA = "\e[35m"
|
|
45
|
+
|
|
46
|
+
attr_reader :total_hits, :total_skips, :is_suspended, :is_active
|
|
47
|
+
|
|
48
|
+
def initialize(quota_manager, safety_monitor, on_capture, custom_set_trace_id = nil)
|
|
49
|
+
@quota_manager = quota_manager
|
|
50
|
+
@safety_monitor = safety_monitor
|
|
51
|
+
@on_capture = on_capture
|
|
52
|
+
@custom_set_trace_id = custom_set_trace_id
|
|
53
|
+
|
|
54
|
+
@mutex = Monitor.new
|
|
55
|
+
@path_mutex = Monitor.new
|
|
56
|
+
@active_locations = {} # [resolved_file_path, line] => Array<ActiveProbeContext>
|
|
57
|
+
@probe_map = {} # probe_id => Probe
|
|
58
|
+
@instrumented_files = Set.new
|
|
59
|
+
@path_cache = {}
|
|
60
|
+
@caller_locals_cache = ObjectSpace::WeakMap.new if RUBY_ENGINE == 'ruby'
|
|
61
|
+
@duration_starts = {} # [probe_id, correlation_id] => [start_time_monotonic, created_at_time]
|
|
62
|
+
@next_duration_cleanup = monotonic_time + DURATION_TTL_SECONDS
|
|
63
|
+
|
|
64
|
+
@global_config = {}
|
|
65
|
+
@safe_evaluation = Evaluator.safe_evaluation_enabled?
|
|
66
|
+
@total_hits = 0
|
|
67
|
+
@total_skips = 0
|
|
68
|
+
@is_suspended = false
|
|
69
|
+
@is_active = false
|
|
70
|
+
@closed = false
|
|
71
|
+
@owner_pid = Process.pid
|
|
72
|
+
@generation = -1
|
|
73
|
+
|
|
74
|
+
@log = Logger.get_logger('hyperprobe:inspector')
|
|
75
|
+
@targeted_tracepoints = {} # location_key => TracePoint instance
|
|
76
|
+
# JRuby's compiled LINE site is activated by CALL/RETURN interest.
|
|
77
|
+
# Share one empty activation hook rather than adding CALL to every probe.
|
|
78
|
+
@jruby_call_hook = TracePoint.new(:call) {} if RUBY_PLATFORM.include?('java')
|
|
79
|
+
@stdout_queue = SizedQueue.new(100)
|
|
80
|
+
@stdout_thread = Thread.new do
|
|
81
|
+
loop { $stdout.write(@stdout_queue.pop) }
|
|
82
|
+
rescue Exception
|
|
83
|
+
# A broken or blocked output stream must only affect the agent worker.
|
|
84
|
+
end
|
|
85
|
+
@stdout_thread.name = 'hyperprobe-stdout'
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def set_global_config(config)
|
|
89
|
+
@mutex.synchronize do
|
|
90
|
+
@global_config = (config || {}).dup
|
|
91
|
+
disabled = @global_config.fetch(:disable_safe_evaluation) { @global_config['disable_safe_evaluation'] }
|
|
92
|
+
@safe_evaluation = Evaluator.safe_evaluation_enabled?(disabled)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def set_probes(probes, generation: nil)
|
|
97
|
+
@mutex.synchronize do
|
|
98
|
+
return if @closed
|
|
99
|
+
return if generation && generation <= @generation
|
|
100
|
+
@generation = generation if generation
|
|
101
|
+
|
|
102
|
+
new_active_locations = {}
|
|
103
|
+
new_probe_map = {}
|
|
104
|
+
new_instrumented_files = Set.new
|
|
105
|
+
|
|
106
|
+
Array(probes).first(MAX_PROBES).each do |probe|
|
|
107
|
+
probe_id = probe.respond_to?(:id) ? probe.id : probe[:id]
|
|
108
|
+
next unless probe_id
|
|
109
|
+
|
|
110
|
+
new_probe_map[probe_id] = probe
|
|
111
|
+
|
|
112
|
+
runtime_loc = probe.respond_to?(:runtime_location) ? probe.runtime_location : probe[:runtime_location]
|
|
113
|
+
runtime_line = (probe.respond_to?(:runtime_line) ? probe.runtime_line : probe[:runtime_line]).to_i
|
|
114
|
+
|
|
115
|
+
resolved_path = resolve_filepath(runtime_loc)
|
|
116
|
+
primary_key = [resolved_path, runtime_line]
|
|
117
|
+
|
|
118
|
+
new_active_locations[primary_key] ||= []
|
|
119
|
+
probe_type = probe.respond_to?(:type) ? probe.type : probe[:type]
|
|
120
|
+
local_names = nil
|
|
121
|
+
if probe_type_to_i(probe_type) == 1
|
|
122
|
+
previous = @active_locations[primary_key]&.find { |ctx| ctx[:probe].equal?(probe) && !ctx[:is_secondary] }
|
|
123
|
+
local_names = previous ? previous[:local_names] : LexicalScope.locals_for(resolved_path, runtime_line)
|
|
124
|
+
end
|
|
125
|
+
new_active_locations[primary_key] << { probe: probe, is_secondary: false, local_names: local_names }
|
|
126
|
+
new_instrumented_files.add(resolved_path)
|
|
127
|
+
|
|
128
|
+
# Duration secondary location
|
|
129
|
+
is_duration = probe_type == 5 || probe_type == :PROBE_TYPE_DURATION
|
|
130
|
+
if is_duration
|
|
131
|
+
sec_line = (probe.respond_to?(:secondary_runtime_line) ? probe.secondary_runtime_line : probe[:secondary_runtime_line]).to_i
|
|
132
|
+
if sec_line.positive?
|
|
133
|
+
sec_loc = probe.respond_to?(:secondary_runtime_location) ? probe.secondary_runtime_location : probe[:secondary_runtime_location]
|
|
134
|
+
sec_loc = runtime_loc if sec_loc.nil? || sec_loc.empty?
|
|
135
|
+
resolved_sec_path = resolve_filepath(sec_loc)
|
|
136
|
+
secondary_key = [resolved_sec_path, sec_line]
|
|
137
|
+
|
|
138
|
+
new_active_locations[secondary_key] ||= []
|
|
139
|
+
new_active_locations[secondary_key] << { probe: probe, is_secondary: true }
|
|
140
|
+
new_instrumented_files.add(resolved_sec_path)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
@active_locations = new_active_locations
|
|
146
|
+
@probe_map = new_probe_map
|
|
147
|
+
@instrumented_files = new_instrumented_files
|
|
148
|
+
@duration_starts.delete_if { |(id, _), _entry| !new_probe_map.key?(id) }
|
|
149
|
+
|
|
150
|
+
update_tracepoint_state_unlocked
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def suspend
|
|
155
|
+
@mutex.synchronize do
|
|
156
|
+
return if @closed || @is_suspended
|
|
157
|
+
|
|
158
|
+
@is_suspended = true
|
|
159
|
+
disable_all_tracepoints_unlocked
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def resume
|
|
164
|
+
@mutex.synchronize do
|
|
165
|
+
return if @closed || !@is_suspended
|
|
166
|
+
|
|
167
|
+
@is_suspended = false
|
|
168
|
+
update_tracepoint_state_unlocked
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def get_stats
|
|
173
|
+
@mutex.synchronize do
|
|
174
|
+
cleanup_durations_unlocked(monotonic_time)
|
|
175
|
+
stats = { hits: @total_hits, skips: @total_skips }
|
|
176
|
+
@total_hits = 0
|
|
177
|
+
@total_skips = 0
|
|
178
|
+
stats
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def close
|
|
183
|
+
@stdout_thread&.kill
|
|
184
|
+
@mutex.synchronize do
|
|
185
|
+
return if @closed
|
|
186
|
+
|
|
187
|
+
@closed = true
|
|
188
|
+
disable_all_tracepoints_unlocked
|
|
189
|
+
@active_locations.clear
|
|
190
|
+
@probe_map.clear
|
|
191
|
+
@instrumented_files.clear
|
|
192
|
+
@duration_starts.clear
|
|
193
|
+
@caller_locals_cache = nil
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def after_fork
|
|
198
|
+
# The child must not acquire locks or use resources owned by parent threads.
|
|
199
|
+
@closed = true
|
|
200
|
+
@is_suspended = true
|
|
201
|
+
@targeted_tracepoints.each_value { |tp| tp.disable rescue nil }
|
|
202
|
+
@jruby_call_hook&.disable
|
|
203
|
+
@is_active = false
|
|
204
|
+
rescue Exception
|
|
205
|
+
nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
private
|
|
209
|
+
|
|
210
|
+
def update_tracepoint_state_unlocked
|
|
211
|
+
if @closed || @is_suspended || @active_locations.empty?
|
|
212
|
+
disable_all_tracepoints_unlocked
|
|
213
|
+
return
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
@jruby_call_hook&.enable
|
|
217
|
+
|
|
218
|
+
# 1. Remove stale tracepoints
|
|
219
|
+
active_keys = @active_locations.keys.to_set
|
|
220
|
+
@targeted_tracepoints.delete_if do |loc_key, tp|
|
|
221
|
+
unless active_keys.include?(loc_key)
|
|
222
|
+
tp.disable rescue nil
|
|
223
|
+
true
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# 2. Setup or enable targeted tracepoint for each active location
|
|
228
|
+
@active_locations.each_key do |loc_key|
|
|
229
|
+
next if @targeted_tracepoints.key?(loc_key)
|
|
230
|
+
|
|
231
|
+
enable_location_tracepoint_unlocked(loc_key)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
@is_active = !@targeted_tracepoints.empty?
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def create_tracepoint_for_location(loc_key, resolved_path, lineno)
|
|
238
|
+
TracePoint.new(:line) do |t|
|
|
239
|
+
next if @is_suspended || @closed || @owner_pid != Process.pid
|
|
240
|
+
# Fast integer check first to minimize overhead on uninstrumented lines
|
|
241
|
+
next unless t.lineno == lineno
|
|
242
|
+
|
|
243
|
+
contexts = nil
|
|
244
|
+
# Never wait for installation or shutdown on an application thread.
|
|
245
|
+
next unless @mutex.try_enter
|
|
246
|
+
begin
|
|
247
|
+
# Installation resolves symlinks; never resolve filesystem paths here.
|
|
248
|
+
next unless t.path == resolved_path || @path_cache[t.path] == resolved_path || File.expand_path(t.path) == resolved_path
|
|
249
|
+
contexts = @active_locations[loc_key]
|
|
250
|
+
if contexts && !contexts.empty?
|
|
251
|
+
handle_hit(contexts, t.binding)
|
|
252
|
+
end
|
|
253
|
+
ensure
|
|
254
|
+
@mutex.exit
|
|
255
|
+
end
|
|
256
|
+
rescue SignalException, SystemExit
|
|
257
|
+
raise
|
|
258
|
+
rescue Exception
|
|
259
|
+
# Includes trap-context ThreadError; host termination remains untouched.
|
|
260
|
+
# This boundary never encloses application code.
|
|
261
|
+
nil
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def enable_location_tracepoint_unlocked(loc_key)
|
|
266
|
+
resolved_path, lineno = loc_key
|
|
267
|
+
|
|
268
|
+
target_method = find_method_target(resolved_path, lineno)
|
|
269
|
+
targeted_enabled = false
|
|
270
|
+
tp = nil
|
|
271
|
+
|
|
272
|
+
if target_method
|
|
273
|
+
begin
|
|
274
|
+
tp = create_tracepoint_for_location(loc_key, resolved_path, lineno)
|
|
275
|
+
tp.enable(target: target_method, target_line: lineno)
|
|
276
|
+
targeted_enabled = true
|
|
277
|
+
rescue ArgumentError, StandardError
|
|
278
|
+
# Target option not accepted by interpreter or line is outside method instructions.
|
|
279
|
+
# Must discard and disable this TracePoint because CRuby does not allow un-targeted
|
|
280
|
+
# re-enable on a TracePoint instance once target has been attempted.
|
|
281
|
+
tp&.disable rescue nil
|
|
282
|
+
tp = nil
|
|
283
|
+
targeted_enabled = false
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
unless targeted_enabled
|
|
288
|
+
begin
|
|
289
|
+
tp = create_tracepoint_for_location(loc_key, resolved_path, lineno)
|
|
290
|
+
tp.enable
|
|
291
|
+
rescue StandardError
|
|
292
|
+
# TracePoint enable error
|
|
293
|
+
tp = nil
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
@targeted_tracepoints[loc_key] = tp if tp
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def disable_all_tracepoints_unlocked
|
|
301
|
+
@jruby_call_hook&.disable
|
|
302
|
+
@targeted_tracepoints.each_value do |tp|
|
|
303
|
+
tp.disable rescue nil
|
|
304
|
+
end
|
|
305
|
+
@targeted_tracepoints.clear
|
|
306
|
+
@is_active = false
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def method_line_range(m)
|
|
310
|
+
return nil unless defined?(RubyVM::InstructionSequence) && RubyVM::InstructionSequence.respond_to?(:of)
|
|
311
|
+
|
|
312
|
+
iseq = RubyVM::InstructionSequence.of(m) rescue nil
|
|
313
|
+
return nil unless iseq
|
|
314
|
+
|
|
315
|
+
data = iseq.to_a[4] rescue nil
|
|
316
|
+
if data.is_a?(Hash) && data[:code_location]
|
|
317
|
+
loc = data[:code_location]
|
|
318
|
+
return (loc[0]..loc[2]) if loc.is_a?(Array) && loc.length >= 3
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
lines = []
|
|
322
|
+
collect_lines = ->(seq) {
|
|
323
|
+
(seq.trace_points rescue []).each { |tp_info| lines << tp_info[0] }
|
|
324
|
+
(seq.each_child rescue []).each { |child| collect_lines.call(child) }
|
|
325
|
+
}
|
|
326
|
+
collect_lines.call(iseq)
|
|
327
|
+
return (lines.min..lines.max) unless lines.empty?
|
|
328
|
+
|
|
329
|
+
nil
|
|
330
|
+
rescue StandardError
|
|
331
|
+
nil
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def find_method_target(file_path, line_number)
|
|
335
|
+
return nil unless defined?(RubyVM::InstructionSequence)
|
|
336
|
+
best_match = nil
|
|
337
|
+
best_line = 0
|
|
338
|
+
deadline = monotonic_time + 0.05
|
|
339
|
+
|
|
340
|
+
ObjectSpace.each_object(Module) do |mod|
|
|
341
|
+
break if monotonic_time >= deadline
|
|
342
|
+
next if SINGLETON_CLASS.bind(mod).call || MODULE_NAME.bind(mod).call.nil?
|
|
343
|
+
|
|
344
|
+
[:instance_methods, :methods].each do |mtype|
|
|
345
|
+
implementation = mtype == :instance_methods ? INSTANCE_METHODS : METHODS
|
|
346
|
+
methods_list = implementation.bind(mod).call(false)
|
|
347
|
+
methods_list.each do |sym|
|
|
348
|
+
break if monotonic_time >= deadline
|
|
349
|
+
implementation = mtype == :instance_methods ? INSTANCE_METHOD : METHOD
|
|
350
|
+
m = implementation.bind(mod).call(sym) rescue nil
|
|
351
|
+
next unless m
|
|
352
|
+
|
|
353
|
+
loc = m.source_location
|
|
354
|
+
next unless loc
|
|
355
|
+
|
|
356
|
+
m_file = loc[0]
|
|
357
|
+
m_line = loc[1]
|
|
358
|
+
|
|
359
|
+
next unless m_line <= line_number && m_line > best_line
|
|
360
|
+
next unless m_file == file_path || @path_cache[m_file] == file_path || File.expand_path(m_file) == file_path
|
|
361
|
+
|
|
362
|
+
range = method_line_range(m)
|
|
363
|
+
if range
|
|
364
|
+
next unless range.cover?(line_number)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
best_match = m
|
|
368
|
+
best_line = m_line
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
best_match
|
|
374
|
+
rescue StandardError
|
|
375
|
+
nil
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def resolve_filepath(runtime_location)
|
|
379
|
+
return '' if runtime_location.nil? || runtime_location.empty?
|
|
380
|
+
|
|
381
|
+
cached = @path_mutex.synchronize { @path_cache[runtime_location] }
|
|
382
|
+
return cached if cached
|
|
383
|
+
|
|
384
|
+
if File.exist?(runtime_location)
|
|
385
|
+
resolved = File.realpath(runtime_location) rescue File.expand_path(runtime_location)
|
|
386
|
+
@path_mutex.synchronize { @path_cache[runtime_location] = resolved }
|
|
387
|
+
return resolved
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
cwd = Dir.pwd
|
|
391
|
+
parts = runtime_location.tr('\\', '/').split('/').reject(&:empty?)
|
|
392
|
+
(0...parts.length).each do |i|
|
|
393
|
+
suffix = File.join(*parts[i..])
|
|
394
|
+
attempt = File.expand_path(suffix, cwd)
|
|
395
|
+
if File.exist?(attempt)
|
|
396
|
+
resolved = File.realpath(attempt) rescue attempt
|
|
397
|
+
@path_mutex.synchronize { @path_cache[runtime_location] = resolved }
|
|
398
|
+
return resolved
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
fallback = File.expand_path(runtime_location, cwd)
|
|
403
|
+
@path_mutex.synchronize { @path_cache[runtime_location] = fallback }
|
|
404
|
+
fallback
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def handle_hit(contexts, binding_ctx)
|
|
408
|
+
start_time = monotonic_time
|
|
409
|
+
begin
|
|
410
|
+
contexts.each do |ctx|
|
|
411
|
+
probe = ctx[:probe]
|
|
412
|
+
is_secondary = ctx[:is_secondary]
|
|
413
|
+
process_single_probe_hit(probe, binding_ctx, is_secondary, ctx[:local_names])
|
|
414
|
+
rescue SignalException, SystemExit
|
|
415
|
+
raise
|
|
416
|
+
rescue Exception
|
|
417
|
+
# One broken probe must not prevent another probe or the host line.
|
|
418
|
+
next
|
|
419
|
+
end
|
|
420
|
+
rescue SignalException, SystemExit
|
|
421
|
+
raise
|
|
422
|
+
rescue Exception
|
|
423
|
+
nil
|
|
424
|
+
ensure
|
|
425
|
+
elapsed_ms = (monotonic_time - start_time) * 1000.0
|
|
426
|
+
@safety_monitor.report_pause_duration(elapsed_ms) rescue nil
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def process_single_probe_hit(probe, binding_ctx, is_secondary, local_names = nil)
|
|
431
|
+
probe_id = probe.respond_to?(:id) ? probe.id : probe[:id]
|
|
432
|
+
probe_type = probe.respond_to?(:type) ? probe.type : probe[:type]
|
|
433
|
+
probe_type_num = probe_type.is_a?(Integer) ? probe_type : probe_type_to_i(probe_type)
|
|
434
|
+
|
|
435
|
+
# 1. Rate-limiting admission check
|
|
436
|
+
unless probe_type_num == 5 || @quota_manager.can_evaluate?
|
|
437
|
+
@mutex.synchronize { @total_skips += 1 }
|
|
438
|
+
return
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# 2. Evaluate Condition (if any)
|
|
442
|
+
condition = probe.respond_to?(:condition) ? probe.condition : probe[:condition]
|
|
443
|
+
if condition && !condition.empty?
|
|
444
|
+
begin
|
|
445
|
+
passed = Evaluator.eval_condition(condition, binding_ctx, safe: @safe_evaluation)
|
|
446
|
+
return unless passed
|
|
447
|
+
rescue StandardError, ScriptError, SecurityError => e
|
|
448
|
+
report_error(probe_id, "Condition evaluation failed: #{e.class.name}: #{e.message}")
|
|
449
|
+
return
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# 3. Dispatch to Probe Type Handler
|
|
454
|
+
case probe_type_num
|
|
455
|
+
when 1 # SNAPSHOT
|
|
456
|
+
handle_snapshot(probe, binding_ctx, local_names)
|
|
457
|
+
when 2 # LOG
|
|
458
|
+
handle_log(probe, binding_ctx)
|
|
459
|
+
when 3 # COUNTER
|
|
460
|
+
handle_counter(probe, binding_ctx)
|
|
461
|
+
when 4 # METRIC
|
|
462
|
+
handle_metric(probe, binding_ctx)
|
|
463
|
+
when 5 # DURATION
|
|
464
|
+
handle_duration(probe, binding_ctx, is_secondary)
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def handle_snapshot(probe, binding_ctx, local_names = nil)
|
|
469
|
+
probe_id = probe.respond_to?(:id) ? probe.id : probe[:id]
|
|
470
|
+
|
|
471
|
+
max_depth = get_probe_positive_int(probe, :max_object_depth, 3)
|
|
472
|
+
max_array_length = get_probe_positive_int(probe, :max_array_length, 3)
|
|
473
|
+
max_object_properties = get_probe_positive_int(probe, :max_object_properties, 50)
|
|
474
|
+
max_string_length = get_probe_positive_int(probe, :max_string_length, 1024)
|
|
475
|
+
stack_frame_depth = [get_probe_positive_int(probe, :stack_frame_depth, 3), MAX_STACK_FRAMES].min
|
|
476
|
+
redact_keys = @global_config[:redact_keys] || @global_config['redact_keys']
|
|
477
|
+
redact_values = @global_config[:redact_values] || @global_config['redact_values']
|
|
478
|
+
|
|
479
|
+
serializer = Serializer.new(
|
|
480
|
+
max_depth: max_depth,
|
|
481
|
+
max_array_length: max_array_length,
|
|
482
|
+
max_object_properties: max_object_properties,
|
|
483
|
+
max_string_length: max_string_length,
|
|
484
|
+
redact_keys: redact_keys,
|
|
485
|
+
redact_values: redact_values
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
visited = {}
|
|
489
|
+
|
|
490
|
+
# 1. Watch Expressions
|
|
491
|
+
watch_expressions = get_probe_opt(probe, :watch_expressions) || []
|
|
492
|
+
serialized_watches = {}
|
|
493
|
+
if watch_expressions && !watch_expressions.empty?
|
|
494
|
+
raw_watches = Evaluator.evaluate_watches(watch_expressions.to_a, binding_ctx, safe: @safe_evaluation)
|
|
495
|
+
raw_watches.each do |expr, val|
|
|
496
|
+
watch_path = "watch[#{JSON.generate(expr)}]"
|
|
497
|
+
serialized_watches[expr] = serializer.serialize(val, watch_path, 0, visited)
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# 2. Unified Stack Frames & Multi-Frame Local Variables Extraction
|
|
502
|
+
stack_frames, captured_vars = extract_stack_and_variables(
|
|
503
|
+
probe,
|
|
504
|
+
binding_ctx,
|
|
505
|
+
stack_frame_depth,
|
|
506
|
+
serializer,
|
|
507
|
+
visited,
|
|
508
|
+
local_names
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
event = build_base_event(probe_id)
|
|
512
|
+
event[:stack_frames] = stack_frames
|
|
513
|
+
event[:captured_vars_json] = Serializer.safe_dump_json(captured_vars)
|
|
514
|
+
event[:watch_results_json] = Serializer.safe_dump_json(serialized_watches)
|
|
515
|
+
|
|
516
|
+
emit_event(probe, event)
|
|
517
|
+
rescue StandardError, ScriptError, SecurityError => e
|
|
518
|
+
report_error(probe_id, "Snapshot capture failed: #{e.class.name}: #{e.message}")
|
|
519
|
+
ensure
|
|
520
|
+
serializer&.finish_capture
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def handle_log(probe, binding_ctx)
|
|
524
|
+
probe_id = probe.respond_to?(:id) ? probe.id : probe[:id]
|
|
525
|
+
template = get_probe_opt(probe, :template) || ''
|
|
526
|
+
max_string_length = get_probe_positive_int(probe, :max_string_length, 1024)
|
|
527
|
+
redact_values = @global_config[:redact_values] || @global_config['redact_values']
|
|
528
|
+
|
|
529
|
+
evaluated = Evaluator.evaluate_log_template(template, binding_ctx, safe: @safe_evaluation)
|
|
530
|
+
|
|
531
|
+
# Redact values
|
|
532
|
+
if redact_values && !redact_values.empty?
|
|
533
|
+
regex = Serializer.new(redact_values: redact_values).send(:compile_regex, redact_values)
|
|
534
|
+
evaluated = evaluated.gsub(regex, '[REDACTED Value]') if regex
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# Truncate string
|
|
538
|
+
if evaluated.length > max_string_length
|
|
539
|
+
head = evaluated[0...max_string_length]
|
|
540
|
+
truncated_head = head.sub(/\b\w*$/, '').rstrip
|
|
541
|
+
truncated_head = head if truncated_head.empty?
|
|
542
|
+
evaluated = "#{truncated_head}... [Truncated: +#{evaluated.length - max_string_length} more chars]"
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# Stdout logging if configured
|
|
546
|
+
should_log_stdout = get_probe_opt(probe, :should_log_to_stdout)
|
|
547
|
+
if should_log_stdout
|
|
548
|
+
log_level = (get_probe_opt(probe, :log_level) || 'INFO').to_s.upcase
|
|
549
|
+
color = LEVEL_COLORS[log_level] || "\e[37m"
|
|
550
|
+
timestamp = Time.now.utc.iso8601(3)
|
|
551
|
+
out_line = "#{COLOR_GRAY}[#{timestamp}]#{COLOR_RESET} " \
|
|
552
|
+
"#{COLOR_BOLD}#{color}[#{log_level}]#{COLOR_RESET} " \
|
|
553
|
+
"#{COLOR_BOLD}#{COLOR_MAGENTA}[HyperProbe LOG]#{COLOR_RESET} " \
|
|
554
|
+
"#{evaluated}\n"
|
|
555
|
+
@stdout_queue.push(out_line, true) rescue nil
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
event = build_base_event(probe_id)
|
|
559
|
+
event[:evaluated_log] = evaluated
|
|
560
|
+
emit_event(probe, event)
|
|
561
|
+
rescue StandardError, ScriptError, SecurityError => e
|
|
562
|
+
report_error(probe_id, "Log evaluation failed: #{e.class.name}: #{e.message}")
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def handle_counter(probe, _binding_ctx)
|
|
566
|
+
probe_id = probe.respond_to?(:id) ? probe.id : probe[:id]
|
|
567
|
+
event = build_base_event(probe_id)
|
|
568
|
+
event[:metric_value] = 1.0
|
|
569
|
+
emit_event(probe, event)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def handle_metric(probe, binding_ctx)
|
|
573
|
+
probe_id = probe.respond_to?(:id) ? probe.id : probe[:id]
|
|
574
|
+
metric_expr = get_probe_opt(probe, :metric_expression)
|
|
575
|
+
return if metric_expr.nil? || metric_expr.empty?
|
|
576
|
+
|
|
577
|
+
begin
|
|
578
|
+
val = Evaluator.evaluate_metric(metric_expr, binding_ctx, safe: @safe_evaluation)
|
|
579
|
+
rescue StandardError, ScriptError, SecurityError => e
|
|
580
|
+
report_error(probe_id, "Metric evaluation failed: #{e.class.name}: #{e.message}")
|
|
581
|
+
return
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
event = build_base_event(probe_id)
|
|
585
|
+
if val
|
|
586
|
+
event[:metric_value] = val
|
|
587
|
+
else
|
|
588
|
+
event[:capture_error] = 'Metric evaluation failed: expression must return a finite number'
|
|
589
|
+
end
|
|
590
|
+
emit_event(probe, event)
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def handle_duration(probe, binding_ctx, is_secondary)
|
|
594
|
+
probe_id = probe.respond_to?(:id) ? probe.id : probe[:id]
|
|
595
|
+
corr_expr = get_probe_opt(probe, :correlation_expression)
|
|
596
|
+
|
|
597
|
+
corr_id = nil
|
|
598
|
+
begin
|
|
599
|
+
corr_id = Evaluator.evaluate_correlation(corr_expr, binding_ctx, safe: @safe_evaluation)
|
|
600
|
+
rescue StandardError, ScriptError, SecurityError => e
|
|
601
|
+
report_error(probe_id, "Duration correlation failed: #{e.message}")
|
|
602
|
+
return
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
dur_key = [probe_id, corr_id]
|
|
606
|
+
|
|
607
|
+
if !is_secondary
|
|
608
|
+
now_mono = monotonic_time
|
|
609
|
+
@mutex.synchronize do
|
|
610
|
+
cleanup_durations_unlocked(now_mono)
|
|
611
|
+
if @duration_starts.size >= MAX_DURATION_ENTRIES
|
|
612
|
+
# Evict oldest entry to prevent unbounded memory growth
|
|
613
|
+
@duration_starts.shift
|
|
614
|
+
end
|
|
615
|
+
@duration_starts[dur_key] = [now_mono, now_mono]
|
|
616
|
+
end
|
|
617
|
+
else
|
|
618
|
+
now_mono = monotonic_time
|
|
619
|
+
start_entry = nil
|
|
620
|
+
@mutex.synchronize do
|
|
621
|
+
cleanup_durations_unlocked(now_mono)
|
|
622
|
+
start_entry = @duration_starts.delete(dur_key)
|
|
623
|
+
end
|
|
624
|
+
return unless start_entry
|
|
625
|
+
unless @quota_manager.can_evaluate?
|
|
626
|
+
@mutex.synchronize { @total_skips += 1 }
|
|
627
|
+
return
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
duration_ms = (now_mono - start_entry[0]) * 1000.0
|
|
631
|
+
event = build_base_event(probe_id)
|
|
632
|
+
event[:metric_value] = duration_ms
|
|
633
|
+
emit_event(probe, event)
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def build_base_event(probe_id)
|
|
638
|
+
event = {
|
|
639
|
+
probe_id: probe_id,
|
|
640
|
+
timestamp_ms: (Time.now.to_f * 1000).to_i,
|
|
641
|
+
stack_frames: [],
|
|
642
|
+
captured_vars_json: '',
|
|
643
|
+
watch_results_json: '',
|
|
644
|
+
evaluated_log: '',
|
|
645
|
+
metric_value: 0.0,
|
|
646
|
+
capture_error: '',
|
|
647
|
+
trace_id: nil
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
probe = @probe_map[probe_id]
|
|
651
|
+
if probe && get_probe_opt(probe, :should_capture_trace_id)
|
|
652
|
+
trace_id = TraceExtractor.extract_trace_context(@custom_set_trace_id)
|
|
653
|
+
event[:trace_id] = trace_id if trace_id && !trace_id.empty?
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
event
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def emit_event(_probe, event)
|
|
660
|
+
@mutex.synchronize { @total_hits += 1 }
|
|
661
|
+
@on_capture.call(event)
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def report_error(probe_id, error_message)
|
|
665
|
+
event = build_base_event(probe_id)
|
|
666
|
+
event[:capture_error] = error_message
|
|
667
|
+
emit_event(nil, event)
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def extract_stack_and_variables(probe, binding_ctx, stack_frame_depth, serializer, visited, local_names)
|
|
671
|
+
stack_frames = []
|
|
672
|
+
captured_vars = []
|
|
673
|
+
|
|
674
|
+
snapshot_frames(binding_ctx, local_names, stack_frame_depth).each_with_index do |(loc, current_b, frame_local_names), frame_idx|
|
|
675
|
+
|
|
676
|
+
func = loc&.label || loc&.base_label || '(anonymous)'
|
|
677
|
+
file = loc ? (loc.absolute_path || loc.path || 'unknown') : 'unknown'
|
|
678
|
+
line_no = loc&.lineno || 0
|
|
679
|
+
|
|
680
|
+
# If frame_idx == 0 and location is unpopulated, fallback to probe location
|
|
681
|
+
if frame_idx == 0 && (file == 'unknown' || line_no == 0)
|
|
682
|
+
runtime_loc = probe.respond_to?(:runtime_location) ? probe.runtime_location : probe[:runtime_location]
|
|
683
|
+
line_no = (probe.respond_to?(:runtime_line) ? probe.runtime_line : probe[:runtime_line]).to_i
|
|
684
|
+
file = runtime_loc
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
stack_frames << {
|
|
688
|
+
function_name: func,
|
|
689
|
+
file_name: file,
|
|
690
|
+
line_number: line_no,
|
|
691
|
+
column_number: 0
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
locals_hash = {}
|
|
695
|
+
closure_hash = {}
|
|
696
|
+
if current_b
|
|
697
|
+
capture_closures = get_probe_opt(probe, :capture_closures)
|
|
698
|
+
capture_closures = @global_config[:capture_closures] if capture_closures.nil?
|
|
699
|
+
LOCAL_VARIABLES.bind(current_b).call.first(128).each do |var_sym|
|
|
700
|
+
is_local = frame_local_names && frame_local_names.include?(var_sym)
|
|
701
|
+
next unless is_local || capture_closures == true
|
|
702
|
+
var_name = var_sym.to_s
|
|
703
|
+
|
|
704
|
+
begin
|
|
705
|
+
target = is_local ? locals_hash : closure_hash
|
|
706
|
+
target[var_name] = LOCAL_GET.bind(current_b).call(var_sym)
|
|
707
|
+
rescue StandardError, ScriptError => e
|
|
708
|
+
locals_hash[var_name] = "[Error accessing variable: #{e.message}]"
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
serialized_locals = locals_hash.empty? ? {} : serializer.serialize(locals_hash, "frame[#{frame_idx}].scopes[0]", 0, visited)
|
|
714
|
+
|
|
715
|
+
captured_vars << [
|
|
716
|
+
{
|
|
717
|
+
'type' => 'local',
|
|
718
|
+
'name' => 'Local',
|
|
719
|
+
'vars' => serialized_locals
|
|
720
|
+
}
|
|
721
|
+
]
|
|
722
|
+
unless closure_hash.empty?
|
|
723
|
+
captured_vars.last << {
|
|
724
|
+
'type' => 'closure', 'name' => 'Closure',
|
|
725
|
+
'vars' => serializer.serialize(closure_hash, "frame[#{frame_idx}].scopes[1]", 0, visited)
|
|
726
|
+
}
|
|
727
|
+
end
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
[stack_frames, captured_vars]
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
def snapshot_frames(binding_ctx, local_names, depth)
|
|
734
|
+
return [] unless depth.positive?
|
|
735
|
+
|
|
736
|
+
collect = proc do |locations, inspector|
|
|
737
|
+
frames = []
|
|
738
|
+
locations.first(50).each_with_index do |loc, index|
|
|
739
|
+
path = loc.path || ''
|
|
740
|
+
next if path.empty? || path.include?('hyperprobe/') || path.include?('<internal:') ||
|
|
741
|
+
path.include?('org/jruby/') || path.include?('org.jruby.') ||
|
|
742
|
+
path.include?('java/lang/') || path.include?('sun/reflect') || path.include?('jdk/internal')
|
|
743
|
+
|
|
744
|
+
current_b = nil
|
|
745
|
+
names = nil
|
|
746
|
+
if frames.empty?
|
|
747
|
+
current_b = binding_ctx
|
|
748
|
+
names = local_names
|
|
749
|
+
elsif inspector
|
|
750
|
+
begin
|
|
751
|
+
# Keep the inspector's original index, including native frames.
|
|
752
|
+
current_b = inspector.frame_binding(index)
|
|
753
|
+
iseq = inspector.frame_iseq(index) if current_b
|
|
754
|
+
names = caller_local_names(iseq) if iseq
|
|
755
|
+
current_b = nil unless names
|
|
756
|
+
rescue StandardError, ScriptError, SecurityError
|
|
757
|
+
current_b = nil
|
|
758
|
+
end
|
|
759
|
+
end
|
|
760
|
+
frames << [loc, current_b, names]
|
|
761
|
+
break if frames.length >= depth
|
|
762
|
+
end
|
|
763
|
+
frames.empty? ? [[nil, binding_ctx, local_names]] : frames
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
scan_limit = RUBY_ENGINE == 'ruby' && depth > 1 ? MAX_INSPECTOR_STACK_DEPTH + 1 : 50
|
|
767
|
+
locations = caller_locations(0, scan_limit) || []
|
|
768
|
+
if RUBY_ENGINE == 'ruby' && depth > 1 && locations.length <= MAX_INSPECTOR_STACK_DEPTH
|
|
769
|
+
begin
|
|
770
|
+
return DebugInspector.open { |inspector| collect.call(inspector.backtrace_locations, inspector) }
|
|
771
|
+
rescue StandardError, ScriptError, SecurityError
|
|
772
|
+
# An unavailable inspector must not discard the probe-hit locals.
|
|
773
|
+
end
|
|
774
|
+
end
|
|
775
|
+
collect.call(locations, nil)
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def caller_local_names(iseq)
|
|
779
|
+
return @caller_locals_cache[iseq] if @caller_locals_cache.key?(iseq)
|
|
780
|
+
@caller_locals_cache = ObjectSpace::WeakMap.new if @caller_locals_cache.size >= MAX_CALLER_SCOPES
|
|
781
|
+
|
|
782
|
+
# to_a expands nested bytecode, so reject large code trees before dumping
|
|
783
|
+
# them. These admission limits are not a hard native allocation deadline.
|
|
784
|
+
pending = [iseq]
|
|
785
|
+
count = 0
|
|
786
|
+
bytes = 0
|
|
787
|
+
until pending.empty?
|
|
788
|
+
current = pending.pop
|
|
789
|
+
count += 1
|
|
790
|
+
size_before = ISEQ_MEMORY_SIZE.call(current)
|
|
791
|
+
bytes += size_before
|
|
792
|
+
return @caller_locals_cache[iseq] = nil if count > MAX_CALLER_ISEQS || bytes > MAX_CALLER_ISEQ_BYTES
|
|
793
|
+
|
|
794
|
+
ISEQ_EACH_CHILD.bind(current).call do |child|
|
|
795
|
+
pending << child
|
|
796
|
+
return @caller_locals_cache[iseq] = nil if count + pending.length > MAX_CALLER_ISEQS
|
|
797
|
+
end
|
|
798
|
+
# Enumerating children can materialize a lazily loaded binary ISeq.
|
|
799
|
+
bytes += [0, ISEQ_MEMORY_SIZE.call(current) - size_before].max
|
|
800
|
+
return @caller_locals_cache[iseq] = nil if bytes > MAX_CALLER_ISEQ_BYTES
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
# The compiled local table excludes enclosing variables. Weak keys avoid
|
|
804
|
+
# retaining reloaded code; only names are cached, never live bindings.
|
|
805
|
+
data = ISEQ_TO_A.bind(iseq).call
|
|
806
|
+
names = if data[0] == 'YARVInstructionSequence/SimpleDataFormat' && data[10].is_a?(Array)
|
|
807
|
+
data[10].grep(Symbol).first(128).freeze
|
|
808
|
+
end
|
|
809
|
+
@caller_locals_cache[iseq] = names
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
def get_probe_positive_int(probe, key, default_val)
|
|
813
|
+
val = get_probe_opt(probe, key)
|
|
814
|
+
return val if val.is_a?(Integer) && val >= 0
|
|
815
|
+
|
|
816
|
+
global_val = @global_config[key] || @global_config[key.to_s]
|
|
817
|
+
return global_val if global_val.is_a?(Integer) && global_val >= 0
|
|
818
|
+
|
|
819
|
+
default_val
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
def get_probe_opt(probe, key)
|
|
823
|
+
if probe.respond_to?(key)
|
|
824
|
+
presence = "has_#{key}?"
|
|
825
|
+
return nil if probe.respond_to?(presence) && !probe.public_send(presence)
|
|
826
|
+
probe.public_send(key)
|
|
827
|
+
elsif probe.is_a?(Hash)
|
|
828
|
+
probe.key?(key) ? probe[key] : probe[key.to_s]
|
|
829
|
+
end
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
def probe_type_to_i(type)
|
|
833
|
+
case type
|
|
834
|
+
when :PROBE_TYPE_SNAPSHOT, 'PROBE_TYPE_SNAPSHOT', 1 then 1
|
|
835
|
+
when :PROBE_TYPE_LOG, 'PROBE_TYPE_LOG', 2 then 2
|
|
836
|
+
when :PROBE_TYPE_COUNTER, 'PROBE_TYPE_COUNTER', 3 then 3
|
|
837
|
+
when :PROBE_TYPE_METRIC, 'PROBE_TYPE_METRIC', 4 then 4
|
|
838
|
+
when :PROBE_TYPE_DURATION, 'PROBE_TYPE_DURATION', 5 then 5
|
|
839
|
+
else 0
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def cleanup_durations_unlocked(now)
|
|
844
|
+
return if now < @next_duration_cleanup && @duration_starts.size < MAX_DURATION_ENTRIES
|
|
845
|
+
|
|
846
|
+
@duration_starts.delete_if do |_key, (_start_time, created_at)|
|
|
847
|
+
now - created_at > DURATION_TTL_SECONDS
|
|
848
|
+
end
|
|
849
|
+
@next_duration_cleanup = now + DURATION_TTL_SECONDS
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
def monotonic_time
|
|
853
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
854
|
+
end
|
|
855
|
+
end
|
|
856
|
+
end
|
|
857
|
+
end
|