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.
- checksums.yaml +7 -0
- data/LICENSE +13 -0
- data/README.md +157 -0
- data/lib/hyperprobe/agent.rb +438 -0
- data/lib/hyperprobe/core/broker.rb +213 -0
- data/lib/hyperprobe/core/evaluator.rb +128 -0
- data/lib/hyperprobe/core/logger.rb +177 -0
- data/lib/hyperprobe/core/monitoring_engine.rb +683 -0
- data/lib/hyperprobe/core/quota.rb +111 -0
- data/lib/hyperprobe/core/safe_ast_validator.rb +91 -0
- data/lib/hyperprobe/core/safety.rb +179 -0
- data/lib/hyperprobe/core/serializer.rb +425 -0
- data/lib/hyperprobe/core/trace_extractor.rb +158 -0
- data/lib/hyperprobe/lambda.rb +90 -0
- data/lib/hyperprobe/protos/agent_pb.rb +33 -0
- data/lib/hyperprobe/protos/agent_services_pb.rb +31 -0
- data/lib/hyperprobe/protos.rb +14 -0
- data/lib/hyperprobe/railtie.rb +16 -0
- data/lib/hyperprobe/version.rb +5 -0
- data/lib/hyperprobe-agent.rb +3 -0
- data/lib/hyperprobe.rb +153 -0
- metadata +149 -0
|
@@ -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
|