Skip to content

fix: Log4Net integration filters SDK-internal log events to prevent infinite loops#5238

Closed
jamescrosswell wants to merge 7 commits into
mainfrom
fix/log4net-filter-sdk-logs
Closed

fix: Log4Net integration filters SDK-internal log events to prevent infinite loops#5238
jamescrosswell wants to merge 7 commits into
mainfrom
fix/log4net-filter-sdk-logs

Conversation

@jamescrosswell
Copy link
Copy Markdown
Collaborator

@jamescrosswell jamescrosswell commented May 16, 2026

By default, SentryOptions.DiagnosticLogger is either null (when Debug is off) or a ConsoleDiagnosticLogger/DebugDiagnosticLogger that writes to Console.Error or Debug.WriteLine. Neither of those has anything to do with log4net, so _options.LogWarning(...) in LogRateLimited never touches SentryAppender at all. No loop possible.

The problem occurs when the user does something like:

options.Debug = true;
options.DiagnosticLogger = new Log4NetDiagnosticLogger(LogManager.GetLogger("Sentry"));

At that point _options.LogWarning(...)Log4NetDiagnosticLogger.Log → log4net → SentryAppender.AppendCaptureEvent → background worker → 429 → repeat. The user has literally wired the SDK's own error output back into the SDK's event pipeline.

Solution

I've tried to add this:

private void LogRateLimited(SentryId? eventId, string responseDetail)
{
IsLoggingRateLimited.Value = true;
try
{
_options.LogWarning(
"{0}: Sentry rejected the envelope '{1}' due to rate limiting. " +
"This may indicate that you are sending too much data or have exceeded your quota. " +
"See https://docs.sentry.io/product/accounts/quotas/ for more information. " +
"Server response: {2}",
_typeName,
eventId,
responseDetail);
}
finally
{
IsLoggingRateLimited.Value = false;
}
}

And then this:

if (IsReentrant.Value || _isLoggingRateLimited())
{
return;
}
IsReentrant.Value = true;
try
{
AppendCore(loggingEvent);
}
finally
{
IsReentrant.Value = false;
}

But ultimately I don't think there's any good solution to this problem. See this comment in my discussion with the AI overlords.

Aside (consistency)

Added an AsyncLocal<bool> re-entrance guard to SentryAppender.Append (matching the Serilog and NLog pattern) so that synchronous re-entrant log calls are dropped.

This doesn't address the original issue which stems from the Background Worker. The background worker is started with Task.Run(DoWorkAsync) long before any appender call sets IsReentrant = true. So when LogRateLimited → _options.LogWarning fires on the background worker thread, IsReentrant.Value is always false there.

Closes #2953

#skip-changelog

…nfinite loops

Adds a logger-name filter (analogous to Serilog/MEL IsFromSentry checks) and an
AsyncLocal re-entrance guard so that SDK diagnostic logs routed through a user-supplied
Log4NetDiagnosticLogger are not captured as Sentry events, breaking the
429 → DiagnosticLogger.Log → SentryAppender → CaptureEvent → 429 cycle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread src/Sentry.Log4Net/SentryAppender.cs
@codecov
Copy link
Copy Markdown

codecov Bot commented May 16, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.58%. Comparing base (a9cb3b2) to head (079230c).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5238      +/-   ##
==========================================
- Coverage   74.12%   73.58%   -0.55%     
==========================================
  Files         508      494      -14     
  Lines       18282    17893     -389     
  Branches     3574     3486      -88     
==========================================
- Hits        13551    13166     -385     
  Misses       3861     3861              
+ Partials      870      866       -4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The AsyncLocal re-entrance guard already prevents infinite loops from
SDK-internal log events re-entering the appender. The IsSentryLogger
name-based filter was redundant and silently dropped events from any
user logger named "Sentry" or prefixed with "Sentry." — a valid naming
choice that would have caused silent data loss.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread src/Sentry.Log4Net/SentryAppender.cs
Comment thread src/Sentry.Log4Net/SentryAppender.cs
Comment thread src/Sentry.Log4Net/SentryAppender.cs Outdated
Comment thread src/Sentry/Http/HttpTransportBase.cs Outdated
Comment thread src/Sentry.Log4Net/SentryAppender.cs Outdated
Comment on lines +423 to 451
public void Append_IsLoggingRateLimited_DoesNotCaptureEvent()
{
_ = _fixture.Hub.IsEnabled.Returns(true);
var capturedEvents = 0;
_fixture.Hub.When(h => h.CaptureEvent(Arg.Any<SentryEvent>()))
.Do(_ =>
{
capturedEvents++;
// Simulate 429 - the transport will set IsLoggingRateLimited before logging the 429
_fixture.IsLoggingRateLimited = true;
try
{
var sut = _fixture.GetSut();
var reentrantEvt = new LoggingEvent(null, null, "app-logger", Level.Error, "429", null);
sut.DoAppend(reentrantEvt);
}
finally
{
_fixture.IsLoggingRateLimited = false;
}
});

var sut = _fixture.GetSut();
var evt = new LoggingEvent(null, null, "app-logger", Level.Error, "original", null);
sut.DoAppend(evt);

Assert.Equal(1, capturedEvents);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Append_IsLoggingRateLimited_DoesNotCaptureEvent doesn't isolate the rate-limit guard from the re-entrancy guard

The rate-limit test triggers DoAppend from inside the CaptureEvent callback, so IsReentrant.Value is already true when the inner call hits the guard — both conditions fire simultaneously and the test can't prove the _isLoggingRateLimited() check independently blocks capture. A bug that removed only that check would leave this test green.

Evidence
  • SentryAppender.Append sets IsReentrant.Value = true before calling AppendCore, and resets it in finally.
  • AppendCore calls _hub.CaptureEvent, which executes the .Do() callback synchronously while still inside the try block.
  • At that point IsReentrant.Value is true AND _fixture.IsLoggingRateLimited is set to true, so the guard if (IsReentrant.Value || _isLoggingRateLimited()) is satisfied by either condition alone.
  • Removing the _isLoggingRateLimited() branch from the production if would not cause this test to fail.
  • A proper isolation test would call sut.DoAppend from outside any active appender frame (e.g., directly before the outer call, with IsLoggingRateLimited = true already set).

Identified by Warden code-review · UQV-B4J

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.

Log4Net integration should filter SDK logs

2 participants