RailsExceptionLog
Exception logging and triage for Rails applications
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:migrateThen 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 URLRestart 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:doctorRestart 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:
- Generates a migration that renames the
rails_nexus_*tables, keeping every recorded exception. - Converts
config/initializers/rails_nexus.rbtoconfig/initializers/rails_exception_log.rb. - Replaces the
RailsNexus::Enginemount withRailsExceptionLog::Engineat/rails_exception_log. - Updates
RailsNexus::/RailsNexus.references and"/rails_nexus"links inapp/,config/, andlib/. - Removes
gem "rails_nexus"from the Gemfile (kept ifbackup_nexusstill 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:customizeThis 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)
endChoosing 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 writtenPer-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 } }
endProtect 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
endConfiguration
# 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
endKeyboard 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 = falseWebhook 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)
endRate limiting: 10-minute cooldown per exception class + controller combination.
Cleanup / Retention
config.retention_days = 90 # Auto-delete after 90 daysRake 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 configurationFrontend 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 testBuild the gem:
gem build rails_exception_log.gemspecThe 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.0The 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.