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.
@@ -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