hyperprobe-agent 1.2.27.pre.1
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 +157 -0
- data/lib/hyperprobe/agent.rb +438 -0
- data/lib/hyperprobe/core/broker.rb +213 -0
- data/lib/hyperprobe/core/evaluator.rb +128 -0
- data/lib/hyperprobe/core/logger.rb +177 -0
- data/lib/hyperprobe/core/monitoring_engine.rb +683 -0
- data/lib/hyperprobe/core/quota.rb +111 -0
- data/lib/hyperprobe/core/safe_ast_validator.rb +91 -0
- data/lib/hyperprobe/core/safety.rb +179 -0
- data/lib/hyperprobe/core/serializer.rb +425 -0
- data/lib/hyperprobe/core/trace_extractor.rb +158 -0
- data/lib/hyperprobe/lambda.rb +90 -0
- data/lib/hyperprobe/protos/agent_pb.rb +33 -0
- data/lib/hyperprobe/protos/agent_services_pb.rb +31 -0
- data/lib/hyperprobe/protos.rb +14 -0
- data/lib/hyperprobe/railtie.rb +16 -0
- data/lib/hyperprobe/version.rb +5 -0
- data/lib/hyperprobe-agent.rb +3 -0
- data/lib/hyperprobe.rb +153 -0
- metadata +149 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'socket'
|
|
5
|
+
require_relative '../protos'
|
|
6
|
+
|
|
7
|
+
if RUBY_PLATFORM !~ /java/
|
|
8
|
+
begin
|
|
9
|
+
require 'grpc'
|
|
10
|
+
rescue LoadError
|
|
11
|
+
# Handled gracefully if grpc gem is absent
|
|
12
|
+
end
|
|
13
|
+
else
|
|
14
|
+
require 'java'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module HyperProbe
|
|
18
|
+
module Core
|
|
19
|
+
class BrokerClient
|
|
20
|
+
attr_reader :agent_id, :service_id, :environment, :commit_sha, :agent_version, :stub
|
|
21
|
+
|
|
22
|
+
def initialize(
|
|
23
|
+
broker_url:,
|
|
24
|
+
service_id:,
|
|
25
|
+
environment:,
|
|
26
|
+
commit_sha:,
|
|
27
|
+
agent_id: nil,
|
|
28
|
+
agent_version: '1.0.0',
|
|
29
|
+
rpc_timeout_sec: 10.0,
|
|
30
|
+
enable_keep_alive: true
|
|
31
|
+
)
|
|
32
|
+
@service_id = service_id.to_s
|
|
33
|
+
@environment = environment.to_s
|
|
34
|
+
@commit_sha = commit_sha.to_s
|
|
35
|
+
@agent_id = agent_id || SecureRandom.uuid
|
|
36
|
+
@agent_version = agent_version.to_s
|
|
37
|
+
@rpc_timeout_sec = rpc_timeout_sec.to_f
|
|
38
|
+
@hostname = ENV['HOSTNAME'] || Socket.gethostname rescue 'unknown'
|
|
39
|
+
@is_jruby = RUBY_PLATFORM =~ /java/
|
|
40
|
+
@log = Logger.get_logger('hyperprobe:broker')
|
|
41
|
+
|
|
42
|
+
if @is_jruby
|
|
43
|
+
init_jruby_client(broker_url)
|
|
44
|
+
else
|
|
45
|
+
init_cruby_client(broker_url, enable_keep_alive)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def get_probes(timeout_sec = nil)
|
|
50
|
+
req = Hyperprobe::Agent::V1::GetProbesRequest.new(
|
|
51
|
+
agent_id: @agent_id,
|
|
52
|
+
service_id: @service_id,
|
|
53
|
+
environment: @environment,
|
|
54
|
+
commit_sha: @commit_sha,
|
|
55
|
+
language: @is_jruby ? 'jruby' : 'ruby',
|
|
56
|
+
agent_version: @agent_version,
|
|
57
|
+
hostname: @hostname
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if @is_jruby
|
|
61
|
+
call_jruby_rpc('/hyperprobe.agent.v1.AgentBroker/GetProbes', req, Hyperprobe::Agent::V1::GetProbesResponse, timeout_sec)
|
|
62
|
+
else
|
|
63
|
+
deadline = calculate_deadline(timeout_sec || @rpc_timeout_sec)
|
|
64
|
+
metadata = { 'x-hp-service-id' => @service_id }
|
|
65
|
+
@stub.get_probes(req, metadata: metadata, deadline: deadline)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def report_telemetry(events, timeout_sec = nil)
|
|
70
|
+
return [] if events.nil? || events.empty?
|
|
71
|
+
|
|
72
|
+
proto_events = events.map { |evt| build_telemetry_event_proto(evt) }
|
|
73
|
+
batch = Hyperprobe::Agent::V1::TelemetryBatch.new(
|
|
74
|
+
agent_id: @agent_id,
|
|
75
|
+
events: proto_events
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if @is_jruby
|
|
79
|
+
response = call_jruby_rpc('/hyperprobe.agent.v1.AgentBroker/ReportTelemetry', batch, Hyperprobe::Agent::V1::TelemetryResponse, timeout_sec)
|
|
80
|
+
response.finished_probe_ids.to_a
|
|
81
|
+
else
|
|
82
|
+
deadline = calculate_deadline(timeout_sec || @rpc_timeout_sec)
|
|
83
|
+
metadata = { 'x-hp-service-id' => @service_id }
|
|
84
|
+
response = @stub.report_telemetry(batch, metadata: metadata, deadline: deadline)
|
|
85
|
+
response.finished_probe_ids.to_a
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def estimate_event_size(event)
|
|
90
|
+
proto = build_telemetry_event_proto(event)
|
|
91
|
+
proto.to_proto.bytesize
|
|
92
|
+
rescue StandardError
|
|
93
|
+
JSON.generate(event).bytesize rescue 512
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def shutdown
|
|
97
|
+
# No-op / cleanup
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def init_cruby_client(broker_url, enable_keep_alive)
|
|
103
|
+
target, creds, channel_args = parse_broker_url(broker_url, enable_keep_alive)
|
|
104
|
+
@stub = Hyperprobe::Agent::V1::AgentBroker::Stub.new(
|
|
105
|
+
target,
|
|
106
|
+
creds,
|
|
107
|
+
channel_args: channel_args
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def init_jruby_client(broker_url)
|
|
112
|
+
raw_url = broker_url.to_s.strip
|
|
113
|
+
if raw_url.start_with?('http://', 'https://')
|
|
114
|
+
@base_url = raw_url
|
|
115
|
+
else
|
|
116
|
+
@base_url = "http://#{raw_url}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
timeout_ms = (@rpc_timeout_sec * 1000).to_i
|
|
120
|
+
@http_client = java.net.http.HttpClient.newBuilder
|
|
121
|
+
.version(java.net.http.HttpClient::Version::HTTP_2)
|
|
122
|
+
.connectTimeout(java.time.Duration.ofMillis(timeout_ms))
|
|
123
|
+
.build
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def call_jruby_rpc(method_path, request_proto, response_class, timeout_sec)
|
|
127
|
+
raw_bytes = request_proto.to_proto
|
|
128
|
+
framed = "\x00" + [raw_bytes.bytesize].pack('N') + raw_bytes
|
|
129
|
+
|
|
130
|
+
uri = java.net.URI.create("#{@base_url}#{method_path}")
|
|
131
|
+
body_publisher = java.net.http.HttpRequest::BodyPublishers.ofByteArray(framed.to_java_bytes)
|
|
132
|
+
|
|
133
|
+
req_builder = java.net.http.HttpRequest.newBuilder(uri)
|
|
134
|
+
.version(java.net.http.HttpClient::Version::HTTP_2)
|
|
135
|
+
.header('Content-Type', 'application/grpc')
|
|
136
|
+
.header('TE', 'trailers')
|
|
137
|
+
.header('x-hp-service-id', @service_id)
|
|
138
|
+
.POST(body_publisher)
|
|
139
|
+
|
|
140
|
+
effective_timeout = timeout_sec || @rpc_timeout_sec
|
|
141
|
+
if effective_timeout && effective_timeout.positive?
|
|
142
|
+
req_builder.timeout(java.time.Duration.ofMillis((effective_timeout * 1000).to_i))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
http_response = @http_client.send(req_builder.build, java.net.http.HttpResponse::BodyHandlers.ofByteArray)
|
|
146
|
+
response_bytes = String.from_java_bytes(http_response.body)
|
|
147
|
+
|
|
148
|
+
# Strip 5-byte gRPC prefix header (1 byte compression flag + 4 bytes length)
|
|
149
|
+
proto_payload = response_bytes.length >= 5 ? response_bytes[5..-1] : ''
|
|
150
|
+
response_class.decode(proto_payload)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def build_telemetry_event_proto(evt)
|
|
154
|
+
return evt if evt.is_a?(Hyperprobe::Agent::V1::TelemetryEvent)
|
|
155
|
+
|
|
156
|
+
stack_frames = Array(evt[:stack_frames]).map do |frame|
|
|
157
|
+
Hyperprobe::Agent::V1::StackFrame.new(
|
|
158
|
+
function_name: frame[:function_name].to_s,
|
|
159
|
+
file_name: frame[:file_name].to_s,
|
|
160
|
+
line_number: frame[:line_number].to_i,
|
|
161
|
+
column_number: frame[:column_number].to_i,
|
|
162
|
+
class_name: frame[:class_name] ? frame[:class_name].to_s : nil
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
proto_params = {
|
|
167
|
+
probe_id: evt[:probe_id].to_s,
|
|
168
|
+
timestamp_ms: (evt[:timestamp_ms] || (Time.now.to_f * 1000)).to_i,
|
|
169
|
+
stack_frames: stack_frames,
|
|
170
|
+
captured_vars_json: evt[:captured_vars_json].to_s,
|
|
171
|
+
watch_results_json: evt[:watch_results_json].to_s,
|
|
172
|
+
evaluated_log: evt[:evaluated_log].to_s,
|
|
173
|
+
metric_value: evt[:metric_value].to_f,
|
|
174
|
+
capture_error: evt[:capture_error].to_s
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
proto_params[:trace_id] = evt[:trace_id].to_s if evt[:trace_id] && !evt[:trace_id].to_s.empty?
|
|
178
|
+
|
|
179
|
+
Hyperprobe::Agent::V1::TelemetryEvent.new(proto_params)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def calculate_deadline(timeout_sec)
|
|
183
|
+
return nil unless timeout_sec && timeout_sec.positive?
|
|
184
|
+
|
|
185
|
+
Time.now + timeout_sec
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def parse_broker_url(url, enable_keep_alive)
|
|
189
|
+
raw_url = url.to_s.strip
|
|
190
|
+
channel_args = {}
|
|
191
|
+
|
|
192
|
+
if enable_keep_alive
|
|
193
|
+
channel_args['grpc.keepalive_time_ms'] = 60_000
|
|
194
|
+
channel_args['grpc.keepalive_timeout_ms'] = 20_000
|
|
195
|
+
channel_args['grpc.keepalive_permit_without_calls'] = 1
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
if raw_url.start_with?('https://')
|
|
199
|
+
target = raw_url.sub('https://', '')
|
|
200
|
+
creds = GRPC::Core::ChannelCredentials.new
|
|
201
|
+
[target, creds, channel_args]
|
|
202
|
+
elsif raw_url.start_with?('http://')
|
|
203
|
+
target = raw_url.sub('http://', '')
|
|
204
|
+
creds = :this_channel_is_insecure
|
|
205
|
+
[target, creds, channel_args]
|
|
206
|
+
else
|
|
207
|
+
creds = :this_channel_is_insecure
|
|
208
|
+
[raw_url, creds, channel_args]
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'safe_ast_validator'
|
|
4
|
+
|
|
5
|
+
module HyperProbe
|
|
6
|
+
module Core
|
|
7
|
+
class Evaluator
|
|
8
|
+
PLACEHOLDER_REGEX = /\$?\{([^}]+)\}|\#\{([^}]+)\}/.freeze
|
|
9
|
+
JS_FLOAT_PREFIX_RE = /^\s*([+-]?(?:(?:[0-9]+\.?[0-9]*)|(?:\.[0-9]+))(?:[eE][+-]?[0-9]+)?)/.freeze
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def eval_condition(condition, binding_ctx)
|
|
13
|
+
return true if condition.nil? || condition.strip.empty?
|
|
14
|
+
|
|
15
|
+
clean_cond = condition.strip
|
|
16
|
+
SafeASTValidator.validate!(clean_cond)
|
|
17
|
+
result = binding_ctx.eval(clean_cond)
|
|
18
|
+
!!result
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def evaluate_watches(watch_expressions, binding_ctx)
|
|
22
|
+
results = {}
|
|
23
|
+
return results if watch_expressions.nil? || watch_expressions.empty?
|
|
24
|
+
|
|
25
|
+
watch_expressions.each do |expr|
|
|
26
|
+
clean_expr = expr.strip
|
|
27
|
+
next if clean_expr.empty?
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
SafeASTValidator.validate!(clean_expr)
|
|
31
|
+
results[expr] = binding_ctx.eval(clean_expr)
|
|
32
|
+
rescue StandardError, ScriptError, SecurityError => e
|
|
33
|
+
results[expr] = "Error: #{e.class.name}: #{e.message}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
results
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def evaluate_log_template(template, binding_ctx)
|
|
40
|
+
return '' if template.nil? || template.empty?
|
|
41
|
+
|
|
42
|
+
template.gsub(PLACEHOLDER_REGEX) do |_match|
|
|
43
|
+
expr = (Regexp.last_match(1) || Regexp.last_match(2)).strip
|
|
44
|
+
begin
|
|
45
|
+
SafeASTValidator.validate!(expr)
|
|
46
|
+
val = binding_ctx.eval(expr)
|
|
47
|
+
val.is_a?(String) ? val : val.inspect
|
|
48
|
+
rescue StandardError, ScriptError, SecurityError => e
|
|
49
|
+
"<Error: #{e.class.name}: #{e.message}>"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def evaluate_metric(metric_expression, binding_ctx)
|
|
55
|
+
return nil if metric_expression.nil? || metric_expression.strip.empty?
|
|
56
|
+
|
|
57
|
+
clean_expr = metric_expression.strip
|
|
58
|
+
SafeASTValidator.validate!(clean_expr)
|
|
59
|
+
raw_val = binding_ctx.eval(clean_expr)
|
|
60
|
+
coerce_metric_value(raw_val)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def evaluate_correlation(correlation_expression, binding_ctx)
|
|
64
|
+
return 'static-singleton' if correlation_expression.nil? || correlation_expression.strip.empty?
|
|
65
|
+
|
|
66
|
+
clean_expr = correlation_expression.strip
|
|
67
|
+
SafeASTValidator.validate!(clean_expr)
|
|
68
|
+
raw_val = binding_ctx.eval(clean_expr)
|
|
69
|
+
normalize_correlation_value(raw_val)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def coerce_metric_value(value)
|
|
73
|
+
return nil if value == true || value == false
|
|
74
|
+
return nil if value.nil?
|
|
75
|
+
|
|
76
|
+
if value.is_a?(Numeric)
|
|
77
|
+
float_val = value.to_f
|
|
78
|
+
return float_val if float_val.finite?
|
|
79
|
+
|
|
80
|
+
return nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if value.is_a?(String)
|
|
84
|
+
match = JS_FLOAT_PREFIX_RE.match(value)
|
|
85
|
+
if match
|
|
86
|
+
float_val = match[1].to_f
|
|
87
|
+
return float_val if float_val.finite?
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def normalize_correlation_value(value)
|
|
95
|
+
return 'static-singleton' if value.nil?
|
|
96
|
+
|
|
97
|
+
if value == true || value == false
|
|
98
|
+
type_name = value ? 'true' : 'false'
|
|
99
|
+
raise TypeError, "Correlation expression must evaluate to a string or number, got boolean (#{type_name})"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
if value.is_a?(String)
|
|
103
|
+
if value.start_with?('Error: ')
|
|
104
|
+
raise StandardError, value
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
return value
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if value.is_a?(Integer)
|
|
111
|
+
return value.to_s
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if value.is_a?(Float)
|
|
115
|
+
return 'NaN' if value.nan?
|
|
116
|
+
return 'Infinity' if value.infinite? && value.positive?
|
|
117
|
+
return '-Infinity' if value.infinite? && value.negative?
|
|
118
|
+
return value.to_i.to_s if (value % 1.0).zero?
|
|
119
|
+
|
|
120
|
+
return value.to_s
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
raise TypeError, "Correlation expression must evaluate to a string or number, got #{value.class.name}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
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
|
+
$stdout.puts(formatted)
|
|
102
|
+
$stdout.flush rescue nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def error(message, exception = nil)
|
|
106
|
+
timestamp = Time.now.utc.strftime('%Y-%m-%d %H:%M:%S.%L')
|
|
107
|
+
formatted = "#{timestamp} \e[31m[HyperProbe ERROR]\e[0m [#{@namespace}] #{message}"
|
|
108
|
+
$stderr.puts(formatted)
|
|
109
|
+
if exception
|
|
110
|
+
$stderr.puts(" #{exception.class}: #{exception.message}")
|
|
111
|
+
$stderr.puts(exception.backtrace.first(5).map { |l| " at #{l}" }.join("\n")) if exception.backtrace
|
|
112
|
+
end
|
|
113
|
+
$stderr.flush rescue nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def force_info(message)
|
|
117
|
+
timestamp = Time.now.utc.strftime('%Y-%m-%d %H:%M:%S.%L')
|
|
118
|
+
formatted = "#{timestamp} \e[34mHyperProbe\e[0m -- [#{@namespace}] #{message}"
|
|
119
|
+
$stdout.puts(formatted)
|
|
120
|
+
$stdout.flush rescue nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
alias forceInfo force_info
|
|
124
|
+
|
|
125
|
+
def force_error(message, exception = nil)
|
|
126
|
+
error(message, exception)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
alias forceError force_error
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def write_log(message, stream)
|
|
134
|
+
now = monotonic_ms
|
|
135
|
+
delta_str = ''
|
|
136
|
+
|
|
137
|
+
@mutex.synchronize do
|
|
138
|
+
delta = now - @last_log_time_ms
|
|
139
|
+
delta_str = " \e[#{@color}m+#{delta}ms\e[0m" if delta > 0
|
|
140
|
+
@last_log_time_ms = now
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
prefix = if colors_enabled?(stream)
|
|
144
|
+
"\e[1m\e[#{@color}m#{@namespace}\e[0m"
|
|
145
|
+
else
|
|
146
|
+
@namespace
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
stream.puts(" #{prefix} \e[90m#{message}\e[0m#{delta_str}")
|
|
150
|
+
stream.flush rescue nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def select_namespace_color(ns)
|
|
154
|
+
hash_val = 0
|
|
155
|
+
ns.each_char.with_index do |char, idx|
|
|
156
|
+
hash_val += (idx + 1) * char.ord
|
|
157
|
+
end
|
|
158
|
+
COLOR_CODES[hash_val % COLOR_CODES.length]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def colors_enabled?(stream)
|
|
162
|
+
configured = ENV['DEBUG_COLORS']
|
|
163
|
+
if configured
|
|
164
|
+
return !DISABLED_COLOR_VALUES.include?(configured.strip.downcase)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
stream.respond_to?(:tty?) && stream.tty?
|
|
168
|
+
rescue StandardError
|
|
169
|
+
false
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def monotonic_ms
|
|
173
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|