Introduction
Log4j2 rolling file appender file lock errors occur when the appender cannot rename or delete the active log file during rollover because another process holds a lock on it. On Windows, this is particularly common because the operating system does not allow renaming or deleting open files. The error manifests as log entries being lost during rollover, the active log file growing without bound, or the application throwing java.nio.file.AccessDeniedException when attempting to rotate logs. In production, this leads to disk space exhaustion when the log file cannot be rolled and continues growing.
Symptoms
2024-03-15 10:23:45,123 main ERROR Unable to move file logs/app.log to logs/app-2024-03-15.log: java.nio.file.AccessDeniedException: logs\app.log -> logs\app-2024-03-15.log
2024-03-15 10:23:45,124 main ERROR Unable to rollover file logs/app.logOr:
WARN RollingFileManager Unable to delete old log file: logs/app-2024-03-14.log
java.nio.file.FileSystemException: logs\app-2024-03-14.log: The process cannot access the file because it is being used by another process.Disk space alert fires:
$ ls -lh logs/
-rw-r--r-- 1 app app 8.7G Mar 15 10:30 app.log # File never rolled, grew to 8.7GBCommon Causes
- Windows file locking: Windows does not allow renaming files that are open for writing
- Antivirus or backup software scanning log files: External process locks the file during scan
- Log file opened by another application: Log viewer or tail utility holds an exclusive lock
- Incorrect RolloverStrategy configuration: Size-based and time-based triggers conflict
- immediateFlush=false with DirectWriteRolloverStrategy: Buffered data not flushed before rollover
- File permission issues: Log directory permissions prevent the application from creating or deleting files
Step-by-Step Fix
Step 1: Configure DirectWriteRolloverStrategy for Windows
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<RollingFile name="RollingFile"
filePattern="logs/app-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
<Policies>
<SizeBasedTriggeringPolicy size="100 MB"/>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<DirectWriteRolloverStrategy maxFiles="30"/>
</RollingFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="RollingFile"/>
</Root>
</Loggers>
</Configuration>DirectWriteRolloverStrategy writes directly to the rolled file name, avoiding the rename step that fails on Windows.
Step 2: Enable file locking in the appender
<RollingFile name="RollingFile"
fileName="logs/app.log"
filePattern="logs/app-%d{yyyy-MM-dd}-%i.log.gz"
filePermissions="rw-rw-r--">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
<Policies>
<SizeBasedTriggeringPolicy size="50MB"/>
<TimeBasedTriggeringPolicy interval="6" modulate="true"/>
</Policies>
<DefaultRolloverStrategy max="20" compressionLevel="9">
<Delete basePath="logs" maxDepth="1">
<IfFileName glob="app-*.log.gz"/>
<IfLastModified age="30d"/>
</Delete>
</DefaultRolloverStrategy>
</RollingFile>Step 3: Configure async appender with proper flushing
```xml <Configuration status="WARN"> <Appenders> <RollingFile name="RollingFile" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd}-%i.log.gz" immediateFlush="true"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> <Policies> <SizeBasedTriggeringPolicy size="100MB"/> </Policies> <DefaultRolloverStrategy max="10"/> </RollingFile>
<Async name="Async" bufferSize="256" includeLocation="false"> <AppenderRef ref="RollingFile"/> </Async> </Appenders> <Loggers> <Root level="info"> <AppenderRef ref="Async"/> </Root> </Loggers> </Configuration> ```
Setting immediateFlush="true" ensures data is written to disk before rollover, preventing data loss.
Step 4: Handle rollover failure gracefully
```java import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configuration;
public class LogFileMonitor {
public static void checkRolloverStatus() { LoggerContext context = (LoggerContext) LogManager.getContext(false); Configuration config = context.getConfiguration();
config.getAppenders().values().stream() .filter(appender -> appender instanceof RollingFileManager) .forEach(appender -> { RollingFileManager manager = ((RollingFileAppender) appender).getManager(); File file = new File(manager.getFileName()); if (file.length() > 100 * 1024 * 1024) { // 100MB log.warn("Log file {} exceeds 100MB, rollover may be failing", file.getPath()); } }); } } ```
Prevention
- Use
DirectWriteRolloverStrategyon Windows to avoid file rename issues - Set
immediateFlush="true"to prevent data loss during rollover - Configure
maxFilesto prevent unlimited log file accumulation - Add a cron job or Windows scheduled task to verify log file sizes and rotation
- Use
Deleteaction inDefaultRolloverStrategyto automatically clean old logs - Monitor disk space on the log directory and alert before it reaches 80% capacity