hyperprobe-agent 1.2.27.pre.1 → 1.2.27.pre.3

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,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
@@ -16,62 +16,58 @@ module HyperProbe
16
16
 
17
17
  # Ephemeral Lambda wrapper
18
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}"
19
+ agent = nil
20
+ begin
21
+ HyperProbe.start(options.merge(is_lambda: true)) unless HyperProbe.instance
22
+ agent = HyperProbe.instance
23
+ agent = nil if agent && agent.agent_disabled?
24
+ if agent
25
+ remaining_ms = context.respond_to?(:get_remaining_time_in_millis) ? context.get_remaining_time_in_millis : nil
26
+ sync_timeout_ms = options[:lambda_sync_timeout_ms] || 2000
27
+ if remaining_ms
28
+ dynamic_budget = (remaining_ms * 0.2).to_i
29
+ sync_timeout_ms = dynamic_budget < 100 ? 0 : [sync_timeout_ms, dynamic_budget].min
44
30
  end
31
+ agent.force_sync(sync_timeout_ms / 1000.0) if sync_timeout_ms.positive?
45
32
  end
33
+ rescue SignalException, SystemExit
34
+ raise
35
+ rescue Exception
36
+ # This boundary never encloses application handler execution.
46
37
  end
47
38
 
48
39
  begin
49
- if handler.parameters.length == 1
40
+ params = handler.parameters
41
+ has_keywords = params.any? { |type, _| type == :keyreq || type == :key || type == :keyrest }
42
+
43
+ if has_keywords
44
+ handler.call(event: event, context: context)
45
+ elsif params.length == 1 && (params.first[0] == :req || params.first[0] == :opt)
50
46
  handler.call(event)
51
47
  else
52
- handler.call(event: event, context: context)
48
+ handler.call(event, context)
53
49
  end
54
50
  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
51
+ begin
52
+ if agent
53
+ remaining_ms = context.respond_to?(:get_remaining_time_in_millis) ? context.get_remaining_time_in_millis : nil
54
+ flush_timeout_ms = options[:flush_timeout_ms] || 1500
55
+ if remaining_ms.nil? || remaining_ms > 500
56
+ flush_timeout_ms = [remaining_ms - 200, flush_timeout_ms].min if remaining_ms
65
57
  agent.force_flush(flush_timeout_ms / 1000.0)
66
- rescue StandardError => e
67
- warn "[HyperProbe Lambda] Failed to flush telemetry: #{e.message}"
68
58
  end
69
- else
70
- warn '[HyperProbe Lambda] Aborting snapshot flush to prevent Lambda timeout.'
71
59
  end
60
+ rescue SignalException, SystemExit
61
+ raise
62
+ rescue Exception
63
+ # Never mask a handler exception with instrumentation cleanup.
72
64
  end
73
65
  end
74
66
  end
67
+ rescue SignalException, SystemExit
68
+ raise
69
+ rescue Exception
70
+ handler
75
71
  end
76
72
 
77
73
  def stop
@@ -79,11 +75,17 @@ module HyperProbe
79
75
  if agent && !agent.agent_disabled?
80
76
  begin
81
77
  agent.force_flush(1.0)
82
- rescue StandardError
78
+ rescue SignalException, SystemExit
79
+ raise
80
+ rescue Exception
83
81
  # Best effort
84
82
  end
85
83
  end
86
84
  HyperProbe.shutdown
85
+ rescue SignalException, SystemExit
86
+ raise
87
+ rescue Exception
88
+ nil
87
89
  end
88
90
  end
89
91
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ # Generated by rake proto:descriptor. DO NOT EDIT.
3
+ module Hyperprobe
4
+ module JavaProto
5
+ DESCRIPTOR_DATA = "\n\x0b\x61gent.proto\x12\x13hyperprobe.agent.v1\"\x9c\x01\n\x10GetProbesRequest\x12\x10\n\x08\x61gent_id\x18\x01 \x01(\t\x12\x12\n\nservice_id\x18\x02 \x01(\t\x12\x13\n\x0b\x65nvironment\x18\x03 \x01(\t\x12\x12\n\ncommit_sha\x18\x04 \x01(\t\x12\x10\n\x08language\x18\x05 \x01(\t\x12\x15\n\ragent_version\x18\x06 \x01(\t\x12\x10\n\x08hostname\x18\x07 \x01(\t\"\xbc\x01\n\x11GetProbesResponse\x12*\n\x06probes\x18\x01 \x03(\x0b\x32\x1a.hyperprobe.agent.v1.Probe\x12\"\n\x15next_sync_interval_ms\x18\x02 \x01(\x05H\x00\x88\x01\x01\x12=\n\rglobal_config\x18\x03 \x01(\x0b\x32&.hyperprobe.agent.v1.AgentGlobalConfigB\x18\n\x16_next_sync_interval_ms\"U\n\x16SourceMapStatusRequest\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\x13\n\x0b\x65nvironment\x18\x02 \x01(\t\x12\x12\n\ncommit_sha\x18\x03 \x01(\t\"C\n\x17SourceMapStatusResponse\x12\x13\n\x0bis_uploader\x18\x01 \x01(\x08\x12\x13\n\x0bis_complete\x18\x02 \x01(\x08\"n\n\x0eSourceMapChunk\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\x13\n\x0b\x65nvironment\x18\x02 \x01(\t\x12\x12\n\ncommit_sha\x18\x03 \x01(\t\x12\x11\n\tfile_name\x18\x04 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x05 \x01(\x0c\"!\n\x0eUploadResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"[\n\x1cMarkSourceMapCompleteRequest\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\x13\n\x0b\x65nvironment\x18\x02 \x01(\t\x12\x12\n\ncommit_sha\x18\x03 \x01(\t\"0\n\x1dMarkSourceMapCompleteResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"\x85\x03\n\x11\x41gentGlobalConfig\x12\x13\n\x0bredact_keys\x18\x01 \x03(\t\x12\x15\n\rredact_values\x18\x02 \x03(\t\x12\x1d\n\x10max_object_depth\x18\x03 \x01(\x05H\x00\x88\x01\x01\x12\x1d\n\x10max_array_length\x18\x04 \x01(\x05H\x01\x88\x01\x01\x12\x1e\n\x11stack_frame_depth\x18\x05 \x01(\x05H\x02\x88\x01\x01\x12\"\n\x15max_object_properties\x18\x06 \x01(\x05H\x03\x88\x01\x01\x12\x1e\n\x11max_string_length\x18\x07 \x01(\x05H\x04\x88\x01\x01\x12\x1d\n\x10\x63\x61pture_closures\x18\x08 \x01(\x08H\x05\x88\x01\x01\x42\x13\n\x11_max_object_depthB\x13\n\x11_max_array_lengthB\x14\n\x12_stack_frame_depthB\x18\n\x16_max_object_propertiesB\x14\n\x12_max_string_lengthB\x13\n\x11_capture_closures\"\x91\x07\n\x05Probe\x12\n\n\x02id\x18\x01 \x01(\t\x12,\n\x04type\x18\x02 \x01(\x0e\x32\x1e.hyperprobe.agent.v1.ProbeType\x12\x18\n\x10runtime_location\x18\x03 \x01(\t\x12\x14\n\x0cruntime_line\x18\x04 \x01(\x05\x12\"\n\x1asecondary_runtime_location\x18\x05 \x01(\t\x12\x1e\n\x16secondary_runtime_line\x18\x06 \x01(\x05\x12\x11\n\tcondition\x18\x07 \x01(\t\x12\x10\n\x08template\x18\x08 \x01(\t\x12\x13\n\x0bmetric_name\x18\t \x01(\t\x12\x19\n\x11metric_expression\x18\n \x01(\t\x12\x19\n\x11watch_expressions\x18\x0b \x03(\t\x12\x11\n\thit_limit\x18\x0c \x01(\x05\x12\x13\n\x0b\x65xpiry_time\x18\r \x01(\x03\x12\x16\n\x0eruntime_column\x18\x0e \x01(\x05\x12 \n\x18secondary_runtime_column\x18\x0f \x01(\x05\x12\x1e\n\x16\x63orrelation_expression\x18\x10 \x01(\t\x12\x1d\n\x10max_object_depth\x18\x11 \x01(\x05H\x00\x88\x01\x01\x12\x1d\n\x10max_array_length\x18\x12 \x01(\x05H\x01\x88\x01\x01\x12\x1e\n\x11stack_frame_depth\x18\x13 \x01(\x05H\x02\x88\x01\x01\x12\"\n\x15max_object_properties\x18\x14 \x01(\x05H\x03\x88\x01\x01\x12\x1e\n\x11max_string_length\x18\x15 \x01(\x05H\x04\x88\x01\x01\x12$\n\x17should_capture_trace_id\x18\x16 \x01(\x08H\x05\x88\x01\x01\x12\x16\n\tlog_level\x18\x17 \x01(\tH\x06\x88\x01\x01\x12!\n\x14should_log_to_stdout\x18\x18 \x01(\x08H\x07\x88\x01\x01\x12\x1d\n\x10\x63\x61pture_closures\x18\x19 \x01(\x08H\x08\x88\x01\x01\x42\x13\n\x11_max_object_depthB\x13\n\x11_max_array_lengthB\x14\n\x12_stack_frame_depthB\x18\n\x16_max_object_propertiesB\x14\n\x12_max_string_lengthB\x1a\n\x18_should_capture_trace_idB\x0c\n\n_log_levelB\x17\n\x15_should_log_to_stdoutB\x13\n\x11_capture_closures\"W\n\x0eTelemetryBatch\x12\x10\n\x08\x61gent_id\x18\x01 \x01(\t\x12\x33\n\x06\x65vents\x18\x02 \x03(\x0b\x32#.hyperprobe.agent.v1.TelemetryEvent\"\x8a\x01\n\nStackFrame\x12\x15\n\rfunction_name\x18\x01 \x01(\t\x12\x11\n\tfile_name\x18\x02 \x01(\t\x12\x13\n\x0bline_number\x18\x03 \x01(\x05\x12\x15\n\rcolumn_number\x18\x04 \x01(\x05\x12\x17\n\nclass_name\x18\x05 \x01(\tH\x00\x88\x01\x01\x42\r\n\x0b_class_name\"\x8f\x02\n\x0eTelemetryEvent\x12\x10\n\x08probe_id\x18\x01 \x01(\t\x12\x14\n\x0ctimestamp_ms\x18\x02 \x01(\x03\x12\x35\n\x0cstack_frames\x18\x03 \x03(\x0b\x32\x1f.hyperprobe.agent.v1.StackFrame\x12\x1a\n\x12\x63\x61ptured_vars_json\x18\x04 \x01(\t\x12\x1a\n\x12watch_results_json\x18\x05 \x01(\t\x12\x15\n\revaluated_log\x18\x06 \x01(\t\x12\x14\n\x0cmetric_value\x18\x07 \x01(\x01\x12\x15\n\rcapture_error\x18\x08 \x01(\t\x12\x15\n\x08trace_id\x18\t \x01(\tH\x00\x88\x01\x01\x42\x0b\n\t_trace_id\"/\n\x11TelemetryResponse\x12\x1a\n\x12\x66inished_probe_ids\x18\x01 \x03(\t*\x9c\x01\n\tProbeType\x12\x1a\n\x16PROBE_TYPE_UNSPECIFIED\x10\x00\x12\x17\n\x13PROBE_TYPE_SNAPSHOT\x10\x01\x12\x12\n\x0ePROBE_TYPE_LOG\x10\x02\x12\x16\n\x12PROBE_TYPE_COUNTER\x10\x03\x12\x15\n\x11PROBE_TYPE_METRIC\x10\x04\x12\x17\n\x13PROBE_TYPE_DURATION\x10\x05\x32\x99\x04\n\x0b\x41gentBroker\x12Z\n\tGetProbes\x12%.hyperprobe.agent.v1.GetProbesRequest\x1a&.hyperprobe.agent.v1.GetProbesResponse\x12^\n\x0fReportTelemetry\x12#.hyperprobe.agent.v1.TelemetryBatch\x1a&.hyperprobe.agent.v1.TelemetryResponse\x12o\n\x12GetSourceMapStatus\x12+.hyperprobe.agent.v1.SourceMapStatusRequest\x1a,.hyperprobe.agent.v1.SourceMapStatusResponse\x12]\n\x0fUploadSourceMap\x12#.hyperprobe.agent.v1.SourceMapChunk\x1a#.hyperprobe.agent.v1.UploadResponse(\x01\x12~\n\x15MarkSourceMapComplete\x12\x31.hyperprobe.agent.v1.MarkSourceMapCompleteRequest\x1a\x32.hyperprobe.agent.v1.MarkSourceMapCompleteResponseB\tB\x05\x41gentP\x00\x62\x06proto3".b.freeze
6
+ end
7
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'java'
4
+ require_relative 'agent_descriptor'
5
+
6
+ begin
7
+ require_relative '../jars/hyperprobe-grpc.jar'
8
+ JavaUtilities.get_proxy_class('co.hyperprobe.transport.ProtoCodec')
9
+ rescue Java::JavaLang::LinkageError => e
10
+ raise LoadError, "Cannot load HyperProbe protobuf codec (Java 11+ required): #{e.message}"
11
+ end
12
+
13
+ module Hyperprobe
14
+ module Agent
15
+ module V1
16
+ end
17
+ end
18
+
19
+ # Only the SDK's canonical messages are exposed, not Google's Ruby runtime API.
20
+ module JavaProto
21
+ CODEC = JavaUtilities.get_proxy_class('co.hyperprobe.transport.ProtoCodec').new(DESCRIPTOR_DATA.to_java_bytes)
22
+ CLASSES = {}
23
+
24
+ class Message
25
+ class << self
26
+ attr_accessor :descriptor, :fields
27
+
28
+ def decode(bytes)
29
+ raise TypeError, 'Expected protobuf bytes as a String' unless bytes.is_a?(String)
30
+ raise ArgumentError, 'Protobuf payload exceeds 4 MiB' if bytes.bytesize > 4 * 1024 * 1024
31
+
32
+ from_java(CODEC.decode(descriptor, bytes.to_java_bytes))
33
+ rescue Java::JavaLang::Exception => e
34
+ raise ArgumentError, "Invalid protobuf: #{e.message}"
35
+ end
36
+
37
+ def encode(message)
38
+ raise TypeError, "Expected #{self}" unless message.is_a?(self)
39
+
40
+ message.to_proto
41
+ end
42
+
43
+ def from_java(message)
44
+ result = new
45
+ message.getAllFields.each do |field, value|
46
+ decoded = field.isRepeated ? value.map { |item| decode_value(field, item) } : decode_value(field, value)
47
+ result.public_send("#{field.getName}=", decoded)
48
+ end
49
+ result.instance_variable_set(:@unknown_fields, message.getUnknownFields)
50
+ result
51
+ end
52
+
53
+ def decode_value(field, value)
54
+ case field.getJavaType.to_s
55
+ when 'MESSAGE' then CLASSES.fetch(field.getMessageType.getFullName).from_java(value)
56
+ when 'ENUM' then value.getIndex < 0 ? value.getNumber : value.getName.to_sym
57
+ when 'BYTE_STRING' then String.from_java_bytes(value.toByteArray)
58
+ else value
59
+ end
60
+ end
61
+
62
+ def normalize(field, value)
63
+ case field.getJavaType.to_s
64
+ when 'MESSAGE'
65
+ klass = CLASSES.fetch(field.getMessageType.getFullName)
66
+ value = klass.new(value) if value.is_a?(Hash)
67
+ raise TypeError, "Expected #{klass} for #{field.getName}" unless value.is_a?(klass)
68
+ when 'ENUM'
69
+ enum = field.getEnumType
70
+ if value.is_a?(Symbol)
71
+ entry = enum.findValueByName(value.to_s)
72
+ raise ArgumentError, "Unknown enum #{value}" unless entry
73
+ return value
74
+ end
75
+ integer!(field, value, 32)
76
+ entry = enum.findValueByNumber(value)
77
+ value = entry.getName.to_sym if entry
78
+ when 'INT', 'LONG'
79
+ integer!(field, value, field.getJavaType.to_s == 'INT' ? 32 : 64)
80
+ when 'DOUBLE', 'FLOAT'
81
+ raise TypeError, "Expected number for #{field.getName}" unless value.is_a?(Numeric) && !value.is_a?(Complex)
82
+ value = value.to_f
83
+ when 'BOOLEAN'
84
+ raise TypeError, "Expected boolean for #{field.getName}" unless value.equal?(true) || value.equal?(false)
85
+ when 'STRING', 'BYTE_STRING'
86
+ raise TypeError, "Expected String for #{field.getName}" unless value.is_a?(String)
87
+ value = field.getJavaType.to_s == 'STRING' ? value.encode(Encoding::UTF_8) : value.b
88
+ raise ArgumentError, 'Invalid UTF-8 protobuf string' unless value.valid_encoding?
89
+ value = value.dup.freeze
90
+ else
91
+ raise ArgumentError, "Unsupported canonical field #{field.getName}"
92
+ end
93
+ value
94
+ end
95
+
96
+ def integer!(field, value, bits)
97
+ raise TypeError, "Expected Integer for #{field.getName}" unless value.is_a?(Integer)
98
+ raise RangeError, "Out of range for #{field.getName}" unless value >= -(2**(bits - 1)) && value < 2**(bits - 1)
99
+ end
100
+
101
+ def java_value(field, value)
102
+ case field.getJavaType.to_s
103
+ when 'MESSAGE' then value.to_java_message
104
+ when 'ENUM'
105
+ enum = field.getEnumType
106
+ value.is_a?(Symbol) ? enum.findValueByName(value.to_s) : enum.findValueByNumberCreatingIfUnknown(value)
107
+ when 'BYTE_STRING' then CODEC.bytes(value.to_java_bytes)
108
+ when 'STRING' then value.to_java(:string)
109
+ when 'INT' then value.to_java(:int)
110
+ when 'LONG' then value.to_java(:long)
111
+ when 'FLOAT' then value.to_java(:float)
112
+ when 'DOUBLE' then value.to_java(:double)
113
+ when 'BOOLEAN' then value.to_java(:boolean)
114
+ end
115
+ end
116
+ end
117
+
118
+ def initialize(values = {})
119
+ raise ArgumentError, 'Expected a field Hash' unless values.is_a?(Hash)
120
+
121
+ @values = {}
122
+ values.each do |name, value|
123
+ key = name.to_s
124
+ raise ArgumentError, "Unknown field #{key}" unless self.class.fields.key?(key)
125
+ public_send("#{key}=", value) unless value.nil?
126
+ end
127
+ end
128
+
129
+ def to_java_message
130
+ builder = CODEC.builder(self.class.descriptor)
131
+ builder.mergeUnknownFields(@unknown_fields) if @unknown_fields
132
+ @values.each do |name, value|
133
+ field = self.class.fields.fetch(name)
134
+ if field.isRepeated
135
+ # Arrays remain mutable; scalar setters already normalize and freeze strings.
136
+ value.each { |item| builder.addRepeatedField(field, self.class.java_value(field, self.class.normalize(field, item))) }
137
+ else
138
+ builder.setField(field, self.class.java_value(field, value))
139
+ end
140
+ end
141
+ builder.build
142
+ end
143
+
144
+ def to_proto
145
+ String.from_java_bytes(to_java_message.toByteArray)
146
+ end
147
+
148
+ def serialized_size
149
+ to_java_message.getSerializedSize
150
+ end
151
+
152
+ def ==(other)
153
+ other.instance_of?(self.class) && to_java_message == other.to_java_message
154
+ end
155
+ alias eql? ==
156
+
157
+ def hash
158
+ to_java_message.hashCode
159
+ end
160
+ end
161
+
162
+ CODEC.getFile.getEnumTypes.each do |enum|
163
+ mod = Module.new
164
+ enum.getValues.each { |value| mod.const_set(value.getName, value.getNumber) }
165
+ Agent::V1.const_set(enum.getName, mod)
166
+ end
167
+
168
+ CODEC.getFile.getMessageTypes.each do |descriptor|
169
+ klass = Class.new(Message)
170
+ klass.descriptor = descriptor
171
+ klass.fields = descriptor.getFields.each_with_object({}) { |field, fields| fields[field.getName] = field }.freeze
172
+ CLASSES[descriptor.getFullName] = klass
173
+ Agent::V1.const_set(descriptor.getName, klass)
174
+ klass.fields.each do |name, field|
175
+ klass.define_method(name) do
176
+ return @values[name] if @values.key?(name)
177
+ return @values[name] = [] if field.isRepeated
178
+ return nil if field.getJavaType.to_s == 'MESSAGE'
179
+
180
+ self.class.decode_value(field, field.getDefaultValue)
181
+ end
182
+ klass.define_method("#{name}=") do |value|
183
+ if value.nil? && field.hasPresence
184
+ @values.delete(name)
185
+ elsif field.isRepeated
186
+ raise TypeError, "Expected Array for #{name}" unless value.is_a?(Array)
187
+ @values[name] = value.map { |item| self.class.normalize(field, item) }
188
+ else
189
+ @values[name] = self.class.normalize(field, value)
190
+ end
191
+ end
192
+ if field.hasPresence
193
+ klass.define_method("has_#{name}?") { @values.key?(name) }
194
+ end
195
+ end
196
+ end
197
+ CLASSES.freeze
198
+ end
199
+ end
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- protos_dir = File.expand_path('protos', __dir__)
4
- $LOAD_PATH.unshift(protos_dir) unless $LOAD_PATH.include?(protos_dir)
5
-
6
- require 'agent_pb'
7
-
8
- if RUBY_PLATFORM !~ /java/
3
+ if RUBY_PLATFORM =~ /java/
4
+ require_relative 'protos/java_messages'
5
+ else
6
+ protos_dir = File.expand_path('protos', __dir__)
7
+ $LOAD_PATH.unshift(protos_dir) unless $LOAD_PATH.include?(protos_dir)
8
+ require 'agent_pb'
9
9
  begin
10
10
  require 'agent_services_pb'
11
11
  rescue LoadError
12
- # JRuby or environments without C-grpc gem
12
+ # Environments without the C-grpc gem can still use the messages.
13
13
  end
14
14
  end
@@ -10,6 +10,12 @@ module HyperProbe
10
10
  if enabled && has_service_id
11
11
  HyperProbe.start
12
12
  end
13
+
14
+ if defined?(::Spring) && ::Spring.respond_to?(:after_fork)
15
+ ::Spring.after_fork do
16
+ HyperProbe.after_fork
17
+ end
18
+ end
13
19
  end
14
20
  end
15
21
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HyperProbe
4
- VERSION = '1.2.27-1'
4
+ VERSION = '1.2.27.pre.3'
5
5
  end
data/lib/hyperprobe.rb CHANGED
@@ -1,23 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'hyperprobe/version'
4
- require_relative 'hyperprobe/protos'
5
- require_relative 'hyperprobe/core/logger'
6
- require_relative 'hyperprobe/agent'
7
- require_relative 'hyperprobe/lambda'
8
- require_relative 'hyperprobe/railtie' if defined?(::Rails::Railtie)
3
+ begin
4
+ require_relative 'hyperprobe/version'
5
+ require_relative 'hyperprobe/protos'
6
+ require_relative 'hyperprobe/core/logger'
7
+ require_relative 'hyperprobe/agent'
8
+ require_relative 'hyperprobe/lambda'
9
+ require_relative 'hyperprobe/railtie' if defined?(::Rails::Railtie)
10
+ rescue SignalException, SystemExit
11
+ raise
12
+ rescue Exception
13
+ # Missing native dependencies or a broken JVM transport disable only the SDK.
14
+ module HyperProbe
15
+ LOAD_FAILED = true
16
+ end
17
+ end
9
18
 
10
19
  module HyperProbe
11
20
  @instance = nil
12
21
  @mutex = Mutex.new
22
+ @owner_pid = Process.pid
13
23
 
14
24
  class << self
15
25
  def logger(namespace = 'hyperprobe:agent')
16
26
  Core::Logger.get_logger(namespace)
17
27
  end
18
28
  def start(options = {})
19
- @mutex.synchronize do
29
+ reset_after_fork
30
+ return nil if defined?(LOAD_FAILED) || @fork_disabled
31
+ return nil unless @mutex.try_lock
32
+ locked = true
33
+ begin
20
34
  return @instance if @instance
35
+ @stop_requested = false
21
36
 
22
37
  if ENV['HYPERPROBE_DISABLED']&.strip&.upcase == 'YES'
23
38
  puts '[HyperProbe] Explicitly disabled via ENV["HYPERPROBE_DISABLED"].'
@@ -60,7 +75,7 @@ module HyperProbe
60
75
  raw_jruby_ver = defined?(JRUBY_VERSION) ? JRUBY_VERSION.to_s : '0'
61
76
  jruby_ver = Gem::Version.new(raw_jruby_ver)
62
77
  if jruby_ver < Gem::Version.new('9.3.0.0')
63
- warn "\e[1m\e[33m⚠️ [HyperProbe] Unsupported JRuby version (#{raw_jruby_ver}). HyperProbe requires JRuby 9.3+ or 9.4+.\e[0m"
78
+ warn "[HyperProbe] Unsupported JRuby version (#{raw_jruby_ver}); JRuby 9.3+ is required."
64
79
  return nil
65
80
  end
66
81
 
@@ -69,6 +84,11 @@ module HyperProbe
69
84
  return nil
70
85
  end
71
86
 
87
+ unless jruby_scope_preservation_enabled?
88
+ warn '[HyperProbe] Agent disabled: compiled JRuby probes require -J-Djruby.ir.passes=AddCallProtocolInstructions,AddMissingInitsPass and -J-Djruby.ir.jit.passes=AddCallProtocolInstructions,AddMissingInitsPass at process startup. These preserve locals with JIT enabled; benchmark their application-wide cost before deployment.'
89
+ return nil
90
+ end
91
+
72
92
  begin
73
93
  require 'java'
74
94
  java_ver_str = java.lang.System.getProperty('java.specification.version') || '11'
@@ -88,49 +108,111 @@ module HyperProbe
88
108
  end
89
109
  end
90
110
 
91
- @instance = Agent.new(opts)
111
+ if opts.delete(:defer_start) || opts.delete(:deferStart)
112
+ @deferred_options = opts
113
+ return nil
114
+ end
115
+ @native_owner_pid = Process.pid
116
+ candidate = Agent.new(opts)
117
+ return nil if candidate.is_shutdown
118
+ if @stop_requested
119
+ candidate.shutdown
120
+ return nil
121
+ end
122
+ @instance = candidate
123
+ puts "[HyperProbe] Ruby agent successfully started (Version: #{VERSION}, ID: #{@instance.agent_id}, PID: #{@instance.owner_pid})."
92
124
  @instance
125
+ ensure
126
+ @mutex.unlock if locked
93
127
  end
128
+ rescue SignalException, SystemExit
129
+ candidate&.shutdown
130
+ raise
131
+ rescue Exception
132
+ nil
94
133
  end
95
134
 
96
135
  def shutdown
97
- @mutex.synchronize do
98
- return unless @instance
99
-
100
- @instance.shutdown
136
+ reset_after_fork
137
+ @stop_requested = true
138
+ unless @mutex.try_lock
139
+ defer_shutdown
140
+ return nil
141
+ end
142
+ begin
143
+ inst = @instance
101
144
  @instance = nil
145
+ ensure
146
+ @mutex.unlock
102
147
  end
148
+ return unless inst
149
+ inst.shutdown
150
+ puts '[HyperProbe] Ruby agent successfully shut down.'
151
+ rescue ThreadError
152
+ defer_shutdown
153
+ nil
154
+ rescue SignalException, SystemExit
155
+ raise
156
+ rescue Exception
157
+ nil
103
158
  end
104
159
 
105
160
  def instance
106
- @mutex.synchronize { @instance }
161
+ @instance
107
162
  end
108
163
 
109
164
  def after_fork
110
165
  return unless Process.respond_to?(:fork) && !defined?(JRUBY_VERSION)
111
166
 
112
- @mutex.synchronize do
113
- @instance&.after_fork
167
+ return @instance if @owner_pid == Process.pid
168
+ reset_after_fork
169
+ if @fork_disabled
170
+ warn '[HyperProbe] Inherited agent disabled. Use defer_start: true before preloading and after_fork in each worker.'
171
+ return nil
114
172
  end
173
+ options = @deferred_options
174
+ @deferred_options = nil
175
+ start(options) if options
176
+ rescue SignalException, SystemExit
177
+ raise
178
+ rescue Exception
179
+ nil
115
180
  end
116
181
 
117
182
  def wrap_lambda(options = {}, &handler)
183
+ return handler if defined?(LOAD_FAILED)
118
184
  Lambda.wrap(options, &handler)
185
+ rescue SignalException, SystemExit
186
+ raise
187
+ rescue Exception
188
+ handler
119
189
  end
120
190
 
121
191
  def stop_lambda
122
- Lambda.stop
192
+ Lambda.stop if defined?(Lambda)
193
+ rescue SignalException, SystemExit
194
+ raise
195
+ rescue Exception
196
+ nil
123
197
  end
124
198
 
125
199
  def jruby_debug_enabled?
126
200
  begin
127
201
  require 'java'
128
- return true if Java::OrgJrubyUtilCli::Options::DEBUG_FULLTRACE.load == true
202
+ return Java::OrgJruby::RubyInstanceConfig.FULL_TRACE_ENABLED == true
129
203
  rescue StandardError, ScriptError, NameError
130
204
  end
131
205
 
132
- opts = ENV['JRUBY_OPTS'] || ''
133
- opts.include?('--debug') || opts.include?('-Xdebug.fullTrace=true')
206
+ false
207
+ end
208
+
209
+ def jruby_scope_preservation_enabled?
210
+ require 'java'
211
+ required = 'AddCallProtocolInstructions,AddMissingInitsPass'
212
+ Java::OrgJrubyUtilCli::Options::IR_COMPILER_PASSES.load.to_s == required &&
213
+ Java::OrgJrubyUtilCli::Options::IR_JIT_PASSES.load.to_s == required
214
+ rescue StandardError, ScriptError
215
+ false
134
216
  end
135
217
 
136
218
  def valid_broker_url?(url)
@@ -149,5 +231,52 @@ module HyperProbe
149
231
  # Raw gRPC target
150
232
  !raw.include?('://') && !raw.include?('/') && !raw.include?('?')
151
233
  end
234
+
235
+ private
236
+
237
+ def defer_shutdown
238
+ Thread.new do
239
+ inst = @mutex.synchronize do
240
+ current = @instance
241
+ @instance = nil
242
+ current
243
+ end
244
+ inst&.shutdown
245
+ rescue Exception
246
+ # Cleanup waits only on an agent worker, never on an application thread.
247
+ nil
248
+ end
249
+ end
250
+
251
+ def reset_after_fork
252
+ return if @owner_pid == Process.pid
253
+ @owner_pid = Process.pid
254
+ @mutex = Mutex.new
255
+ if @native_owner_pid
256
+ @fork_disabled = true
257
+ @instance&.after_fork
258
+ @instance = nil
259
+ end
260
+ end
261
+
262
+ def puts(message)
263
+ notice($stdout, message)
264
+ end
265
+
266
+ def warn(message)
267
+ notice($stderr, message)
268
+ end
269
+
270
+ def notice(stream, message)
271
+ if IO === stream
272
+ IO.instance_method(:write_nonblock).bind(stream).call("#{message}\n", exception: false)
273
+ elsif defined?(StringIO) && StringIO === stream
274
+ StringIO.instance_method(:write).bind(stream).call("#{message}\n")
275
+ end
276
+ rescue SignalException, SystemExit
277
+ raise
278
+ rescue Exception
279
+ nil
280
+ end
152
281
  end
153
282
  end