findbug 0.2.0
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/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +375 -0
- data/Rakefile +12 -0
- data/app/controllers/findbug/application_controller.rb +105 -0
- data/app/controllers/findbug/dashboard_controller.rb +93 -0
- data/app/controllers/findbug/errors_controller.rb +129 -0
- data/app/controllers/findbug/performance_controller.rb +80 -0
- data/app/jobs/findbug/alert_job.rb +40 -0
- data/app/jobs/findbug/cleanup_job.rb +132 -0
- data/app/jobs/findbug/persist_job.rb +158 -0
- data/app/models/findbug/error_event.rb +197 -0
- data/app/models/findbug/performance_event.rb +237 -0
- data/app/views/findbug/dashboard/index.html.erb +199 -0
- data/app/views/findbug/errors/index.html.erb +137 -0
- data/app/views/findbug/errors/show.html.erb +185 -0
- data/app/views/findbug/performance/index.html.erb +168 -0
- data/app/views/findbug/performance/show.html.erb +203 -0
- data/app/views/layouts/findbug/application.html.erb +601 -0
- data/lib/findbug/alerts/channels/base.rb +75 -0
- data/lib/findbug/alerts/channels/discord.rb +155 -0
- data/lib/findbug/alerts/channels/email.rb +179 -0
- data/lib/findbug/alerts/channels/slack.rb +149 -0
- data/lib/findbug/alerts/channels/webhook.rb +143 -0
- data/lib/findbug/alerts/dispatcher.rb +126 -0
- data/lib/findbug/alerts/throttler.rb +110 -0
- data/lib/findbug/background_persister.rb +142 -0
- data/lib/findbug/capture/context.rb +301 -0
- data/lib/findbug/capture/exception_handler.rb +141 -0
- data/lib/findbug/capture/exception_subscriber.rb +228 -0
- data/lib/findbug/capture/message_handler.rb +104 -0
- data/lib/findbug/capture/middleware.rb +247 -0
- data/lib/findbug/configuration.rb +381 -0
- data/lib/findbug/engine.rb +109 -0
- data/lib/findbug/performance/instrumentation.rb +336 -0
- data/lib/findbug/performance/transaction.rb +193 -0
- data/lib/findbug/processing/data_scrubber.rb +163 -0
- data/lib/findbug/rails/controller_methods.rb +152 -0
- data/lib/findbug/railtie.rb +222 -0
- data/lib/findbug/storage/circuit_breaker.rb +223 -0
- data/lib/findbug/storage/connection_pool.rb +134 -0
- data/lib/findbug/storage/redis_buffer.rb +285 -0
- data/lib/findbug/tasks/findbug.rake +167 -0
- data/lib/findbug/version.rb +5 -0
- data/lib/findbug.rb +216 -0
- data/lib/generators/findbug/install_generator.rb +67 -0
- data/lib/generators/findbug/templates/POST_INSTALL +41 -0
- data/lib/generators/findbug/templates/create_findbug_error_events.rb +44 -0
- data/lib/generators/findbug/templates/create_findbug_performance_events.rb +47 -0
- data/lib/generators/findbug/templates/initializer.rb +157 -0
- data/sig/findbug.rbs +4 -0
- metadata +251 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "context"
|
|
4
|
+
require_relative "../storage/redis_buffer"
|
|
5
|
+
require "digest"
|
|
6
|
+
require "socket"
|
|
7
|
+
|
|
8
|
+
module Findbug
|
|
9
|
+
module Capture
|
|
10
|
+
# Middleware captures uncaught exceptions at the Rack level.
|
|
11
|
+
#
|
|
12
|
+
# WHY MIDDLEWARE + SUBSCRIBER?
|
|
13
|
+
# ============================
|
|
14
|
+
#
|
|
15
|
+
# You might wonder: "We already have ExceptionSubscriber. Why middleware?"
|
|
16
|
+
#
|
|
17
|
+
# The subscriber catches errors reported via Rails.error, but:
|
|
18
|
+
# 1. Not all Rails errors go through Rails.error
|
|
19
|
+
# 2. Errors in middleware (before Rails) don't hit the subscriber
|
|
20
|
+
# 3. Some gems raise directly without using Rails.error
|
|
21
|
+
#
|
|
22
|
+
# The middleware is a safety net that catches EVERYTHING at the Rack level.
|
|
23
|
+
#
|
|
24
|
+
# MIDDLEWARE ORDER
|
|
25
|
+
# ================
|
|
26
|
+
#
|
|
27
|
+
# We're inserted AFTER ActionDispatch::ShowExceptions:
|
|
28
|
+
#
|
|
29
|
+
# [Rack Stack]
|
|
30
|
+
# ...
|
|
31
|
+
# ActionDispatch::ShowExceptions ← Converts exceptions to 500 pages
|
|
32
|
+
# Findbug::Capture::Middleware ← WE ARE HERE
|
|
33
|
+
# ...
|
|
34
|
+
# YourController
|
|
35
|
+
#
|
|
36
|
+
# When an exception bubbles up:
|
|
37
|
+
# 1. Controller raises
|
|
38
|
+
# 2. WE catch it first, capture it, then re-raise
|
|
39
|
+
# 3. ShowExceptions catches it and renders 500 page
|
|
40
|
+
#
|
|
41
|
+
# We capture and RE-RAISE so Rails can still do its thing.
|
|
42
|
+
#
|
|
43
|
+
class Middleware
|
|
44
|
+
def initialize(app)
|
|
45
|
+
@app = app
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def call(env)
|
|
49
|
+
# Skip if Findbug is disabled
|
|
50
|
+
return @app.call(env) unless Findbug.enabled?
|
|
51
|
+
|
|
52
|
+
# Set up request context
|
|
53
|
+
setup_context(env)
|
|
54
|
+
|
|
55
|
+
# Call the next middleware/app
|
|
56
|
+
response = @app.call(env)
|
|
57
|
+
|
|
58
|
+
# Capture any error that was stored in the environment
|
|
59
|
+
# (Some Rails error handlers store the error but don't re-raise)
|
|
60
|
+
capture_env_exception(env)
|
|
61
|
+
|
|
62
|
+
response
|
|
63
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
64
|
+
# Capture the exception
|
|
65
|
+
capture_exception(e, env)
|
|
66
|
+
|
|
67
|
+
# Re-raise so Rails can handle it (show 500 page, etc.)
|
|
68
|
+
raise
|
|
69
|
+
ensure
|
|
70
|
+
# Always clean up context
|
|
71
|
+
Context.clear!
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Set up context from the Rack request
|
|
77
|
+
#
|
|
78
|
+
# WHY SET UP CONTEXT HERE?
|
|
79
|
+
# ------------------------
|
|
80
|
+
# The middleware runs BEFORE controllers. By setting up context here,
|
|
81
|
+
# all request data is available even if the error occurs early.
|
|
82
|
+
#
|
|
83
|
+
def setup_context(env)
|
|
84
|
+
# Only set up if not already set (avoid overwriting controller-set context)
|
|
85
|
+
return if Context.request.present?
|
|
86
|
+
|
|
87
|
+
rack_request = Rack::Request.new(env)
|
|
88
|
+
|
|
89
|
+
# Skip non-HTTP paths (assets, etc.)
|
|
90
|
+
return unless should_capture_path?(rack_request.path)
|
|
91
|
+
|
|
92
|
+
Context.set_request(Context.from_rack_request(rack_request))
|
|
93
|
+
|
|
94
|
+
# Add automatic breadcrumb for the request
|
|
95
|
+
Context.add_breadcrumb(
|
|
96
|
+
message: "HTTP Request",
|
|
97
|
+
category: "http",
|
|
98
|
+
data: {
|
|
99
|
+
method: rack_request.request_method,
|
|
100
|
+
path: rack_request.path
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Capture an exception
|
|
106
|
+
def capture_exception(exception, env)
|
|
107
|
+
return unless should_capture_exception?(exception)
|
|
108
|
+
|
|
109
|
+
# Check if this exception was already captured by the subscriber
|
|
110
|
+
# (to avoid duplicates)
|
|
111
|
+
return if already_captured?(env, exception)
|
|
112
|
+
|
|
113
|
+
# Mark as captured
|
|
114
|
+
mark_captured(env, exception)
|
|
115
|
+
|
|
116
|
+
# Build event data
|
|
117
|
+
event_data = build_event_data(exception, env)
|
|
118
|
+
|
|
119
|
+
# Push to Redis (async)
|
|
120
|
+
Storage::RedisBuffer.push_error(event_data)
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
# NEVER let Findbug crash your app
|
|
123
|
+
Findbug.logger.error("[Findbug] Middleware capture failed: #{e.message}")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Capture exceptions stored in env (by error handlers)
|
|
127
|
+
def capture_env_exception(env)
|
|
128
|
+
# ActionDispatch::ShowExceptions stores the exception in env
|
|
129
|
+
exception = env["action_dispatch.exception"]
|
|
130
|
+
return unless exception
|
|
131
|
+
|
|
132
|
+
capture_exception(exception, env)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def should_capture_exception?(exception)
|
|
136
|
+
return false unless Findbug.config.should_capture_exception?(exception)
|
|
137
|
+
|
|
138
|
+
# Skip exceptions that indicate normal HTTP flows
|
|
139
|
+
# These are "expected" and don't need tracking
|
|
140
|
+
exception_class = exception.class.name
|
|
141
|
+
|
|
142
|
+
expected_exceptions = %w[
|
|
143
|
+
ActionController::RoutingError
|
|
144
|
+
ActionController::UnknownFormat
|
|
145
|
+
ActionController::BadRequest
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
!expected_exceptions.include?(exception_class)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def should_capture_path?(path)
|
|
152
|
+
Findbug.config.should_capture_path?(path)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Check if already captured (deduplication)
|
|
156
|
+
def already_captured?(env, exception)
|
|
157
|
+
captured_id = env["findbug.captured_exception_id"]
|
|
158
|
+
return false unless captured_id
|
|
159
|
+
|
|
160
|
+
captured_id == exception.object_id
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def mark_captured(env, exception)
|
|
164
|
+
env["findbug.captured_exception_id"] = exception.object_id
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def build_event_data(exception, env)
|
|
168
|
+
{
|
|
169
|
+
# Exception details
|
|
170
|
+
exception_class: exception.class.name,
|
|
171
|
+
message: exception.message,
|
|
172
|
+
backtrace: clean_backtrace(exception.backtrace),
|
|
173
|
+
|
|
174
|
+
# Metadata
|
|
175
|
+
severity: "error",
|
|
176
|
+
handled: false,
|
|
177
|
+
source: "middleware",
|
|
178
|
+
|
|
179
|
+
# Context
|
|
180
|
+
context: Context.to_h,
|
|
181
|
+
|
|
182
|
+
# Fingerprint
|
|
183
|
+
fingerprint: generate_fingerprint(exception),
|
|
184
|
+
|
|
185
|
+
# Timing
|
|
186
|
+
captured_at: Time.now.utc.iso8601(3),
|
|
187
|
+
|
|
188
|
+
# Environment
|
|
189
|
+
environment: Findbug.config.environment,
|
|
190
|
+
release: Findbug.config.release,
|
|
191
|
+
server: server_info
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def clean_backtrace(backtrace)
|
|
196
|
+
return [] unless backtrace
|
|
197
|
+
|
|
198
|
+
backtrace.first(50).map do |line|
|
|
199
|
+
if defined?(Rails.root) && Rails.root
|
|
200
|
+
line.sub(Rails.root.to_s + "/", "")
|
|
201
|
+
else
|
|
202
|
+
line
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def generate_fingerprint(exception)
|
|
208
|
+
components = [
|
|
209
|
+
exception.class.name,
|
|
210
|
+
normalize_message(exception.message),
|
|
211
|
+
top_frame(exception.backtrace)
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
Digest::SHA256.hexdigest(components.join("\n"))
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def normalize_message(message)
|
|
218
|
+
return "" unless message
|
|
219
|
+
|
|
220
|
+
message
|
|
221
|
+
.gsub(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i, "{uuid}")
|
|
222
|
+
.gsub(/\b\d+\.?\d*\b/, "{number}")
|
|
223
|
+
.gsub(/'[^']*'/, "'{string}'")
|
|
224
|
+
.gsub(/"[^"]*"/, '"{string}"')
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def top_frame(backtrace)
|
|
228
|
+
return "" unless backtrace&.any?
|
|
229
|
+
|
|
230
|
+
app_line = backtrace.find do |line|
|
|
231
|
+
line.include?("/app/") || line.include?("/lib/")
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
(app_line || backtrace.first).to_s
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def server_info
|
|
238
|
+
{
|
|
239
|
+
hostname: Socket.gethostname,
|
|
240
|
+
pid: Process.pid,
|
|
241
|
+
ruby_version: RUBY_VERSION,
|
|
242
|
+
rails_version: (Rails.version if defined?(Rails))
|
|
243
|
+
}
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Findbug
|
|
4
|
+
# Configuration holds all settings for Findbug.
|
|
5
|
+
#
|
|
6
|
+
# WHY THIS PATTERN?
|
|
7
|
+
# -----------------
|
|
8
|
+
# This is the standard Ruby gem configuration pattern. Users call:
|
|
9
|
+
#
|
|
10
|
+
# Findbug.configure do |config|
|
|
11
|
+
# config.redis_url = "redis://localhost:6379/1"
|
|
12
|
+
# config.enabled = Rails.env.production?
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# Benefits:
|
|
16
|
+
# 1. All settings in one place (easy to find/audit)
|
|
17
|
+
# 2. Sensible defaults (works without configuration)
|
|
18
|
+
# 3. Type checking and validation at startup (fail fast)
|
|
19
|
+
# 4. Isolated from global state (each setting is an instance variable)
|
|
20
|
+
#
|
|
21
|
+
class Configuration
|
|
22
|
+
# ----- Core Settings -----
|
|
23
|
+
|
|
24
|
+
# Whether Findbug is enabled. Disable in test environments to avoid noise.
|
|
25
|
+
# Default: true (enabled)
|
|
26
|
+
attr_accessor :enabled
|
|
27
|
+
|
|
28
|
+
# Redis connection URL. We use a SEPARATE Redis connection from your app
|
|
29
|
+
# to avoid any interference with your caching/Sidekiq.
|
|
30
|
+
# Default: redis://localhost:6379/1 (note: database 1, not 0)
|
|
31
|
+
attr_accessor :redis_url
|
|
32
|
+
|
|
33
|
+
# Size of the Redis connection pool. More connections = more concurrent writes.
|
|
34
|
+
# Rule of thumb: match your Puma/Unicorn worker count.
|
|
35
|
+
# Default: 5
|
|
36
|
+
attr_accessor :redis_pool_size
|
|
37
|
+
|
|
38
|
+
# Timeout for getting a connection from the pool (in seconds).
|
|
39
|
+
# If all connections are busy, we wait this long before giving up.
|
|
40
|
+
# Default: 1 second (fast fail to avoid blocking your app)
|
|
41
|
+
attr_accessor :redis_pool_timeout
|
|
42
|
+
|
|
43
|
+
# ----- Error Capture Settings -----
|
|
44
|
+
|
|
45
|
+
# Sample rate for error capture (0.0 to 1.0).
|
|
46
|
+
# 1.0 = capture 100% of errors
|
|
47
|
+
# 0.5 = capture 50% of errors (randomly sampled)
|
|
48
|
+
# Useful for extremely high-traffic apps where you don't need every error.
|
|
49
|
+
# Default: 1.0 (capture everything)
|
|
50
|
+
attr_accessor :sample_rate
|
|
51
|
+
|
|
52
|
+
# Exception classes to ignore. These won't be captured at all.
|
|
53
|
+
# Common ignores: ActiveRecord::RecordNotFound (404s), ActionController::RoutingError
|
|
54
|
+
# Default: empty array
|
|
55
|
+
attr_accessor :ignored_exceptions
|
|
56
|
+
|
|
57
|
+
# Paths to ignore (regex patterns). Useful for health checks, assets, etc.
|
|
58
|
+
# Example: [/^\/health/, /^\/assets/]
|
|
59
|
+
# Default: empty array
|
|
60
|
+
attr_accessor :ignored_paths
|
|
61
|
+
|
|
62
|
+
# ----- Performance Monitoring Settings -----
|
|
63
|
+
|
|
64
|
+
# Whether to enable performance monitoring (request timing, SQL queries).
|
|
65
|
+
# Default: true
|
|
66
|
+
attr_accessor :performance_enabled
|
|
67
|
+
|
|
68
|
+
# Sample rate for performance monitoring (0.0 to 1.0).
|
|
69
|
+
# Performance data is more voluminous than errors, so you might want to sample.
|
|
70
|
+
# Default: 0.1 (10% of requests)
|
|
71
|
+
attr_accessor :performance_sample_rate
|
|
72
|
+
|
|
73
|
+
# Threshold in ms. Only record requests slower than this.
|
|
74
|
+
# Helps reduce noise from fast requests.
|
|
75
|
+
# Default: 0 (record all sampled requests)
|
|
76
|
+
attr_accessor :slow_request_threshold_ms
|
|
77
|
+
|
|
78
|
+
# Threshold in ms for flagging slow SQL queries.
|
|
79
|
+
# Default: 100ms
|
|
80
|
+
attr_accessor :slow_query_threshold_ms
|
|
81
|
+
|
|
82
|
+
# ----- Data Scrubbing (Security) -----
|
|
83
|
+
|
|
84
|
+
# Field names to scrub from captured data. These will be replaced with [FILTERED].
|
|
85
|
+
# CRITICAL for PII/security compliance.
|
|
86
|
+
# Default: common sensitive fields
|
|
87
|
+
attr_accessor :scrub_fields
|
|
88
|
+
|
|
89
|
+
# Whether to scrub request headers.
|
|
90
|
+
# Default: true (scrubs Authorization, Cookie, etc.)
|
|
91
|
+
attr_accessor :scrub_headers
|
|
92
|
+
|
|
93
|
+
# Additional headers to scrub (beyond defaults).
|
|
94
|
+
# Default: empty array
|
|
95
|
+
attr_accessor :scrub_header_names
|
|
96
|
+
|
|
97
|
+
# ----- Storage & Retention -----
|
|
98
|
+
|
|
99
|
+
# How many days to keep error/performance data in the database.
|
|
100
|
+
# Older records are automatically deleted by the cleanup job.
|
|
101
|
+
# Default: 30 days
|
|
102
|
+
attr_accessor :retention_days
|
|
103
|
+
|
|
104
|
+
# Maximum buffer size in Redis (number of events).
|
|
105
|
+
# Prevents Redis memory from growing unbounded if DB persistence falls behind.
|
|
106
|
+
# Default: 10000 events
|
|
107
|
+
attr_accessor :max_buffer_size
|
|
108
|
+
|
|
109
|
+
# Redis key TTL for buffered events (in seconds).
|
|
110
|
+
# Events older than this are automatically expired by Redis.
|
|
111
|
+
# Default: 86400 (24 hours)
|
|
112
|
+
attr_accessor :buffer_ttl
|
|
113
|
+
|
|
114
|
+
# ----- Background Jobs -----
|
|
115
|
+
|
|
116
|
+
# Queue name for Findbug's background jobs.
|
|
117
|
+
# Default: "findbug"
|
|
118
|
+
attr_accessor :queue_name
|
|
119
|
+
|
|
120
|
+
# Batch size for persistence job (how many events to move from Redis to DB at once).
|
|
121
|
+
# Larger = more efficient, but uses more memory.
|
|
122
|
+
# Default: 100
|
|
123
|
+
attr_accessor :persist_batch_size
|
|
124
|
+
|
|
125
|
+
# Interval (in seconds) for the background persister thread.
|
|
126
|
+
# This is how often events are moved from Redis to the database.
|
|
127
|
+
# Default: 30 seconds
|
|
128
|
+
attr_accessor :persist_interval
|
|
129
|
+
|
|
130
|
+
# Whether to use the built-in background persister thread.
|
|
131
|
+
# Set to false if you want to use ActiveJob/Sidekiq instead.
|
|
132
|
+
# Default: true
|
|
133
|
+
attr_accessor :auto_persist
|
|
134
|
+
|
|
135
|
+
# ----- Web Dashboard -----
|
|
136
|
+
|
|
137
|
+
# Username for basic auth on the dashboard.
|
|
138
|
+
# Default: nil (dashboard disabled if not set)
|
|
139
|
+
attr_accessor :web_username
|
|
140
|
+
|
|
141
|
+
# Password for basic auth on the dashboard.
|
|
142
|
+
# Default: nil (dashboard disabled if not set)
|
|
143
|
+
attr_accessor :web_password
|
|
144
|
+
|
|
145
|
+
# Path prefix for the dashboard. The dashboard will be mounted at this path.
|
|
146
|
+
# Default: "/findbug"
|
|
147
|
+
attr_accessor :web_path
|
|
148
|
+
|
|
149
|
+
# ----- Alert Settings -----
|
|
150
|
+
|
|
151
|
+
# Alert configuration object (set via block)
|
|
152
|
+
attr_reader :alerts
|
|
153
|
+
|
|
154
|
+
# ----- Misc -----
|
|
155
|
+
|
|
156
|
+
# Release/version identifier (e.g., git SHA, semantic version).
|
|
157
|
+
# Useful for tracking which deploy introduced a bug.
|
|
158
|
+
# Default: nil (auto-detected from ENV['FINDBUG_RELEASE'] or Git)
|
|
159
|
+
attr_accessor :release
|
|
160
|
+
|
|
161
|
+
# Environment name override.
|
|
162
|
+
# Default: Rails.env
|
|
163
|
+
attr_accessor :environment
|
|
164
|
+
|
|
165
|
+
# Custom logger. If nil, uses Rails.logger.
|
|
166
|
+
# Default: nil
|
|
167
|
+
attr_accessor :logger
|
|
168
|
+
|
|
169
|
+
def initialize
|
|
170
|
+
# Set sensible defaults
|
|
171
|
+
@enabled = true
|
|
172
|
+
|
|
173
|
+
# Redis defaults - note we use database 1 to avoid conflicts
|
|
174
|
+
@redis_url = ENV.fetch("FINDBUG_REDIS_URL", "redis://localhost:6379/1")
|
|
175
|
+
@redis_pool_size = ENV.fetch("FINDBUG_REDIS_POOL_SIZE", 5).to_i
|
|
176
|
+
@redis_pool_timeout = 1
|
|
177
|
+
|
|
178
|
+
# Error capture defaults
|
|
179
|
+
@sample_rate = 1.0
|
|
180
|
+
@ignored_exceptions = []
|
|
181
|
+
@ignored_paths = []
|
|
182
|
+
|
|
183
|
+
# Performance defaults
|
|
184
|
+
@performance_enabled = true
|
|
185
|
+
@performance_sample_rate = 0.1
|
|
186
|
+
@slow_request_threshold_ms = 0
|
|
187
|
+
@slow_query_threshold_ms = 100
|
|
188
|
+
|
|
189
|
+
# Security defaults - these are CRITICAL
|
|
190
|
+
@scrub_fields = %w[
|
|
191
|
+
password password_confirmation
|
|
192
|
+
secret secret_key secret_token
|
|
193
|
+
api_key api_secret
|
|
194
|
+
access_token refresh_token
|
|
195
|
+
credit_card card_number cvv
|
|
196
|
+
ssn social_security
|
|
197
|
+
private_key
|
|
198
|
+
]
|
|
199
|
+
@scrub_headers = true
|
|
200
|
+
@scrub_header_names = []
|
|
201
|
+
|
|
202
|
+
# Storage defaults
|
|
203
|
+
@retention_days = 30
|
|
204
|
+
@max_buffer_size = 10_000
|
|
205
|
+
@buffer_ttl = 86_400 # 24 hours
|
|
206
|
+
|
|
207
|
+
# Job defaults
|
|
208
|
+
@queue_name = "findbug"
|
|
209
|
+
@persist_batch_size = 100
|
|
210
|
+
@persist_interval = 30
|
|
211
|
+
@auto_persist = true
|
|
212
|
+
|
|
213
|
+
# Web defaults
|
|
214
|
+
@web_username = ENV["FINDBUG_USERNAME"]
|
|
215
|
+
@web_password = ENV["FINDBUG_PASSWORD"]
|
|
216
|
+
@web_path = "/findbug"
|
|
217
|
+
|
|
218
|
+
# Alerts - initialized empty, configured via block
|
|
219
|
+
@alerts = AlertConfiguration.new
|
|
220
|
+
|
|
221
|
+
# Misc
|
|
222
|
+
@release = ENV["FINDBUG_RELEASE"]
|
|
223
|
+
@environment = nil # Will use Rails.env if not set
|
|
224
|
+
@logger = nil # Will use Rails.logger if not set
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# DSL for configuring alerts
|
|
228
|
+
#
|
|
229
|
+
# Example:
|
|
230
|
+
# config.alerts do |alerts|
|
|
231
|
+
# alerts.email enabled: true, recipients: ["team@example.com"]
|
|
232
|
+
# alerts.slack enabled: true, webhook_url: ENV["SLACK_WEBHOOK"]
|
|
233
|
+
# end
|
|
234
|
+
#
|
|
235
|
+
def alerts
|
|
236
|
+
if block_given?
|
|
237
|
+
yield @alerts
|
|
238
|
+
else
|
|
239
|
+
@alerts
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Validate configuration at startup
|
|
244
|
+
# Raises ConfigurationError if something is wrong
|
|
245
|
+
def validate!
|
|
246
|
+
validate_sample_rates!
|
|
247
|
+
validate_redis!
|
|
248
|
+
validate_web_auth!
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Check if the dashboard should be enabled
|
|
252
|
+
def web_enabled?
|
|
253
|
+
web_username.present? && web_password.present?
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Check if we should capture this exception class
|
|
257
|
+
def should_capture_exception?(exception)
|
|
258
|
+
return false unless enabled
|
|
259
|
+
return false if ignored_exceptions.any? { |klass| exception.is_a?(klass) }
|
|
260
|
+
|
|
261
|
+
# Apply sampling
|
|
262
|
+
rand <= sample_rate
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Check if we should capture this request path
|
|
266
|
+
def should_capture_path?(path)
|
|
267
|
+
return false unless enabled
|
|
268
|
+
return false if ignored_paths.any? { |pattern| path.match?(pattern) }
|
|
269
|
+
|
|
270
|
+
true
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Check if we should capture performance for this request
|
|
274
|
+
def should_capture_performance?
|
|
275
|
+
return false unless enabled
|
|
276
|
+
return false unless performance_enabled
|
|
277
|
+
|
|
278
|
+
# Apply sampling
|
|
279
|
+
rand <= performance_sample_rate
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
private
|
|
283
|
+
|
|
284
|
+
def validate_sample_rates!
|
|
285
|
+
unless sample_rate.between?(0.0, 1.0)
|
|
286
|
+
raise ConfigurationError, "sample_rate must be between 0.0 and 1.0"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
unless performance_sample_rate.between?(0.0, 1.0)
|
|
290
|
+
raise ConfigurationError, "performance_sample_rate must be between 0.0 and 1.0"
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def validate_redis!
|
|
295
|
+
return unless enabled
|
|
296
|
+
|
|
297
|
+
unless redis_url.present?
|
|
298
|
+
raise ConfigurationError, "redis_url is required when Findbug is enabled"
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def validate_web_auth!
|
|
303
|
+
# If one is set, both must be set
|
|
304
|
+
if (web_username.present? && web_password.blank?) ||
|
|
305
|
+
(web_username.blank? && web_password.present?)
|
|
306
|
+
raise ConfigurationError, "Both web_username and web_password must be set for dashboard authentication"
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Nested class for alert configuration
|
|
312
|
+
#
|
|
313
|
+
# WHY A SEPARATE CLASS?
|
|
314
|
+
# Alerts have their own sub-configuration (multiple channels, each with settings).
|
|
315
|
+
# Nesting keeps the main Configuration cleaner.
|
|
316
|
+
#
|
|
317
|
+
class AlertConfiguration
|
|
318
|
+
attr_accessor :throttle_period
|
|
319
|
+
|
|
320
|
+
def initialize
|
|
321
|
+
@channels = {}
|
|
322
|
+
@throttle_period = 300 # 5 minutes default
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Configure email alerts
|
|
326
|
+
def email(enabled:, recipients: [], **options)
|
|
327
|
+
@channels[:email] = {
|
|
328
|
+
enabled: enabled,
|
|
329
|
+
recipients: Array(recipients),
|
|
330
|
+
**options
|
|
331
|
+
}
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Configure Slack alerts
|
|
335
|
+
def slack(enabled:, webhook_url: nil, channel: nil, **options)
|
|
336
|
+
@channels[:slack] = {
|
|
337
|
+
enabled: enabled,
|
|
338
|
+
webhook_url: webhook_url,
|
|
339
|
+
channel: channel,
|
|
340
|
+
**options
|
|
341
|
+
}
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Configure Discord alerts
|
|
345
|
+
def discord(enabled:, webhook_url: nil, **options)
|
|
346
|
+
@channels[:discord] = {
|
|
347
|
+
enabled: enabled,
|
|
348
|
+
webhook_url: webhook_url,
|
|
349
|
+
**options
|
|
350
|
+
}
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Configure generic webhook alerts
|
|
354
|
+
def webhook(enabled:, url: nil, headers: {}, **options)
|
|
355
|
+
@channels[:webhook] = {
|
|
356
|
+
enabled: enabled,
|
|
357
|
+
url: url,
|
|
358
|
+
headers: headers,
|
|
359
|
+
**options
|
|
360
|
+
}
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Get configuration for a specific channel
|
|
364
|
+
def channel(name)
|
|
365
|
+
@channels[name.to_sym]
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Get all enabled channels
|
|
369
|
+
def enabled_channels
|
|
370
|
+
@channels.select { |_, config| config[:enabled] }
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Check if any alerts are configured
|
|
374
|
+
def any_enabled?
|
|
375
|
+
enabled_channels.any?
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Custom error for configuration issues
|
|
380
|
+
class ConfigurationError < StandardError; end
|
|
381
|
+
end
|