hyperprobe-agent 1.2.27.pre.1

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.
@@ -0,0 +1,683 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require 'json'
5
+ require_relative 'logger'
6
+
7
+ begin
8
+ require 'binding_of_caller'
9
+ rescue LoadError
10
+ # binding_of_caller is optional
11
+ end
12
+
13
+ module HyperProbe
14
+ module Core
15
+ class MonitoringEngine
16
+ DURATION_TTL_SECONDS = 60.0
17
+
18
+ LEVEL_COLORS = {
19
+ 'ERROR' => "\e[31m",
20
+ 'WARN' => "\e[33m",
21
+ 'INFO' => "\e[32m",
22
+ 'DEBUG' => "\e[36m"
23
+ }.freeze
24
+ COLOR_RESET = "\e[0m"
25
+ COLOR_BOLD = "\e[1m"
26
+ COLOR_GRAY = "\e[90m"
27
+ COLOR_MAGENTA = "\e[35m"
28
+
29
+ attr_reader :total_hits, :total_skips, :is_suspended, :is_active
30
+
31
+ def initialize(quota_manager, safety_monitor, on_capture, custom_set_trace_id = nil)
32
+ @quota_manager = quota_manager
33
+ @safety_monitor = safety_monitor
34
+ @on_capture = on_capture
35
+ @custom_set_trace_id = custom_set_trace_id
36
+
37
+ @mutex = Mutex.new
38
+ @active_locations = {} # [resolved_file_path, line] => Array<ActiveProbeContext>
39
+ @probe_map = {} # probe_id => Probe
40
+ @instrumented_files = Set.new
41
+ @path_cache = {}
42
+ @duration_starts = {} # [probe_id, correlation_id] => [start_time_monotonic, created_at_time]
43
+ @next_duration_cleanup = monotonic_time + DURATION_TTL_SECONDS
44
+
45
+ @global_config = {}
46
+ @total_hits = 0
47
+ @total_skips = 0
48
+ @is_suspended = false
49
+ @is_active = false
50
+ @closed = false
51
+
52
+ @log = Logger.get_logger('hyperprobe:inspector')
53
+ @targeted_tracepoints = {} # location_key => TracePoint instance
54
+ end
55
+
56
+ def set_global_config(config)
57
+ @mutex.synchronize do
58
+ @global_config = (config || {}).dup
59
+ end
60
+ end
61
+
62
+ def set_probes(probes)
63
+ @mutex.synchronize do
64
+ return if @closed
65
+
66
+ new_active_locations = {}
67
+ new_probe_map = {}
68
+ new_instrumented_files = Set.new
69
+
70
+ Array(probes).each do |probe|
71
+ probe_id = probe.respond_to?(:id) ? probe.id : probe[:id]
72
+ next unless probe_id
73
+
74
+ new_probe_map[probe_id] = probe
75
+
76
+ runtime_loc = probe.respond_to?(:runtime_location) ? probe.runtime_location : probe[:runtime_location]
77
+ runtime_line = (probe.respond_to?(:runtime_line) ? probe.runtime_line : probe[:runtime_line]).to_i
78
+
79
+ resolved_path = resolve_filepath(runtime_loc)
80
+ primary_key = [resolved_path, runtime_line]
81
+
82
+ new_active_locations[primary_key] ||= []
83
+ new_active_locations[primary_key] << { probe: probe, is_secondary: false }
84
+ new_instrumented_files.add(resolved_path)
85
+
86
+ probe_type = probe.respond_to?(:type) ? probe.type : probe[:type]
87
+ # Duration secondary location
88
+ is_duration = probe_type == 5 || probe_type == :PROBE_TYPE_DURATION
89
+ if is_duration
90
+ sec_line = (probe.respond_to?(:secondary_runtime_line) ? probe.secondary_runtime_line : probe[:secondary_runtime_line]).to_i
91
+ if sec_line.positive?
92
+ sec_loc = probe.respond_to?(:secondary_runtime_location) ? probe.secondary_runtime_location : probe[:secondary_runtime_location]
93
+ sec_loc = runtime_loc if sec_loc.nil? || sec_loc.empty?
94
+ resolved_sec_path = resolve_filepath(sec_loc)
95
+ secondary_key = [resolved_sec_path, sec_line]
96
+
97
+ new_active_locations[secondary_key] ||= []
98
+ new_active_locations[secondary_key] << { probe: probe, is_secondary: true }
99
+ new_instrumented_files.add(resolved_sec_path)
100
+ end
101
+ end
102
+ end
103
+
104
+ @active_locations = new_active_locations
105
+ @probe_map = new_probe_map
106
+ @instrumented_files = new_instrumented_files
107
+
108
+ update_tracepoint_state_unlocked
109
+ end
110
+ end
111
+
112
+ def suspend
113
+ @mutex.synchronize do
114
+ return if @closed || @is_suspended
115
+
116
+ @is_suspended = true
117
+ disable_all_tracepoints_unlocked
118
+ end
119
+ end
120
+
121
+ def resume
122
+ @mutex.synchronize do
123
+ return if @closed || !@is_suspended
124
+
125
+ @is_suspended = false
126
+ update_tracepoint_state_unlocked
127
+ end
128
+ end
129
+
130
+ def get_stats
131
+ @mutex.synchronize do
132
+ cleanup_durations_unlocked(monotonic_time)
133
+ stats = { hits: @total_hits, skips: @total_skips }
134
+ @total_hits = 0
135
+ @total_skips = 0
136
+ stats
137
+ end
138
+ end
139
+
140
+ def close
141
+ @mutex.synchronize do
142
+ return if @closed
143
+
144
+ @closed = true
145
+ disable_all_tracepoints_unlocked
146
+ @active_locations.clear
147
+ @probe_map.clear
148
+ @instrumented_files.clear
149
+ @duration_starts.clear
150
+ end
151
+ end
152
+
153
+ private
154
+
155
+ def update_tracepoint_state_unlocked
156
+ if @closed || @is_suspended || @active_locations.empty?
157
+ disable_all_tracepoints_unlocked
158
+ return
159
+ end
160
+
161
+ # 1. Remove stale tracepoints
162
+ active_keys = @active_locations.keys.to_set
163
+ @targeted_tracepoints.delete_if do |loc_key, tp|
164
+ unless active_keys.include?(loc_key)
165
+ tp.disable rescue nil
166
+ true
167
+ end
168
+ end
169
+
170
+ # 2. Setup or enable targeted tracepoint for each active location
171
+ @active_locations.each_key do |loc_key|
172
+ next if @targeted_tracepoints.key?(loc_key)
173
+
174
+ enable_location_tracepoint_unlocked(loc_key)
175
+ end
176
+
177
+ @is_active = !@targeted_tracepoints.empty?
178
+ end
179
+
180
+ def enable_location_tracepoint_unlocked(loc_key)
181
+ resolved_path, lineno = loc_key
182
+
183
+ tp = TracePoint.new(:line) do |t|
184
+ next if @is_suspended || @closed
185
+
186
+ # Check path match
187
+ t_path = @path_cache[t.path] || resolve_filepath(t.path)
188
+ next unless t_path == resolved_path && t.lineno == lineno
189
+
190
+ contexts = nil
191
+ @mutex.synchronize do
192
+ contexts = @active_locations[loc_key]
193
+ end
194
+
195
+ if contexts && !contexts.empty?
196
+ binding_ctx = t.binding
197
+ handle_hit(contexts, binding_ctx)
198
+ end
199
+ end
200
+
201
+ target_method = find_method_target(resolved_path, lineno)
202
+ targeted_enabled = false
203
+
204
+ if target_method
205
+ begin
206
+ tp.enable(target: target_method, target_line: lineno)
207
+ targeted_enabled = true
208
+ rescue ArgumentError, StandardError
209
+ # Target option not accepted by interpreter
210
+ end
211
+ end
212
+
213
+ unless targeted_enabled
214
+ begin
215
+ tp.enable
216
+ rescue StandardError
217
+ # TracePoint enable error
218
+ end
219
+ end
220
+
221
+ @targeted_tracepoints[loc_key] = tp
222
+ end
223
+
224
+ def disable_all_tracepoints_unlocked
225
+ @targeted_tracepoints.each_value do |tp|
226
+ tp.disable rescue nil
227
+ end
228
+ @targeted_tracepoints.clear
229
+ @is_active = false
230
+ end
231
+
232
+ def find_method_target(file_path, line_number)
233
+ best_match = nil
234
+ best_line = 0
235
+
236
+ ObjectSpace.each_object(Module) do |mod|
237
+ next if mod.singleton_class? || mod.name.nil?
238
+
239
+ [:instance_methods, :methods].each do |mtype|
240
+ methods_list = mod.send(mtype, false) rescue []
241
+ methods_list.each do |sym|
242
+ m = (mtype == :instance_methods ? mod.instance_method(sym) : mod.method(sym)) rescue nil
243
+ next unless m
244
+
245
+ loc = m.source_location
246
+ next unless loc
247
+
248
+ m_file = loc[0]
249
+ m_line = loc[1]
250
+
251
+ if (m_file == file_path || resolve_filepath(m_file) == file_path) && m_line <= line_number && m_line > best_line
252
+ best_match = m
253
+ best_line = m_line
254
+ end
255
+ end
256
+ end
257
+ end
258
+
259
+ best_match
260
+ rescue StandardError
261
+ nil
262
+ end
263
+
264
+ def resolve_filepath(runtime_location)
265
+ return '' if runtime_location.nil? || runtime_location.empty?
266
+
267
+ cached = @path_cache[runtime_location]
268
+ return cached if cached
269
+
270
+ if File.exist?(runtime_location)
271
+ resolved = File.realpath(runtime_location) rescue File.expand_path(runtime_location)
272
+ @path_cache[runtime_location] = resolved
273
+ return resolved
274
+ end
275
+
276
+ cwd = Dir.pwd
277
+ parts = runtime_location.tr('\\', '/').split('/').reject(&:empty?)
278
+ (0...parts.length).each do |i|
279
+ suffix = File.join(*parts[i..])
280
+ attempt = File.expand_path(suffix, cwd)
281
+ if File.exist?(attempt)
282
+ resolved = File.realpath(attempt) rescue attempt
283
+ @path_cache[runtime_location] = resolved
284
+ return resolved
285
+ end
286
+ end
287
+
288
+ fallback = File.expand_path(runtime_location, cwd)
289
+ @path_cache[runtime_location] = fallback
290
+ fallback
291
+ end
292
+
293
+ def handle_hit(contexts, binding_ctx)
294
+ start_time = monotonic_time
295
+ begin
296
+ contexts.each do |ctx|
297
+ probe = ctx[:probe]
298
+ is_secondary = ctx[:is_secondary]
299
+ process_single_probe_hit(probe, binding_ctx, is_secondary)
300
+ end
301
+ rescue StandardError => e
302
+ # Never crash application thread on probe hit
303
+ ensure
304
+ elapsed_ms = (monotonic_time - start_time) * 1000.0
305
+ @safety_monitor.report_pause_duration(elapsed_ms) rescue nil
306
+ end
307
+ end
308
+
309
+ def process_single_probe_hit(probe, binding_ctx, is_secondary)
310
+ probe_id = probe.respond_to?(:id) ? probe.id : probe[:id]
311
+ probe_type = probe.respond_to?(:type) ? probe.type : probe[:type]
312
+ probe_type_num = probe_type.is_a?(Integer) ? probe_type : probe_type_to_i(probe_type)
313
+
314
+ # 1. Rate-limiting admission check
315
+ unless @quota_manager.can_evaluate?
316
+ @mutex.synchronize { @total_skips += 1 }
317
+ return
318
+ end
319
+
320
+ # 2. Evaluate Condition (if any)
321
+ condition = probe.respond_to?(:condition) ? probe.condition : probe[:condition]
322
+ if condition && !condition.empty?
323
+ begin
324
+ passed = Evaluator.eval_condition(condition, binding_ctx)
325
+ return unless passed
326
+ rescue StandardError, ScriptError => e
327
+ report_error(probe_id, "Condition evaluation failed: #{e.class.name}: #{e.message}")
328
+ return
329
+ end
330
+ end
331
+
332
+ # 3. Dispatch to Probe Type Handler
333
+ case probe_type_num
334
+ when 1 # SNAPSHOT
335
+ handle_snapshot(probe, binding_ctx)
336
+ when 2 # LOG
337
+ handle_log(probe, binding_ctx)
338
+ when 3 # COUNTER
339
+ handle_counter(probe, binding_ctx)
340
+ when 4 # METRIC
341
+ handle_metric(probe, binding_ctx)
342
+ when 5 # DURATION
343
+ handle_duration(probe, binding_ctx, is_secondary)
344
+ end
345
+ end
346
+
347
+ def handle_snapshot(probe, binding_ctx)
348
+ probe_id = probe.respond_to?(:id) ? probe.id : probe[:id]
349
+
350
+ max_depth = get_probe_positive_int(probe, :max_object_depth, 3)
351
+ max_array_length = get_probe_positive_int(probe, :max_array_length, 3)
352
+ max_object_properties = get_probe_positive_int(probe, :max_object_properties, 50)
353
+ max_string_length = get_probe_positive_int(probe, :max_string_length, 1024)
354
+ stack_frame_depth = get_probe_positive_int(probe, :stack_frame_depth, 3)
355
+ redact_keys = @global_config[:redact_keys] || @global_config['redact_keys']
356
+ redact_values = @global_config[:redact_values] || @global_config['redact_values']
357
+
358
+ serializer = Serializer.new(
359
+ max_depth: max_depth,
360
+ max_array_length: max_array_length,
361
+ max_object_properties: max_object_properties,
362
+ max_string_length: max_string_length,
363
+ redact_keys: redact_keys,
364
+ redact_values: redact_values
365
+ )
366
+
367
+ visited = {}
368
+
369
+ # 1. Watch Expressions
370
+ watch_expressions = get_probe_opt(probe, :watch_expressions) || []
371
+ serialized_watches = {}
372
+ if watch_expressions && !watch_expressions.empty?
373
+ raw_watches = Evaluator.evaluate_watches(watch_expressions, binding_ctx)
374
+ raw_watches.each do |expr, val|
375
+ watch_path = "watch[#{JSON.generate(expr)}]"
376
+ serialized_watches[expr] = serializer.serialize(val, watch_path, 0, visited)
377
+ end
378
+ end
379
+
380
+ # 2. Unified Stack Frames & Multi-Frame Local Variables Extraction
381
+ stack_frames, captured_vars = extract_stack_and_variables(
382
+ probe,
383
+ binding_ctx,
384
+ stack_frame_depth,
385
+ serializer,
386
+ visited
387
+ )
388
+
389
+ event = build_base_event(probe_id)
390
+ event[:stack_frames] = stack_frames
391
+ event[:captured_vars_json] = Serializer.safe_dump_json(captured_vars)
392
+ event[:watch_results_json] = Serializer.safe_dump_json(serialized_watches)
393
+
394
+ emit_event(probe, event)
395
+ rescue StandardError => e
396
+ report_error(probe_id, "Snapshot capture failed: #{e.class.name}: #{e.message}")
397
+ end
398
+
399
+ def handle_log(probe, binding_ctx)
400
+ probe_id = probe.respond_to?(:id) ? probe.id : probe[:id]
401
+ template = get_probe_opt(probe, :template) || ''
402
+ max_string_length = get_probe_positive_int(probe, :max_string_length, 1024)
403
+ redact_values = @global_config[:redact_values] || @global_config['redact_values']
404
+
405
+ evaluated = Evaluator.evaluate_log_template(template, binding_ctx)
406
+
407
+ # Redact values
408
+ if redact_values && !redact_values.empty?
409
+ regex = Serializer.new(redact_values: redact_values).send(:compile_regex, redact_values)
410
+ evaluated = evaluated.gsub(regex, '[REDACTED Value]') if regex
411
+ end
412
+
413
+ # Truncate string
414
+ if evaluated.length > max_string_length
415
+ head = evaluated[0...max_string_length]
416
+ truncated_head = head.sub(/\b\w*$/, '').rstrip
417
+ truncated_head = head if truncated_head.empty?
418
+ evaluated = "#{truncated_head}... [Truncated: +#{evaluated.length - max_string_length} more chars]"
419
+ end
420
+
421
+ # Stdout logging if configured
422
+ should_log_stdout = get_probe_opt(probe, :should_log_to_stdout)
423
+ if should_log_stdout
424
+ log_level = (get_probe_opt(probe, :log_level) || 'INFO').to_s.upcase
425
+ color = LEVEL_COLORS[log_level] || "\e[37m"
426
+ timestamp = Time.now.utc.iso8601(3)
427
+ out_line = "#{COLOR_GRAY}[#{timestamp}]#{COLOR_RESET} " \
428
+ "#{COLOR_BOLD}#{color}[#{log_level}]#{COLOR_RESET} " \
429
+ "#{COLOR_BOLD}#{COLOR_MAGENTA}[HyperProbe LOG]#{COLOR_RESET} " \
430
+ "#{evaluated}\n"
431
+ $stdout.write(out_line) rescue nil
432
+ end
433
+
434
+ event = build_base_event(probe_id)
435
+ event[:evaluated_log] = evaluated
436
+ emit_event(probe, event)
437
+ rescue StandardError => e
438
+ report_error(probe_id, "Log evaluation failed: #{e.class.name}: #{e.message}")
439
+ end
440
+
441
+ def handle_counter(probe, _binding_ctx)
442
+ probe_id = probe.respond_to?(:id) ? probe.id : probe[:id]
443
+ event = build_base_event(probe_id)
444
+ event[:metric_value] = 1.0
445
+ emit_event(probe, event)
446
+ end
447
+
448
+ def handle_metric(probe, binding_ctx)
449
+ probe_id = probe.respond_to?(:id) ? probe.id : probe[:id]
450
+ metric_expr = get_probe_opt(probe, :metric_expression)
451
+ return if metric_expr.nil? || metric_expr.empty?
452
+
453
+ raw_val = nil
454
+ begin
455
+ raw_val = binding_ctx.eval(metric_expr)
456
+ val = Evaluator.coerce_metric_value(raw_val)
457
+ rescue StandardError, ScriptError => e
458
+ report_error(probe_id, "Metric evaluation failed: #{e.class.name}: #{e.message}")
459
+ return
460
+ end
461
+
462
+ event = build_base_event(probe_id)
463
+ if val
464
+ event[:metric_value] = val
465
+ else
466
+ event[:capture_error] = "Metric evaluation failed: #{raw_val.inspect}"
467
+ end
468
+ emit_event(probe, event)
469
+ end
470
+
471
+ def handle_duration(probe, binding_ctx, is_secondary)
472
+ probe_id = probe.respond_to?(:id) ? probe.id : probe[:id]
473
+ corr_expr = get_probe_opt(probe, :correlation_expression)
474
+
475
+ corr_id = nil
476
+ begin
477
+ corr_id = Evaluator.evaluate_correlation(corr_expr, binding_ctx)
478
+ rescue StandardError, ScriptError => e
479
+ report_error(probe_id, "Duration correlation failed: #{e.message}")
480
+ return
481
+ end
482
+
483
+ dur_key = [probe_id, corr_id]
484
+
485
+ if !is_secondary
486
+ now_mono = monotonic_time
487
+ @mutex.synchronize do
488
+ cleanup_durations_unlocked(now_mono)
489
+ @duration_starts[dur_key] = [now_mono, now_mono]
490
+ end
491
+ else
492
+ now_mono = monotonic_time
493
+ start_entry = nil
494
+ @mutex.synchronize do
495
+ cleanup_durations_unlocked(now_mono)
496
+ start_entry = @duration_starts.delete(dur_key)
497
+ end
498
+ return unless start_entry
499
+
500
+ duration_ms = (now_mono - start_entry[0]) * 1000.0
501
+ event = build_base_event(probe_id)
502
+ event[:metric_value] = duration_ms
503
+ emit_event(probe, event)
504
+ end
505
+ end
506
+
507
+ def build_base_event(probe_id)
508
+ event = {
509
+ probe_id: probe_id,
510
+ timestamp_ms: (Time.now.to_f * 1000).to_i,
511
+ stack_frames: [],
512
+ captured_vars_json: '',
513
+ watch_results_json: '',
514
+ evaluated_log: '',
515
+ metric_value: 0.0,
516
+ capture_error: '',
517
+ trace_id: nil
518
+ }
519
+
520
+ probe = @probe_map[probe_id]
521
+ if probe && get_probe_opt(probe, :should_capture_trace_id)
522
+ trace_id = TraceExtractor.extract_trace_context(@custom_set_trace_id)
523
+ event[:trace_id] = trace_id if trace_id && !trace_id.empty?
524
+ end
525
+
526
+ event
527
+ end
528
+
529
+ def emit_event(_probe, event)
530
+ @mutex.synchronize { @total_hits += 1 }
531
+ @on_capture.call(event)
532
+ end
533
+
534
+ def report_error(probe_id, error_message)
535
+ event = build_base_event(probe_id)
536
+ event[:capture_error] = error_message
537
+ emit_event(nil, event)
538
+ end
539
+
540
+ def extract_stack_and_variables(probe, binding_ctx, stack_frame_depth, serializer, visited)
541
+ sdk_path_snippet = 'hyperprobe/'
542
+ all_locations = caller_locations(0, 50) || []
543
+
544
+ user_locations = all_locations.reject do |loc|
545
+ p = (loc.respond_to?(:path) ? loc.path : loc.to_s) || ''
546
+ p.include?(sdk_path_snippet) ||
547
+ p.include?('<internal:') ||
548
+ p.include?('org/jruby/') ||
549
+ p.include?('org.jruby.') ||
550
+ p.include?('java/lang/') ||
551
+ p.include?('sun/reflect') ||
552
+ p.include?('jdk/internal')
553
+ end
554
+
555
+ # Capture caller bindings if available (via binding_of_caller)
556
+ user_caller_bindings = []
557
+ begin
558
+ if defined?(Binding) && binding_ctx.respond_to?(:of_caller)
559
+ (1..50).each do |depth|
560
+ b = binding_ctx.of_caller(depth) rescue nil
561
+ break unless b
562
+
563
+ loc = b.source_location
564
+ next unless loc
565
+
566
+ p = (loc[0] || '').to_s
567
+ next if p.include?(sdk_path_snippet) ||
568
+ p.include?('<internal:') ||
569
+ p.include?('org/jruby/') ||
570
+ p.include?('org.jruby.') ||
571
+ p.include?('java/lang/') ||
572
+ p.include?('sun/reflect') ||
573
+ p.include?('jdk/internal')
574
+
575
+ user_caller_bindings << b
576
+ end
577
+ end
578
+ rescue StandardError, ScriptError
579
+ # Fallback if caller binding inspection is restricted
580
+ end
581
+
582
+ stack_frames = []
583
+ captured_vars = []
584
+
585
+ total_frames = [user_locations.length, stack_frame_depth].min
586
+ total_frames = [total_frames, 1].max
587
+
588
+ (0...total_frames).each do |frame_idx|
589
+ loc = user_locations[frame_idx]
590
+
591
+ func = loc&.label || loc&.base_label || '(anonymous)'
592
+ file = loc ? resolve_filepath(loc.absolute_path || loc.path || 'unknown') : 'unknown'
593
+ line_no = loc&.lineno || 0
594
+
595
+ # If frame_idx == 0 and location is unpopulated, fallback to probe location
596
+ if frame_idx == 0 && (file == 'unknown' || line_no == 0)
597
+ runtime_loc = probe.respond_to?(:runtime_location) ? probe.runtime_location : probe[:runtime_location]
598
+ line_no = (probe.respond_to?(:runtime_line) ? probe.runtime_line : probe[:runtime_line]).to_i
599
+ file = resolve_filepath(runtime_loc)
600
+ end
601
+
602
+ stack_frames << {
603
+ function_name: func,
604
+ file_name: file,
605
+ line_number: line_no,
606
+ column_number: 0
607
+ }
608
+
609
+ # Resolve frame binding (Frame 0 uses binding_ctx; Frame 1..N uses user_caller_bindings)
610
+ current_b = (frame_idx == 0) ? binding_ctx : user_caller_bindings[frame_idx]
611
+
612
+ locals_hash = {}
613
+ if current_b
614
+ current_b.local_variables.each do |var_sym|
615
+ var_name = var_sym.to_s
616
+ next if var_name.start_with?('_') && var_name != '_x'
617
+
618
+ begin
619
+ locals_hash[var_name] = current_b.local_variable_get(var_sym)
620
+ rescue StandardError, ScriptError => e
621
+ locals_hash[var_name] = "[Error accessing variable: #{e.message}]"
622
+ end
623
+ end
624
+ end
625
+
626
+ serialized_locals = locals_hash.empty? ? {} : serializer.serialize(locals_hash, "frame[#{frame_idx}].scopes[0]", 0, visited)
627
+
628
+ captured_vars << [
629
+ {
630
+ 'type' => 'local',
631
+ 'name' => 'Local',
632
+ 'vars' => serialized_locals
633
+ }
634
+ ]
635
+ end
636
+
637
+ [stack_frames, captured_vars]
638
+ end
639
+
640
+ def get_probe_positive_int(probe, key, default_val)
641
+ val = get_probe_opt(probe, key)
642
+ return val.to_i if val.is_a?(Numeric) && val.to_i.positive?
643
+
644
+ global_val = @global_config[key] || @global_config[key.to_s]
645
+ return global_val.to_i if global_val.is_a?(Numeric) && global_val.to_i.positive?
646
+
647
+ default_val
648
+ end
649
+
650
+ def get_probe_opt(probe, key)
651
+ if probe.respond_to?(key)
652
+ probe.public_send(key)
653
+ elsif probe.is_a?(Hash)
654
+ probe[key] || probe[key.to_s]
655
+ end
656
+ end
657
+
658
+ def probe_type_to_i(type)
659
+ case type
660
+ when :PROBE_TYPE_SNAPSHOT, 'PROBE_TYPE_SNAPSHOT', 1 then 1
661
+ when :PROBE_TYPE_LOG, 'PROBE_TYPE_LOG', 2 then 2
662
+ when :PROBE_TYPE_COUNTER, 'PROBE_TYPE_COUNTER', 3 then 3
663
+ when :PROBE_TYPE_METRIC, 'PROBE_TYPE_METRIC', 4 then 4
664
+ when :PROBE_TYPE_DURATION, 'PROBE_TYPE_DURATION', 5 then 5
665
+ else 0
666
+ end
667
+ end
668
+
669
+ def cleanup_durations_unlocked(now)
670
+ return if now < @next_duration_cleanup
671
+
672
+ @duration_starts.delete_if do |_key, (_start_time, created_at)|
673
+ now - created_at > DURATION_TTL_SECONDS
674
+ end
675
+ @next_duration_cleanup = now + DURATION_TTL_SECONDS
676
+ end
677
+
678
+ def monotonic_time
679
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
680
+ end
681
+ end
682
+ end
683
+ end