fluyenta-ruby 0.1.14
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/CHANGELOG.md +68 -0
- data/LICENSE +11 -0
- data/README.md +571 -0
- data/lib/brainzlab/beacon/client.rb +227 -0
- data/lib/brainzlab/beacon/provisioner.rb +44 -0
- data/lib/brainzlab/beacon.rb +215 -0
- data/lib/brainzlab/configuration.rb +676 -0
- data/lib/brainzlab/context.rb +90 -0
- data/lib/brainzlab/cortex/cache.rb +59 -0
- data/lib/brainzlab/cortex/client.rb +159 -0
- data/lib/brainzlab/cortex/provisioner.rb +49 -0
- data/lib/brainzlab/cortex.rb +223 -0
- data/lib/brainzlab/debug.rb +305 -0
- data/lib/brainzlab/dendrite/client.rb +250 -0
- data/lib/brainzlab/dendrite/provisioner.rb +44 -0
- data/lib/brainzlab/dendrite.rb +195 -0
- 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 +1329 -0
- data/lib/brainzlab/devtools/assets/devtools.js +396 -0
- data/lib/brainzlab/devtools/assets/logo.svg +6 -0
- data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
- data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
- data/lib/brainzlab/devtools/data/collector.rb +248 -0
- data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
- data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
- data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
- data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
- data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
- data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
- data/lib/brainzlab/devtools.rb +75 -0
- data/lib/brainzlab/errors.rb +490 -0
- data/lib/brainzlab/flux/buffer.rb +96 -0
- data/lib/brainzlab/flux/client.rb +68 -0
- data/lib/brainzlab/flux/provisioner.rb +124 -0
- data/lib/brainzlab/flux.rb +184 -0
- data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
- data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
- data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
- data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
- data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
- data/lib/brainzlab/instrumentation/action_view.rb +380 -0
- data/lib/brainzlab/instrumentation/active_job.rb +569 -0
- data/lib/brainzlab/instrumentation/active_record.rb +559 -0
- data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
- data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
- data/lib/brainzlab/instrumentation/aws.rb +183 -0
- data/lib/brainzlab/instrumentation/dalli.rb +108 -0
- data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
- data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
- data/lib/brainzlab/instrumentation/excon.rb +152 -0
- data/lib/brainzlab/instrumentation/faraday.rb +181 -0
- data/lib/brainzlab/instrumentation/good_job.rb +102 -0
- data/lib/brainzlab/instrumentation/grape.rb +293 -0
- data/lib/brainzlab/instrumentation/graphql.rb +252 -0
- data/lib/brainzlab/instrumentation/httparty.rb +193 -0
- data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
- data/lib/brainzlab/instrumentation/net_http.rb +114 -0
- data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
- data/lib/brainzlab/instrumentation/railties.rb +134 -0
- data/lib/brainzlab/instrumentation/redis.rb +324 -0
- data/lib/brainzlab/instrumentation/resque.rb +114 -0
- data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
- data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
- data/lib/brainzlab/instrumentation/stripe.rb +163 -0
- data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
- data/lib/brainzlab/instrumentation.rb +360 -0
- data/lib/brainzlab/nerve/client.rb +235 -0
- data/lib/brainzlab/nerve/provisioner.rb +44 -0
- data/lib/brainzlab/nerve.rb +219 -0
- data/lib/brainzlab/pulse/client.rb +203 -0
- data/lib/brainzlab/pulse/instrumentation.rb +401 -0
- data/lib/brainzlab/pulse/propagation.rb +241 -0
- data/lib/brainzlab/pulse/provisioner.rb +114 -0
- data/lib/brainzlab/pulse/tracer.rb +111 -0
- data/lib/brainzlab/pulse.rb +294 -0
- data/lib/brainzlab/rails/log_formatter.rb +807 -0
- data/lib/brainzlab/rails/log_subscriber.rb +334 -0
- data/lib/brainzlab/rails/railtie.rb +606 -0
- data/lib/brainzlab/recall/buffer.rb +66 -0
- data/lib/brainzlab/recall/client.rb +158 -0
- data/lib/brainzlab/recall/logger.rb +116 -0
- data/lib/brainzlab/recall/provisioner.rb +130 -0
- data/lib/brainzlab/recall.rb +175 -0
- data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
- data/lib/brainzlab/reflex/client.rb +150 -0
- data/lib/brainzlab/reflex/provisioner.rb +116 -0
- data/lib/brainzlab/reflex.rb +421 -0
- data/lib/brainzlab/sentinel/client.rb +236 -0
- data/lib/brainzlab/sentinel/provisioner.rb +44 -0
- data/lib/brainzlab/sentinel.rb +165 -0
- data/lib/brainzlab/signal/client.rb +60 -0
- data/lib/brainzlab/signal/provisioner.rb +115 -0
- data/lib/brainzlab/signal.rb +136 -0
- data/lib/brainzlab/synapse/client.rb +308 -0
- data/lib/brainzlab/synapse/provisioner.rb +44 -0
- data/lib/brainzlab/synapse.rb +270 -0
- 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 +290 -0
- data/lib/brainzlab/utilities/health_check.rb +294 -0
- data/lib/brainzlab/utilities/log_formatter.rb +254 -0
- data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
- data/lib/brainzlab/utilities.rb +17 -0
- data/lib/brainzlab/vault/cache.rb +80 -0
- data/lib/brainzlab/vault/client.rb +216 -0
- data/lib/brainzlab/vault/provisioner.rb +49 -0
- data/lib/brainzlab/vault.rb +262 -0
- data/lib/brainzlab/version.rb +5 -0
- data/lib/brainzlab/vision/client.rb +175 -0
- data/lib/brainzlab/vision/provisioner.rb +136 -0
- data/lib/brainzlab/vision.rb +155 -0
- data/lib/brainzlab-sdk.rb +3 -0
- data/lib/brainzlab.rb +306 -0
- data/lib/generators/brainzlab/install/install_generator.rb +63 -0
- data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
- metadata +251 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Utilities
|
|
5
|
+
# Health check utility for application health endpoints
|
|
6
|
+
# Provides checks for database, cache, queues, and external services
|
|
7
|
+
#
|
|
8
|
+
# @example Basic usage in Rails routes
|
|
9
|
+
# # config/routes.rb
|
|
10
|
+
# mount BrainzLab::Utilities::HealthCheck::Engine => "/health"
|
|
11
|
+
#
|
|
12
|
+
# @example Manual usage
|
|
13
|
+
# result = BrainzLab::Utilities::HealthCheck.run
|
|
14
|
+
# result[:status] # => "healthy" or "unhealthy"
|
|
15
|
+
# result[:checks] # => { database: { status: "ok", latency_ms: 5 }, ... }
|
|
16
|
+
#
|
|
17
|
+
class HealthCheck
|
|
18
|
+
CHECKS = %i[database redis cache queue memory disk].freeze
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
# Run all configured health checks
|
|
22
|
+
def run(checks: nil)
|
|
23
|
+
checks_to_run = checks || CHECKS
|
|
24
|
+
results = {}
|
|
25
|
+
overall_healthy = true
|
|
26
|
+
|
|
27
|
+
checks_to_run.each do |check|
|
|
28
|
+
result = send("check_#{check}")
|
|
29
|
+
results[check] = result
|
|
30
|
+
overall_healthy = false if result[:status] != 'ok'
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
results[check] = { status: 'error', message: e.message }
|
|
33
|
+
overall_healthy = false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
status: overall_healthy ? 'healthy' : 'unhealthy',
|
|
38
|
+
timestamp: Time.now.utc.iso8601,
|
|
39
|
+
checks: results
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Quick check - just returns status
|
|
44
|
+
def healthy?
|
|
45
|
+
result = run
|
|
46
|
+
result[:status] == 'healthy'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Database connectivity check
|
|
50
|
+
def check_database
|
|
51
|
+
return { status: 'skip', message: 'ActiveRecord not loaded' } unless defined?(ActiveRecord::Base)
|
|
52
|
+
|
|
53
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
54
|
+
ActiveRecord::Base.connection.execute('SELECT 1')
|
|
55
|
+
latency = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(2)
|
|
56
|
+
|
|
57
|
+
{ status: 'ok', latency_ms: latency }
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
{ status: 'error', message: e.message }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Redis connectivity check
|
|
63
|
+
def check_redis
|
|
64
|
+
return { status: 'skip', message: 'Redis not configured' } unless defined?(Redis)
|
|
65
|
+
|
|
66
|
+
redis = find_redis_connection
|
|
67
|
+
return { status: 'skip', message: 'No Redis connection found' } unless redis
|
|
68
|
+
|
|
69
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
70
|
+
redis.ping
|
|
71
|
+
latency = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(2)
|
|
72
|
+
|
|
73
|
+
{ status: 'ok', latency_ms: latency }
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
{ status: 'error', message: e.message }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Rails cache check
|
|
79
|
+
def check_cache
|
|
80
|
+
return { status: 'skip', message: 'Rails not loaded' } unless defined?(Rails)
|
|
81
|
+
|
|
82
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
83
|
+
key = "brainzlab_health_check_#{SecureRandom.hex(4)}"
|
|
84
|
+
Rails.cache.write(key, 'ok', expires_in: 10.seconds)
|
|
85
|
+
value = Rails.cache.read(key)
|
|
86
|
+
Rails.cache.delete(key)
|
|
87
|
+
latency = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(2)
|
|
88
|
+
|
|
89
|
+
if value == 'ok'
|
|
90
|
+
{ status: 'ok', latency_ms: latency }
|
|
91
|
+
else
|
|
92
|
+
{ status: 'error', message: 'Cache read/write failed' }
|
|
93
|
+
end
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
{ status: 'error', message: e.message }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Queue system check
|
|
99
|
+
def check_queue
|
|
100
|
+
if defined?(SolidQueue)
|
|
101
|
+
check_solid_queue
|
|
102
|
+
elsif defined?(Sidekiq)
|
|
103
|
+
check_sidekiq
|
|
104
|
+
elsif defined?(GoodJob)
|
|
105
|
+
check_good_job
|
|
106
|
+
else
|
|
107
|
+
{ status: 'skip', message: 'No queue system detected' }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Memory usage check
|
|
112
|
+
def check_memory
|
|
113
|
+
mem_info = memory_usage
|
|
114
|
+
|
|
115
|
+
status = if mem_info[:percentage] > 90
|
|
116
|
+
'warning'
|
|
117
|
+
elsif mem_info[:percentage] > 95
|
|
118
|
+
'error'
|
|
119
|
+
else
|
|
120
|
+
'ok'
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
{
|
|
124
|
+
status: status,
|
|
125
|
+
used_mb: mem_info[:used_mb],
|
|
126
|
+
percentage: mem_info[:percentage]
|
|
127
|
+
}
|
|
128
|
+
rescue StandardError => e
|
|
129
|
+
{ status: 'error', message: e.message }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Disk space check
|
|
133
|
+
def check_disk
|
|
134
|
+
disk_info = disk_usage
|
|
135
|
+
|
|
136
|
+
status = if disk_info[:percentage] > 90
|
|
137
|
+
'warning'
|
|
138
|
+
elsif disk_info[:percentage] > 95
|
|
139
|
+
'error'
|
|
140
|
+
else
|
|
141
|
+
'ok'
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
{
|
|
145
|
+
status: status,
|
|
146
|
+
used_gb: disk_info[:used_gb],
|
|
147
|
+
available_gb: disk_info[:available_gb],
|
|
148
|
+
percentage: disk_info[:percentage]
|
|
149
|
+
}
|
|
150
|
+
rescue StandardError => e
|
|
151
|
+
{ status: 'error', message: e.message }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Register a custom health check
|
|
155
|
+
def register(name, &block)
|
|
156
|
+
custom_checks[name.to_sym] = block
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def custom_checks
|
|
160
|
+
@custom_checks ||= {}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def find_redis_connection
|
|
166
|
+
# Try common Redis connection sources
|
|
167
|
+
if defined?(Redis.current) && Redis.current
|
|
168
|
+
Redis.current
|
|
169
|
+
elsif defined?(Sidekiq) && Sidekiq.respond_to?(:redis)
|
|
170
|
+
Sidekiq.redis { |conn| return conn }
|
|
171
|
+
elsif defined?(Rails) && Rails.application.config.respond_to?(:redis)
|
|
172
|
+
Rails.application.config.redis
|
|
173
|
+
end
|
|
174
|
+
rescue StandardError
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def check_solid_queue
|
|
179
|
+
return { status: 'skip', message: 'SolidQueue not loaded' } unless defined?(SolidQueue)
|
|
180
|
+
|
|
181
|
+
# Check if processes are running
|
|
182
|
+
if defined?(SolidQueue::Process)
|
|
183
|
+
process_count = SolidQueue::Process.where('last_heartbeat_at > ?', 5.minutes.ago).count
|
|
184
|
+
{
|
|
185
|
+
status: process_count.positive? ? 'ok' : 'warning',
|
|
186
|
+
processes: process_count
|
|
187
|
+
}
|
|
188
|
+
else
|
|
189
|
+
{ status: 'ok', message: 'SolidQueue configured' }
|
|
190
|
+
end
|
|
191
|
+
rescue StandardError => e
|
|
192
|
+
{ status: 'error', message: e.message }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def check_sidekiq
|
|
196
|
+
return { status: 'skip', message: 'Sidekiq not loaded' } unless defined?(Sidekiq)
|
|
197
|
+
|
|
198
|
+
stats = Sidekiq::Stats.new
|
|
199
|
+
{
|
|
200
|
+
status: 'ok',
|
|
201
|
+
processed: stats.processed,
|
|
202
|
+
failed: stats.failed,
|
|
203
|
+
queues: stats.queues,
|
|
204
|
+
workers: stats.workers_size
|
|
205
|
+
}
|
|
206
|
+
rescue StandardError => e
|
|
207
|
+
{ status: 'error', message: e.message }
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def check_good_job
|
|
211
|
+
return { status: 'skip', message: 'GoodJob not loaded' } unless defined?(GoodJob)
|
|
212
|
+
|
|
213
|
+
{
|
|
214
|
+
status: 'ok',
|
|
215
|
+
pending: GoodJob::Job.where(performed_at: nil).count,
|
|
216
|
+
running: GoodJob::Job.running.count
|
|
217
|
+
}
|
|
218
|
+
rescue StandardError => e
|
|
219
|
+
{ status: 'error', message: e.message }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def memory_usage
|
|
223
|
+
# Use /proc/self/status on Linux, ps on macOS
|
|
224
|
+
if File.exist?('/proc/self/status')
|
|
225
|
+
status = File.read('/proc/self/status')
|
|
226
|
+
vm_rss = status.match(/VmRSS:\s+(\d+)\s+kB/)&.captures&.first.to_i
|
|
227
|
+
used_mb = (vm_rss / 1024.0).round(2)
|
|
228
|
+
else
|
|
229
|
+
# macOS fallback
|
|
230
|
+
pid = Process.pid
|
|
231
|
+
output = `ps -o rss= -p #{pid}`.strip
|
|
232
|
+
used_mb = (output.to_i / 1024.0).round(2)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Estimate percentage (based on typical container memory)
|
|
236
|
+
max_mb = ENV.fetch('MEMORY_LIMIT_MB', 512).to_i
|
|
237
|
+
percentage = ((used_mb / max_mb) * 100).round(2)
|
|
238
|
+
|
|
239
|
+
{ used_mb: used_mb, percentage: percentage }
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def disk_usage
|
|
243
|
+
output = `df -k /`.split("\n").last.split
|
|
244
|
+
total = output[1].to_i / 1024 / 1024.0
|
|
245
|
+
used = output[2].to_i / 1024 / 1024.0
|
|
246
|
+
available = output[3].to_i / 1024 / 1024.0
|
|
247
|
+
percentage = ((used / total) * 100).round(2)
|
|
248
|
+
|
|
249
|
+
{
|
|
250
|
+
used_gb: used.round(2),
|
|
251
|
+
available_gb: available.round(2),
|
|
252
|
+
percentage: percentage
|
|
253
|
+
}
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Rails Engine for mounting health endpoints
|
|
258
|
+
if defined?(::Rails::Engine)
|
|
259
|
+
class Engine < ::Rails::Engine
|
|
260
|
+
isolate_namespace BrainzLab::Utilities::HealthCheck
|
|
261
|
+
|
|
262
|
+
routes.draw do
|
|
263
|
+
get '/', to: 'health#show'
|
|
264
|
+
get '/live', to: 'health#live'
|
|
265
|
+
get '/ready', to: 'health#ready'
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Controller for health endpoints
|
|
271
|
+
if defined?(ActionController::API)
|
|
272
|
+
class HealthController < ActionController::API
|
|
273
|
+
def show
|
|
274
|
+
result = HealthCheck.run
|
|
275
|
+
status = result[:status] == 'healthy' ? :ok : :service_unavailable
|
|
276
|
+
render json: result, status: status
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def live
|
|
280
|
+
# Liveness probe - just check if the app is running
|
|
281
|
+
render json: { status: 'ok', timestamp: Time.now.utc.iso8601 }
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def ready
|
|
285
|
+
# Readiness probe - check critical dependencies
|
|
286
|
+
result = HealthCheck.run(checks: %i[database redis])
|
|
287
|
+
status = result[:status] == 'healthy' ? :ok : :service_unavailable
|
|
288
|
+
render json: result, status: status
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Utilities
|
|
5
|
+
# Beautiful log formatter for Rails development
|
|
6
|
+
# Provides colorized, structured output with request timing
|
|
7
|
+
#
|
|
8
|
+
# @example Usage in Rails
|
|
9
|
+
# # config/environments/development.rb
|
|
10
|
+
# config.log_formatter = BrainzLab::Utilities::LogFormatter.new
|
|
11
|
+
#
|
|
12
|
+
# # Or use the Rails integration
|
|
13
|
+
# BrainzLab::Utilities::LogFormatter.install!
|
|
14
|
+
#
|
|
15
|
+
class LogFormatter < ::Logger::Formatter
|
|
16
|
+
COLORS = {
|
|
17
|
+
debug: "\e[36m", # Cyan
|
|
18
|
+
info: "\e[32m", # Green
|
|
19
|
+
warn: "\e[33m", # Yellow
|
|
20
|
+
error: "\e[31m", # Red
|
|
21
|
+
fatal: "\e[35m", # Magenta
|
|
22
|
+
reset: "\e[0m",
|
|
23
|
+
dim: "\e[2m",
|
|
24
|
+
bold: "\e[1m",
|
|
25
|
+
blue: "\e[34m",
|
|
26
|
+
gray: "\e[90m"
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
SEVERITY_ICONS = {
|
|
30
|
+
'DEBUG' => '🔍',
|
|
31
|
+
'INFO' => 'ℹ️ ',
|
|
32
|
+
'WARN' => '⚠️ ',
|
|
33
|
+
'ERROR' => '❌',
|
|
34
|
+
'FATAL' => '💀'
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
HTTP_METHODS = {
|
|
38
|
+
'GET' => "\e[32m", # Green
|
|
39
|
+
'POST' => "\e[33m", # Yellow
|
|
40
|
+
'PUT' => "\e[34m", # Blue
|
|
41
|
+
'PATCH' => "\e[34m", # Blue
|
|
42
|
+
'DELETE' => "\e[31m", # Red
|
|
43
|
+
'HEAD' => "\e[36m", # Cyan
|
|
44
|
+
'OPTIONS' => "\e[36m" # Cyan
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
def initialize(colorize: nil, show_timestamp: true, show_severity: true, compact: false)
|
|
48
|
+
super()
|
|
49
|
+
@colorize = colorize.nil? ? $stdout.tty? : colorize
|
|
50
|
+
@show_timestamp = show_timestamp
|
|
51
|
+
@show_severity = show_severity
|
|
52
|
+
@compact = compact
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def call(severity, timestamp, progname, msg)
|
|
56
|
+
return '' if msg.nil? || msg.to_s.strip.empty?
|
|
57
|
+
|
|
58
|
+
message = format_message(msg)
|
|
59
|
+
return '' if skip_message?(message)
|
|
60
|
+
|
|
61
|
+
formatted = build_output(severity, timestamp, progname, message)
|
|
62
|
+
"#{formatted}\n"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Install as Rails logger formatter
|
|
66
|
+
def self.install!
|
|
67
|
+
return unless defined?(Rails)
|
|
68
|
+
|
|
69
|
+
Rails.application.configure do
|
|
70
|
+
config.log_formatter = BrainzLab::Utilities::LogFormatter.new(
|
|
71
|
+
colorize: BrainzLab.configuration.log_formatter_colors,
|
|
72
|
+
compact: BrainzLab.configuration.log_formatter_compact_assets
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Also hook into ActiveSupport::TaggedLogging if present
|
|
77
|
+
return unless defined?(ActiveSupport::TaggedLogging) && Rails.logger.respond_to?(:formatter=)
|
|
78
|
+
|
|
79
|
+
Rails.logger.formatter = new
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def format_message(msg)
|
|
85
|
+
case msg
|
|
86
|
+
when String
|
|
87
|
+
msg
|
|
88
|
+
when Exception
|
|
89
|
+
"#{msg.class}: #{msg.message}\n#{msg.backtrace&.first(10)&.join("\n")}"
|
|
90
|
+
else
|
|
91
|
+
msg.inspect
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def skip_message?(message)
|
|
96
|
+
return false unless BrainzLab.configuration.log_formatter_hide_assets
|
|
97
|
+
|
|
98
|
+
# Skip asset pipeline noise
|
|
99
|
+
message.include?('/assets/') ||
|
|
100
|
+
message.include?('Asset pipeline') ||
|
|
101
|
+
message.match?(%r{Started GET "/assets/})
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_output(severity, timestamp, _progname, message)
|
|
105
|
+
parts = []
|
|
106
|
+
|
|
107
|
+
if @show_timestamp
|
|
108
|
+
ts = colorize(timestamp.strftime('%H:%M:%S.%L'), :gray)
|
|
109
|
+
parts << ts
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if @show_severity
|
|
113
|
+
sev = format_severity(severity)
|
|
114
|
+
parts << sev
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
parts << format_content(message, severity)
|
|
118
|
+
|
|
119
|
+
parts.join(' ')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def format_severity(severity)
|
|
123
|
+
icon = SEVERITY_ICONS[severity] || ''
|
|
124
|
+
text = severity.ljust(5)
|
|
125
|
+
|
|
126
|
+
if @colorize
|
|
127
|
+
color = severity_color(severity)
|
|
128
|
+
"#{icon}#{color}#{text}#{COLORS[:reset]}"
|
|
129
|
+
else
|
|
130
|
+
"#{icon}[#{text}]"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def severity_color(severity)
|
|
135
|
+
case severity
|
|
136
|
+
when 'DEBUG' then COLORS[:debug]
|
|
137
|
+
when 'INFO' then COLORS[:info]
|
|
138
|
+
when 'WARN' then COLORS[:warn]
|
|
139
|
+
when 'ERROR' then COLORS[:error]
|
|
140
|
+
when 'FATAL' then COLORS[:fatal]
|
|
141
|
+
else COLORS[:reset]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def format_content(message, severity)
|
|
146
|
+
# Handle Rails request log patterns
|
|
147
|
+
if (request_match = message.match(/Started (GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS) "([^"]+)"/))
|
|
148
|
+
format_request_started(request_match[1], request_match[2])
|
|
149
|
+
elsif (completed_match = message.match(/Completed (\d+) .+ in (\d+(?:\.\d+)?)ms/))
|
|
150
|
+
format_request_completed(completed_match[1].to_i, completed_match[2].to_f)
|
|
151
|
+
elsif message.include?('Processing by')
|
|
152
|
+
format_processing(message)
|
|
153
|
+
elsif message.include?('Parameters:')
|
|
154
|
+
format_parameters(message)
|
|
155
|
+
elsif message.include?('Rendering') || message.include?('Rendered')
|
|
156
|
+
format_rendering(message)
|
|
157
|
+
elsif %w[ERROR FATAL].include?(severity)
|
|
158
|
+
format_error(message)
|
|
159
|
+
else
|
|
160
|
+
message
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def format_request_started(method, path)
|
|
165
|
+
method_color = HTTP_METHODS[method] || COLORS[:reset]
|
|
166
|
+
|
|
167
|
+
if @colorize
|
|
168
|
+
"#{COLORS[:bold]}→#{COLORS[:reset]} #{method_color}#{method}#{COLORS[:reset]} #{path}"
|
|
169
|
+
else
|
|
170
|
+
"→ #{method} #{path}"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def format_request_completed(status, duration)
|
|
175
|
+
status_color = case status
|
|
176
|
+
when 200..299 then COLORS[:info]
|
|
177
|
+
when 300..399 then COLORS[:blue]
|
|
178
|
+
when 400..499 then COLORS[:warn]
|
|
179
|
+
when 500..599 then COLORS[:error]
|
|
180
|
+
else COLORS[:reset]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
duration_color = case duration
|
|
184
|
+
when 0..100 then COLORS[:info]
|
|
185
|
+
when 100..500 then COLORS[:warn]
|
|
186
|
+
else COLORS[:error]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
if @colorize
|
|
190
|
+
"#{COLORS[:bold]}←#{COLORS[:reset]} #{status_color}#{status}#{COLORS[:reset]} #{duration_color}#{duration.round(1)}ms#{COLORS[:reset]}"
|
|
191
|
+
else
|
|
192
|
+
"← #{status} #{duration.round(1)}ms"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def format_processing(message)
|
|
197
|
+
if (match = message.match(/Processing by (\w+)#(\w+)/))
|
|
198
|
+
controller, action = match.captures
|
|
199
|
+
if @colorize
|
|
200
|
+
" #{COLORS[:dim]}#{controller}##{action}#{COLORS[:reset]}"
|
|
201
|
+
else
|
|
202
|
+
" #{controller}##{action}"
|
|
203
|
+
end
|
|
204
|
+
else
|
|
205
|
+
" #{message}"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def format_parameters(message)
|
|
210
|
+
return message unless BrainzLab.configuration.log_formatter_show_params
|
|
211
|
+
|
|
212
|
+
if @colorize
|
|
213
|
+
" #{COLORS[:dim]}#{message}#{COLORS[:reset]}"
|
|
214
|
+
else
|
|
215
|
+
" #{message}"
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def format_rendering(message)
|
|
220
|
+
if @compact
|
|
221
|
+
# Compact: just show the template name
|
|
222
|
+
if (match = message.match(/Render(?:ed|ing) ([^\s]+)/))
|
|
223
|
+
template = match[1].split('/').last
|
|
224
|
+
if @colorize
|
|
225
|
+
" #{COLORS[:gray]}#{template}#{COLORS[:reset]}"
|
|
226
|
+
else
|
|
227
|
+
" #{template}"
|
|
228
|
+
end
|
|
229
|
+
else
|
|
230
|
+
''
|
|
231
|
+
end
|
|
232
|
+
elsif @colorize
|
|
233
|
+
" #{COLORS[:dim]}#{message}#{COLORS[:reset]}"
|
|
234
|
+
else
|
|
235
|
+
" #{message}"
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def format_error(message)
|
|
240
|
+
if @colorize
|
|
241
|
+
"#{COLORS[:error]}#{message}#{COLORS[:reset]}"
|
|
242
|
+
else
|
|
243
|
+
message
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def colorize(text, color)
|
|
248
|
+
return text unless @colorize
|
|
249
|
+
|
|
250
|
+
"#{COLORS[color]}#{text}#{COLORS[:reset]}"
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|