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.
- checksums.yaml +4 -4
- data/README.md +316 -87
- data/lib/hyperprobe/agent.rb +243 -144
- data/lib/hyperprobe/core/broker.rb +58 -88
- data/lib/hyperprobe/core/evaluator.rb +358 -68
- data/lib/hyperprobe/core/lexical_scope.rb +222 -0
- data/lib/hyperprobe/core/logger.rb +33 -12
- data/lib/hyperprobe/core/monitoring_engine.rb +294 -120
- data/lib/hyperprobe/core/safe_ast_validator.rb +140 -61
- data/lib/hyperprobe/core/safety.rb +92 -15
- data/lib/hyperprobe/core/serializer.rb +354 -342
- data/lib/hyperprobe/core/transports/java_grpc.rb +57 -0
- data/lib/hyperprobe/lambda.rb +44 -42
- data/lib/hyperprobe/protos/agent_descriptor.rb +7 -0
- data/lib/hyperprobe/protos/java_messages.rb +199 -0
- data/lib/hyperprobe/protos.rb +7 -7
- data/lib/hyperprobe/railtie.rb +6 -0
- data/lib/hyperprobe/version.rb +1 -1
- data/lib/hyperprobe.rb +149 -20
- metadata +25 -9
|
@@ -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
|
data/lib/hyperprobe/lambda.rb
CHANGED
|
@@ -16,62 +16,58 @@ module HyperProbe
|
|
|
16
16
|
|
|
17
17
|
# Ephemeral Lambda wrapper
|
|
18
18
|
lambda do |event:, context:|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
48
|
+
handler.call(event, context)
|
|
53
49
|
end
|
|
54
50
|
ensure
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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
|
data/lib/hyperprobe/protos.rb
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
#
|
|
12
|
+
# Environments without the C-grpc gem can still use the messages.
|
|
13
13
|
end
|
|
14
14
|
end
|
data/lib/hyperprobe/railtie.rb
CHANGED
data/lib/hyperprobe/version.rb
CHANGED
data/lib/hyperprobe.rb
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require_relative 'hyperprobe/
|
|
5
|
-
require_relative 'hyperprobe/
|
|
6
|
-
require_relative 'hyperprobe/
|
|
7
|
-
require_relative 'hyperprobe/
|
|
8
|
-
require_relative 'hyperprobe/
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
@
|
|
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
|
-
@
|
|
113
|
-
|
|
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
|
|
202
|
+
return Java::OrgJruby::RubyInstanceConfig.FULL_TRACE_ENABLED == true
|
|
129
203
|
rescue StandardError, ScriptError, NameError
|
|
130
204
|
end
|
|
131
205
|
|
|
132
|
-
|
|
133
|
-
|
|
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
|