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,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ripper'
|
|
4
|
+
|
|
5
|
+
module HyperProbe
|
|
6
|
+
module Core
|
|
7
|
+
# Installation-time analysis only. Retain the result on the probe, not a
|
|
8
|
+
# binding. nil means unknown: callers must omit locals, not fall back to all.
|
|
9
|
+
# Tokenless lines and lines shared by different scopes are intentionally
|
|
10
|
+
# unknown. Source must still match the code loaded by the application.
|
|
11
|
+
# Parsers with incomplete lambda ASTs (including JRuby 10.1.1's arrow
|
|
12
|
+
# lambdas) fail closed; ordinary lambda/proc blocks remain supported.
|
|
13
|
+
class LexicalScope
|
|
14
|
+
MAX_SOURCE_BYTES = 512 * 1024
|
|
15
|
+
MAX_NODES = 20_000
|
|
16
|
+
MAX_DEPTH = 128
|
|
17
|
+
CLASS = Object.instance_method(:class)
|
|
18
|
+
Scope = Struct.new(:parent, :declared, :assigned, :locals, :visible)
|
|
19
|
+
private_constant :CLASS, :Scope
|
|
20
|
+
|
|
21
|
+
def self.locals_for(path, line)
|
|
22
|
+
return nil unless CLASS.bind(path).call.equal?(String) && CLASS.bind(line).call.equal?(Integer)
|
|
23
|
+
return nil unless line.positive?
|
|
24
|
+
return nil unless File.file?(path)
|
|
25
|
+
|
|
26
|
+
source = File.open(path, 'rb') do |file|
|
|
27
|
+
return nil unless file.stat.file? && file.stat.size <= MAX_SOURCE_BYTES
|
|
28
|
+
file.read(MAX_SOURCE_BYTES + 1)
|
|
29
|
+
end
|
|
30
|
+
return nil if source.bytesize > MAX_SOURCE_BYTES
|
|
31
|
+
|
|
32
|
+
new(source).locals_for(line)
|
|
33
|
+
rescue StandardError, SystemStackError
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(source)
|
|
38
|
+
tree = Ripper.sexp(source)
|
|
39
|
+
raise ArgumentError, 'Invalid Ruby source' unless tree
|
|
40
|
+
|
|
41
|
+
# Check iteratively before any recursive AST work (including positions).
|
|
42
|
+
stack = [[tree, 0]]
|
|
43
|
+
count = 0
|
|
44
|
+
until stack.empty?
|
|
45
|
+
node, depth = stack.pop
|
|
46
|
+
count += 1
|
|
47
|
+
raise ArgumentError, 'AST limit' if count > MAX_NODES || depth > MAX_DEPTH
|
|
48
|
+
node.each { |child| stack << [child, depth + 1] if child.is_a?(Array) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@lambda_headers = lambda_headers(source)
|
|
52
|
+
@lambda_index = 0
|
|
53
|
+
@scopes = []
|
|
54
|
+
@lines = {}
|
|
55
|
+
@unknown_lines = {}
|
|
56
|
+
walk(tree, scope(nil))
|
|
57
|
+
raise ArgumentError, 'Unmatched lambda' unless @lambda_index == @lambda_headers.length
|
|
58
|
+
|
|
59
|
+
# Ancestors are complete before descendants, including assignments
|
|
60
|
+
# textually after a block. Such names are conservatively kept private.
|
|
61
|
+
@scopes.each do |current|
|
|
62
|
+
ancestors = current.parent ? current.parent.visible : []
|
|
63
|
+
current.locals = (current.declared + (current.assigned - ancestors)).uniq.freeze
|
|
64
|
+
current.visible = (ancestors + current.locals).uniq
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def locals_for(line)
|
|
69
|
+
return nil if @unknown_lines[line]
|
|
70
|
+
|
|
71
|
+
owners = @lines[line]
|
|
72
|
+
owners && owners.length == 1 ? owners.first.locals.dup : nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def scope(parent)
|
|
78
|
+
current = Scope.new(parent, [], [], nil, nil)
|
|
79
|
+
@scopes << current
|
|
80
|
+
current
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def walk(node, current)
|
|
84
|
+
return unless node.is_a?(Array)
|
|
85
|
+
|
|
86
|
+
kind = node[0]
|
|
87
|
+
if kind.is_a?(Symbol) && kind.to_s.start_with?('@')
|
|
88
|
+
first = node[2][0]
|
|
89
|
+
last = first + node[1].count("\n")
|
|
90
|
+
last -= 1 if node[1].end_with?("\n")
|
|
91
|
+
(first..last).each do |line|
|
|
92
|
+
owners = (@lines[line] ||= [])
|
|
93
|
+
owners << current unless owners.any? { |owner| owner.equal?(current) }
|
|
94
|
+
end
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
case kind
|
|
99
|
+
when :def, :defs
|
|
100
|
+
# A line alone cannot distinguish definition from default evaluation.
|
|
101
|
+
header, params, body = kind == :def ? [node[1..2], node[2], node[3]] : [node[1..4], node[4], node[5]]
|
|
102
|
+
token_positions(header).each { |position| @unknown_lines[position[0]] = true }
|
|
103
|
+
method_scope = scope(nil)
|
|
104
|
+
parameters(params, method_scope)
|
|
105
|
+
walk(node[1], current) if kind == :defs
|
|
106
|
+
walk(params, method_scope)
|
|
107
|
+
walk(body, method_scope)
|
|
108
|
+
when :class, :module, :sclass
|
|
109
|
+
inner = scope(nil)
|
|
110
|
+
token_positions(node[1...-1]).each { |position| @unknown_lines[position[0]] = true }
|
|
111
|
+
walk(node[1...-1], current)
|
|
112
|
+
walk(node[-1], inner)
|
|
113
|
+
when :do_block, :brace_block, :lambda
|
|
114
|
+
inner = scope(current)
|
|
115
|
+
parameters(node[1], inner)
|
|
116
|
+
if kind == :lambda
|
|
117
|
+
raise ArgumentError, 'Incomplete lambda AST' unless node[1].is_a?(Array)
|
|
118
|
+
|
|
119
|
+
header = @lambda_headers.fetch(@lambda_index)
|
|
120
|
+
@lambda_index += 1
|
|
121
|
+
inner.declared.concat(header[:locals])
|
|
122
|
+
# Ripper leaves lambda semicolon locals out of sexp entirely.
|
|
123
|
+
# Validate the association when there are positioned parameters.
|
|
124
|
+
positions = token_positions(node[1])
|
|
125
|
+
unless positions.all? { |position| (position <=> header[:start]) >= 0 && (position <=> header[:finish]) <= 0 }
|
|
126
|
+
raise ArgumentError, 'Ambiguous lambda header'
|
|
127
|
+
end
|
|
128
|
+
unless token_positions(node[2]).all? { |position| (position <=> header[:finish]) >= 0 }
|
|
129
|
+
raise ArgumentError, 'Ambiguous lambda body'
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
walk(node[1], inner)
|
|
133
|
+
walk(node[2], inner)
|
|
134
|
+
when :var_field
|
|
135
|
+
current.assigned << node[1][1].to_sym if node[1] && node[1][0] == :@ident
|
|
136
|
+
node.each { |child| walk(child, current) }
|
|
137
|
+
when :BEGIN, :END
|
|
138
|
+
raise ArgumentError, 'Unsupported execution scope'
|
|
139
|
+
else
|
|
140
|
+
node.each { |child| walk(child, current) }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def parameters(node, current)
|
|
145
|
+
return unless node.is_a?(Array)
|
|
146
|
+
|
|
147
|
+
case node[0]
|
|
148
|
+
when :paren
|
|
149
|
+
parameters(node[1], current)
|
|
150
|
+
when :block_var
|
|
151
|
+
parameters(node[1], current)
|
|
152
|
+
declare(node[2], current)
|
|
153
|
+
when :params
|
|
154
|
+
declare(node[1], current)
|
|
155
|
+
(node[2] || []).each { |pair| declare(pair[0], current) }
|
|
156
|
+
declare(node[3], current)
|
|
157
|
+
declare(node[4], current)
|
|
158
|
+
(node[5] || []).each { |pair| declare(pair[0], current) }
|
|
159
|
+
declare(node[6], current)
|
|
160
|
+
declare(node[7], current)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def declare(node, current)
|
|
165
|
+
return unless node.is_a?(Array)
|
|
166
|
+
|
|
167
|
+
if node[0] == :@ident || node[0] == :@label
|
|
168
|
+
current.declared << node[1].delete_suffix(':').to_sym
|
|
169
|
+
else
|
|
170
|
+
node.each { |child| declare(child, current) }
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def token_positions(node)
|
|
175
|
+
return [] unless node.is_a?(Array)
|
|
176
|
+
return [node[2]] if node[0].is_a?(Symbol) && node[0].to_s.start_with?('@')
|
|
177
|
+
|
|
178
|
+
node.flat_map { |child| token_positions(child) }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def lambda_headers(source)
|
|
182
|
+
tokens = Ripper.lex(source)
|
|
183
|
+
raise ArgumentError, 'Token limit' if tokens.length > MAX_NODES
|
|
184
|
+
|
|
185
|
+
headers = []
|
|
186
|
+
work = 0
|
|
187
|
+
tokens.each_with_index do |token, index|
|
|
188
|
+
next unless token[1] == :on_tlambda
|
|
189
|
+
|
|
190
|
+
header = { start: token[0], finish: token[0], locals: [] }
|
|
191
|
+
depth = 0
|
|
192
|
+
shadow = false
|
|
193
|
+
cursor = index + 1
|
|
194
|
+
while cursor < tokens.length
|
|
195
|
+
work += 1
|
|
196
|
+
raise ArgumentError, 'Lambda header limit' if work > MAX_NODES
|
|
197
|
+
position, type, text = tokens[cursor]
|
|
198
|
+
cursor += 1
|
|
199
|
+
header[:finish] = position
|
|
200
|
+
break if depth.zero? && (type == :on_tlambeg || (type == :on_kw && text == 'do'))
|
|
201
|
+
|
|
202
|
+
if shadow
|
|
203
|
+
if type == :on_rparen && depth == 1
|
|
204
|
+
shadow = false
|
|
205
|
+
elsif !%i[on_ident on_comma on_sp on_nl on_ignored_nl on_comment].include?(type)
|
|
206
|
+
raise ArgumentError, 'Ambiguous lambda shadow declaration'
|
|
207
|
+
end
|
|
208
|
+
header[:locals] << text.to_sym if type == :on_ident
|
|
209
|
+
end
|
|
210
|
+
depth += 1 if type == :on_lparen
|
|
211
|
+
depth -= 1 if type == :on_rparen
|
|
212
|
+
shadow = true if depth == 1 && type == :on_semicolon
|
|
213
|
+
end
|
|
214
|
+
headers << header
|
|
215
|
+
end
|
|
216
|
+
headers
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
private_class_method :new
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module HyperProbe
|
|
6
|
+
module Core
|
|
7
|
+
class Logger
|
|
8
|
+
COLOR_CODES = [32, 33, 34, 35, 36, 31, 92, 93, 94, 95, 96, 91].freeze # Vibrant ANSI colors
|
|
9
|
+
DISABLED_COLOR_VALUES = %w[0 false no off].freeze
|
|
10
|
+
SELECTOR_SEPARATOR = /[\s,]+/.freeze
|
|
11
|
+
|
|
12
|
+
@logger_cache = {}
|
|
13
|
+
@cache_mutex = Mutex.new
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def get_logger(namespace = 'hyperprobe:agent')
|
|
17
|
+
@cache_mutex.synchronize do
|
|
18
|
+
@logger_cache[namespace.to_s] ||= new(namespace.to_s)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def is_namespace_enabled?(namespace, debug_value = nil)
|
|
23
|
+
val = debug_value.nil? ? ENV['DEBUG'] : debug_value
|
|
24
|
+
return false if val.nil? || val.strip.empty?
|
|
25
|
+
|
|
26
|
+
enabled_patterns, skipped_patterns = parse_selectors(val)
|
|
27
|
+
|
|
28
|
+
# Check exclusions first
|
|
29
|
+
return false if skipped_patterns.any? { |p| p.match?(namespace) }
|
|
30
|
+
|
|
31
|
+
# Check inclusions
|
|
32
|
+
enabled_patterns.any? { |p| p.match?(namespace) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def parse_selectors(value)
|
|
36
|
+
enabled = []
|
|
37
|
+
skipped = []
|
|
38
|
+
|
|
39
|
+
(value || '').split(SELECTOR_SEPARATOR).each do |selector|
|
|
40
|
+
clean = selector.strip
|
|
41
|
+
next if clean.empty?
|
|
42
|
+
|
|
43
|
+
is_skip = clean.start_with?('-')
|
|
44
|
+
raw_pattern = is_skip ? clean[1..-1] : clean
|
|
45
|
+
next if raw_pattern.nil? || raw_pattern.empty?
|
|
46
|
+
|
|
47
|
+
regex = compile_selector(raw_pattern)
|
|
48
|
+
if is_skip
|
|
49
|
+
skipped << regex
|
|
50
|
+
else
|
|
51
|
+
enabled << regex
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
[enabled, skipped]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def compile_selector(pattern)
|
|
59
|
+
regex_str = '^' + Regexp.escape(pattern).gsub('\\*', '.*?') + '$'
|
|
60
|
+
Regexp.new(regex_str, Regexp::IGNORECASE)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def reset_cache!
|
|
64
|
+
@cache_mutex.synchronize do
|
|
65
|
+
@logger_cache.clear
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
attr_reader :namespace, :enabled, :color
|
|
71
|
+
|
|
72
|
+
def initialize(namespace = 'hyperprobe:agent')
|
|
73
|
+
@namespace = namespace.to_s
|
|
74
|
+
@enabled = self.class.is_namespace_enabled?(@namespace)
|
|
75
|
+
@color = select_namespace_color(@namespace)
|
|
76
|
+
@last_log_time_ms = monotonic_ms
|
|
77
|
+
@mutex = Mutex.new
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def debug_enabled?
|
|
81
|
+
@enabled
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
alias is_debug_enabled debug_enabled?
|
|
85
|
+
|
|
86
|
+
def debug(message)
|
|
87
|
+
return unless @enabled
|
|
88
|
+
|
|
89
|
+
write_log(message, $stdout)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def info(message)
|
|
93
|
+
if @enabled
|
|
94
|
+
write_log(message, $stdout)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def warn(message)
|
|
99
|
+
timestamp = Time.now.utc.strftime('%Y-%m-%d %H:%M:%S.%L')
|
|
100
|
+
formatted = "#{timestamp} \e[33m[HyperProbe WARN]\e[0m [#{@namespace}] #{message}"
|
|
101
|
+
emit($stdout, formatted)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def error(message, exception = nil)
|
|
105
|
+
timestamp = Time.now.utc.strftime('%Y-%m-%d %H:%M:%S.%L')
|
|
106
|
+
formatted = "#{timestamp} \e[31m[HyperProbe ERROR]\e[0m [#{@namespace}] #{message}"
|
|
107
|
+
emit($stderr, formatted)
|
|
108
|
+
if exception
|
|
109
|
+
type = Kernel.instance_method(:class).bind(exception).call
|
|
110
|
+
emit($stderr, " #{Module.instance_method(:name).bind(type).call}")
|
|
111
|
+
end
|
|
112
|
+
rescue SignalException, SystemExit
|
|
113
|
+
raise
|
|
114
|
+
rescue Exception
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def force_info(message)
|
|
119
|
+
timestamp = Time.now.utc.strftime('%Y-%m-%d %H:%M:%S.%L')
|
|
120
|
+
formatted = "#{timestamp} \e[34mHyperProbe\e[0m -- [#{@namespace}] #{message}"
|
|
121
|
+
emit($stdout, formatted)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
alias forceInfo force_info
|
|
125
|
+
|
|
126
|
+
def force_error(message, exception = nil)
|
|
127
|
+
error(message, exception)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
alias forceError force_error
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def write_log(message, stream)
|
|
135
|
+
now = monotonic_ms
|
|
136
|
+
delta_str = ''
|
|
137
|
+
|
|
138
|
+
return unless @mutex.try_lock
|
|
139
|
+
begin
|
|
140
|
+
delta = now - @last_log_time_ms
|
|
141
|
+
delta_str = " \e[#{@color}m+#{delta}ms\e[0m" if delta > 0
|
|
142
|
+
@last_log_time_ms = now
|
|
143
|
+
ensure
|
|
144
|
+
@mutex.unlock
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
prefix = if colors_enabled?(stream)
|
|
148
|
+
"\e[1m\e[#{@color}m#{@namespace}\e[0m"
|
|
149
|
+
else
|
|
150
|
+
@namespace
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
emit(stream, " #{prefix} \e[90m#{message}\e[0m#{delta_str}")
|
|
154
|
+
rescue SignalException, SystemExit
|
|
155
|
+
raise
|
|
156
|
+
rescue Exception
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def emit(stream, message)
|
|
161
|
+
line = "#{message}\n".byteslice(0, 4096)
|
|
162
|
+
if IO === stream
|
|
163
|
+
IO.instance_method(:write_nonblock).bind(stream).call(line, exception: false)
|
|
164
|
+
elsif defined?(StringIO) && StringIO === stream
|
|
165
|
+
StringIO.instance_method(:write).bind(stream).call(line)
|
|
166
|
+
end
|
|
167
|
+
rescue SignalException, SystemExit
|
|
168
|
+
raise
|
|
169
|
+
rescue Exception
|
|
170
|
+
# Never block or call arbitrary application output adapters.
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def select_namespace_color(ns)
|
|
175
|
+
hash_val = 0
|
|
176
|
+
ns.each_char.with_index do |char, idx|
|
|
177
|
+
hash_val += (idx + 1) * char.ord
|
|
178
|
+
end
|
|
179
|
+
COLOR_CODES[hash_val % COLOR_CODES.length]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def colors_enabled?(stream)
|
|
183
|
+
configured = ENV['DEBUG_COLORS']
|
|
184
|
+
if configured
|
|
185
|
+
return !DISABLED_COLOR_VALUES.include?(configured.strip.downcase)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
IO === stream && IO.instance_method(:tty?).bind(stream).call
|
|
189
|
+
rescue StandardError
|
|
190
|
+
false
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def monotonic_ms
|
|
194
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|