The project is in a healthy, maintained state
A beautiful exception logging gem that stores Rails exceptions in a database with a modern Tailwind UI dashboard
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

>= 0.5
>= 1.0
~> 1.4

Runtime

>= 7.0, < 9.0
 Project Readme

RailsExceptionLog

Exception logging and triage for Rails applications

Gem Version GitHub Sponsors

RailsExceptionLog captures the exceptions your Rails application raises, stores them in your own database, and gives you a dashboard to search, triage, and resolve them. No external service required.

Features

🚨 Exception Capture

  • Automatic Capture — Requests, ActiveJob, Sidekiq, rake tasks, and rails runner, with no code changes
  • Non-blocking — Records are written on a background thread with a bounded queue
  • Where It Happened — The file, line, and method in your code, with the surrounding source and git blame
  • Exception Dashboard — Dark/light theme, keyboard shortcuts, tabbed detail view
  • Cause Chains — Track root cause through chained exceptions
  • Breadcrumbs — Activity trail leading up to each error
  • Storm Protection — Circuit breaker for error floods
  • User Impact Ranking — See which errors affect the most users
  • Platform Detection — iOS, Android, Web, and API with automatic categorization

🔄 Workflow

  • Assignment — Assign exceptions to team members
  • Priority — Set critical/high/medium/low priority levels
  • Snooze — Temporarily silence exceptions (1h, 4h, 1d, 1w)
  • Mute — Permanently silence resolved exceptions
  • Comments — Add notes and status changes

🔍 Source Code Integration

  • Inline Source — View source code directly in backtraces
  • Git Blame — See who wrote each line and when
  • On-demand Loading — AJAX-powered source fetching
  • Security — Path traversal protection, read-only access

🔔 Notifications

  • Webhooks — POST to Slack, Discord, or custom endpoints
  • Telegram — Built-in notifier with rate limiting

🛠️ Developer Experience

  • Keyboard Shortcuts — Full keyboard navigation (/, j, k, Enter, ?)
  • Dark/Light Theme — Auto-detect or manual toggle, remembers preference
  • Ransack Search — Advanced search and filtering
  • RSS Feed — Subscribe to exception updates
  • Cleanup / Retention — Auto-delete old exceptions
  • Structured Logging — JSON log output with request context
  • Tailwind-compatible — Works with or without Tailwind CSS

Requirements

  • Ruby 3.2 or newer
  • Rails 8.0 or newer
  • SQLite 3, PostgreSQL, or MySQL/MariaDB

Installation

First time (5 minutes)

# Gemfile
gem "rails_exception_log"
bundle install
bin/rails generate rails_exception_log:install   # migration, initializer, route
bin/rails db:migrate

Then open config/initializers/rails_exception_log.rb and say who may see the dashboard (it denies everyone until you do):

config.auth_block = ->(controller) { controller.current_user&.admin? }

Check the setup and record a sample exception:

bin/rails rails_exception_log:doctor   # ✓/✗ for each part, with the fix for anything wrong
bin/rails rails_exception_log:test     # records a test exception and prints its URL

Restart the app server and open /rails_exception_log. From now on exceptions from requests, jobs, rake tasks, and rails runner are recorded automatically; nothing needs to be added to your controllers.

Updating

The install generator is safe to run again: it only adds what is missing and never overwrites your initializer.

bundle update rails_exception_log
bin/rails generate rails_exception_log:install
bin/rails db:migrate
bin/rails rails_exception_log:doctor

Restart the app server afterwards. In production, assets:precompile runs as part of your deploy; if you precompile in development, run it again (the doctor warns when the precompiled assets are out of date).

Moving from rails_nexus

Replace gem "rails_nexus" with gem "rails_exception_log", run bundle install, then run the same install generator. It detects the rails_nexus setup and:

  1. Generates a migration that renames the rails_nexus_* tables, keeping every recorded exception.
  2. Converts config/initializers/rails_nexus.rb to config/initializers/rails_exception_log.rb.
  3. Replaces the RailsNexus::Engine mount with RailsExceptionLog::Engine at /rails_exception_log.
  4. Updates RailsNexus:: / RailsNexus. references and "/rails_nexus" links in app/, config/, and lib/.
  5. Removes gem "rails_nexus" from the Gemfile (kept if backup_nexus still needs it).

Then run bin/rails db:migrate and bin/rails rails_exception_log:doctor.

Optional UI customization

The mounted dashboard already includes its CSS, Turbo, and Stimulus controllers. To copy the UI into your host application for customization, run:

bin/rails generate rails_exception_log:customize

This generates:

  • Self-contained layout with sidebar, topbar, and theme toggle
  • Tabbed detail view with metadata grid
  • Keyboard shortcuts dialog
  • Stimulus controllers for theme, sidebar, and interactions

Options: --layout-only, --views-only, --stimulus-only, --initializer-only

Start logging exceptions

Once the gem is installed, exceptions are recorded automatically. There is nothing to add to your controllers or jobs:

Where the error happens How it is caught
Any controller, including engine and API controllers Rack middleware
ActiveJob (any adapter) perform.active_job event, final failure only (attempts that retry_on will retry are skipped)
Plain Sidekiq jobs Sidekiq error handler, registered automatically
Rake tasks that load the app (=> :environment) Wrapper around Rake::Task#execute
rails runner, Rails.error.record/report Rails error reporter (Rails.error) subscriber

Each exception is recorded once even when several of these see it, and RailsExceptionLog always re-raises so Rails keeps its normal error handling.

Records are written on a background thread, so capture never slows down the request or job that failed. The queue is bounded (async_queue_size) and is drained when the process exits. In the test environment writes are synchronous so your tests can assert on them.

Each capture includes the full filtered request context, request ID, user agent, current-user identity, normalized fingerprint, cause chain, bounded backtrace, exception instance variables, runtime/system health, and any exception_data returned by your configuration. Jobs also store their class, ID, queue, attempt count, and arguments. Custom and exception data appear in the Context tab. Passwords, tokens, cookies, authorization headers, and the host application's configured filter parameters are replaced with [FILTERED].

When structured logging is enabled, the Rails logger receives the same context as a single JSON event. log_backtrace_limit, log_params, and log_user_info control how much is included in that event.

Exceptions you rescue yourself

begin
  PaymentGateway.charge(order)
rescue PaymentGateway::Timeout => e
  RailsExceptionLog.capture(e, data: { order_id: order.id })
  retry_later(order)
end

Choosing what is captured

config.capture_requests = true        # Rack middleware
config.capture_jobs = true            # ActiveJob
config.capture_sidekiq = true         # plain Sidekiq jobs
config.capture_rake_tasks = true      # rake tasks
config.capture_rails_errors = true    # Rails.error subscriber (runner, etc.)
config.capture_handled_errors = false # also record Rails.error.handle { }
config.capture_enabled = true         # master switch

# 404s and malformed requests are skipped by default. Subclasses match too.
config.ignored_exceptions += ["Pundit::NotAuthorizedError"]

config.async_capture = nil            # nil = on, except in the test environment
config.async_queue_size = 1000        # exceptions waiting to be written

Per-controller exception data

ExceptionLoggable is optional. Include it when a controller needs its own exception_data, or to keep the older rescue_from setup working:

class ApplicationController < ActionController::Base
  include RailsExceptionLog::ExceptionLoggable
  self.exception_data = ->(controller) { { tenant: controller.current_tenant&.slug } }
end

Protect the dashboard

The dashboard contains sensitive information. Do not expose it to unauthenticated users.

Authentication is applied globally by RailsExceptionLog::ApplicationController, including workflow mutations and source-code viewing. If auth_block is missing, returns false/nil, or raises an error, every dashboard request returns 403 Forbidden. Configure authentication:

# config/initializers/rails_exception_log.rb
Rails.application.config.to_prepare do
  RailsExceptionLog.configure do |config|
    config.auth_block = lambda do |controller|
      controller.current_user&.admin?
    end
  end
end

Configuration

# config/initializers/rails_exception_log.rb
Rails.application.config.to_prepare do
  RailsExceptionLog.configure do |config|
    # ─── General ─────────────────────────────────────────────
    config.application_name = "My App"       # Dashboard title
    config.per_page = 50                     # Items per page (default: 30)

    # ─── Authentication ──────────────────────────────────────
    config.auth_block = lambda do |controller|
      controller.current_user&.admin?
    end

    # ─── Exception Data ──────────────────────────────────────
    config.exception_data = lambda do |controller|
      {
        user_id: controller.current_user&.id,
        request_id: controller.request.request_id,
        environment: Rails.env
      }
    end

    # ─── Notifications ───────────────────────────────────────
    config.after_create = lambda do |exception|
      MyNotifier.alert(exception)
    end

    config.webhooks = ["https://hooks.slack.com/services/xxx"]
    config.webhook_timeout = 5
    config.webhook_headers = { "Authorization" => "Bearer token" }

    # ─── Cleanup ─────────────────────────────────────────────
    config.retention_days = 90               # Auto-delete after N days

    # ─── Appearance ──────────────────────────────────────────
    config.theme = "auto"                    # "light", "dark", or "auto"
    config.body_class = ""                   # Custom body CSS class
    config.use_host_layout = false           # Use host app's layout

    # ─── Advanced UI ─────────────────────────────────────────
    config.show_stats = true                 # Stats overview cards
    config.keyboard_shortcuts = true         # Keyboard shortcuts
    config.enable_navigation = true          # Prev/next in detail view
    config.backtrace_limit = 30              # Frames before collapse
    config.show_metadata = true              # Metadata grid
    config.show_environment = false          # Environment tab
    config.show_request = true               # Request params tab
    config.page_size_options = [25, 50, 100] # Page size selector

    # ─── Sidebar Links ───────────────────────────────────────
    config.sidebar_links = [
      { label: "GitHub", url: "https://github.com/myorg/myapp" },
      { label: "Docs", url: "/docs" }
    ]

    # ─── Logging ─────────────────────────────────────────────
    config.logging_enabled = true
    config.log_level = :info                 # :debug, :info, :warn, :error, :fatal
    config.log_file = "log/rails_exception_log.log"    # JSON log file (nil = none)
    config.log_backtrace = true
    config.log_params = true
    config.log_user_info = true
    config.log_backtrace_limit = 20
    config.deduplication_enabled = false
    config.deduplication_window = 300        # Seconds
  end
end

Keyboard shortcuts

Press ? in the dashboard to see all shortcuts:

Key Action
/ Focus search
j or ↓ Navigate down
k or ↑ Navigate up
Enter Open selected exception
p or ← Previous exception
n or → Next exception
t Toggle dark/light theme
1-5 Switch tabs
Esc Close panel / Clear
? Show shortcuts dialog

Search & Filter

  • Exception Type — Select dropdown, auto-submits on change
  • Source — Exact controller/action filter
  • Platform and Priority — Narrow errors by client and workflow priority
  • Status and Assignee — Show active, muted, snoozed, or unassigned errors
  • Occurrences — Focus on repeatedly occurring fingerprints
  • Time Range — Today, 3 days, 7 days, 30 days
  • Search — Search message, exception class, controller, and action with a 400ms debounce
  • Sortable Columns — Sort by exception, platform, source, message, count, workflow, or last seen
  • Active Filters — Pills with one-click dismiss

RailsExceptionLog uses Ransack for filtering and sorting and Pagy for bounded, configurable pagination. Filter, sort, page-size, and page parameters are preserved across Turbo Frame updates.

Notifications

Webhooks

config.webhooks = [
  "https://hooks.slack.com/services/T00/B00/xxx",
  "https://discord.com/api/webhooks/xxx"
]
config.webhook_timeout = 5
config.webhook_headers = { "Authorization" => "Bearer token" }
config.webhook_allowed_hosts = ["hooks.slack.com", "*.example.com"] # optional
config.webhook_denied_hosts = ["blocked.example.com"]
# HTTP is only available in development, and only with this explicit opt-in:
config.webhook_allow_http_in_development = false

Webhook destinations must use HTTPS, cannot contain URL credentials, and are resolved and pinned to a validated public IP before connecting. Private, loopback, link-local, metadata, multicast, unspecified, reserved, and redirect destinations are rejected. Logged delivery URLs redact their path and query so provider tokens are not persisted.

POST JSON to each URL on every new exception:

{
  "exception_class": "RuntimeError",
  "controller_name": "users",
  "action_name": "show",
  "message": "Something went wrong",
  "created_at": "2026-08-22T12:00:00Z",
  "dashboard_url": "http://localhost:3000/rails_exception_log/logged_exceptions/42"
}

Telegram

Set TELEGRAM_TOKEN and TELEGRAM_CHAT_ID environment variables:

export TELEGRAM_TOKEN="your-bot-token"
export TELEGRAM_CHAT_ID="your-chat-id"

Configure the notifier:

config.after_create = lambda do |exception|
  RailsExceptionLog::TelegramNotifier.new.notify(exception)
end

Rate limiting: 10-minute cooldown per exception class + controller combination.

Cleanup / Retention

config.retention_days = 90  # Auto-delete after 90 days

Rake tasks:

rake rails_exception_log:doctor           # Check the setup and show how to fix problems
rake rails_exception_log:test             # Record a sample exception
rake rails_exception_log:cleanup          # Delete exceptions older than retention_days
rake rails_exception_log:stats            # Show exception statistics
rake rails_exception_log:tail             # Tail JSON logs in real-time
rake rails_exception_log:export           # Export exceptions to JSON
rake rails_exception_log:test_webhook     # Test webhook configuration

Frontend setup

Hotwire (default)

RailsExceptionLog ships a prebuilt JavaScript asset containing Turbo and its namespaced Stimulus controllers. It does not require importmap-rails, does not modify the host JavaScript entry point, and works in hosts using Importmap, jsbundling/esbuild/Bun, or no JavaScript bundler. The bundle reuses a host Stimulus application exposed as window.Stimulus; otherwise it starts one engine-scoped application and reuses it across repeated loads.

Styling

RailsExceptionLog ships with its own CSS (rails_exception_log/application.css) that works out of the box with dark/light themes. The engine layout does not depend on the host application's stylesheet.

Tailwind CSS

If your app uses Tailwind, add the gem's views as a source:

/* Tailwind v4 */
@import "tailwindcss";
@source "/path/to/rails_exception_log/app/views";

Asset pipelines

The engine registers and precompiles its fingerprintable JavaScript and CSS assets automatically. Both Propshaft and Sprockets hosts can mount RailsExceptionLog without changing the host asset manifest.

The entry point is rails_exception_log/application; it does not replace or depend on the host app's application entry point. Turbo and Stimulus are served locally from their Rails gems rather than from a CDN.

Stimulus controllers

Four controllers are included and registered by the engine's entry point:

Controller Registered as Purpose
rails_exception_log_controller.js rails_exception_log Keyboard nav, search, tabs, copy, backtrace expand
rails_exception_log_detail_controller.js rails_exception_log-detail Exception detail interactions
rails_exception_log_sidebar_controller.js rails_exception_log-sidebar Mobile drawer toggle
rails_exception_log_theme_controller.js rails_exception_log-theme Dark/light/auto theme toggle

Data storage

The engine creates a rails_exception_log_exceptions table with columns for exception class, controller/action, message, backtrace, request, environment, user information, user agent, remote IP, and timestamps.

RailsExceptionLog applies Rails' configured parameter filters plus built-in password, token, authorization, cookie, session, and secret filters before persistence. Application-provided free-form messages can still contain sensitive data, so apply your normal database encryption, retention, backup, and access-control policies.

Security

The dashboard denies every request unless auth_block returns a truthy value. Webhooks use HTTPS by default, resolve and pin public destinations, reject redirects and private/reserved networks, and support explicit host allow/deny lists. See SECURITY.md for private vulnerability reporting.

Generators

Generator Description
rails generate rails_exception_log:install Install, update, or convert from rails_nexus (safe to re-run)
rails generate rails_exception_log:customize Generate customizable views, layout, and JS
rails generate rails_exception_log:customize --layout-only Only the layout
rails generate rails_exception_log:customize --views-only Only the views
rails generate rails_exception_log:customize --stimulus-only Only the Stimulus controller

Development

Run the test suite:

cd /path/to/rails_exception_log
RAILS_ENV=test bundle exec rails test

Build the gem:

gem build rails_exception_log.gemspec

The repository includes a Rails dummy application under test/dummy for engine integration testing.

CI/CD

GitHub Actions runs the test matrix, database compatibility checks, asset builds, and security checks on pushes and pull requests. Releases are published to RubyGems when a version tag is pushed.

Before the first release, set the RubyGems account's MFA level to UI and gem signin, then create an API key with only the Push rubygems scope, limited to the rails_exception_log gem. Leave MFA disabled for this CI key so RubyGems does not request a one-time password during automated releases. Add the key to the GitHub repository (or its release environment) as an Actions secret named RUBYGEMS_API_KEY. Never commit the key to the repository.

The publish command has no interactive input. A missing key or an account/key that requires API MFA therefore fails the release instead of waiting for an OTP prompt.

Create the release environment in GitHub and add required reviewers if you want a manual approval before publishing.

To release a version, update lib/rails_exception_log/version.rb and CHANGELOG.md, merge the changes to main, then push a matching tag:

git tag v2.3.0
git push origin v2.3.0

The workflow verifies that the tag matches the gem version, runs the tests, builds and validates the gem, then publishes it and creates the GitHub release.

Support

If you find RailsExceptionLog useful, consider supporting the project:

  • ⭐ Star the repo — Help others discover RailsExceptionLog
  • 🐛 Report issues — Help improve the gem
  • 💡 Contribute — Submit pull requests
  • 💰 Sponsor — GitHub Sponsors

Your support helps maintain and improve RailsExceptionLog for the Rails community.

License

RailsExceptionLog is released under the MIT License.