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,425 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'time'
5
+ require 'set'
6
+
7
+ module HyperProbe
8
+ module Core
9
+ class Serializer
10
+ DEFAULT_MAX_DEPTH = 3
11
+ DEFAULT_MAX_ARRAY_LENGTH = 3
12
+ DEFAULT_MAX_OBJECT_PROPERTIES = 50
13
+ DEFAULT_MAX_STRING_LENGTH = 1024
14
+
15
+ def initialize(
16
+ max_depth: DEFAULT_MAX_DEPTH,
17
+ max_array_length: DEFAULT_MAX_ARRAY_LENGTH,
18
+ max_object_properties: DEFAULT_MAX_OBJECT_PROPERTIES,
19
+ max_string_length: DEFAULT_MAX_STRING_LENGTH,
20
+ redact_keys: nil,
21
+ redact_values: nil
22
+ )
23
+ @max_depth = max_depth || DEFAULT_MAX_DEPTH
24
+ @max_array_length = max_array_length || DEFAULT_MAX_ARRAY_LENGTH
25
+ @max_object_properties = max_object_properties || DEFAULT_MAX_OBJECT_PROPERTIES
26
+ @max_string_length = max_string_length || DEFAULT_MAX_STRING_LENGTH
27
+ @redact_keys_re = compile_regex(redact_keys)
28
+ @redact_values_re = compile_regex(redact_values)
29
+ end
30
+
31
+ def serialize(obj, path = '$', depth = 0, visited = {})
32
+ return nil if obj.nil?
33
+ return obj if obj == true || obj == false
34
+ return serialize_number(obj) if obj.is_a?(Numeric)
35
+ return serialize_string(obj) if obj.is_a?(String)
36
+ return obj.to_s if obj.is_a?(Symbol)
37
+ return serialize_callable(obj) if callable?(obj)
38
+ return serialize_time(obj) if time_object?(obj)
39
+ return serialize_exception(obj) if obj.is_a?(Exception)
40
+ return serialize_java_throwable(obj) if java_throwable?(obj)
41
+ return obj.inspect if obj.is_a?(Regexp) || obj.is_a?(Range)
42
+
43
+ # Reference / Cycle check (immediate primitives like symbols/integers/floats/true/false are handled above)
44
+ obj_id = obj.object_id
45
+ if visited.key?(obj_id)
46
+ return "[REF - #{visited[obj_id]}]"
47
+ end
48
+
49
+ # Depth check
50
+ if depth > @max_depth
51
+ return summary_of(obj)
52
+ end
53
+
54
+ # Track visited for complex objects
55
+ visited[obj_id] = path
56
+
57
+ if obj.is_a?(Array)
58
+ serialize_array(obj, path, depth, visited)
59
+ elsif obj.is_a?(Set)
60
+ serialize_array(obj.to_a, path, depth, visited)
61
+ elsif obj.is_a?(Hash)
62
+ serialize_hash(obj, path, depth, visited)
63
+ elsif java_map?(obj)
64
+ serialize_java_map(obj, path, depth, visited)
65
+ elsif java_list?(obj)
66
+ serialize_java_list(obj, path, depth, visited)
67
+ elsif java_set?(obj)
68
+ serialize_java_set(obj, path, depth, visited)
69
+ elsif obj.is_a?(Struct) || (defined?(OpenStruct) && obj.is_a?(OpenStruct))
70
+ serialize_struct(obj, path, depth, visited)
71
+ elsif java_object?(obj)
72
+ serialize_java_pojo(obj, path, depth, visited)
73
+ else
74
+ serialize_object(obj, path, depth, visited)
75
+ end
76
+ end
77
+
78
+ def self.safe_dump_json(data)
79
+ JSON.generate(data)
80
+ rescue StandardError => e
81
+ JSON.generate({ 'error' => "JSON serialization failed: #{e.message}" })
82
+ end
83
+
84
+ private
85
+
86
+ def compile_regex(patterns)
87
+ return nil if patterns.nil? || patterns.empty?
88
+
89
+ valid = Array(patterns).map(&:to_s).map(&:strip).reject(&:empty?)
90
+ return nil if valid.empty?
91
+
92
+ Regexp.union(valid.map { |p| Regexp.new(p, Regexp::IGNORECASE) })
93
+ rescue StandardError
94
+ nil
95
+ end
96
+
97
+ def serialize_number(num)
98
+ if num.is_a?(Float)
99
+ return 'NaN' if num.nan?
100
+ return 'Infinity' if num.infinite? && num.positive?
101
+ return '-Infinity' if num.infinite? && num.negative?
102
+ end
103
+ num
104
+ end
105
+
106
+ def serialize_string(str)
107
+ val = str.dup
108
+ if @redact_values_re && @redact_values_re.match?(val)
109
+ val = val.gsub(@redact_values_re, '[REDACTED Value]')
110
+ end
111
+
112
+ if val.length > @max_string_length
113
+ head = val[0...@max_string_length]
114
+ # Match Node.js truncated head word boundary trimming
115
+ truncated_head = head.sub(/\b\w*$/, '').rstrip
116
+ truncated_head = head if truncated_head.empty?
117
+ val = "#{truncated_head}... [Truncated: +#{val.length - @max_string_length} more chars]"
118
+ end
119
+ val
120
+ end
121
+
122
+ def callable?(obj)
123
+ obj.is_a?(Proc) || obj.is_a?(Method) || obj.is_a?(UnboundMethod)
124
+ end
125
+
126
+ def serialize_callable(obj)
127
+ name = if obj.respond_to?(:name) && obj.name
128
+ ": #{obj.name}"
129
+ elsif obj.lambda?
130
+ ': (lambda)'
131
+ else
132
+ ''
133
+ end
134
+ "[Function#{name}]"
135
+ end
136
+
137
+ def time_object?(obj)
138
+ obj.is_a?(Time) || obj.is_a?(Date) || (defined?(DateTime) && obj.is_a?(DateTime))
139
+ end
140
+
141
+ def serialize_time(obj)
142
+ obj.respond_to?(:iso8601) ? obj.iso8601 : obj.to_s
143
+ end
144
+
145
+ def serialize_exception(obj)
146
+ stack = if obj.backtrace
147
+ obj.backtrace.first(3).join("\n") + '...'
148
+ end
149
+ {
150
+ 'name' => obj.class.name,
151
+ 'message' => obj.message,
152
+ 'stack' => stack
153
+ }
154
+ end
155
+
156
+ def serialize_array(arr, path, depth, visited)
157
+ result = []
158
+ len = [arr.length, @max_array_length].min
159
+
160
+ (0...len).each do |i|
161
+ elem = arr[i]
162
+ elem_path = "#{path}[#{i}]"
163
+ result << serialize(elem, elem_path, depth + 1, visited)
164
+ rescue StandardError => e
165
+ result << "[Error: #{e.message}]"
166
+ end
167
+
168
+ if arr.length > @max_array_length
169
+ result << "[+ #{arr.length - @max_array_length} more items truncated]"
170
+ end
171
+
172
+ result
173
+ end
174
+
175
+ def serialize_hash(hash, path, depth, visited)
176
+ result = {}
177
+ keys = hash.keys
178
+ limit = @max_object_properties
179
+ num_to_process = [keys.length, limit].min
180
+
181
+ (0...num_to_process).each do |i|
182
+ key = keys[i]
183
+ key_str = key.to_s
184
+
185
+ if @redact_keys_re && @redact_keys_re.match?(key_str)
186
+ result[key_str] = '[REDACTED Key]'
187
+ next
188
+ end
189
+
190
+ child_path = path == '$' ? key_str : "#{path}.#{key_str}"
191
+ val = hash[key]
192
+ result[key_str] = serialize(val, child_path, depth + 1, visited)
193
+ rescue StandardError => e
194
+ result[key_str] = "[Error accessing property: #{e.message}]"
195
+ end
196
+
197
+ if keys.length > limit
198
+ result['__probe_meta'] = "+ #{keys.length - limit} more properties truncated"
199
+ end
200
+
201
+ result
202
+ end
203
+
204
+ def serialize_struct(struct_obj, path, depth, visited)
205
+ hash = struct_obj.to_h
206
+ serialize_hash(hash, path, depth, visited)
207
+ rescue StandardError
208
+ serialize_object(struct_obj, path, depth, visited)
209
+ end
210
+
211
+ # Java Collection & POJO Serialization for JRuby
212
+ def serialize_java_map(map, path, depth, visited)
213
+ result = {}
214
+ keys = (map.respond_to?(:keySet) ? map.keySet.to_a : map.keys) rescue []
215
+ limit = @max_object_properties
216
+ num_to_process = [keys.length, limit].min
217
+
218
+ (0...num_to_process).each do |i|
219
+ key = keys[i]
220
+ key_str = key.to_s
221
+
222
+ if @redact_keys_re && @redact_keys_re.match?(key_str)
223
+ result[key_str] = '[REDACTED Key]'
224
+ next
225
+ end
226
+
227
+ child_path = path == '$' ? key_str : "#{path}.#{key_str}"
228
+ val = map.respond_to?(:get) ? map.get(key) : map[key] rescue map[key]
229
+ result[key_str] = serialize(val, child_path, depth + 1, visited)
230
+ rescue StandardError => e
231
+ result[key_str] = "[Error accessing property: #{e.message}]"
232
+ end
233
+
234
+ if keys.length > limit
235
+ result['__probe_meta'] = "+ #{keys.length - limit} more properties truncated"
236
+ end
237
+
238
+ result
239
+ end
240
+
241
+ def serialize_java_list(list, path, depth, visited)
242
+ result = []
243
+ size = (list.respond_to?(:size) ? list.size : list.length) rescue 0
244
+ len = [size, @max_array_length].min
245
+
246
+ (0...len).each do |i|
247
+ elem = list.respond_to?(:get) ? list.get(i) : list[i] rescue list[i]
248
+ elem_path = "#{path}[#{i}]"
249
+ result << serialize(elem, elem_path, depth + 1, visited)
250
+ rescue StandardError => e
251
+ result << "[Error: #{e.message}]"
252
+ end
253
+
254
+ if size > @max_array_length
255
+ result << "[+ #{size - @max_array_length} more items truncated]"
256
+ end
257
+
258
+ result
259
+ end
260
+
261
+ def serialize_java_set(set, path, depth, visited)
262
+ arr = (set.respond_to?(:toArray) ? set.toArray.to_a : set.to_a) rescue []
263
+ serialize_array(arr, path, depth, visited)
264
+ end
265
+
266
+ def serialize_java_throwable(throwable)
267
+ stack_elements = nil
268
+ if throwable.respond_to?(:getStackTrace)
269
+ stack_elements = (throwable.getStackTrace.to_a.first(3).map(&:toString).join("\n") + '...') rescue nil
270
+ end
271
+ class_name = throwable.respond_to?(:getClass) ? throwable.getClass.getName : throwable.class.name
272
+ message = throwable.respond_to?(:getMessage) ? throwable.getMessage : throwable.to_s
273
+ {
274
+ 'name' => class_name,
275
+ 'message' => message || '',
276
+ 'stack' => stack_elements
277
+ }
278
+ end
279
+
280
+ def serialize_java_pojo(obj, path, depth, visited)
281
+ ivars = obj.instance_variables rescue []
282
+ if ivars && !ivars.empty?
283
+ return serialize_object(obj, path, depth, visited)
284
+ end
285
+
286
+ result = {}
287
+ ignored_methods = %w[getClass getDeclaringClass hashCode toString equals wait notify notifyAll]
288
+ getters = []
289
+ begin
290
+ getters = obj.class.instance_methods(false).select do |m|
291
+ name = m.to_s
292
+ (name.start_with?('get') && name.length > 3 || name.start_with?('is') && name.length > 2) && !ignored_methods.include?(name)
293
+ end
294
+ rescue StandardError
295
+ getters = []
296
+ end
297
+
298
+ limit = @max_object_properties
299
+ num_to_process = [getters.length, limit].min
300
+
301
+ (0...num_to_process).each do |i|
302
+ getter_sym = getters[i]
303
+ raw_name = getter_sym.to_s
304
+ prop_name = if raw_name.start_with?('get')
305
+ raw_name[3..].sub(/^[A-Z]/, &:downcase)
306
+ elsif raw_name.start_with?('is')
307
+ raw_name[2..].sub(/^[A-Z]/, &:downcase)
308
+ else
309
+ raw_name
310
+ end
311
+
312
+ if @redact_keys_re && @redact_keys_re.match?(prop_name)
313
+ result[prop_name] = '[REDACTED Key]'
314
+ next
315
+ end
316
+
317
+ child_path = path == '$' ? prop_name : "#{path}.#{prop_name}"
318
+ val = obj.public_send(getter_sym)
319
+ result[prop_name] = serialize(val, child_path, depth + 1, visited)
320
+ rescue StandardError => e
321
+ result[prop_name] = "[Error accessing property: #{e.message}]"
322
+ end
323
+
324
+ if getters.length > limit
325
+ result['__probe_meta'] = "+ #{getters.length - limit} more properties truncated"
326
+ end
327
+
328
+ result.empty? ? summary_of(obj) : result
329
+ end
330
+
331
+ def java_object?(obj)
332
+ return false unless defined?(JRUBY_VERSION) || defined?(Java)
333
+ return true if defined?(java.lang.Object) && obj.is_a?(java.lang.Object)
334
+ return true if obj.respond_to?(:java_class)
335
+ false
336
+ rescue StandardError
337
+ false
338
+ end
339
+
340
+ def java_map?(obj)
341
+ return false unless defined?(JRUBY_VERSION) || defined?(Java)
342
+ return true if defined?(java.util.Map) && obj.is_a?(java.util.Map)
343
+ return true if defined?(Java::JavaUtil::Map) && obj.is_a?(Java::JavaUtil::Map)
344
+ false
345
+ rescue StandardError
346
+ false
347
+ end
348
+
349
+ def java_list?(obj)
350
+ return false unless defined?(JRUBY_VERSION) || defined?(Java)
351
+ return true if defined?(java.util.List) && obj.is_a?(java.util.List)
352
+ return true if defined?(Java::JavaUtil::List) && obj.is_a?(Java::JavaUtil::List)
353
+ false
354
+ rescue StandardError
355
+ false
356
+ end
357
+
358
+ def java_set?(obj)
359
+ return false unless defined?(JRUBY_VERSION) || defined?(Java)
360
+ return true if defined?(java.util.Set) && obj.is_a?(java.util.Set)
361
+ return true if defined?(Java::JavaUtil::Set) && obj.is_a?(Java::JavaUtil::Set)
362
+ false
363
+ rescue StandardError
364
+ false
365
+ end
366
+
367
+ def java_throwable?(obj)
368
+ return false unless defined?(JRUBY_VERSION) || defined?(Java)
369
+ return true if defined?(java.lang.Throwable) && obj.is_a?(java.lang.Throwable)
370
+ return true if defined?(Java::JavaLang::Throwable) && obj.is_a?(Java::JavaLang::Throwable)
371
+ false
372
+ rescue StandardError
373
+ false
374
+ end
375
+
376
+ def serialize_object(obj, path, depth, visited)
377
+ result = {}
378
+ ivars = obj.instance_variables
379
+ limit = @max_object_properties
380
+ num_to_process = [ivars.length, limit].min
381
+
382
+ (0...num_to_process).each do |i|
383
+ ivar_sym = ivars[i]
384
+ key_str = ivar_sym.to_s.sub(/^@/, '')
385
+
386
+ if @redact_keys_re && @redact_keys_re.match?(key_str)
387
+ result[key_str] = '[REDACTED Key]'
388
+ next
389
+ end
390
+
391
+ child_path = path == '$' ? key_str : "#{path}.#{key_str}"
392
+ val = obj.instance_variable_get(ivar_sym)
393
+ result[key_str] = serialize(val, child_path, depth + 1, visited)
394
+ rescue StandardError => e
395
+ result[key_str] = "[Error accessing property: #{e.message}]"
396
+ end
397
+
398
+ if ivars.length > limit
399
+ result['__probe_meta'] = "+ #{ivars.length - limit} more properties truncated"
400
+ end
401
+
402
+ # If an object has no instance variables, check if it responds to meaningful getters or to_s
403
+ if ivars.empty?
404
+ return summary_of(obj)
405
+ end
406
+
407
+ result
408
+ end
409
+
410
+ def summary_of(obj)
411
+ return 'null' if obj.nil?
412
+
413
+ case obj
414
+ when String then "[Str: #{obj.length}]"
415
+ when Array then "[Arr: #{obj.length}]"
416
+ when Set then "[Set: #{obj.length}]"
417
+ when Hash then "[Obj: #{obj.length} keys]"
418
+ when Proc, Method then '[Function]'
419
+ else
420
+ "[Obj: #{obj.class.name}]"
421
+ end
422
+ end
423
+ end
424
+ end
425
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HyperProbe
4
+ module Core
5
+ class TraceExtractor
6
+ class << self
7
+ def extract_trace_context(custom_get_trace_id = nil)
8
+ # 1. Custom User Hook (Passed via options[:set_trace_id])
9
+ if custom_get_trace_id.respond_to?(:call)
10
+ begin
11
+ trace_id = custom_get_trace_id.call
12
+ return trace_id.to_s if trace_id && !trace_id.to_s.empty?
13
+ rescue StandardError
14
+ # Never fail on user hook error
15
+ end
16
+ end
17
+
18
+ # 2. OpenTelemetry
19
+ if defined?(::OpenTelemetry::Trace)
20
+ begin
21
+ span = ::OpenTelemetry::Trace.current_span
22
+ if span && span.respond_to?(:context)
23
+ ctx = span.context
24
+ if ctx
25
+ if ctx.respond_to?(:hex_trace_id)
26
+ trace_id = ctx.hex_trace_id
27
+ return trace_id if trace_id && trace_id != '00000000000000000000000000000000'
28
+ elsif ctx.respond_to?(:trace_id) && ctx.trace_id
29
+ raw_id = ctx.trace_id
30
+ hex_id = raw_id.is_a?(String) ? raw_id.unpack1('H*') : raw_id.to_s(16)
31
+ return hex_id if hex_id && hex_id != '00000000000000000000000000000000'
32
+ end
33
+ end
34
+ end
35
+ rescue StandardError
36
+ # Silently ignore OTel errors
37
+ end
38
+ end
39
+
40
+ # 3. Datadog
41
+ if defined?(::Datadog::Tracing)
42
+ begin
43
+ if ::Datadog::Tracing.respond_to?(:active_trace)
44
+ trace = ::Datadog::Tracing.active_trace
45
+ trace_id = trace&.id&.to_s
46
+ return trace_id if trace_id && !trace_id.empty?
47
+ end
48
+ if ::Datadog::Tracing.respond_to?(:active_span)
49
+ span = ::Datadog::Tracing.active_span
50
+ trace_id = span&.trace_id&.to_s
51
+ return trace_id if trace_id && !trace_id.empty?
52
+ end
53
+ rescue StandardError
54
+ # Silently ignore Datadog errors
55
+ end
56
+ end
57
+
58
+ # 4. New Relic
59
+ if defined?(::NewRelic::Agent)
60
+ begin
61
+ if defined?(::NewRelic::Agent::Tracer) && ::NewRelic::Agent::Tracer.respond_to?(:current_trace_id)
62
+ trace_id = ::NewRelic::Agent::Tracer.current_trace_id
63
+ return trace_id if trace_id && !trace_id.empty?
64
+ end
65
+ if ::NewRelic::Agent.respond_to?(:linking_metadata)
66
+ metadata = ::NewRelic::Agent.linking_metadata
67
+ trace_id = metadata['trace.id'] || metadata[:trace_id]
68
+ return trace_id.to_s if trace_id && !trace_id.to_s.empty?
69
+ end
70
+ rescue StandardError
71
+ # Silently ignore New Relic errors
72
+ end
73
+ end
74
+
75
+ # 5. Elastic APM
76
+ if defined?(::ElasticAPM)
77
+ begin
78
+ if ::ElasticAPM.respond_to?(:current_transaction)
79
+ txn = ::ElasticAPM.current_transaction
80
+ trace_id = txn&.trace_id
81
+ return trace_id if trace_id && !trace_id.empty?
82
+ end
83
+ rescue StandardError
84
+ # Silently ignore Elastic APM errors
85
+ end
86
+ end
87
+
88
+ # 6. Java APMs (When running on JRuby / JVM)
89
+ if defined?(JRUBY_VERSION) || defined?(Java)
90
+ trace_id = extract_java_apm_trace_context
91
+ return trace_id if trace_id && !trace_id.empty?
92
+ end
93
+
94
+ nil
95
+ end
96
+
97
+ private
98
+
99
+ def extract_java_apm_trace_context
100
+ # 6a. Java OpenTelemetry
101
+ if defined?(Java::IoOpentelemetryApiTrace::Span)
102
+ begin
103
+ span = Java::IoOpentelemetryApiTrace::Span.current
104
+ if span
105
+ ctx = span.getSpanContext rescue nil
106
+ tid = ctx&.getTraceId
107
+ return tid.to_s if tid && tid.to_s != '00000000000000000000000000000000' && !tid.to_s.empty?
108
+ end
109
+ rescue StandardError
110
+ end
111
+ end
112
+
113
+ # 6b. Java Datadog
114
+ if defined?(Java::DatadogTraceApi::CorrelationIdentifier)
115
+ begin
116
+ tid = Java::DatadogTraceApi::CorrelationIdentifier.getTraceId
117
+ return tid.to_s if tid && tid.to_s != '0' && !tid.to_s.empty?
118
+ rescue StandardError
119
+ end
120
+ end
121
+
122
+ # 6c. Java New Relic
123
+ if defined?(Java::ComNewrelicApiAgent::NewRelic)
124
+ begin
125
+ tid = Java::ComNewrelicApiAgent::NewRelic.getAgent.getTraceMetadata.getTraceId rescue nil
126
+ return tid.to_s if tid && !tid.to_s.empty?
127
+ rescue StandardError
128
+ end
129
+ end
130
+
131
+ # 6d. Java Brave / Zipkin
132
+ if defined?(Java::Brave::Tracing)
133
+ begin
134
+ tracer = Java::Brave::Tracing.currentTracer rescue nil
135
+ span = tracer&.currentSpan rescue nil
136
+ tid = span&.context&.traceIdString rescue nil
137
+ return tid.to_s if tid && !tid.to_s.empty?
138
+ rescue StandardError
139
+ end
140
+ end
141
+
142
+ # 6e. Java SkyWalking
143
+ if defined?(Java::OrgApacheSkywalkingApmToolkitTrace::TraceContext)
144
+ begin
145
+ tid = Java::OrgApacheSkywalkingApmToolkitTrace::TraceContext.traceId rescue nil
146
+ return tid.to_s if tid && !tid.to_s.empty? && tid.to_s != 'N/A'
147
+ rescue StandardError
148
+ end
149
+ end
150
+
151
+ nil
152
+ rescue StandardError
153
+ nil
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HyperProbe
4
+ module Lambda
5
+ class << self
6
+ def wrap(options = {}, &handler)
7
+ is_aws_lambda = !ENV['AWS_LAMBDA_FUNCTION_NAME'].nil?
8
+ is_local_emulator = ENV['IS_OFFLINE'] == 'true' || ENV['AWS_SAM_LOCAL'] == 'true'
9
+ is_ephemeral = (options[:is_lambda] || is_aws_lambda) && !is_local_emulator
10
+
11
+ # If running locally in emulator, standard background agent mode
12
+ unless is_ephemeral
13
+ HyperProbe.start(options)
14
+ return handler
15
+ end
16
+
17
+ # Ephemeral Lambda wrapper
18
+ lambda do |event:, context:|
19
+ unless HyperProbe.instance
20
+ HyperProbe.start(options.merge(is_lambda: true))
21
+ end
22
+
23
+ agent = HyperProbe.instance
24
+ is_active = agent && !agent.agent_disabled?
25
+
26
+ if is_active
27
+ remaining_ms = context.respond_to?(:get_remaining_time_in_millis) ? context.get_remaining_time_in_millis : nil
28
+ sync_timeout_ms = options[:lambda_sync_timeout_ms] || 2000
29
+
30
+ if remaining_ms
31
+ dynamic_budget = (remaining_ms * 0.2).to_i
32
+ if dynamic_budget < 100
33
+ sync_timeout_ms = 0
34
+ else
35
+ sync_timeout_ms = [sync_timeout_ms, dynamic_budget].min
36
+ end
37
+ end
38
+
39
+ if sync_timeout_ms.positive?
40
+ begin
41
+ agent.force_sync(sync_timeout_ms / 1000.0)
42
+ rescue StandardError => e
43
+ warn "[HyperProbe Lambda] Failed to sync probes: #{e.message}"
44
+ end
45
+ end
46
+ end
47
+
48
+ begin
49
+ if handler.parameters.length == 1
50
+ handler.call(event)
51
+ else
52
+ handler.call(event: event, context: context)
53
+ end
54
+ ensure
55
+ if is_active
56
+ remaining_ms = context.respond_to?(:get_remaining_time_in_millis) ? context.get_remaining_time_in_millis : 0
57
+ flush_timeout_ms = options[:flush_timeout_ms] || 1500
58
+
59
+ if remaining_ms && remaining_ms > 500
60
+ flush_timeout_ms = [remaining_ms - 200, flush_timeout_ms].min
61
+ end
62
+
63
+ if remaining_ms > 500 || !context.respond_to?(:get_remaining_time_in_millis)
64
+ begin
65
+ agent.force_flush(flush_timeout_ms / 1000.0)
66
+ rescue StandardError => e
67
+ warn "[HyperProbe Lambda] Failed to flush telemetry: #{e.message}"
68
+ end
69
+ else
70
+ warn '[HyperProbe Lambda] Aborting snapshot flush to prevent Lambda timeout.'
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def stop
78
+ agent = HyperProbe.instance
79
+ if agent && !agent.agent_disabled?
80
+ begin
81
+ agent.force_flush(1.0)
82
+ rescue StandardError
83
+ # Best effort
84
+ end
85
+ end
86
+ HyperProbe.shutdown
87
+ end
88
+ end
89
+ end
90
+ end