brainzlab 0.1.11 → 0.1.20
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/CHANGELOG.md +7 -0
- data/README.md +210 -3
- data/lib/brainzlab/beacon/client.rb +21 -1
- data/lib/brainzlab/configuration.rb +81 -4
- data/lib/brainzlab/cortex/client.rb +21 -1
- data/lib/brainzlab/debug.rb +305 -0
- data/lib/brainzlab/dendrite/client.rb +21 -1
- data/lib/brainzlab/development/logger.rb +150 -0
- data/lib/brainzlab/development/store.rb +121 -0
- data/lib/brainzlab/development.rb +72 -0
- data/lib/brainzlab/devtools/assets/devtools.css +245 -109
- data/lib/brainzlab/devtools/assets/devtools.js +40 -0
- data/lib/brainzlab/devtools/middleware/asset_server.rb +1 -0
- data/lib/brainzlab/devtools/middleware/debug_panel.rb +1 -0
- data/lib/brainzlab/devtools/middleware/error_page.rb +56 -8
- data/lib/brainzlab/errors.rb +490 -0
- data/lib/brainzlab/flux/buffer.rb +2 -2
- data/lib/brainzlab/flux/client.rb +2 -2
- data/lib/brainzlab/instrumentation/active_support_cache.rb +60 -30
- data/lib/brainzlab/instrumentation/net_http.rb +21 -16
- data/lib/brainzlab/instrumentation.rb +6 -0
- data/lib/brainzlab/nerve/client.rb +21 -1
- data/lib/brainzlab/pulse/client.rb +66 -5
- data/lib/brainzlab/pulse.rb +24 -5
- data/lib/brainzlab/rails/log_formatter.rb +1 -1
- data/lib/brainzlab/rails/railtie.rb +18 -3
- data/lib/brainzlab/recall/buffer.rb +3 -1
- data/lib/brainzlab/recall/client.rb +74 -6
- data/lib/brainzlab/recall.rb +19 -2
- data/lib/brainzlab/reflex/client.rb +66 -5
- data/lib/brainzlab/reflex.rb +40 -8
- data/lib/brainzlab/sentinel/client.rb +21 -1
- data/lib/brainzlab/synapse/client.rb +21 -1
- data/lib/brainzlab/testing/event_store.rb +377 -0
- data/lib/brainzlab/testing/helpers.rb +650 -0
- data/lib/brainzlab/testing/matchers.rb +391 -0
- data/lib/brainzlab/testing.rb +327 -0
- data/lib/brainzlab/utilities/circuit_breaker.rb +32 -3
- data/lib/brainzlab/vault/client.rb +21 -1
- data/lib/brainzlab/version.rb +1 -1
- data/lib/brainzlab/vision/client.rb +53 -6
- data/lib/brainzlab.rb +67 -0
- data/lib/fluyenta-ruby.rb +3 -0
- metadata +34 -11
data/lib/brainzlab/recall.rb
CHANGED
|
@@ -31,14 +31,24 @@ module BrainzLab
|
|
|
31
31
|
def log(level, message, **data)
|
|
32
32
|
config = BrainzLab.configuration
|
|
33
33
|
return unless config.recall_effectively_enabled?
|
|
34
|
+
return unless config.level_enabled?(level)
|
|
35
|
+
|
|
36
|
+
entry = build_entry(level, message, data)
|
|
37
|
+
|
|
38
|
+
# Log debug output for the operation
|
|
39
|
+
log_debug_operation(level, message, data)
|
|
40
|
+
|
|
41
|
+
# In development mode, log locally instead of sending to server
|
|
42
|
+
if config.development_mode?
|
|
43
|
+
Development.record(service: :recall, event_type: 'log', payload: entry)
|
|
44
|
+
return
|
|
45
|
+
end
|
|
34
46
|
|
|
35
47
|
# Auto-provision project on first log if app_name is configured
|
|
36
48
|
ensure_provisioned!
|
|
37
49
|
|
|
38
|
-
return unless config.level_enabled?(level)
|
|
39
50
|
return unless config.valid?
|
|
40
51
|
|
|
41
|
-
entry = build_entry(level, message, data)
|
|
42
52
|
buffer.push(entry)
|
|
43
53
|
end
|
|
44
54
|
|
|
@@ -153,6 +163,13 @@ module BrainzLab
|
|
|
153
163
|
end
|
|
154
164
|
end
|
|
155
165
|
end
|
|
166
|
+
|
|
167
|
+
def log_debug_operation(level, message, data)
|
|
168
|
+
return unless BrainzLab::Debug.enabled?
|
|
169
|
+
|
|
170
|
+
truncated_message = message.to_s.length > 50 ? "#{message.to_s[0..47]}..." : message.to_s
|
|
171
|
+
BrainzLab::Debug.log_operation(:recall, "#{level.to_s.upcase} \"#{truncated_message}\"", **data.slice(*data.keys.first(3)))
|
|
172
|
+
end
|
|
156
173
|
end
|
|
157
174
|
end
|
|
158
175
|
end
|
|
@@ -31,20 +31,29 @@ module BrainzLab
|
|
|
31
31
|
|
|
32
32
|
def post(path, body)
|
|
33
33
|
uri = URI.join(@config.reflex_url, path)
|
|
34
|
+
|
|
35
|
+
# Call on_send callback if configured
|
|
36
|
+
invoke_on_send(:reflex, :post, path, body)
|
|
37
|
+
|
|
38
|
+
# Log debug output for request
|
|
39
|
+
log_debug_request(path, body)
|
|
40
|
+
|
|
34
41
|
request = Net::HTTP::Post.new(uri)
|
|
35
42
|
request['Content-Type'] = 'application/json'
|
|
36
43
|
request['Authorization'] = "Bearer #{@config.reflex_auth_key}"
|
|
37
44
|
request['User-Agent'] = "brainzlab-sdk-ruby/#{BrainzLab::VERSION}"
|
|
38
45
|
request.body = JSON.generate(body)
|
|
39
46
|
|
|
40
|
-
execute_with_retry(uri, request)
|
|
47
|
+
execute_with_retry(uri, request, path)
|
|
41
48
|
rescue StandardError => e
|
|
42
|
-
|
|
49
|
+
handle_error(e, context: { path: path, body_size: body.to_s.length })
|
|
43
50
|
nil
|
|
44
51
|
end
|
|
45
52
|
|
|
46
|
-
def execute_with_retry(uri, request)
|
|
53
|
+
def execute_with_retry(uri, request, path)
|
|
47
54
|
retries = 0
|
|
55
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
56
|
+
|
|
48
57
|
begin
|
|
49
58
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
50
59
|
http.use_ssl = uri.scheme == 'https'
|
|
@@ -52,6 +61,10 @@ module BrainzLab
|
|
|
52
61
|
http.read_timeout = 10
|
|
53
62
|
|
|
54
63
|
response = http.request(request)
|
|
64
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
|
|
65
|
+
|
|
66
|
+
# Log debug output for response
|
|
67
|
+
log_debug_response(response.code.to_i, duration_ms)
|
|
55
68
|
|
|
56
69
|
case response.code.to_i
|
|
57
70
|
when 200..299
|
|
@@ -63,7 +76,10 @@ module BrainzLab
|
|
|
63
76
|
when 429, 500..599
|
|
64
77
|
raise RetryableError, "Server error: #{response.code}"
|
|
65
78
|
else
|
|
66
|
-
|
|
79
|
+
handle_error(
|
|
80
|
+
StandardError.new("Reflex API error: #{response.code}"),
|
|
81
|
+
context: { path: path, status: response.code, body: response.body }
|
|
82
|
+
)
|
|
67
83
|
nil
|
|
68
84
|
end
|
|
69
85
|
rescue RetryableError, Net::OpenTimeout, Net::ReadTimeout => e
|
|
@@ -72,12 +88,57 @@ module BrainzLab
|
|
|
72
88
|
sleep(RETRY_DELAY * retries)
|
|
73
89
|
retry
|
|
74
90
|
end
|
|
75
|
-
|
|
91
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
|
|
92
|
+
log_debug_response(0, duration_ms, error: e.message)
|
|
93
|
+
handle_error(e, context: { path: path, retries: retries })
|
|
76
94
|
nil
|
|
77
95
|
end
|
|
78
96
|
end
|
|
79
97
|
|
|
98
|
+
def log_debug_request(path, body)
|
|
99
|
+
return unless BrainzLab::Debug.enabled?
|
|
100
|
+
|
|
101
|
+
data = if body.is_a?(Hash) && body[:errors]
|
|
102
|
+
{ count: body[:errors].size }
|
|
103
|
+
elsif body.is_a?(Hash) && body[:exception]
|
|
104
|
+
{ exception: body[:exception][:type] }
|
|
105
|
+
else
|
|
106
|
+
{}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
BrainzLab::Debug.log_request(:reflex, 'POST', path, data: data)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def log_debug_response(status, duration_ms, error: nil)
|
|
113
|
+
return unless BrainzLab::Debug.enabled?
|
|
114
|
+
|
|
115
|
+
BrainzLab::Debug.log_response(:reflex, status, duration_ms, error: error)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def invoke_on_send(service, method, path, payload)
|
|
119
|
+
return unless @config.on_send
|
|
120
|
+
|
|
121
|
+
@config.on_send.call(service, method, path, payload)
|
|
122
|
+
rescue StandardError => e
|
|
123
|
+
# Don't let callback errors break the SDK
|
|
124
|
+
log_error("on_send callback error: #{e.message}")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def handle_error(error, context: {})
|
|
128
|
+
log_error("#{error.message}")
|
|
129
|
+
|
|
130
|
+
# Call on_error callback if configured
|
|
131
|
+
return unless @config.on_error
|
|
132
|
+
|
|
133
|
+
@config.on_error.call(error, context.merge(service: :reflex))
|
|
134
|
+
rescue StandardError => e
|
|
135
|
+
# Don't let callback errors break the SDK
|
|
136
|
+
log_error("on_error callback error: #{e.message}")
|
|
137
|
+
end
|
|
138
|
+
|
|
80
139
|
def log_error(message)
|
|
140
|
+
BrainzLab::Debug.log(message, level: :error) if BrainzLab::Debug.enabled?
|
|
141
|
+
|
|
81
142
|
return unless @config.logger
|
|
82
143
|
|
|
83
144
|
@config.logger.error("[BrainzLab::Reflex] #{message}")
|
data/lib/brainzlab/reflex.rb
CHANGED
|
@@ -15,15 +15,24 @@ module BrainzLab
|
|
|
15
15
|
return if excluded?(exception)
|
|
16
16
|
return if sampled_out?
|
|
17
17
|
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return unless BrainzLab.configuration.reflex_valid?
|
|
18
|
+
# Log debug output for the operation
|
|
19
|
+
log_debug_capture(exception)
|
|
22
20
|
|
|
23
21
|
payload = build_payload(exception, context)
|
|
24
22
|
payload = run_before_send(payload, exception)
|
|
25
23
|
return if payload.nil?
|
|
26
24
|
|
|
25
|
+
# In development mode, log locally instead of sending to server
|
|
26
|
+
if BrainzLab.configuration.development_mode?
|
|
27
|
+
Development.record(service: :reflex, event_type: 'error', payload: payload)
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Auto-provision project on first capture if app_name is configured
|
|
32
|
+
ensure_provisioned!
|
|
33
|
+
|
|
34
|
+
return unless BrainzLab.configuration.reflex_valid?
|
|
35
|
+
|
|
27
36
|
client.send_error(payload)
|
|
28
37
|
end
|
|
29
38
|
|
|
@@ -32,15 +41,24 @@ module BrainzLab
|
|
|
32
41
|
return if capture_disabled?
|
|
33
42
|
return if sampled_out?
|
|
34
43
|
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return unless BrainzLab.configuration.reflex_valid?
|
|
44
|
+
# Log debug output for the operation
|
|
45
|
+
log_debug_message(message, level)
|
|
39
46
|
|
|
40
47
|
payload = build_message_payload(message, level, context)
|
|
41
48
|
payload = run_before_send(payload, nil)
|
|
42
49
|
return if payload.nil?
|
|
43
50
|
|
|
51
|
+
# In development mode, log locally instead of sending to server
|
|
52
|
+
if BrainzLab.configuration.development_mode?
|
|
53
|
+
Development.record(service: :reflex, event_type: 'message', payload: payload)
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Auto-provision project on first capture if app_name is configured
|
|
58
|
+
ensure_provisioned!
|
|
59
|
+
|
|
60
|
+
return unless BrainzLab.configuration.reflex_valid?
|
|
61
|
+
|
|
44
62
|
client.send_error(payload)
|
|
45
63
|
end
|
|
46
64
|
|
|
@@ -384,6 +402,20 @@ module BrainzLab
|
|
|
384
402
|
frame
|
|
385
403
|
end
|
|
386
404
|
end
|
|
405
|
+
|
|
406
|
+
def log_debug_capture(exception)
|
|
407
|
+
return unless BrainzLab::Debug.enabled?
|
|
408
|
+
|
|
409
|
+
truncated_message = exception.message.to_s.length > 40 ? "#{exception.message.to_s[0..37]}..." : exception.message.to_s
|
|
410
|
+
BrainzLab::Debug.log_operation(:reflex, "capture #{exception.class.name}: \"#{truncated_message}\"")
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def log_debug_message(message, level)
|
|
414
|
+
return unless BrainzLab::Debug.enabled?
|
|
415
|
+
|
|
416
|
+
truncated_message = message.to_s.length > 40 ? "#{message.to_s[0..37]}..." : message.to_s
|
|
417
|
+
BrainzLab::Debug.log_operation(:reflex, "message [#{level.to_s.upcase}] \"#{truncated_message}\"")
|
|
418
|
+
end
|
|
387
419
|
end
|
|
388
420
|
end
|
|
389
421
|
end
|
|
@@ -209,7 +209,27 @@ module BrainzLab
|
|
|
209
209
|
end
|
|
210
210
|
|
|
211
211
|
def log_error(operation, error)
|
|
212
|
-
|
|
212
|
+
structured_error = ErrorHandler.wrap(error, service: 'Sentinel', operation: operation)
|
|
213
|
+
BrainzLab.debug_log("[Sentinel::Client] #{operation} failed: #{structured_error.message}")
|
|
214
|
+
|
|
215
|
+
# Call on_error callback if configured
|
|
216
|
+
if @config.on_error
|
|
217
|
+
@config.on_error.call(structured_error, { service: 'Sentinel', operation: operation })
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def handle_response_error(response, operation)
|
|
222
|
+
return if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated) || response.is_a?(Net::HTTPNoContent) || response.is_a?(Net::HTTPAccepted)
|
|
223
|
+
|
|
224
|
+
structured_error = ErrorHandler.from_response(response, service: 'Sentinel', operation: operation)
|
|
225
|
+
BrainzLab.debug_log("[Sentinel::Client] #{operation} failed: #{structured_error.message}")
|
|
226
|
+
|
|
227
|
+
# Call on_error callback if configured
|
|
228
|
+
if @config.on_error
|
|
229
|
+
@config.on_error.call(structured_error, { service: 'Sentinel', operation: operation })
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
structured_error
|
|
213
233
|
end
|
|
214
234
|
end
|
|
215
235
|
end
|
|
@@ -281,7 +281,27 @@ module BrainzLab
|
|
|
281
281
|
end
|
|
282
282
|
|
|
283
283
|
def log_error(operation, error)
|
|
284
|
-
|
|
284
|
+
structured_error = ErrorHandler.wrap(error, service: 'Synapse', operation: operation)
|
|
285
|
+
BrainzLab.debug_log("[Synapse::Client] #{operation} failed: #{structured_error.message}")
|
|
286
|
+
|
|
287
|
+
# Call on_error callback if configured
|
|
288
|
+
if @config.on_error
|
|
289
|
+
@config.on_error.call(structured_error, { service: 'Synapse', operation: operation })
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def handle_response_error(response, operation)
|
|
294
|
+
return if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated) || response.is_a?(Net::HTTPNoContent) || response.is_a?(Net::HTTPAccepted)
|
|
295
|
+
|
|
296
|
+
structured_error = ErrorHandler.from_response(response, service: 'Synapse', operation: operation)
|
|
297
|
+
BrainzLab.debug_log("[Synapse::Client] #{operation} failed: #{structured_error.message}")
|
|
298
|
+
|
|
299
|
+
# Call on_error callback if configured
|
|
300
|
+
if @config.on_error
|
|
301
|
+
@config.on_error.call(structured_error, { service: 'Synapse', operation: operation })
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
structured_error
|
|
285
305
|
end
|
|
286
306
|
end
|
|
287
307
|
end
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Testing
|
|
5
|
+
# Thread-safe store for captured events, logs, errors, and metrics during tests
|
|
6
|
+
#
|
|
7
|
+
# This class is used internally by the testing helpers to store all
|
|
8
|
+
# captured data from stubbed SDK calls.
|
|
9
|
+
class EventStore
|
|
10
|
+
def initialize
|
|
11
|
+
@mutex = Mutex.new
|
|
12
|
+
@events = []
|
|
13
|
+
@metrics = []
|
|
14
|
+
@logs = []
|
|
15
|
+
@errors = []
|
|
16
|
+
@error_messages = []
|
|
17
|
+
@traces = []
|
|
18
|
+
@alerts = []
|
|
19
|
+
@notifications = []
|
|
20
|
+
@triggers = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# === Events (Flux) ===
|
|
24
|
+
|
|
25
|
+
def record_event(name, properties = {})
|
|
26
|
+
@mutex.synchronize do
|
|
27
|
+
@events << {
|
|
28
|
+
name: name.to_s,
|
|
29
|
+
properties: properties,
|
|
30
|
+
timestamp: Time.now.utc
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def events
|
|
36
|
+
@mutex.synchronize { @events.dup }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def events_named(name)
|
|
40
|
+
@mutex.synchronize do
|
|
41
|
+
@events.select { |e| e[:name] == name.to_s }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def event_tracked?(name, properties = nil)
|
|
46
|
+
@mutex.synchronize do
|
|
47
|
+
@events.any? do |event|
|
|
48
|
+
next false unless event[:name] == name.to_s
|
|
49
|
+
next true if properties.nil?
|
|
50
|
+
|
|
51
|
+
properties_match?(event[:properties], properties)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def last_event
|
|
57
|
+
@mutex.synchronize { @events.last }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def clear_events!
|
|
61
|
+
@mutex.synchronize { @events.clear }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# === Metrics (Flux) ===
|
|
65
|
+
|
|
66
|
+
def record_metric(type, name, value, opts = {})
|
|
67
|
+
@mutex.synchronize do
|
|
68
|
+
@metrics << {
|
|
69
|
+
type: type.to_sym,
|
|
70
|
+
name: name.to_s,
|
|
71
|
+
value: value,
|
|
72
|
+
tags: opts[:tags] || {},
|
|
73
|
+
timestamp: Time.now.utc
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def metrics
|
|
79
|
+
@mutex.synchronize { @metrics.dup }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def metrics_named(name)
|
|
83
|
+
@mutex.synchronize do
|
|
84
|
+
@metrics.select { |m| m[:name] == name.to_s }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def metric_recorded?(type, name, value: nil, tags: nil)
|
|
89
|
+
@mutex.synchronize do
|
|
90
|
+
@metrics.any? do |metric|
|
|
91
|
+
next false unless metric[:type] == type.to_sym
|
|
92
|
+
next false unless metric[:name] == name.to_s
|
|
93
|
+
next false if value && metric[:value] != value
|
|
94
|
+
next false if tags && !properties_match?(metric[:tags], tags)
|
|
95
|
+
|
|
96
|
+
true
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def clear_metrics!
|
|
102
|
+
@mutex.synchronize { @metrics.clear }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# === Logs (Recall) ===
|
|
106
|
+
|
|
107
|
+
def record_log(level, message, data = {})
|
|
108
|
+
@mutex.synchronize do
|
|
109
|
+
@logs << {
|
|
110
|
+
level: level.to_sym,
|
|
111
|
+
message: message.to_s,
|
|
112
|
+
data: data,
|
|
113
|
+
timestamp: Time.now.utc
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def logs
|
|
119
|
+
@mutex.synchronize { @logs.dup }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def logs_at_level(level)
|
|
123
|
+
@mutex.synchronize do
|
|
124
|
+
@logs.select { |l| l[:level] == level.to_sym }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def logged?(level, message = nil, data = nil)
|
|
129
|
+
@mutex.synchronize do
|
|
130
|
+
@logs.any? do |log|
|
|
131
|
+
next false unless log[:level] == level.to_sym
|
|
132
|
+
next true if message.nil?
|
|
133
|
+
|
|
134
|
+
message_matches = case message
|
|
135
|
+
when Regexp
|
|
136
|
+
log[:message].match?(message)
|
|
137
|
+
else
|
|
138
|
+
log[:message].include?(message.to_s)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
next false unless message_matches
|
|
142
|
+
next true if data.nil?
|
|
143
|
+
|
|
144
|
+
properties_match?(log[:data], data)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def clear_logs!
|
|
150
|
+
@mutex.synchronize { @logs.clear }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# === Errors (Reflex) ===
|
|
154
|
+
|
|
155
|
+
def record_error(exception, context = {})
|
|
156
|
+
@mutex.synchronize do
|
|
157
|
+
@errors << {
|
|
158
|
+
exception: exception,
|
|
159
|
+
error_class: exception.class.name,
|
|
160
|
+
message: exception.message,
|
|
161
|
+
backtrace: exception.backtrace,
|
|
162
|
+
context: context,
|
|
163
|
+
timestamp: Time.now.utc
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def record_error_message(message, level, context = {})
|
|
169
|
+
@mutex.synchronize do
|
|
170
|
+
@error_messages << {
|
|
171
|
+
message: message.to_s,
|
|
172
|
+
level: level.to_sym,
|
|
173
|
+
context: context,
|
|
174
|
+
timestamp: Time.now.utc
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def errors
|
|
180
|
+
@mutex.synchronize { @errors.dup }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def error_messages
|
|
184
|
+
@mutex.synchronize { @error_messages.dup }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def error_captured?(error_class = nil, message: nil, context: nil)
|
|
188
|
+
@mutex.synchronize do
|
|
189
|
+
@errors.any? do |error|
|
|
190
|
+
if error_class
|
|
191
|
+
next false unless error[:error_class] == error_class.to_s ||
|
|
192
|
+
(error_class.is_a?(Class) && error[:exception].is_a?(error_class))
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
if message
|
|
196
|
+
message_matches = case message
|
|
197
|
+
when Regexp
|
|
198
|
+
error[:message].match?(message)
|
|
199
|
+
else
|
|
200
|
+
error[:message].include?(message.to_s)
|
|
201
|
+
end
|
|
202
|
+
next false unless message_matches
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
next false if context && !properties_match?(error[:context], context)
|
|
206
|
+
|
|
207
|
+
true
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def last_error
|
|
213
|
+
@mutex.synchronize { @errors.last }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def clear_errors!
|
|
217
|
+
@mutex.synchronize do
|
|
218
|
+
@errors.clear
|
|
219
|
+
@error_messages.clear
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# === Traces (Pulse) ===
|
|
224
|
+
|
|
225
|
+
def record_trace(name, opts = {})
|
|
226
|
+
@mutex.synchronize do
|
|
227
|
+
@traces << {
|
|
228
|
+
name: name.to_s,
|
|
229
|
+
options: opts,
|
|
230
|
+
timestamp: Time.now.utc
|
|
231
|
+
}
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def traces
|
|
236
|
+
@mutex.synchronize { @traces.dup }
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def trace_recorded?(name, opts = nil)
|
|
240
|
+
@mutex.synchronize do
|
|
241
|
+
@traces.any? do |trace|
|
|
242
|
+
next false unless trace[:name] == name.to_s
|
|
243
|
+
next true if opts.nil?
|
|
244
|
+
|
|
245
|
+
properties_match?(trace[:options], opts)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def clear_traces!
|
|
251
|
+
@mutex.synchronize { @traces.clear }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# === Alerts (Signal) ===
|
|
255
|
+
|
|
256
|
+
def record_alert(name, message, severity, channels, data)
|
|
257
|
+
@mutex.synchronize do
|
|
258
|
+
@alerts << {
|
|
259
|
+
name: name.to_s,
|
|
260
|
+
message: message.to_s,
|
|
261
|
+
severity: severity.to_sym,
|
|
262
|
+
channels: channels,
|
|
263
|
+
data: data,
|
|
264
|
+
timestamp: Time.now.utc
|
|
265
|
+
}
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def alerts
|
|
270
|
+
@mutex.synchronize { @alerts.dup }
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def alert_sent?(name, message: nil, severity: nil)
|
|
274
|
+
@mutex.synchronize do
|
|
275
|
+
@alerts.any? do |alert|
|
|
276
|
+
next false unless alert[:name] == name.to_s
|
|
277
|
+
next false if message && !alert[:message].include?(message.to_s)
|
|
278
|
+
next false if severity && alert[:severity] != severity.to_sym
|
|
279
|
+
|
|
280
|
+
true
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def clear_alerts!
|
|
286
|
+
@mutex.synchronize { @alerts.clear }
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# === Notifications (Signal) ===
|
|
290
|
+
|
|
291
|
+
def record_notification(channel, message, title, data)
|
|
292
|
+
@mutex.synchronize do
|
|
293
|
+
@notifications << {
|
|
294
|
+
channel: Array(channel).map(&:to_s),
|
|
295
|
+
message: message.to_s,
|
|
296
|
+
title: title,
|
|
297
|
+
data: data,
|
|
298
|
+
timestamp: Time.now.utc
|
|
299
|
+
}
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def notifications
|
|
304
|
+
@mutex.synchronize { @notifications.dup }
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def clear_notifications!
|
|
308
|
+
@mutex.synchronize { @notifications.clear }
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# === Triggers (Signal) ===
|
|
312
|
+
|
|
313
|
+
def record_trigger(rule_name, context)
|
|
314
|
+
@mutex.synchronize do
|
|
315
|
+
@triggers << {
|
|
316
|
+
rule_name: rule_name.to_s,
|
|
317
|
+
context: context,
|
|
318
|
+
timestamp: Time.now.utc
|
|
319
|
+
}
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def triggers
|
|
324
|
+
@mutex.synchronize { @triggers.dup }
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def clear_triggers!
|
|
328
|
+
@mutex.synchronize { @triggers.clear }
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# === General ===
|
|
332
|
+
|
|
333
|
+
def clear!
|
|
334
|
+
@mutex.synchronize do
|
|
335
|
+
@events.clear
|
|
336
|
+
@metrics.clear
|
|
337
|
+
@logs.clear
|
|
338
|
+
@errors.clear
|
|
339
|
+
@error_messages.clear
|
|
340
|
+
@traces.clear
|
|
341
|
+
@alerts.clear
|
|
342
|
+
@notifications.clear
|
|
343
|
+
@triggers.clear
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def empty?
|
|
348
|
+
@mutex.synchronize do
|
|
349
|
+
@events.empty? &&
|
|
350
|
+
@metrics.empty? &&
|
|
351
|
+
@logs.empty? &&
|
|
352
|
+
@errors.empty? &&
|
|
353
|
+
@error_messages.empty? &&
|
|
354
|
+
@traces.empty? &&
|
|
355
|
+
@alerts.empty? &&
|
|
356
|
+
@notifications.empty? &&
|
|
357
|
+
@triggers.empty?
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
private
|
|
362
|
+
|
|
363
|
+
def properties_match?(actual, expected)
|
|
364
|
+
expected.all? do |key, value|
|
|
365
|
+
actual_value = actual[key] || actual[key.to_s] || actual[key.to_sym]
|
|
366
|
+
|
|
367
|
+
case value
|
|
368
|
+
when Regexp
|
|
369
|
+
actual_value.to_s.match?(value)
|
|
370
|
+
else
|
|
371
|
+
actual_value == value
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|