Skip to content

Log the real status of exception responses and honour log_rescued_responses - #64

Merged
PetrHeinz merged 10 commits into
mainfrom
claude/exception-response-status
Oct 2, 2026
Merged

PetrHeinz merged 10 commits into
mainfrom
claude/exception-response-status

Conversation

@PetrHeinz

@PetrHeinz PetrHeinz commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

When a controller raises, the gem logs the request row and a fatal error row, but no http_response_sent row. The status the client gets is never recorded, so dashboards and alerts on 5xx stay blind, and a 404 from ActiveRecord::RecordNotFound shows up only as a fatal error. Yesterday's red-team reproduced this on all seven Rails versions it tested: an exception gave no response row at all, and RecordNotFound was logged as fatal.

What changes:

  • The Rails integration sets the new Logtail::Integrations::Rack::HTTPEvents.status_for_exception resolver (from the logtail-rack PR Log a response row with status 500 when the app raises logtail-ruby-rack#27), so the response row that rack now logs for an exception gets the status Rails responds with: ActionDispatch::ExceptionWrapper.new(nil, exception).status_code. That's the same call ShowExceptions and DebugExceptions use, so it honours config.action_dispatch.rescue_responses (RecordNotFound → 404, ParameterMissing → 400, anything else → 500) and unwraps ActionView::Template::Error the way Rails does, on every Rails version.
  • The gem silences DebugExceptions' own logging and logs the error row from its ErrorEvent middleware instead. That middleware now honours config.action_dispatch.log_rescued_responses = false (Rails 7.0+) like DebugExceptions does, through the same request env key: no error row for rescued responses such as RecordNotFound. The response row with the 404 is still logged.
  • ErrorEvent's own logging can no longer replace the app's exception: a StandardError while building or writing the error row goes to Logtail::Config.instance.debug, and the original exception is always re-raised. This is local to ErrorEvent, so the PR only needs the new rack API.

Behaviour and compatibility:

  • Every request whose controller raises now produces a response row with the status the client receives, for example Completed 404 Not Found in 12.3ms.
  • Error rows stay at level fatal, as before. Following config.action_dispatch.debug_exception_log_level is left out on purpose: load_defaults 7.1 sets it to :error, which would turn every exception row of most apps from fatal into error and break alerts on level = fatal.
  • Apps that turned log_rescued_responses off stop getting error rows for rescued responses, matching their Rails logs. Nothing changes for apps that keep the default.
  • To change the logged status for an exception class, use config.action_dispatch.rescue_responses, which the integration follows.

Targets the 0.2.15 patch release.

Depends on logtail-rack 0.2.9, which isn't released yet. The TEMP: run CI against the logtail-rack branch until 0.2.9 is released commit points the root Gemfile and every gemfiles/*.gemfile at the claude/response-on-exception branch of logtail-ruby-rack, so CI runs against the real API. The gemspec floor isn't bumped here: at merge time, after logtail-rack 0.2.9 is released, the TEMP commit gets replaced by a follow-up commit that requires logtail-rack ~> 0.2, >= 0.2.9. Until then this must not be merged.

It also carries the "Expect real response durations in the request specs" commit that the other open logtail-rails PRs share, byte for byte: logtail-rack 0.2.8 times responses with the monotonic clock, which Timecop doesn't freeze, so the three specs expecting 0.0ms now fail against the released rack too.

The first commit only adds the tests and is expected to fail on CI; the TEMP commit and the fixes come after it.

🤖 Generated with Claude Code

PetrHeinz and others added 7 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>
@PetrHeinz PetrHeinz changed the title Log the real status of exception responses and honour Rails' exception log settings Log the real status of exception responses and honour log_rescued_responses Oct 1, 2026
@PetrHeinz
PetrHeinz marked this pull request as ready for review October 2, 2026 12:08
@PetrHeinz
PetrHeinz merged commit 3cad8b1 into main Oct 2, 2026
66 checks passed
@PetrHeinz
PetrHeinz deleted the claude/exception-response-status branch October 2, 2026 12:08
PetrHeinz added a commit that referenced this pull request Oct 2, 2026
…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>
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