Log4j2 LevelRangeFilter: One Log File per Level Range

Use the Log4j2 LevelRangeFilter to send each range of log levels to its own file. Learn why minLevel holds the more severe level, the defaults that changed in Log4j 2.21.0 and the mistakes that leave a file empty.

Log4j2

The Log4j2 LevelRangeFilter lets a log event through when its level lies between the two bounds minLevel and maxLevel, and drops every other event. The bound minLevel is the more severe level, such as WARN, and maxLevel is the less severe one, such as INFO, because Log4j2 gives severe levels smaller numbers.

We use LevelRangeFilter to split one log stream into files by level, for example a debug file for developers, an application file for support and an error file that triggers an alert.

The following example puts the filter inside a file appender, so logs/booking.log gets only the INFO and WARN events. The XML comment shows which levels pass.

<File name="AppFile" fileName="logs/booking.log">
  <!-- TRACE, DEBUG: denied | INFO, WARN: accepted | ERROR, FATAL: denied -->
  <LevelRangeFilter minLevel="WARN" maxLevel="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
  <PatternLayout pattern="%-5level %msg%n"/>
</File>

Notice that minLevel=”WARN” and maxLevel=”INFO” look reversed. When we swap the two values, the range is empty and the file stays empty.

Next, we see how Log4j2 orders the levels, build a booking app that writes three files, and cover the defaults that changed in Log4j 2.21.0. After that, we compare LevelRangeFilter with ThresholdFilter and LevelMatchFilter and fix the common mistakes.

1. How LevelRangeFilter Compares Levels

Every Log4j2 level has a number, which the method Level.intLevel() returns. A more severe level has a smaller number, so FATAL is 100 and TRACE is 600. The filter checks whether the number of the event level lies between the numbers of minLevel and maxLevel, both bounds included.

LevelintLevel()
OFF0
FATAL100
ERROR200
WARN300
INFO400
DEBUG500
TRACE600
ALL2147483647

So minLevel always holds the more severe level, i.e. the one with the smaller number. The range minLevel=”WARN” maxLevel=”INFO” covers 300 to 400, which matches WARN and INFO. The method Level.isInRange() runs the same check in Java.

boolean warn = Level.WARN.isInRange(Level.WARN, Level.INFO);       // true
boolean error = Level.ERROR.isInRange(Level.WARN, Level.INFO);     // false (200 is below 300)
boolean swapped = Level.WARN.isInRange(Level.INFO, Level.WARN);    // false (empty range)
Log4j2 level scale from OFF 0 through FATAL 100, ERROR 200, WARN 300, INFO 400, DEBUG 500, TRACE 600 to ALL. Three brackets mark the ranges FATAL to ERROR for booking-error.log, WARN to INFO for booking.log and DEBUG to TRACE for booking-debug.log, each labeled with its minLevel and maxLevel.
The minLevel of each range is the more severe end of the scale, with the smaller number.

1.1. onMatch and onMismatch

When the level is in the range, the filter returns the onMatch result, otherwise it returns the onMismatch result. Each result is one of three values.

  • ACCEPT logs the event and skips the other filters at the same stage.
  • DENY drops the event.
  • NEUTRAL lets the next filter decide, and if no filter follows, Log4j2 logs the event.

For a file that holds one level range, we set onMatch=”ACCEPT” and onMismatch=”DENY”. We use NEUTRAL for onMatch only when another filter, such as a MarkerFilter, must also check the event.

2. LevelRangeFilter Example With Three Log Files

A seat booking app logs every step at TRACE and DEBUG level, normal bookings at INFO, double bookings at WARN and failed payments at ERROR. The developers want the TRACE and DEBUG lines in their own file, support reads the INFO and WARN lines, and an alert tool watches a file with only ERROR and FATAL lines.

The following example is a small BookingService that logs with the Log4j2 API (log4j-api and log4j-core 2.26.1) on Java 25. The service books seat 12 for Lokesh, and seat 14 for Alex fails.

<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-core</artifactId>
  <version>2.26.1</version>
</dependency>
log.trace("Entering book({}, {})", seat, name);
log.debug("Checking seat {}", seat);
if (seat == 14) {
  log.warn("Seat {} is already booked", seat);
  log.error("Payment failed for {}", name);
  log.fatal("Booking database is down");
  return;
}
log.info("Booked seat {} for {}", seat, name);

The configuration file has three file appenders, and each appender has its own LevelRangeFilter. The root logger is set to TRACE, so all six levels reach the appenders, and the console appender has no filter.

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
  <Appenders>
    <Console name="Console" target="SYSTEM_OUT">
      <PatternLayout pattern="%-5level %msg%n"/>
    </Console>

    <!-- TRACE and DEBUG -->
    <File name="DebugFile" fileName="logs/booking-debug.log">
      <LevelRangeFilter minLevel="DEBUG" maxLevel="TRACE" onMatch="ACCEPT" onMismatch="DENY"/>
      <PatternLayout pattern="%-5level %msg%n"/>
    </File>

    <!-- INFO and WARN -->
    <File name="AppFile" fileName="logs/booking.log">
      <LevelRangeFilter minLevel="WARN" maxLevel="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
      <PatternLayout pattern="%-5level %msg%n"/>
    </File>

    <!-- ERROR and FATAL -->
    <File name="ErrorFile" fileName="logs/booking-error.log">
      <LevelRangeFilter minLevel="FATAL" maxLevel="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
      <PatternLayout pattern="%-5level %msg%n"/>
    </File>
  </Appenders>

  <Loggers>
    <Root level="TRACE">
      <AppenderRef ref="Console"/>
      <AppenderRef ref="DebugFile"/>
      <AppenderRef ref="AppFile"/>
      <AppenderRef ref="ErrorFile"/>
    </Root>
  </Loggers>
</Configuration>

The console prints all eight events of the two bookings. Each file gets only the events in its range, and every event lands in one of the three files only.

TRACE Entering book(12, Lokesh)
DEBUG Checking seat 12
INFO  Booked seat 12 for Lokesh
TRACE Entering book(14, Alex)
DEBUG Checking seat 14
WARN  Seat 14 is already booked
ERROR Payment failed for Alex
FATAL Booking database is down
TRACE Entering book(12, Lokesh)
DEBUG Checking seat 12
TRACE Entering book(14, Alex)
DEBUG Checking seat 14
INFO  Booked seat 12 for Lokesh
WARN  Seat 14 is already booked
ERROR Payment failed for Alex
FATAL Booking database is down

The old way to get a single level in a file was a range with the same level on both ends, such as minLevel=”DEBUG” maxLevel=”DEBUG”. That range still works, but it leaves TRACE and FATAL events out of every file, so we cover the whole scale with adjacent ranges.

2.1. The Same Filter in log4j2.properties

Projects that keep the Log4j2 configuration in a properties file declare the filter as a component of the appender. The key appender.app.filter.range.type names the plugin, and the other keys under range set its attributes.

appender.app.type = File
appender.app.name = AppFile
appender.app.fileName = logs/booking.log
appender.app.layout.type = PatternLayout
appender.app.layout.pattern = %-5level %msg%n
appender.app.filter.range.type = LevelRangeFilter
appender.app.filter.range.minLevel = WARN
appender.app.filter.range.maxLevel = INFO
appender.app.filter.range.onMatch = ACCEPT
appender.app.filter.range.onMismatch = DENY

rootLogger.level = TRACE
rootLogger.appenderRef.app.ref = AppFile

With the same six log calls, logs/booking.log gets only INFO and WARN, as with the XML file.

2.2. Rolling Files in Production

A plain File appender grows forever, so in production we use a RollingFile appender with the same filter. The filter goes inside the appender, next to the layout and the policies.

<RollingFile name="AppFile" fileName="logs/booking.log"
             filePattern="logs/booking-%d{yyyy-MM-dd}-%i.log.gz">
  <LevelRangeFilter minLevel="WARN" maxLevel="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
  <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%t] %c{1} - %msg%n"/>
  <Policies>
    <TimeBasedTriggeringPolicy/>
    <SizeBasedTriggeringPolicy size="20 MB"/>
  </Policies>
  <DefaultRolloverStrategy max="10"/>
</RollingFile>

The appender starts a new file every day and whenever the file reaches 20 MB, and %i numbers the archives of one day. The production configuration in the example project uses one RollingFile per range.

2026-10-04 20:43:37.046 INFO  [main] LevelRangeFilterDemo - Booked seat 12 for Lokesh
2026-10-04 20:43:37.046 WARN  [main] LevelRangeFilterDemo - Seat 14 is already booked

3. Where Log4j2 Checks the Filter

An event passes several checks on its way to a file, and LevelRangeFilter sees the event only when the earlier checks let it through. The logger level comes first. For example, with <Root level=”INFO”>, Log4j2 drops the TRACE and DEBUG events before any appender gets them, so booking-debug.log stays empty even though its filter accepts DEBUG.

Flow of one log event. First the logger level check drops events below the logger level. Then an optional filter on the AppenderRef, then the filter inside the appender, here LevelRangeFilter, and finally the appender writes to the file. Each check can deny the event.
A LevelRangeFilter only sees the events that the logger level and the earlier filters let through.

We can put the filter in two places on the appender side, and both give the same result for one logger.

  • Inside the appender, as in section 2, the filter applies to every logger that writes to the appender.
  • Inside an AppenderRef, the filter applies only to the logger that holds the reference, so two loggers can send different level ranges to one file.
<Root level="TRACE">
  <AppenderRef ref="AppFile">
    <LevelRangeFilter minLevel="WARN" maxLevel="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
  </AppenderRef>
</Root>

With this configuration and an AppFile without its own filter, booking.log again gets only the INFO and WARN lines.

4. Default Values of LevelRangeFilter

Every attribute is optional. The defaults below come from the Log4j2 filters manual and hold since Log4j 2.21.0.

AttributeDefault since 2.21.0Default up to 2.20.0
minLevelOFFERROR
maxLevelALLERROR
onMatchNEUTRALNEUTRAL
onMismatchDENYDENY

The changed defaults make a filter with only one bound behave differently after an upgrade. For example, <LevelRangeFilter minLevel=”ERROR” onMatch=”ACCEPT” onMismatch=”DENY”/> reads like “ERROR only”. On Log4j 2.20.0 it does accept only ERROR, because maxLevel defaults to ERROR. On 2.21.0 and later, maxLevel is ALL, so the same filter accepts ERROR and every less severe level.

FilterLog4j 2.20.0Log4j 2.21.0 and later
minLevel=”ERROR”ERRORERROR, WARN, INFO, DEBUG, TRACE
maxLevel=”INFO”ERROR, WARN, INFOFATAL, ERROR, WARN, INFO

So we always write both bounds and both results, which keeps the filter’s meaning the same on every Log4j2 version.

5. LevelRangeFilter vs ThresholdFilter and LevelMatchFilter

Log4j2 has two other filters that check only the level. A ThresholdFilter matches events at least as severe as one level, and a LevelMatchFilter matches a single level. The level attribute of an AppenderRef also sets a minimum severity, without any filter.

NeedConfigurationEvents in the file
A range of levels<LevelRangeFilter minLevel=”WARN” maxLevel=”INFO” onMatch=”ACCEPT” onMismatch=”DENY”/>INFO, WARN
One exact level<LevelMatchFilter level=”WARN” onMatch=”ACCEPT” onMismatch=”DENY”/>WARN
A minimum severity<ThresholdFilter level=”INFO” onMatch=”ACCEPT” onMismatch=”DENY”/>INFO to FATAL
A minimum severity per logger<AppenderRef ref=”AppFile” level=”INFO”/>INFO to FATAL

Many older configurations build a range from two ThresholdFilter elements inside <Filters>. The first one denies ERROR and above, and the second one accepts INFO and above. The result is the same as our single LevelRangeFilter, but it is harder to read.

<Filters>
  <ThresholdFilter level="ERROR" onMatch="DENY" onMismatch="NEUTRAL"/>
  <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>

For events that also need a check on something other than the level, we combine LevelRangeFilter with a MarkerFilter or a DynamicThresholdFilter inside <Filters>.

6. Common LevelRangeFilter Mistakes

Most problems with LevelRangeFilter show up as an empty file, without an error message. Three causes cover most of the empty files.

MistakeResultFix
Bounds swapped, minLevel=”INFO” maxLevel=”WARN”Empty file, the range from 400 down to 300 is emptyPut the more severe level in minLevel
Logger level stricter than the range, <Root level=”INFO”> with a DEBUG rangeEmpty debug fileSet the logger level to the least severe level of all ranges
Only one bound setDifferent levels on Log4j 2.20.0 and 2.21.0+Set minLevel and maxLevel

The swapped bounds come from Log4j 1.x, where the LevelRangeFilter in org.apache.log4j.varia used levelMin for the less severe level. Log4j2 numbers the levels the other way round, so configurations copied from Log4j 1.x need the two values swapped.

7. Conclusion

A LevelRangeFilter accepts the events whose level lies between minLevel and maxLevel, and minLevel is always the more severe level. With one filter per appender and adjacent ranges, such as FATAL to ERROR, WARN to INFO and DEBUG to TRACE, every event goes to one file only.

We write both bounds and both results, because the defaults changed in Log4j 2.21.0. When a file stays empty, we check the order of the bounds and the logger level before anything else.

8. References

Happy Learning !!

Source Code on Github

Leave a Comment

    • Additivity determines whether a logger passes its log messages to its parent logger. When it is true, logger will pass its log messages to its own appenders and to those of its parent logger(s). If “com.example” logger and its parent “com” logger both have a console appender, a log message from “com.example” will be printed twice—once by “com.example” and once by “com”.

      When set to false, logger will not pass its log messages to its parent logger(s). So, in previous example, a log message from “com.example” will only be printed once by “com.example” and not by any parent logger.

      Additivity is used for avoiding the duplicate logs in multiple log files. In this example, additivity does not make such difference because there is only one logger.

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.