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,437 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'time'
|
|
5
|
+
require 'set'
|
|
6
|
+
require 'objspace' unless RUBY_PLATFORM.include?('java')
|
|
7
|
+
|
|
8
|
+
module HyperProbe
|
|
9
|
+
module Core
|
|
10
|
+
class Serializer
|
|
11
|
+
DEFAULT_MAX_DEPTH = 3
|
|
12
|
+
DEFAULT_MAX_ARRAY_LENGTH = 3
|
|
13
|
+
DEFAULT_MAX_OBJECT_PROPERTIES = 50
|
|
14
|
+
DEFAULT_MAX_STRING_LENGTH = 1024
|
|
15
|
+
MAX_DEPTH = 16
|
|
16
|
+
MAX_ITEMS = 128
|
|
17
|
+
MAX_STRING_BYTES = 4096
|
|
18
|
+
MAX_NODES = 512
|
|
19
|
+
MAX_BYTES = 65_536
|
|
20
|
+
MAX_PATTERNS = 16
|
|
21
|
+
MAX_PATTERN_BYTES = 256
|
|
22
|
+
HASH_STORAGE_BYTES = 131_072
|
|
23
|
+
MEMORY_SIZE = ObjectSpace.method(:memsize_of) if ObjectSpace.respond_to?(:memsize_of)
|
|
24
|
+
LEGACY_STRING_SLICE = RUBY_PLATFORM.include?('java') && RUBY_VERSION.start_with?('2.6.')
|
|
25
|
+
BUDGET_MARKER = '[Capture budget exhausted]'
|
|
26
|
+
|
|
27
|
+
# Capture implementations once, then bind them directly. Even innocuous
|
|
28
|
+
# calls such as obj.class, ==, or hash[key] can execute application code.
|
|
29
|
+
CORE = {
|
|
30
|
+
klass: Kernel.instance_method(:class),
|
|
31
|
+
kind: Kernel.instance_method(:is_a?),
|
|
32
|
+
identity: BasicObject.instance_method(:__id__),
|
|
33
|
+
equal: BasicObject.instance_method(:equal?),
|
|
34
|
+
ivars: Kernel.instance_method(:instance_variables),
|
|
35
|
+
ivar: Kernel.instance_method(:instance_variable_get),
|
|
36
|
+
class_name: Module.instance_method(:name),
|
|
37
|
+
string_size: String.instance_method(:bytesize),
|
|
38
|
+
string_slice: String.instance_method(:byteslice),
|
|
39
|
+
array_size: Array.instance_method(:length),
|
|
40
|
+
array_get: Array.instance_method(:[]),
|
|
41
|
+
hash_size: Hash.instance_method(:size),
|
|
42
|
+
hash_each: Hash.instance_method(:each_pair),
|
|
43
|
+
integer_bits: Integer.instance_method(:bit_length),
|
|
44
|
+
integer_string: Integer.instance_method(:to_s),
|
|
45
|
+
float_finite: Float.instance_method(:finite?),
|
|
46
|
+
float_nan: Float.instance_method(:nan?),
|
|
47
|
+
float_positive: Float.instance_method(:positive?),
|
|
48
|
+
time_format: Time.instance_method(:strftime),
|
|
49
|
+
time_utc: Time.instance_method(:utc?),
|
|
50
|
+
time_compare: Time.instance_method(:<=>),
|
|
51
|
+
date_format: Date.instance_method(:strftime),
|
|
52
|
+
date_compare: Date.instance_method(:<=>),
|
|
53
|
+
proc_lambda: Proc.instance_method(:lambda?),
|
|
54
|
+
method_name: Method.instance_method(:name),
|
|
55
|
+
unbound_name: UnboundMethod.instance_method(:name),
|
|
56
|
+
struct_size: Struct.instance_method(:size),
|
|
57
|
+
struct_each: Struct.instance_method(:each_pair)
|
|
58
|
+
}.freeze
|
|
59
|
+
SYMBOL_NAME = if Symbol.method_defined?(:name)
|
|
60
|
+
Symbol.instance_method(:name)
|
|
61
|
+
elsif RUBY_PLATFORM.include?('java')
|
|
62
|
+
# JRuby shares the symbol's bytes; slice before encoding.
|
|
63
|
+
Symbol.instance_method(:to_s)
|
|
64
|
+
end
|
|
65
|
+
if Set.instance_method(:each).source_location.nil? && Set.instance_method(:size).source_location.nil?
|
|
66
|
+
SET_EACH = Set.instance_method(:each)
|
|
67
|
+
SET_SIZE = Set.instance_method(:size)
|
|
68
|
+
end
|
|
69
|
+
JAVA_SEND = Java::JavaLang::Object.instance_method(:java_send) if RUBY_PLATFORM.include?('java')
|
|
70
|
+
TIME_MIN = Time.utc(0).freeze
|
|
71
|
+
TIME_MAX = Time.utc(10_000).freeze
|
|
72
|
+
DATE_MIN = Date.new(0, 1, 1).freeze
|
|
73
|
+
DATE_MAX = Date.new(10_000, 1, 1).freeze
|
|
74
|
+
|
|
75
|
+
def initialize(max_depth: DEFAULT_MAX_DEPTH,
|
|
76
|
+
max_array_length: DEFAULT_MAX_ARRAY_LENGTH,
|
|
77
|
+
max_object_properties: DEFAULT_MAX_OBJECT_PROPERTIES,
|
|
78
|
+
max_string_length: DEFAULT_MAX_STRING_LENGTH,
|
|
79
|
+
redact_keys: nil, redact_values: nil)
|
|
80
|
+
@max_depth = limit(max_depth, DEFAULT_MAX_DEPTH, MAX_DEPTH)
|
|
81
|
+
@max_array_length = limit(max_array_length, DEFAULT_MAX_ARRAY_LENGTH, MAX_ITEMS)
|
|
82
|
+
@max_object_properties = limit(max_object_properties, DEFAULT_MAX_OBJECT_PROPERTIES, MAX_ITEMS)
|
|
83
|
+
@max_string_length = limit(max_string_length, DEFAULT_MAX_STRING_LENGTH, MAX_STRING_BYTES)
|
|
84
|
+
@redact_keys_re = compile_regex(redact_keys)
|
|
85
|
+
@redact_values_re = compile_regex(redact_values)
|
|
86
|
+
@capture_lock = Mutex.new
|
|
87
|
+
@capture_context = nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# A supplied visited object is an identity token, never a mutable table.
|
|
91
|
+
# Keep only the active snapshot's context; a different token replaces it.
|
|
92
|
+
# Calls without a token use fresh state without disturbing that context.
|
|
93
|
+
def serialize(obj, path = '$', depth = 0, visited = nil)
|
|
94
|
+
@capture_lock.synchronize do
|
|
95
|
+
shared = !core(:equal, visited, nil)
|
|
96
|
+
if shared && @capture_context && core(:equal, visited, @capture_context[0])
|
|
97
|
+
state = @capture_context[1]
|
|
98
|
+
else
|
|
99
|
+
# Leave room for exhaustion markers while unwinding containers.
|
|
100
|
+
state = { nodes: MAX_NODES, bytes: MAX_BYTES - 4096, visited: {} }
|
|
101
|
+
@capture_context = [visited, state] if shared
|
|
102
|
+
end
|
|
103
|
+
return BUDGET_MARKER if exhausted?(state)
|
|
104
|
+
path = kind?(path, String) ? text(path, state) : '$'
|
|
105
|
+
capture(obj, path, limit(depth, 0, MAX_DEPTH), state)
|
|
106
|
+
end
|
|
107
|
+
rescue SignalException, SystemExit
|
|
108
|
+
raise
|
|
109
|
+
rescue Exception # Snapshot failures must not escape into the client.
|
|
110
|
+
'[Serialization unavailable]'
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.safe_dump_json(data)
|
|
114
|
+
# JSON.generate dispatches to to_json on arbitrary inputs, including
|
|
115
|
+
# singleton methods on strings/containers. Only give it fresh core data.
|
|
116
|
+
new.send(:dump_json, data)
|
|
117
|
+
rescue SignalException, SystemExit
|
|
118
|
+
raise
|
|
119
|
+
rescue Exception
|
|
120
|
+
'{"error":"JSON serialization failed"}'
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def finish_capture
|
|
124
|
+
@capture_lock.synchronize { @capture_context = nil }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def dump_json(data)
|
|
130
|
+
# Captures have already spent their budgets. Allow bounded headroom for
|
|
131
|
+
# frame/watch wrappers and generated summaries/truncation markers, rather
|
|
132
|
+
# than erasing captured fields by applying capture limits a second time.
|
|
133
|
+
@max_depth = MAX_DEPTH * 2
|
|
134
|
+
@max_array_length = @max_object_properties = MAX_NODES * 2
|
|
135
|
+
@max_string_length = MAX_STRING_BYTES * 2
|
|
136
|
+
state = { nodes: MAX_NODES * 4, bytes: MAX_BYTES * 2, visited: {} }
|
|
137
|
+
JSON.generate(capture(data, '$', 0, state))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def core(name, obj, *args, &block)
|
|
141
|
+
CORE.fetch(name).bind(obj).call(*args, &block)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def kind?(obj, klass)
|
|
145
|
+
core(:kind, obj, klass)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def exact?(obj, klass)
|
|
149
|
+
core(:equal, core(:klass, obj), klass)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def limit(value, default, maximum)
|
|
153
|
+
return default unless exact?(value, Integer)
|
|
154
|
+
[[value, 0].max, maximum].min
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def compile_regex(patterns)
|
|
158
|
+
return nil if core(:equal, patterns, nil)
|
|
159
|
+
patterns = [patterns] if kind?(patterns, String)
|
|
160
|
+
return /\A[\s\S]*\z/ unless exact?(patterns, Array)
|
|
161
|
+
count = core(:array_size, patterns)
|
|
162
|
+
return nil if count.zero?
|
|
163
|
+
return /\A[\s\S]*\z/ if count > MAX_PATTERNS
|
|
164
|
+
|
|
165
|
+
sources = []
|
|
166
|
+
count.times do |i|
|
|
167
|
+
pattern = core(:array_get, patterns, i)
|
|
168
|
+
return /\A[\s\S]*\z/ unless kind?(pattern, String)
|
|
169
|
+
return /\A[\s\S]*\z/ if core(:string_size, pattern) > MAX_PATTERN_BYTES
|
|
170
|
+
source = core(:string_slice, pattern, 0, MAX_PATTERN_BYTES).encode('UTF-8', invalid: :replace, undef: :replace).strip
|
|
171
|
+
next if source.empty?
|
|
172
|
+
|
|
173
|
+
# Only literal alternatives with optional anchors and escaped literal
|
|
174
|
+
# punctuation. No repetition, groups, lookaround, backreferences, or
|
|
175
|
+
# character classes: bounded input alone does not prevent ReDoS.
|
|
176
|
+
return /\A[\s\S]*\z/ unless /\A\^?(?:[a-zA-Z0-9 _:@,\/-]|\\[.\^$|?*+()\[\]{}\\-])+(?:\|(?:[a-zA-Z0-9 _:@,\/-]|\\[.\^$|?*+()\[\]{}\\-])+)*\$?\z/.match?(source)
|
|
177
|
+
sources << Regexp.new(source, Regexp::IGNORECASE)
|
|
178
|
+
end
|
|
179
|
+
sources.empty? ? nil : Regexp.union(sources)
|
|
180
|
+
rescue SignalException, SystemExit
|
|
181
|
+
raise
|
|
182
|
+
rescue Exception
|
|
183
|
+
/\A[\s\S]*\z/
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def exhausted?(state)
|
|
187
|
+
state[:nodes] <= 0 || state[:bytes] <= 0
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def emit(value, state)
|
|
191
|
+
return BUDGET_MARKER if state[:bytes] <= 0
|
|
192
|
+
cost = value.bytesize * 6 + 8
|
|
193
|
+
if cost > state[:bytes]
|
|
194
|
+
state[:bytes] = 0
|
|
195
|
+
return BUDGET_MARKER
|
|
196
|
+
end
|
|
197
|
+
# Account conservatively for JSON escaping and container punctuation.
|
|
198
|
+
state[:bytes] -= cost
|
|
199
|
+
value
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def bounded_string(str, offset = 0)
|
|
203
|
+
size = core(:string_size, str) - offset
|
|
204
|
+
raw = core(:string_slice, str, offset, @max_string_length)
|
|
205
|
+
# Older JRuby preserves String subclasses on byteslice; normalize the
|
|
206
|
+
# already-bounded slice before calling encoding methods.
|
|
207
|
+
raw = String.new(raw) if LEGACY_STRING_SLICE
|
|
208
|
+
val = raw.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
|
|
209
|
+
[val, size > @max_string_length, size]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def text(str, state, key: false, offset: 0)
|
|
213
|
+
val, truncated, size = bounded_string(str, offset)
|
|
214
|
+
sensitive = key && @redact_keys_re && (truncated || @redact_keys_re.match?(val))
|
|
215
|
+
if @redact_values_re
|
|
216
|
+
# A cut can split a secret. With value redaction enabled, do not expose
|
|
217
|
+
# any prefix of a string whose remaining bytes we have not examined.
|
|
218
|
+
val = truncated ? '[REDACTED Value]' : val.gsub(@redact_values_re, '[REDACTED Value]')
|
|
219
|
+
end
|
|
220
|
+
if val.bytesize > @max_string_length
|
|
221
|
+
val = val.byteslice(0, @max_string_length).scrub('?')
|
|
222
|
+
truncated = true
|
|
223
|
+
end
|
|
224
|
+
val += "... [Truncated: +#{[size - @max_string_length, 0].max} more bytes]" if truncated
|
|
225
|
+
result = emit(val, state)
|
|
226
|
+
key ? [result, sensitive] : result
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def symbol_text(obj, state)
|
|
230
|
+
return emit('[Symbol]', state) unless SYMBOL_NAME
|
|
231
|
+
text(SYMBOL_NAME.bind(obj).call, state)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def summary(obj, state)
|
|
235
|
+
name = core(:class_name, core(:klass, obj))
|
|
236
|
+
name = kind?(name, String) ? text(name, state) : 'anonymous'
|
|
237
|
+
emit("[Obj: #{name}]", state)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def capture(obj, path, depth, state)
|
|
241
|
+
return BUDGET_MARKER if exhausted?(state)
|
|
242
|
+
state[:nodes] -= 1
|
|
243
|
+
state[:bytes] -= 16
|
|
244
|
+
return nil if core(:equal, obj, nil)
|
|
245
|
+
return true if core(:equal, obj, true)
|
|
246
|
+
return false if core(:equal, obj, false)
|
|
247
|
+
if exact?(obj, Integer)
|
|
248
|
+
return emit('[Integer: too large]', state) if core(:integer_bits, obj) > 256
|
|
249
|
+
state[:bytes] -= 80
|
|
250
|
+
return obj
|
|
251
|
+
end
|
|
252
|
+
if exact?(obj, Float)
|
|
253
|
+
state[:bytes] -= 32
|
|
254
|
+
return obj if core(:float_finite, obj)
|
|
255
|
+
return emit('NaN', state) if core(:float_nan, obj)
|
|
256
|
+
return emit(core(:float_positive, obj) ? 'Infinity' : '-Infinity', state)
|
|
257
|
+
end
|
|
258
|
+
return text(obj, state) if kind?(obj, String)
|
|
259
|
+
return symbol_text(obj, state) if exact?(obj, Symbol)
|
|
260
|
+
if kind?(obj, Proc)
|
|
261
|
+
return emit(core(:proc_lambda, obj) ? '[Function: (lambda)]' : '[Function]', state)
|
|
262
|
+
end
|
|
263
|
+
if kind?(obj, Method) || kind?(obj, UnboundMethod)
|
|
264
|
+
method = kind?(obj, Method) ? :method_name : :unbound_name
|
|
265
|
+
return emit("[Function: #{symbol_text(core(method, obj), state)}]", state)
|
|
266
|
+
end
|
|
267
|
+
if exact?(obj, Time)
|
|
268
|
+
# Formatting an astronomical year can allocate an astronomical string.
|
|
269
|
+
return summary(obj, state) unless core(:time_compare, obj, TIME_MIN) >= 0 && core(:time_compare, obj, TIME_MAX) < 0
|
|
270
|
+
format = core(:time_utc, obj) ? '%Y-%m-%dT%H:%M:%SZ' : '%Y-%m-%dT%H:%M:%S%:z'
|
|
271
|
+
return text(core(:time_format, obj, format), state)
|
|
272
|
+
end
|
|
273
|
+
if exact?(obj, Date) || exact?(obj, DateTime)
|
|
274
|
+
return summary(obj, state) unless core(:date_compare, obj, DATE_MIN) >= 0 && core(:date_compare, obj, DATE_MAX) < 0
|
|
275
|
+
format = exact?(obj, Date) ? '%Y-%m-%d' : '%Y-%m-%dT%H:%M:%S%:z'
|
|
276
|
+
return text(core(:date_format, obj, format), state)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
id = core(:identity, obj)
|
|
280
|
+
return emit("[REF - #{state[:visited][id][1]}]", state) if state[:visited].key?(id)
|
|
281
|
+
if depth > @max_depth
|
|
282
|
+
return emit("[Arr: #{core(:array_size, obj)}]", state) if exact?(obj, Array)
|
|
283
|
+
return emit("[Obj: #{core(:hash_size, obj)} keys]", state) if exact?(obj, Hash)
|
|
284
|
+
return summary(obj, state)
|
|
285
|
+
end
|
|
286
|
+
# Retain bounded references too: object IDs can otherwise be reused by
|
|
287
|
+
# GC between separate roots sharing the same snapshot token.
|
|
288
|
+
state[:visited][id] = [obj, path]
|
|
289
|
+
|
|
290
|
+
if exact?(obj, Array)
|
|
291
|
+
size = core(:array_size, obj)
|
|
292
|
+
return sequence(size, path, depth, state) { |i| core(:array_get, obj, i) }
|
|
293
|
+
end
|
|
294
|
+
if exact?(obj, Set)
|
|
295
|
+
return summary(obj, state) if defined?(MEMORY_SIZE) && MEMORY_SIZE.call(obj) > HASH_STORAGE_BYTES
|
|
296
|
+
result = []
|
|
297
|
+
if defined?(SET_EACH)
|
|
298
|
+
size = SET_SIZE.bind(obj).call
|
|
299
|
+
enumerate = ->(&block) { SET_EACH.bind(obj).call(&block) }
|
|
300
|
+
else
|
|
301
|
+
hash = core(:ivar, obj, :@hash)
|
|
302
|
+
return summary(obj, state) unless exact?(hash, Hash)
|
|
303
|
+
return summary(obj, state) if defined?(MEMORY_SIZE) && MEMORY_SIZE.call(hash) > HASH_STORAGE_BYTES
|
|
304
|
+
size = core(:hash_size, hash)
|
|
305
|
+
enumerate = lambda do |&block|
|
|
306
|
+
core(:hash_each, hash) { |key, _value| block.call(key) }
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
enumerate.call do |key|
|
|
310
|
+
break if result.length >= @max_array_length || exhausted?(state)
|
|
311
|
+
result << capture(key, "#{path}[#{result.length}]", depth + 1, state)
|
|
312
|
+
end
|
|
313
|
+
append_remaining(result, size, state)
|
|
314
|
+
return result
|
|
315
|
+
end
|
|
316
|
+
if exact?(obj, Hash)
|
|
317
|
+
return summary(obj, state) if defined?(MEMORY_SIZE) && MEMORY_SIZE.call(obj) > HASH_STORAGE_BYTES
|
|
318
|
+
return properties(core(:hash_size, obj), path, depth, state) do |&block|
|
|
319
|
+
core(:hash_each, obj, &block)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
if kind?(obj, Struct)
|
|
323
|
+
# Struct#each_pair is native, but some runtimes build all member names.
|
|
324
|
+
size = core(:struct_size, obj)
|
|
325
|
+
return summary(obj, state) if size > MAX_ITEMS
|
|
326
|
+
return properties(size, path, depth, state) { |&block| core(:struct_each, obj, &block) }
|
|
327
|
+
end
|
|
328
|
+
if defined?(OpenStruct) && exact?(obj, OpenStruct)
|
|
329
|
+
table = core(:ivar, obj, :@table)
|
|
330
|
+
return capture(table, path, depth, state) if exact?(table, Hash)
|
|
331
|
+
end
|
|
332
|
+
return java_capture(obj, path, depth, state) if defined?(JAVA_SEND) && kind?(obj, Java::JavaLang::Object)
|
|
333
|
+
|
|
334
|
+
return summary(obj, state) unless kind?(obj, Object)
|
|
335
|
+
# Even bound Exception#to_s calls a message object's to_s on CRuby and
|
|
336
|
+
# JRuby. Keep exceptions and unsupported built-in subclasses opaque.
|
|
337
|
+
if [Array, Hash, Set, Numeric, Exception, Regexp, Range, Time, Date].any? { |type| kind?(obj, type) }
|
|
338
|
+
return summary(obj, state)
|
|
339
|
+
end
|
|
340
|
+
return summary(obj, state) if defined?(OpenStruct) && kind?(obj, OpenStruct)
|
|
341
|
+
return summary(obj, state) if exhausted?(state)
|
|
342
|
+
|
|
343
|
+
# Like Node's own-property-name enumeration, this native operation
|
|
344
|
+
# materializes the full name list. Ruby has no bounded alternative.
|
|
345
|
+
# Only subsequent field reads, recursion, and output are budget-bounded.
|
|
346
|
+
names = core(:ivars, obj)
|
|
347
|
+
size = core(:array_size, names)
|
|
348
|
+
return summary(obj, state) if size.zero?
|
|
349
|
+
properties(size, path, depth, state, ivars: true) do |&block|
|
|
350
|
+
[size, @max_object_properties].min.times do |i|
|
|
351
|
+
break if exhausted?(state)
|
|
352
|
+
name = core(:array_get, names, i)
|
|
353
|
+
block.call(name, core(:ivar, obj, name))
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
rescue SignalException, SystemExit
|
|
357
|
+
raise
|
|
358
|
+
rescue Exception
|
|
359
|
+
emit('[Serialization unavailable]', state)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def sequence(size, path, depth, state)
|
|
363
|
+
result = []
|
|
364
|
+
[size, @max_array_length].min.times do |i|
|
|
365
|
+
break if exhausted?(state)
|
|
366
|
+
result << capture(yield(i), "#{path}[#{i}]", depth + 1, state)
|
|
367
|
+
end
|
|
368
|
+
append_remaining(result, size, state)
|
|
369
|
+
result
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def append_remaining(result, size, state)
|
|
373
|
+
remaining = size - result.length
|
|
374
|
+
result << emit("[+ #{remaining} more items truncated]", state) if remaining > 0
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def properties(size, path, depth, state, ivars: false, &enumerate)
|
|
378
|
+
result = {}
|
|
379
|
+
count = 0
|
|
380
|
+
enumerate.call do |key, value|
|
|
381
|
+
break if count >= @max_object_properties || exhausted?(state)
|
|
382
|
+
count += 1
|
|
383
|
+
state[:nodes] -= 1
|
|
384
|
+
if kind?(key, String)
|
|
385
|
+
name, sensitive = text(key, state, key: true)
|
|
386
|
+
elsif exact?(key, Symbol) && SYMBOL_NAME
|
|
387
|
+
name, sensitive = text(SYMBOL_NAME.bind(key).call, state, key: true, offset: ivars ? 1 : 0)
|
|
388
|
+
elsif exact?(key, Integer) && core(:integer_bits, key) <= 256
|
|
389
|
+
name, sensitive = text(core(:integer_string, key), state, key: true)
|
|
390
|
+
else
|
|
391
|
+
# Never hash, compare, inspect, or stringify application map keys.
|
|
392
|
+
name = emit("[Opaque key #{count}]", state)
|
|
393
|
+
sensitive = !!@redact_keys_re
|
|
394
|
+
end
|
|
395
|
+
child_path = path == '$' ? name : "#{path}.#{name}"
|
|
396
|
+
child_path = child_path.byteslice(0, MAX_STRING_BYTES).scrub('?')
|
|
397
|
+
result[name] = sensitive ? emit('[REDACTED Key]', state) : capture(value, child_path, depth + 1, state)
|
|
398
|
+
end
|
|
399
|
+
result['__probe_meta'] = emit("+ #{size - count} more properties truncated", state) if size > count
|
|
400
|
+
result
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def java_call(obj, method, types = [], *args)
|
|
404
|
+
JAVA_SEND.bind(obj).call(method, types, *args)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def java_capture(obj, path, depth, state)
|
|
408
|
+
# getClass is final. Bound java_send bypasses Ruby singleton overrides;
|
|
409
|
+
# checking the actual Java class excludes Java/Ruby collection subclasses.
|
|
410
|
+
name = java_call(java_call(obj, :getClass), :getName)
|
|
411
|
+
case name
|
|
412
|
+
when 'java.util.ArrayList', 'java.util.LinkedList'
|
|
413
|
+
size = java_call(obj, :size)
|
|
414
|
+
sequence(size, path, depth, state) { |i| java_call(obj, :get, [Java::int], i) }
|
|
415
|
+
when 'java.util.LinkedHashMap'
|
|
416
|
+
size = java_call(obj, :size)
|
|
417
|
+
iterator = java_call(java_call(obj, :entrySet), :iterator)
|
|
418
|
+
properties(size, path, depth, state) do |&block|
|
|
419
|
+
[size, @max_object_properties].min.times do
|
|
420
|
+
break if exhausted?(state) || !java_call(iterator, :hasNext)
|
|
421
|
+
entry = java_call(iterator, :next)
|
|
422
|
+
block.call(java_call(entry, :getKey), java_call(entry, :getValue))
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
when 'java.util.LinkedHashSet'
|
|
426
|
+
size = java_call(obj, :size)
|
|
427
|
+
iterator = java_call(obj, :iterator)
|
|
428
|
+
sequence(size, path, depth, state) { java_call(iterator, :next) }
|
|
429
|
+
else
|
|
430
|
+
# No POJO getters, Throwable accessors, equals, or toString. HashMap
|
|
431
|
+
# and HashSet are opaque too: sparse bucket scans have unbounded work.
|
|
432
|
+
summary(obj, state)
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
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,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'java'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
begin
|
|
7
|
+
require_relative '../../jars/hyperprobe-grpc.jar'
|
|
8
|
+
JavaUtilities.get_proxy_class('co.hyperprobe.transport.GrpcTransport')
|
|
9
|
+
rescue Java::JavaLang::LinkageError => e
|
|
10
|
+
raise LoadError, "Cannot load HyperProbe gRPC transport (Java 11+ required): #{e.message}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module HyperProbe
|
|
14
|
+
module Core
|
|
15
|
+
module Transports
|
|
16
|
+
class JavaGrpc
|
|
17
|
+
class Error < StandardError
|
|
18
|
+
attr_reader :code
|
|
19
|
+
|
|
20
|
+
def initialize(code, message)
|
|
21
|
+
@code = code
|
|
22
|
+
super(message)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(url, service_id, keep_alive)
|
|
27
|
+
raw = url.to_s.strip
|
|
28
|
+
uri = URI.parse(raw.include?('://') ? raw : "http://#{raw}")
|
|
29
|
+
unless %w[http https].include?(uri.scheme) && uri.hostname && uri.port &&
|
|
30
|
+
uri.userinfo.nil? && ['', '/'].include?(uri.path) && uri.query.nil? && uri.fragment.nil?
|
|
31
|
+
raise ArgumentError, 'Broker URL must be http(s)://host:port or host:port'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@client = Java::CoHyperprobeTransport::GrpcTransport.new(uri.hostname, uri.port, uri.scheme == 'https', service_id, keep_alive)
|
|
35
|
+
rescue Java::JavaLang::LinkageError => e
|
|
36
|
+
raise LoadError, "Cannot initialize HyperProbe gRPC transport: #{e.message}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def call(method, payload, deadline)
|
|
40
|
+
bytes = payload.to_java_bytes
|
|
41
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
42
|
+
raise Error.new(4, 'DEADLINE_EXCEEDED') unless remaining.positive?
|
|
43
|
+
|
|
44
|
+
# Saturate before conversion to Java long; never overflow into an unlimited call.
|
|
45
|
+
nanos = [(remaining * 1_000_000_000).to_i, 9_223_372_036_854_775_807].min
|
|
46
|
+
String.from_java_bytes(@client.call(method, bytes, nanos))
|
|
47
|
+
rescue Java::CoHyperprobeTransport::GrpcTransport::RpcException => e
|
|
48
|
+
raise Error.new(e.code, e.message)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def shutdown
|
|
52
|
+
@client.shutdown
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
Binary file
|