FindBug Documentation
Self-hosted error tracking and performance monitoring for Rails. Sentry-like functionality, with every byte of data on your own infrastructure.
Introduction
FindBug is a Rails engine that captures exceptions and performance data from your
application, stores them in Redis (as a high-speed buffer) and your existing relational
database, and surfaces them through a built-in web dashboard mounted at
/findbug. There are no external services, API keys, or per-seat fees.
What you get out of the box:
- Automatic exception capture from controllers, middleware, and
Rails.error - Performance instrumentation for HTTP requests and SQL with automatic N+1 detection
- A web dashboard for browsing and resolving errors
- Alerting over Email, Slack, Discord, or custom webhooks
- A built-in background thread that persists Redis events to your database every 30 seconds (no Sidekiq required)
Requirements
- Ruby 3.1 or newer
- Rails 7.0 or newer (7.x and 8.x both supported)
- Redis 4.0 or newer
- A relational database — PostgreSQL, MySQL, or SQLite (see Database Support)
Installation
Add the gem to your Gemfile:
gem "findbug", "~> 0.5.0"
Install and run the generator:
$ bundle install
$ rails generate findbug:install
$ rails db:migrate
The generator creates:
config/initializers/findbug.rb— your configuration- Three migrations under
db/migrate/:create_findbug_error_eventscreate_findbug_performance_eventscreate_findbug_alert_channels
As of v0.5.0 the migrations detect your adapter at db:migrate time and pick the right column type — jsonb on PostgreSQL, json on MySQL, text on SQLite. No manual editing needed.
Quick start
After running the migrations, set dashboard credentials:
export FINDBUG_USERNAME=admin
export FINDBUG_PASSWORD=your-secure-password
Then start your app and visit http://localhost:3000/findbug.
FindBug is now:
- Capturing unhandled exceptions automatically
- Monitoring 10% of requests (default
performance_sample_rate) - Flushing the Redis buffer to your database every 30 seconds
Configuration
All configuration lives in config/initializers/findbug.rb. Every setting has
a sensible default — you only need to override what's different in your environment.
Core settings
| Setting | Default | Description |
|---|---|---|
enabled | true | Turn FindBug on/off globally. Set to false in test envs. |
redis_url | redis://localhost:6379/1 | Redis URL for the buffer. We recommend a dedicated database number. |
redis_pool_size | 5 | Size of the dedicated Redis connection pool. |
redis_pool_timeout | 1 | Seconds to wait for a connection before giving up (fail-fast). |
Error capture
| Setting | Default | Description |
|---|---|---|
sample_rate | 1.0 | Fraction of errors to capture (0.0–1.0). Lower for very high-traffic apps. |
ignored_exceptions | [] | Array of exception classes to ignore. |
ignored_paths | [] | Array of regexes — requests matching these paths are skipped. |
Findbug.configure do |config|
config.sample_rate = 1.0
config.ignored_exceptions = [
ActiveRecord::RecordNotFound,
ActionController::RoutingError
]
config.ignored_paths = [/^\/health/, /^\/assets/]
end
Performance monitoring
| Setting | Default | Description |
|---|---|---|
performance_enabled | true | Master switch for the performance instrumenter. |
performance_sample_rate | 0.1 | Fraction of requests to instrument (10% is fine for most apps). |
slow_request_threshold_ms | 0 | Only persist requests slower than this. 0 = all sampled requests. |
slow_query_threshold_ms | 100 | SQL queries above this duration are flagged in the dashboard. |
Data scrubbing (security)
FindBug filters sensitive request and context data before it touches Redis.
Field names matching scrub_fields are replaced with [FILTERED].
| Setting | Default | Description |
|---|---|---|
scrub_fields | password, token, api_key, secret, credit_card, ssn, … | Field names to redact. |
scrub_headers | true | Strips Authorization, Cookie, and similar headers. |
scrub_header_names | [] | Additional header names to redact. |
Storage & retention
| Setting | Default | Description |
|---|---|---|
retention_days | 30 | Older records are removed by the cleanup job. |
max_buffer_size | 10_000 | Hard cap on Redis buffer entries — protects Redis memory. |
buffer_ttl | 86_400 | Redis key TTL in seconds (24h). Stale buffered events expire automatically. |
persist_interval | 30 | How often (in seconds) the background thread flushes Redis to the DB. |
persist_batch_size | 100 | How many events to batch-insert per flush. |
auto_persist | true | Use the built-in thread. Set to false to drive persistence via ActiveJob. |
queue_name | "findbug" | Queue name when running in ActiveJob mode. |
Dashboard
| Setting | Default | Description |
|---|---|---|
web_username | nil | HTTP basic-auth username. Dashboard is disabled unless both username and password are set. |
web_password | nil | HTTP basic-auth password. |
web_path | "/findbug" | Mount path of the engine. |
Drive these from ENV["FINDBUG_USERNAME"] / ENV["FINDBUG_PASSWORD"]. The dashboard is automatically disabled if either is blank, so missing env vars in CI are safe.
Alerts
Alert channels (Email, Slack, Discord, Webhook) are created at runtime through the
dashboard at /findbug/alerts — no code changes required.
The only thing you configure in code is the throttle window:
Findbug.configure do |config|
config.alerts do |alerts|
alerts.throttle_period = 5.minutes
end
end
See the Alerts section for channel-by-channel details.
Release & logger
| Setting | Default | Description |
|---|---|---|
release | ENV["FINDBUG_RELEASE"] | A version identifier (e.g. git SHA). Useful for tracking which deploy introduced an error. |
environment | Rails.env | Override the environment label. |
logger | Rails.logger | Replace with your own logger if needed. |
Automatic error capture
FindBug captures these out of the box:
- Unhandled exceptions raised in any controller action
- Errors reported through
Rails.error.handle/Rails.error.report - Any exception that bubbles up through the Rack middleware stack
You generally don't need to do anything to enable this — it's wired up by the Railtie when the engine boots.
Manual error capture
To capture an exception you've rescued, call Findbug.capture_exception:
begin
risky_operation
rescue => e
Findbug.capture_exception(e, user_id: current_user.id)
end
For non-exception events (warnings, informational messages), use capture_message:
Findbug.capture_message("Rate limit exceeded", :warning, user_id: 123)
Severity levels: :info, :warning, :error.
Controller helpers
The Railtie installs several helper methods on ActionController::Base and
ActionController::API. Use them to attach request-scoped context that's
included with every error captured during the request.
findbug_set_user(user)
Attach the current user. id, email, and username (or name) are extracted via try.
before_action :set_findbug_context
def set_findbug_context
findbug_set_user(current_user)
end
findbug_set_context(data)
Deep-merge arbitrary key-value data into the current request's context.
findbug_set_context(
plan: current_user&.plan,
organization_id: current_org&.id
)
findbug_tag(key, value)
Add a single tag — short, filterable values that show up in the dashboard's filter bar.
findbug_tag(:region, "us-east-1")
findbug_capture(exception, extra = {})
Capture an exception with the current request context already merged in.
rescue ExternalAPIError => e
findbug_capture(e, api: "payment_gateway")
# handle gracefully
end
Breadcrumbs
Breadcrumbs are a trail of small events leading up to an error. They're per-request and cleared automatically after the response is sent.
findbug_breadcrumb("User clicked checkout", category: "ui")
findbug_breadcrumb("Payment API called", category: "http", data: { amount: 99.99 })
When an error fires later in the request, the breadcrumbs are attached to it. View them on the error detail page in the dashboard.
Performance tracking
Automatic instrumentation captures, per request:
- Total duration
- SQL query count and total DB time
- Slow queries (above
slow_query_threshold_ms) - N+1 patterns (detected automatically)
- View rendering count and time
To track a custom operation (e.g. an outbound API call), wrap it in Findbug.track_performance:
Findbug.track_performance("external_api_call") do
ExternalAPI.fetch_data
end
Custom transactions get their own row in the findbug_performance_events table with transaction_type = "custom".
Dashboard
The dashboard lives at config.web_path (default /findbug). It exposes:
- Overview — aggregate stats, the most recent unresolved errors, and a throughput chart.
- Errors — filterable list of all error events. Click through to see backtrace, request data, breadcrumbs, and user context.
- Performance — slowest transactions, N+1 hotspots, throughput over time.
- Alerts — manage alert channels (see below).
Authentication is plain HTTP basic auth. The dashboard is automatically disabled if web_username or web_password is blank.
Alerts
FindBug ships with four built-in alert channels. Configure them at /findbug/alerts:
| Channel | What you configure |
|---|---|
| Recipient addresses. Sends via your app's existing ActionMailer setup. | |
| Slack | Incoming-webhook URL. |
| Discord | Webhook URL. |
| Webhook | Any HTTPS endpoint — FindBug POSTs JSON. |
Channel configurations are stored encrypted in the findbug_alert_channels table,
so the alert UI uses Rails encryption — make sure you've set up
active_record.encryption in your Rails app (see
the Rails guide).
Throttling
Same-error notifications are throttled by fingerprint. The default window is 5 minutes —
configurable via config.alerts.throttle_period.
Sending a test notification
Each channel in the UI has a "Send test" button that fires a dummy payload to the channel — useful for verifying webhook URLs and email addresses before going live.
Rake tasks
FindBug ships with several maintenance tasks. Run rake -T findbug for the live list.
| Task | Description |
|---|---|
rails findbug:status | Show current configuration, Redis buffer lengths, and circuit-breaker state. Use this first when debugging. |
rails findbug:test | Raise and capture a test exception. Verifies the capture pipeline end-to-end. |
rails findbug:flush | Manually drain the Redis buffer to the database. Useful when shutting down or debugging. |
rails findbug:cleanup | Run the retention cleanup immediately. Same job the background thread runs daily. |
rails findbug:clear_buffers | Wipe the Redis buffer. Drops un-persisted events — use with care. |
rails findbug:db:stats | Print row counts for the FindBug tables. |
Database support
As of v0.5.0 FindBug is database-agnostic. It introspects your
ActiveRecord::Base.connection.adapter_name at migration and query time and
picks the correct column types and SQL functions automatically.
| Adapter | JSON column type | Time bucketing SQL |
|---|---|---|
| PostgreSQL / PostGIS | jsonb | date_trunc(...) |
| MySQL / Mysql2 | json | DATE_FORMAT(...) / DATE(...) |
| SQLite | text (with JSON serialisation in the model) | strftime(...) / DATE(...) |
The model's JSON accessors normalise reads to native Ruby Hash / Array
regardless of the underlying column type — your code is identical across adapters.
Multi-tenant applications
FindBug works with multi-tenant Rails apps, but the setup depends on how your app isolates tenants. The matrix below shows what's possible per database adapter.
| Adapter | Tenancy model | FindBug support |
|---|---|---|
| PostgreSQL | Schema-per-tenant (Apartment's default) | ✅ First-class — keep FindBug tables in the public schema (instructions below). |
| PostgreSQL · MySQL · SQLite | Row-level (tenant_id column on each table) |
✅ Nothing special — FindBug tables aren't tenant-scoped anyway. |
| MySQL | Database-per-tenant (Apartment with use_schemas = false) |
⚠️ Doable but awkward — you'll need a shared database the app connection can reach for FindBug's tables, plus cross-database references. Not recommended unless tenant count is small. |
| SQLite | File-per-tenant | ❌ Not practical — Apartment supports it nominally, but SQLite isn't typically chosen for multi-tenant production apps. Use row-level scoping instead. |
If you're on PostgreSQL with ros-apartment, follow the three steps below. On any other adapter — or with row-level multi-tenancy — FindBug works out of the box because its tables aren't part of your tenant data model.
PostgreSQL + Apartment (schema-per-tenant)
FindBug's tables must live in the public schema, and the dashboard path must bypass tenant switching.
1. Exclude FindBug models
Apartment.configure do |config|
config.excluded_models = %w[
Findbug::ErrorEvent
Findbug::PerformanceEvent
Findbug::AlertChannel
]
end
2. Exclude the dashboard path from your tenant elevator
class SwitchTenantMiddleware < Apartment::Elevators::Generic
EXCLUDED_PATHS = %w[/findbug].freeze
def parse_tenant_name(request)
return nil if EXCLUDED_PATHS.any? { |p| request.path.start_with?(p) }
# ... your tenant logic
end
end
3. Run migrations in the public schema
Use rails db:migrate as normal — Apartment will skip the excluded models when iterating through tenants, so the FindBug tables are created once in public.
MySQL + Apartment (database-per-tenant)
MySQL has no schemas in the PostgreSQL sense — SCHEMA is just an alias for
DATABASE. Apartment isolates tenants by giving each one its own MySQL database
and switching the connection per request. This works, but FindBug's shared tables need a home.
Two viable approaches:
-
Recommended — separate connection:
point FindBug's models at a dedicated "operational" database via
connects_to. Keeps FindBug's data fully isolated from tenant data, and avoids the cross-databaseGRANT/reference gymnastics. -
Shared base database: store FindBug tables in your "main" database
(the one Apartment connects to before switching). You'll need
GRANT SELECT, INSERT, UPDATE, DELETEon those tables for the per-tenant DB user, and to reference them asmain_db.findbug_error_eventsif your tenant connections can't see them by default.
In both cases, you still:
- Add FindBug models to
Apartment.excluded_modelsso it doesn't try to migrate them per tenant. - Exclude
/findbugfrom your tenant elevator (same code as the PostgreSQL example above).
SQLite multi-tenancy
SQLite has no schemas and no concept of "current database" — Apartment's only option is
a separate .sqlite3 file per tenant. This is rarely used in real applications
and pairs poorly with FindBug (no shared place for the FindBug tables to live without an
ATTACH dance on every connection).
If you're on SQLite and need tenant isolation, use row-level multi-tenancy
instead (a tenant_id column on your domain tables, scoped via
default_scope or a gem like acts_as_tenant).
FindBug's tables aren't tenant-scoped, so no extra setup is needed.
ActiveJob mode
By default a built-in background thread handles persistence. To use ActiveJob instead (with Sidekiq, GoodJob, Solid Queue, etc.):
# config/initializers/findbug.rb
config.auto_persist = false
Then schedule the jobs with your scheduler of choice:
# Every 30 seconds
Findbug::PersistJob.perform_later
# Daily
Findbug::CleanupJob.perform_later
The queue name is taken from config.queue_name (default "findbug").
API reference
Module-level
| Method | Description |
|---|---|
Findbug.config | Access the singleton Configuration object. |
Findbug.configure { |c| ... } | Configure FindBug at startup. Validates and returns the config. |
Findbug.enabled? | true when both config.enabled and config.redis_url are set. |
Findbug.capture_exception(exception, context = {}) | Capture a rescued exception with optional context. |
Findbug.capture_message(message, level = :info, context = {}) | Capture a non-exception event. |
Findbug.track_performance(name) { ... } | Wrap a block in a custom performance transaction. |
Findbug.logger | Internal logger — defaults to Rails.logger. |
Findbug.reset! | Reset cached config/logger/pool. Useful in tests. |
Controller helpers
| Method | Description |
|---|---|
findbug_set_user(user) | Attach the current user (id / email / username are auto-extracted). |
findbug_set_context(hash) | Deep-merge keys into the current request's context. |
findbug_tag(key, value) | Add a single filterable tag. |
findbug_breadcrumb(message, category:, data: {}) | Record a breadcrumb for the current request. |
findbug_capture(exception, extra = {}) | Capture an exception with the current request context already merged. |
Adapter helper
Useful if you're extending FindBug or writing your own migrations against the same multi-DB strategy:
| Method | Returns |
|---|---|
Findbug::AdapterHelper.adapter_name | Lower-cased name, e.g. "postgresql". |
Findbug::AdapterHelper.postgresql? / mysql? / sqlite? | Boolean predicates. |
Findbug::AdapterHelper.json_column_type | :jsonb, :json, or :text. |
Findbug::AdapterHelper.json_default(value) | Adapter-appropriate column default for a JSON field. |
Findbug::AdapterHelper.date_trunc_sql(interval, column) | Truncate-by-time SQL for "minute", "hour", or "day". |
Architecture
FindBug is built around one rule: never block the user's request. Here's how a captured event flows through the system:
- Exception occurs in your app.
- Middleware catches it (or you call
Findbug.capture_exception). - Scrubber filters sensitive fields from params, headers, and context.
- Redis buffer write — a background thread pushes the event to a Redis list. ~1–2 ms, never blocks the request.
- Circuit breaker — if Redis is unreachable, after 5 failures the breaker opens and capture is silently skipped for 30 seconds.
- Background persister — every
persist_intervalseconds (default 30s) pulls batches of events from Redis and bulk-inserts into your DB. - Aggregation — errors are grouped by fingerprint (class + normalised backtrace). Repeats increment
occurrence_count; perf events are stored individually for percentile analysis. - Dashboard queries the DB on demand.
Why a dedicated Redis connection pool?
FindBug maintains its own connection pool, separate from your app's Redis (or Sidekiq's). That way a spike in error capture can't starve your cache or job queue of connections.
Why aggregate by fingerprint?
A single bug can fire thousands of times. Without aggregation, each one would be its own row — making the dashboard useless. The fingerprint groups events by exception class and the top stack frames, so you see "this bug occurred 1,243 times" rather than 1,243 rows.
Testing
FindBug ships with a comprehensive RSpec suite. Clone the repo and run it locally:
$ git clone https://github.com/ITSSOUMIT/findbug.git
$ cd findbug
$ bundle install
$ bundle exec rspec
The suite runs against an in-memory SQLite database to exercise the adapter-agnostic code paths end-to-end, with adapter detection stubbed for PostgreSQL and MySQL-specific behaviour.
Disabling FindBug in tests
The default initializer already does this:
config.enabled = !Rails.env.test?
Troubleshooting
"Dashboard returns 401 Unauthorized"
Make sure both FINDBUG_USERNAME and FINDBUG_PASSWORD are set in the environment. The dashboard is automatically disabled when either is blank — that's a safety feature.
"Errors are captured but never show up in the dashboard"
The background persister might not be running. Check:
rails findbug:status— does it show a non-zero Error Queue Length?- If yes, run
rails findbug:flushto drain manually. - Confirm
auto_persististrue(or that you've scheduledFindbug::PersistJobin ActiveJob mode).
"Circuit breaker is open"
FindBug stopped trying to write to Redis after 5 consecutive failures. It will retry automatically after 30 seconds. Check that config.redis_url points to a reachable Redis instance.
"Migrations fail on MySQL with 'JSON column can't have a default value'"
As of v0.5.0 the migrations skip the default on MySQL automatically (MySQL pre-8.0.13 doesn't support JSON defaults). If you generated the migrations on an older FindBug version, regenerate them with rails generate findbug:install and re-run db:migrate.
"`uninitialized constant Findbug::AdapterHelper`" in a migration
This typically happens if the FindBug gem isn't loaded when the migration runs — make sure it's in your Gemfile outside any group restrictions (or include the db group your migrations run under).