Skip to content

Log an error when an exception bypasses the dead letter queue - #354

Open
nabisobhi wants to merge 2 commits into
masterfrom
nabisobhi-dlq-observability
Open

nabisobhi wants to merge 2 commits into
masterfrom
nabisobhi-dlq-observability

Conversation

@nabisobhi

@nabisobhi nabisobhi commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Why

Dafda lets you register exception types that bypass the dead letter queue and propagate instead, crashing the consumer — the escape hatch for systemic failures like a database outage, where dead-lettering would silently drain a whole topic.

When that bypass fires, nothing is logged from the Consumer's point of view. The consumer just dies, with no record of which message triggered it or which exception was responsible.

Change

Consumer now takes an optional ILogger<Consumer> (defaulting to NullLogger<Consumer>.Instance, so existing call sites and tests are unaffected) and logs before rethrowing:

Exception of type {ExceptionType} bypassed the dead letter queue for message with key {Key} from topic {SourceTopic}. Failing the consumer

Logged at Error level with the exception attached. The real logger is resolved in both AddConsumer overloads.

To log at that point, the bypass check moved out of the exception filter into the catch body, which now rethrows with a bare throw;. That preserves the original stack trace. The check still runs before the retry/backoff branch, so a bypassed exception fails fast without waiting out a backoff delay.

Keeping the semantics of a throwing predicate

The bypass predicate is user-supplied, and moving it out of the exception filter changed one edge case. C# treats an exception thrown by a filter as a non-match, so previously a predicate that threw meant the original handler exception simply escaped — it was never dead-lettered. Evaluated in a catch body, a throwing predicate would instead replace the original exception and hide the real failure.

ShouldBypassDeadLetterQueue now absorbs that, keeping Dispatch clean:

try
{
    return deadLetterQueueBypass(exception);
}
catch
{
    return true;
}

Returning true routes into the bypass branch, whose bare throw; propagates the original exception — matching the old behavior. (false would not: that dead-letters the message, which the old code never did here.)

Overlap worth a reviewer's opinion

ConsumerHostedService.ConsumeAll already logs any escaping exception as "Unhandled error occurred while consuming messaging", so a bypassed exception will now produce two Error entries: this context-rich one, then the generic boundary one. That's a common pattern (detail at the site, generic at the boundary), but if you'd rather avoid the duplication, dropping this to Warning is an easy alternative.

Notes

  • No new NuGet dependencies; Microsoft.Extensions.Logging.Abstractions is already referenced.
  • No public API changeConsumer is internal.
  • Added LoggerSpy<T> test double rather than Mock<ILogger<Consumer>>, because Moq can't proxy a generic closed over an internal type without InternalsVisibleTo("DynamicProxyGenAssembly2").

Tests

Three new tests: the bypass logs the expected level/exception/message, dead-lettered messages do not log the bypass error, and a throwing BypassWhen predicate still surfaces the original exception without dead-lettering. TestConsumer 42/42, full suite 185/185, build clean with 0 warnings.

@nabisobhi
nabisobhi marked this pull request as draft August 30, 2026 18:56
Base automatically changed from nabisobhi-bookish-waddle to master September 10, 2026 12:22
@nabisobhi
nabisobhi force-pushed the nabisobhi-dlq-observability branch from ce37df9 to 47df637 Compare September 10, 2026 12:22
Consumer now accepts an optional ILogger<Consumer> (defaulting to NullLogger) and logs an error with the exception type, partition key and source topic when a registered bypass exception is about to propagate and crash the consumer. The real logger is resolved in both AddConsumer overloads.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Moving the bypass predicate out of the exception filter can replace the original failure when the predicate throws.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds contextual error logging when an exception bypasses the dead-letter queue.

Changes:

  • Injects an optional consumer logger.
  • Logs bypassed exceptions before rethrowing.
  • Adds logging tests and a logger spy.
File summaries
File Description
src/Dafda/Consuming/Consumer.cs Logs bypassed exceptions.
src/Dafda/Configuration/ConsumerServiceCollectionExtensions.cs Resolves consumer loggers.
src/Dafda.Tests/TestDoubles/LoggerSpy.cs Adds a logging test double.
src/Dafda.Tests/Consuming/TestConsumer.cs Tests bypass logging behavior.
src/Dafda.Tests/Builders/ConsumerBuilder.cs Supports logger injection in tests.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Dafda/Consuming/Consumer.cs
Evaluating the bypass predicate outside an exception filter meant a throwing predicate replaced the original handler exception. ShouldBypassDeadLetterQueue now treats a failing predicate as a bypass, so the bare rethrow propagates the original exception instead of dead-lettering it or losing it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The implementation preserves prior behavior and includes focused regression coverage for the new logging path.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@nabisobhi
nabisobhi marked this pull request as ready for review September 17, 2026 12:19
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.

2 participants