cookiejack15 opened a new issue, #4068:
URL: https://github.com/apache/logging-log4j2/issues/4068
## Bug Description
`SLF4JLogger.atFatal()` returns `atLevel(Level.TRACE)` instead of
`atLevel(Level.FATAL)`. This is a copy-paste error introduced in commit
`113a8e85f4` (LOG4J2-3647, 2023-01-13). Every call to
`logger.atFatal().log(...)` through the `log4j-to-slf4j` bridge emits the
message at TRACE level instead of ERROR (SLF4J's equivalent of FATAL). In any
environment where TRACE is disabled — virtually every production deployment —
the message is silently discarded.
**Affected file:**
`log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JLogger.java` (line
366)
**Introduced in:** 2.20.0 (commit 113a8e85f4, LOG4J2-3647)
**Affected versions:** 2.20.0 through 2.24.3 (current latest stable)
**Not affected:** Traditional API `logger.fatal()` works correctly — only
the fluent `LogBuilder` API is affected.
## Root Cause
All `at*()` methods delegate to `atLevel()` with the corresponding level
constant. `atFatal()` was copied from `atTrace()` and the level was never
updated:
```java
public LogBuilder atTrace() {
return atLevel(Level.TRACE); // correct
}
public LogBuilder atDebug() {
return atLevel(Level.DEBUG); // correct
}
public LogBuilder atInfo() {
return atLevel(Level.INFO); // correct
}
public LogBuilder atWarn() {
return atLevel(Level.WARN); // correct
}
public LogBuilder atError() {
return atLevel(Level.ERROR); // correct
}
@Override
public LogBuilder atFatal() {
return atLevel(Level.TRACE); // BUG: should be Level.FATAL
}
```
## What Happens at Runtime
With `Level.TRACE` (the bug), `convertLevel()` returns `TRACE_INT`. Two
paths lead to message loss:
**Path A — Logback backend (`LAZY_LEVEL_CHECK = true`):**
`atFatal()` → `atLevel(Level.TRACE)` → `SLF4JLogBuilder` → `logMessage()`
passes `Level.TRACE` to SLF4J → Logback checks if TRACE is enabled → disabled
in production → message silently dropped.
**Path B — Non-Logback SLF4J backend (`LAZY_LEVEL_CHECK = false`):**
`atFatal()` → `atLevel(Level.TRACE)` → `AbstractLogger.atLevel()` calls
`isEnabled(Level.TRACE)` → returns false → returns `LogBuilder.NOOP` → message
never constructed.
## Proof of Concept
```java
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
// Requires log4j-to-slf4j bridge with SLF4J/Logback backend
// Logger level set to INFO (standard production configuration)
public class AtFatalLogLoss {
private static final Logger logger =
LogManager.getLogger(AtFatalLogLoss.class);
public static void main(String[] args) {
// Traditional API: works correctly
logger.fatal("TRADITIONAL: System crash detected");
// Output: logged at ERROR level in SLF4J ✓
// Fluent API: message silently lost
logger.atFatal().log("FLUENT: System crash detected");
// Output: NOTHING — logged at TRACE, filtered out ✗
}
}
```
## Unit Test
```java
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;
import static org.junit.jupiter.api.Assertions.*;
class SLF4JAtFatalLogLossTest {
@Test
void atFatal_message_is_silently_lost_in_production_config() {
LoggerContext logbackContext = (LoggerContext)
LoggerFactory.getILoggerFactory();
ch.qos.logback.classic.Logger logbackLogger =
logbackContext.getLogger("test.AtFatal");
logbackLogger.setLevel(ch.qos.logback.classic.Level.INFO);
ListAppender<ILoggingEvent> appender = new ListAppender<>();
appender.start();
logbackLogger.addAppender(appender);
Logger log4jLogger = LogManager.getLogger("test.AtFatal");
// Traditional API: message logged correctly as ERROR
log4jLogger.fatal("traditional fatal");
assertEquals(1, appender.list.size());
assertEquals(ch.qos.logback.classic.Level.ERROR,
appender.list.get(0).getLevel());
appender.list.clear();
// Fluent API: message SILENTLY LOST
log4jLogger.atFatal().log("fluent fatal");
assertEquals(1, appender.list.size(),
"atFatal().log() should produce a log event, but the message is
silently lost");
}
@Test
void atFatal_logs_at_trace_level_instead_of_error() {
LoggerContext logbackContext = (LoggerContext)
LoggerFactory.getILoggerFactory();
ch.qos.logback.classic.Logger logbackLogger =
logbackContext.getLogger("test.AtFatalLevel");
logbackLogger.setLevel(ch.qos.logback.classic.Level.TRACE);
ListAppender<ILoggingEvent> appender = new ListAppender<>();
appender.start();
logbackLogger.addAppender(appender);
Logger log4jLogger = LogManager.getLogger("test.AtFatalLevel");
log4jLogger.atFatal().log("this should be ERROR level");
assertEquals(1, appender.list.size());
// FAILS: actual level is TRACE, not ERROR
assertEquals(ch.qos.logback.classic.Level.ERROR,
appender.list.get(0).getLevel(),
"atFatal() should map to SLF4J ERROR, but actual level is: " +
appender.list.get(0).getLevel());
}
}
```
## Proposed Fix
One-word fix — change `TRACE` to `FATAL`:
```diff
@Override
public LogBuilder atFatal() {
- return atLevel(Level.TRACE);
+ return atLevel(Level.FATAL);
}
```
## Affected Versions
| Version | Status |
|---------|--------|
| < 2.20.0 | Not affected |
| 2.20.0 – 2.24.3 | Affected |
The fluent `atFatal()` method in `SLF4JLogger` has been non-functional since
its introduction in 2.20.0 (7 releases, 3+ years).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]