有时我们需要滚动删除日志,不然日志会越积越多。从 log4j2 2.5 之后,在日志滚动的时候可以自定义删除操作,比如我们希望保留3天的日志,可以这么配置:

- name: FileAppender
  fileName: /tmp/log/test.log
  filePattern: /tmp/log/test.log.%d{yyyy-MM-dd}"
  PatternLayout:
    pattern: %-d{yyyy-MM-dd HH:mm:ss.SSS} [%t] [%c] [%p] %m%n
  Policies:
    TimeBasedTriggeringPolicy: {}
  DefaultRolloverStrategy:
    Delete:
      basePath: /tmp/log
      maxDepth: 1
      IfFileName:
        glob: "*.log.*"
      IfLastModified:
        age: 3d

DefaultRolloverStrategy 中的 Delete 部分就是删除相关的配置

  • basePath 定义了扫描日志文件的根路径。
  • maxDepth 定义了遍历的层级,1表示 bashPath 下的所有文件
  • IfFileName 定义了扫描的文件格式
  • IfLastModified 定义了只有在最后访问时间在3天以上的才会被删除
  • 这里需要注意

  • 删除操作只会发生在日志滚动时,而滚动的时机取决于 filePattern 和 Triggering Policies (上面配置中 Policies 部分)
  • IfFileName 指定删除的文件格式,只要符合条件都会被删除,并没有限制是通过当前服务输出。上面这样配置是为了只删除历史日志文件。
  • IfFileName 和 IfLastModified 都属于 pathConditions。pathConditions 是一个数组,决定哪些文件会被删除,如果定义了多个,需要多个条件同时满足才会被删除。
  • 以上能满足一般的需求了,但是偶尔删除过期数据还不足够,比如某个时间日志量激增,可能会导致磁盘占满,所以需要加一个兜底策略。

    - name: FileAppender
      fileName: /tmp/log/test.log
      filePattern: /tmp/log/test.log.%d{yyyy-MM-dd}"
      PatternLayout:
        pattern: %-d{yyyy-MM-dd HH:mm:ss.SSS} [%t] [%c] [%p] %m%n
      Policies:
        TimeBasedTriggeringPolicy: {}
      DefaultRolloverStrategy:
        Delete:
          basePath: /tmp/log
          maxDepth: 1
          IfFileName:
            glob: "*.log.*"
    			IfAny:
            IfLastModified:
              age: 3d
            IfAccumulatedFileSize:
              exceeds: 200GB
    

    这里改了一下条件,IfAny 表示或者,对于文件名符合 .log. 格式的,最后访问时间为3天以上或总共超过 200GB 时都会删除文件。

    IfAccumulatedFileSize 是一个累加的过程,开始不满足,加到一定阈值会满足条件,这个顺序可以通过 PathSorter 来指定,默认是先访问最近修改的文件。

    由于涉及删除文件,在实际修改日志配置后,最好先进行测试,可以把 testMode 设置为 true。

    Delete:
      basePath: /tmp/log
      maxDepth: 1
      testMode: true
    

    这样实际并不会删除文件,而是把要删除的文件通过 StatusLoggerINFO 级别输出。

    StatusLogger 是用来打印 log4j2 内部信息的,用于诊断和调试,可以通过 status 来配置StatusLogger 的日志级别,默认会输出在 System.out

    Configuration:
      status: debug
    

    遇到的问题

    mac 上日志存放到 /tmp 无法删除

    这是由于 mac 上 /tmp 是一个符号链接,而代码实现上会判断 basePath 是否是目录,如果不是目录就直接返回了,不会再进行遍历了。

    具体代码可以参考 FileTreeWalkervisit 方法,也可以通过如下单元测试进行测试

    @Test
    public void test() throws IOException {
        BasicFileAttributes attributes = Files.readAttributes(Paths.get("/tmp"), BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
        assert !attributes.isDirectory();
    

    StatusLogger 日志级别被覆盖

    配置StatusLogger 日志级别为 debug 后,日志并未按预期输出,调试发现 StatusLogger 中打日志相关代码如下

    @Override
    public void logMessage(final String fqcn, final Level level, final Marker marker, final Message msg,
            final Throwable t) {
        StackTraceElement element = null;
        if (fqcn != null) {
            element = getStackTraceElement(fqcn, Thread.currentThread().getStackTrace());
        final StatusData data = new StatusData(element, level, msg, t, null);
        msgLock.lock();
        try {
            messages.add(data);
        } finally {
            msgLock.unlock();
        if (listeners.size() > 0) {
            for (final StatusListener listener : listeners) {
                if (data.getLevel().isMoreSpecificThan(listener.getStatusLevel())) {
                    listener.log(data);
        } else {
            logger.logMessage(fqcn, level, marker, msg, t);
    

    如果有 listener 只会调用 listener 进行输出,StatusConfiguration 中默认会配置一个 StatusConsoleListener ,但是日志级别配置却和配置文件中的不同。

    进一步调试发现,是因为依赖中引用了 com.alibaba.nacos:nacos-client ,这个包中也有 log4j2 的配置,status 的级别是 WARN

    StatusConfiguration 中的 initialize 方法

    public void initialize() {
        if (!this.initialized) {
            if (this.status == Level.OFF) {
                this.initialized = true;
            } else {
                final boolean configured = configureExistingStatusConsoleListener();
                if (!configured) {
                    registerNewStatusConsoleListener();
                migrateSavedLogMessages();
    

    如果已经配置了StatusConsoleListener 则会重新配置

    private boolean configureExistingStatusConsoleListener() {
        boolean configured = false;
        for (final StatusListener statusListener : this.logger.getListeners()) {
            if (statusListener instanceof StatusConsoleListener) {
                final StatusConsoleListener listener = (StatusConsoleListener) statusListener;
                listener.setLevel(this.status);
                this.logger.updateListenerLevel(this.status);
                if (this.verbosity == Verbosity.QUIET) {
                    listener.setFilters(this.verboseClasses);
                configured = true;
        return configured;
    

    所以日志级别被覆盖了,一个简单的方法是再通过代码设置一下级别,比如

    StatusLogger.getLogger().getListeners().forEach(statusListener -> {
        if (statusListener instanceof StatusConsoleListener) {
            ((StatusConsoleListener) statusListener).setLevel(Level.INFO);
    复制代码