Skip to content

Log exception rows at Rails' debug_exception_log_level instead of always fatal - #67

Merged
PetrHeinz merged 14 commits into
mainfrom
claude/exception-log-level
Oct 2, 2026
Merged

PetrHeinz merged 14 commits into
mainfrom
claude/exception-log-level

Conversation

@PetrHeinz

@PetrHeinz PetrHeinz commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

The gem's ErrorEvent middleware has logged every exception at level fatal since the gem's initial commit (v0.1.0, 2021-02-13). Rails 7.1 (October 2023) added config.action_dispatch.debug_exception_log_level, the level ActionDispatch::DebugExceptions logs exceptions at. Its railtie default is :fatal, but config.load_defaults "7.1" and later set it to :error. The gem silences DebugExceptions' own logging (its logger is nil) and logs the exception row itself, so apps never got the level they configured in Rails. This is a long-standing gap, not a regression.

What changes:

  • ErrorEvent reads the request env key action_dispatch.debug_exception_log_level, the same way DebugExceptions does (request.get_header(...) → logger.add(level, ...)). Rails copies the configured level into every request env as a Logger severity, and the exception row is logged at that level.
  • Without the key (Rails before 7.1, or a request env Rails didn't build), the row stays fatal.
  • Unchanged from Log the real status of exception responses and honour log_rescued_responses #64: the log_rescued_responses check, the guard that keeps the app's exception the one that propagates, and the response rows with the real status (500, or 404 for RecordNotFound).

Behaviour change, for the 0.3.0 release notes:

  • Apps on config.load_defaults "7.1" or later now get their exception rows at level error instead of fatal. Alerts or saved searches that filter on level = fatal need adjusting to include error.
  • To keep fatal rows, set config.action_dispatch.debug_exception_log_level = :fatal. The gem silences Rails' own exception logging, so this only changes the rows sent to Better Stack.
  • An explicit setting such as :warn is followed too. Like Rails' own logging, a level below the logger's level isn't sent at all, so :debug with config.log_level = :info sends no exception rows. The response rows are still sent.
  • Unchanged: Rails 7.1+ apps that neither set the option nor load 7.1 or later defaults keep :fatal, and Rails before 7.1 stays at fatal.

PetrHeinz and others added 9 commits October 1, 2026 18:51
When a controller raises, no response row is logged, so 5xx and rescued
responses like RecordNotFound (404) are never recorded. The error row is
always fatal, although the gem silences DebugExceptions, which honours
config.action_dispatch.log_rescued_responses (Rails 7.0+) and
config.action_dispatch.debug_exception_log_level (Rails 7.1+).

The tests render exceptions like a production app and pin a response row
with the status the client gets (500, 404 from rescue_responses, also when
a template error wraps the exception), a fatal error row by default, no
error row for rescued responses when log_rescued_responses is false, and
the error row at debug_exception_log_level. The two existing specs that
raise now expect the response row too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Points logtail-rack at the claude/response-on-exception branch of
logtail-ruby-rack in the root Gemfile and in every gemfiles/*.gemfile, so CI
exercises HTTPEvents.status_for_exception. To be replaced by a follow-up
commit that requires logtail-rack ~> 0.2, >= 0.2.9 once it's released.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…og settings

The integration sets HTTPEvents.status_for_exception to
ActionDispatch::ExceptionWrapper.new(nil, exception).status_code, the status
ShowExceptions renders: config.action_dispatch.rescue_responses (404 for
ActiveRecord::RecordNotFound), with ActionView::Template::Error unwrapped the
way each Rails version does it, 500 otherwise.

ErrorEvent now reads the request env keys DebugExceptions reads, whose
logging the gem silences: no error row for rescued responses when
action_dispatch.log_rescued_responses is false (Rails 7.0+), and the row at
action_dispatch.debug_exception_log_level (Rails 7.1+). Without them, every
exception is logged at fatal as before.

The settings specs now change Rails.application.env_config, as Rails merges
it into every request env and overrides per-request values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
logtail-rack 0.2.8 times responses with the monotonic clock, which Timecop
doesn't freeze, so "Completed 200 OK in 0.0ms" no longer holds. The last CI
run on main still resolved logtail-rack 0.2.7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ErrorEvent logged inside its rescue without a guard, so an error while
building or writing the error event replaced the exception of the app. A
StandardError there now goes to Logtail::Config.instance.debug, and the
original exception is always re-raised. Kept local to ErrorEvent, so this
doesn't depend on logtail-rack's log_safely.

Both tests failed before the guard: the IOError of the logger and the
ArgumentError of the event builder propagated instead of the app's error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… says

config.load_defaults 7.1 sets config.action_dispatch.debug_exception_log_level
to :error, so following it would turn every exception row of most modern
apps, real 500s included, from fatal into error and break alerts on
level = fatal. That is too big a change for a patch release, so the error
row keeps its fatal level, as on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ErrorEvent stops reading action_dispatch.debug_exception_log_level and logs
at fatal exactly as on main, since load_defaults 7.1 sets it to :error for
most apps. Following it may come in a minor release. Honouring
log_rescued_responses, the status resolver and the logging guard stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ErrorEvent has logged every exception at fatal since the gem's first
version. Rails 7.1 added config.action_dispatch.debug_exception_log_level,
the level DebugExceptions logs exceptions at (railtie default :fatal,
config.load_defaults 7.1 and later set :error), but the gem silences
DebugExceptions' logging, so the setting never reached the exception rows.

The test that pinned the row at fatal under :error now expects error, and
new tests pin: :warn logs the RuntimeError and the rescued RecordNotFound at
warn, an app that doesn't set it keeps Rails' :fatal, and on Rails before
7.1, which has no such setting, every exception stays fatal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ErrorEvent reads the request env key
action_dispatch.debug_exception_log_level the way DebugExceptions does
(Rails copies the configured level into every request env as a Logger
severity) and logs the exception row at that level. Without the key, on
Rails before 7.1, the row stays fatal. This brings back the code that
6d8e4c9 took out of the 0.2.15 patch.

Apps on config.load_defaults 7.1 or later now get their exception rows at
error instead of fatal, so alerts and saved searches on level = fatal need
adjusting. Setting debug_exception_log_level to :fatal keeps the old level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@PetrHeinz
PetrHeinz marked this pull request as ready for review October 2, 2026 09:23
@PetrHeinz
PetrHeinz changed the base branch from claude/exception-response-status to main October 2, 2026 12:04
…level

main has #64 as one squashed commit, so lib/logtail-rails/error_event.rb and
spec/logtail-rails/error_event_spec.rb conflicted with this branch's copy of #64.
Both are resolved to this branch's side, which is main's #64 plus this PR's change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@PetrHeinz
PetrHeinz merged commit 9e3ba8a into main Oct 2, 2026
67 checks passed
@PetrHeinz
PetrHeinz deleted the claude/exception-log-level branch October 2, 2026 16:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant