有人知道如何配置或扩展 lo4j2 以在删除后重新创建日志吗?

Does anybody know how to configure or extend lo4j2 to recreate logs after deletion?

如果日志文件在运行时被删除,Log4j2 不会重新创建它们。例如,粗心的管理员删除了应用程序当前写入自己日志的日志文件。
实际结果:日志没有写入文件。
想要的结果:log4j2 在第一次尝试写入文件并继续使用该文件后重新创建文件。

通过 cron 或其他方式手动重新创建是行不通的,因为 log4j2 "remembers" 文件的文件描述符并继续使用它,即使在删除旧文件并创建新文件后也是如此。

在 Whosebug 上,我只找到了一种解决方法 (),它看起来像这样:

package org.apache.log4j;

import java.io.File;
import org.apache.log4j.spi.LoggingEvent;

public class ModifiedRollingFileAppender extends RollingFileAppender {

@Override 
public void append(LoggingEvent event) {
    checkLogFileExist();
    super.append(event);
}

private void checkLogFileExist(){
    File logFile = new File(super.fileName);
    if (!logFile.exists()) {
        this.activateOptions();
    }
}
}

我不喜欢它因为:
1)它"little bit"慢
每次我们写入事件时,我们也会执行 checkLogFileExist() 并检查文件系统中的文件。
2) 它不适用于 Log4j2
Log4j2 基础结构中没有方法 activateOptions()

那么有人遇到同样的问题吗?你是怎么解决的?

更新
我尝试初始化触发策略以手动 "rollover" 删除文件,但它对我不起作用。
我的代码:

final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
// loggerName is name of logger which should work with the file has been deleted.
LoggerConfig loggerConfig = ctx.getConfiguration().getLoggerConfig(loggerName);
// I also know what appender (appenderName) should work with this file.
RollingFileAppender appender = (RollingFileAppender) loggerConfig.getAppenders().get(appenderName);
appender.getTriggeringPolicy().initialize(appender.getManager());

还有我的配置:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="ERROR">
    <Appenders>
        <RollingFile name="FILE_LOG">
            <FileName>../log/temp/server.log</FileName>
            <FilePattern>../log/server/SERVER_%d{yyyy-MM-dd-hh-mm}.log</FilePattern>
            <PatternLayout pattern="%d{dd.MM.yyyy HH:mm:ss} [%t] %-5level %msg%n"/>
            <Policies>
                <SizeBasedTriggeringPolicy size="100 MB" />
            </Policies>
        </RollingFile>
        <RollingFile name="OUTPUT_LOG">
            <FileName>../log/temp/output.log</FileName>
            <FilePattern>../log/output/OUTPUT_%d{yyyy-MM-dd-hh-mm}.log</FilePattern>
            <PatternLayout>
                <Pattern>%d{dd.MM.yyyy HH:mm:ss} %msg</Pattern>
            </PatternLayout>
            <Policies>
                <CronTriggeringPolicy schedule="0 0 * * * ?"/>
                <OnStartupTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="50 MB" />
            </Policies>
        </RollingFile>
    </Appenders>
    <Loggers>
        <Logger name="OUTPUT" level="debug" additivity="false">
            <AppenderRef ref="OUTPUT_LOG" />
        </Logger>
        <Root level="debug">
            <AppenderRef ref="FILE_LOG" />
        </Root>
    </Loggers>
</Configuration>

我终于找到了解决办法。感谢@Alexander 在评论中的提示。

总结:我们可以在检测到文件删除时手动初始化回滚过程

更长:
我是这样实现的:
1) 创建 FileWatchService,它将 (1) 订阅日志文件夹中的日志文件删除事件,以及 (2) 在这些事件发生时通知您。可以通过 java.nio.file.WatchService (https://docs.oracle.com/javase/tutorial/essential/io/notification.html) 来完成。我将在下面提供我的代码。
2) 创建一些其他的 class,当 FileWatchService 通知文件删除时,它将初始化翻转。我还将在下面提供我的完整代码,但主要的魔法将以这种方式发生:

final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
// You should know only appender name.
RollingFileAppender appender = (RollingFileAppender) ctx.getConfiguration().getAppenders().get(appenderName);
if (appender != null) {
  // Manually start rollover logic.
  appender.getManager().rollover();
}


我的代码看起来像这样(不理想,但对我有用):

FileWatchService:

public class FileWatchService implements Runnable {
    private final org.apache.logging.log4j.Logger logger = LogManager.getLogger(FileWatchService.class);
    private WatchService watchService = null;
    private Map<WatchKey,Path> keys = null;
    private String tempPath;


    public FileWatchService(String tempPath) {
        try {
            this.watchService = FileSystems.getDefault().newWatchService();
            this.keys = new HashMap<WatchKey,Path>();
            this.tempPath = tempPath;
            Path path = Paths.get(tempPath);
            register(path);
            logger.info("Watch service has been initiated.");
        }
        catch (Exception e) {
            logger.error("The error occurred in process of registering watch service", e);
        }
    }

    // Method which register folder to watch service.
    private void register(Path tempPath) throws IOException {
        logger.debug("Registering folder {} for watching.", tempPath.getFileName());
        // Registering only for delete events.
        WatchKey key = tempPath.register(watchService, ENTRY_DELETE);
        keys.put(key, tempPath);
    }

    @Override
    public void run() {
        try {
            Thread.currentThread().setName("FileWatchService");
            this.processEvents();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void processEvents() throws InterruptedException {
            WatchKey key;

            // Waiting until event occur.
            while ((key = watchService.take()) != null) {
                // Poll all events when event occur.
                for (WatchEvent<?> event : key.pollEvents()) {
                    // Getting type of event - delete, modify or create.
                    WatchEvent.Kind kind = event.kind();

                    // We are interested only for delete events.
                    if (kind == ENTRY_DELETE) {
                        // Sending "notification" to appender watcher service.
                        logger.debug("Received event about file deletion. File: {}", event.context());
                        AppenderWatcher.hadleLogFileDeletionEvent(this.tempPath + event.context());
                    }
                }
                key.reset();
            }
    }
}

另一个 class 用于初始化翻转(我称之为 AppenderWatcher):

public class AppenderWatcher {
    private static final org.apache.logging.log4j.Logger logger = LogManager.getLogger(AppenderWatcher.class);


    public static void hadleLogFileDeletionEvent(String logFile) {
        File file = new File(logFile);
        if (!checkFileExist(file)) {
            logger.info("File {} is not exist. Starting manual rollover...", file.toString());
            // Getting possible appender name by log-file.
            String appenderName = getAppenderNameByFileName(logFile);
            // Getting appender from list of all appender
            RollingFileAppender appender = (RollingFileAppender) getAppender(appenderName);

            if (appender != null) {
                // Manually start rollover logic.
                appender.getManager().rollover();
                logger.info("Rollover finished");
            }
            else {
                logger.error("Can't get appender {}. Please, check lo4j2 config.", appenderName);
            }

        } else {
            logger.warn("Received notification what file {} was deleted, but it exist.", file.getAbsolutePath());
        }
    }

    // Method which checks is file exist. It need to prevent conflicts with Log4J rolling file logic.
    // When Log4j rotate file it deletes it first and create after.
    private static boolean checkFileExist(File logFile) {
        return logFile.exists();
    }

    // Method which gets appender by name from list of all configured appenders.
    private static Appender getAppender(String appenderName) {
        final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
        return ctx.getConfiguration().getAppenders().get(appenderName);
    }

    // Method which returns name of appender by log file name.
    // ===Here I'm explaining some customer specific moments of log4j config.
    private static String getAppenderNameByFileName(String fileName) {
        return getLoggerNameByFileName(fileName) + "_LOG";
    }

    // This method fully customer specific. 
    private static String getLoggerNameByFileName(String fileName) {
        // File name looks like "../log/temp/uber.log" (example).
        String[] parts = fileName.split("/");

        // Last part should look like "uber.log"
        String lastPart = parts[parts.length - 1];

        // We need only "uber" part.
        String componentName = lastPart.substring(0, lastPart.indexOf("."));
        return componentName.toUpperCase();
    }
}