close

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:

Requirements

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:

Database-agnostic migrations

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:

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

SettingDefaultDescription
enabledtrueTurn FindBug on/off globally. Set to false in test envs.
redis_urlredis://localhost:6379/1Redis URL for the buffer. We recommend a dedicated database number.
redis_pool_size5Size of the dedicated Redis connection pool.
redis_pool_timeout1Seconds to wait for a connection before giving up (fail-fast).

Error capture

SettingDefaultDescription
sample_rate1.0Fraction 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

SettingDefaultDescription
performance_enabledtrueMaster switch for the performance instrumenter.
performance_sample_rate0.1Fraction of requests to instrument (10% is fine for most apps).
slow_request_threshold_ms0Only persist requests slower than this. 0 = all sampled requests.
slow_query_threshold_ms100SQL 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].

SettingDefaultDescription
scrub_fieldspassword, token, api_key, secret, credit_card, ssn, …Field names to redact.
scrub_headerstrueStrips Authorization, Cookie, and similar headers.
scrub_header_names[]Additional header names to redact.

Storage & retention

SettingDefaultDescription
retention_days30Older records are removed by the cleanup job.
max_buffer_size10_000Hard cap on Redis buffer entries — protects Redis memory.
buffer_ttl86_400Redis key TTL in seconds (24h). Stale buffered events expire automatically.
persist_interval30How often (in seconds) the background thread flushes Redis to the DB.
persist_batch_size100How many events to batch-insert per flush.
auto_persisttrueUse the built-in thread. Set to false to drive persistence via ActiveJob.
queue_name"findbug"Queue name when running in ActiveJob mode.

Dashboard

SettingDefaultDescription
web_usernamenilHTTP basic-auth username. Dashboard is disabled unless both username and password are set.
web_passwordnilHTTP basic-auth password.
web_path"/findbug"Mount path of the engine.
Don't commit credentials

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

SettingDefaultDescription
releaseENV["FINDBUG_RELEASE"]A version identifier (e.g. git SHA). Useful for tracking which deploy introduced an error.
environmentRails.envOverride the environment label.
loggerRails.loggerReplace with your own logger if needed.

Automatic error capture

FindBug captures these out of the box:

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 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:

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:

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:

ChannelWhat you configure
EmailRecipient addresses. Sends via your app's existing ActionMailer setup.
SlackIncoming-webhook URL.
DiscordWebhook URL.
WebhookAny 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.

TaskDescription
rails findbug:statusShow current configuration, Redis buffer lengths, and circuit-breaker state. Use this first when debugging.
rails findbug:testRaise and capture a test exception. Verifies the capture pipeline end-to-end.
rails findbug:flushManually drain the Redis buffer to the database. Useful when shutting down or debugging.
rails findbug:cleanupRun the retention cleanup immediately. Same job the background thread runs daily.
rails findbug:clear_buffersWipe the Redis buffer. Drops un-persisted events — use with care.
rails findbug:db:statsPrint 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.

AdapterJSON column typeTime bucketing SQL
PostgreSQL / PostGISjsonbdate_trunc(...)
MySQL / Mysql2jsonDATE_FORMAT(...) / DATE(...)
SQLitetext (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.

AdapterTenancy modelFindBug 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.
TL;DR

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:

In both cases, you still:

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

MethodDescription
Findbug.configAccess 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.loggerInternal logger — defaults to Rails.logger.
Findbug.reset!Reset cached config/logger/pool. Useful in tests.

Controller helpers

MethodDescription
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:

MethodReturns
Findbug::AdapterHelper.adapter_nameLower-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:

  1. Exception occurs in your app.
  2. Middleware catches it (or you call Findbug.capture_exception).
  3. Scrubber filters sensitive fields from params, headers, and context.
  4. Redis buffer write — a background thread pushes the event to a Redis list. ~1–2 ms, never blocks the request.
  5. Circuit breaker — if Redis is unreachable, after 5 failures the breaker opens and capture is silently skipped for 30 seconds.
  6. Background persister — every persist_interval seconds (default 30s) pulls batches of events from Redis and bulk-inserts into your DB.
  7. Aggregation — errors are grouped by fingerprint (class + normalised backtrace). Repeats increment occurrence_count; perf events are stored individually for percentile analysis.
  8. 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:

"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).