解決spring boot logging在兩個目錄生成日誌文件且max-history不生效

解決spring boot logging在兩個目錄生成日誌文件且max-history不生效

背景

  • 使用spring boot 2.1.6-RELEASE
  • 使用默認的spring-boot-starter-logging (logback 1.2.3)作爲日誌記錄組件
  • classpath配置了logback.xml文件,內容如下:
    <?xml version="1.0" encoding="UTF-8"?>
    <configuration scan="true" scanPeriod="60 seconds" debug="false">
        <!-- 定義log文件的目錄 -->
        <property name="logHome" value="/var/log/classpath/"/>
        <property name="maxHistory" value="7"/>
        <property name="maxFileSize" value="10MB"/>
        <property name="totalSizeCap" value="200MB"/>
        <property name="minIndexNum" value="1"/>
        <property name="maxIndexNum" value="10"/>
    
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] [%p] [%class:%line] %msg%n</pattern>
                <charset>UTF-8</charset>
            </encoder>
        </appender>
     	
     	<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>${logHome}/info.log</file>
            <append>true</append>
            <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
                <fileNamePattern>${logHome}/info.%i.log.zip</fileNamePattern>
                <minIndex>${minIndexNum}</minIndex>
                <maxIndex>${maxIndexNum}</maxIndex>
            </rollingPolicy>
            <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
                <maxFileSize>${maxFileSize}</maxFileSize>
            </triggeringPolicy>
            <encoder>
                <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] [%p] [%class:%line] %msg%n</pattern>
                <charset>UTF-8</charset>
            </encoder>
        </appender>
    	
    	<root level="INFO">
            <appender-ref ref="STDOUT"/>
            <appender-ref ref="INFO"/>
        </root>
    </configuration>
    
  • 啓動時指定了外部的logback.xml,cmdline爲:java -Xms256m -Xmx512m -jar myapp.jar --logging.config=/usr/local/config/logback.xml,配置內容如下,兩個配置文件僅有一處logHome不同:
    <?xml version="1.0" encoding="UTF-8"?>
    <configuration scan="true" scanPeriod="60 seconds" debug="false">
        <!-- 定義log文件的目錄 -->
        <property name="logHome" value="/var/log/cmdline/"/>
        <property name="maxHistory" value="7"/>
        <property name="maxFileSize" value="10MB"/>
        <property name="totalSizeCap" value="200MB"/>
        <property name="minIndexNum" value="1"/>
        <property name="maxIndexNum" value="10"/>
    
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] [%p] [%class:%line] %msg%n</pattern>
                <charset>UTF-8</charset>
            </encoder>
        </appender>
     	
     	<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>${logHome}/info.log</file>
            <append>true</append>
            <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
                <fileNamePattern>${logHome}/info.%i.log.zip</fileNamePattern>
                <minIndex>${minIndexNum}</minIndex>
                <maxIndex>${maxIndexNum}</maxIndex>
            </rollingPolicy>
            <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
                <maxFileSize>${maxFileSize}</maxFileSize>
            </triggeringPolicy>
            <encoder>
                <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] [%p] [%class:%line] %msg%n</pattern>
                <charset>UTF-8</charset>
            </encoder>
        </appender>
    	
    	<root level="INFO">
            <appender-ref ref="STDOUT"/>
            <appender-ref ref="INFO"/>
        </root>
    </configuration>
    

現象

使用cmdline啓動並且指定logging.config時,會同時在/var/log/classpath//var/log/cmdline/生成一個info.log文件,但classpath的info.log始終爲空,只會在cmdline下的info.log寫入日誌。

排查

猜測可能是springboot或logback在項目啓動時先讀取了classpath下的logback.xml初始化了日誌文件,等spring context加載到某一個過程發佈一個事件,某一個Listener又根據外部配置再初始化了一次日誌文件,後續都會使用這個配置文件。

  1. 刪除classpath和cmdline文件夾,在啓動類的main方法第一行打上斷點,啓動項目;
  2. 到達main方法第一行時,兩個文件夾均未生成;
  3. step into,剛進入方法第一行,classpath文件夾生成了,並且文件夾下有一個空的info.log;
  4. 繼續step into進入org.springframework.boot.SpringApplication#run(java.lang.String...)方法後,cmdline文件夾還未生成;
  5. 執行org.springframework.boot.SpringApplication#prepareEnvironment方法後發佈了一個ApplicationEnvironmentPreparedEvent事件;
  6. org.springframework.boot.context.logging.LoggingApplicationListener監聽器響應該事件,執行org.springframework.boot.context.logging.LoggingApplicationListener#initialize方法初始化了cmdline文件夾

至此整個過程基本明瞭:

  1. 應該有static屬性或static塊讀取了classpath裏的logback.xml,並初始化了日誌文件;
  2. spring boot在環境準備完畢後纔會初始化日誌文件。

最後發現是logback的org.slf4j.impl.StaticLoggerBinder有一個static塊SINGLETON.init();

/**
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v1.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package org.slf4j.impl;

import ch.qos.logback.core.status.StatusUtil;
import org.slf4j.ILoggerFactory;
import org.slf4j.LoggerFactory;
import org.slf4j.helpers.Util;
import org.slf4j.spi.LoggerFactoryBinder;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.util.ContextInitializer;
import ch.qos.logback.classic.util.ContextSelectorStaticBinder;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;

/**
 * 
 * The binding of {@link LoggerFactory} class with an actual instance of
 * {@link ILoggerFactory} is performed using information returned by this class.
 * 
 * @author Ceki G&uuml;lc&uuml;</a>
 */
public class StaticLoggerBinder implements LoggerFactoryBinder {

    /**
     * Declare the version of the SLF4J API this implementation is compiled
     * against. The value of this field is usually modified with each release.
     */
    // to avoid constant folding by the compiler, this field must *not* be final
    public static String REQUESTED_API_VERSION = "1.7.16"; // !final

    final static String NULL_CS_URL = CoreConstants.CODES_URL + "#null_CS";

    /**
     * The unique instance of this class.
     */
    private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();

    private static Object KEY = new Object();

	/**
	 * 這個方法跟下去可以發現加載了classpath根目錄的logback.xml,並初始化了日誌文件
	 */
    static {
        SINGLETON.init();
    }

    private boolean initialized = false;
    private LoggerContext defaultLoggerContext = new LoggerContext();
    private final ContextSelectorStaticBinder contextSelectorBinder = ContextSelectorStaticBinder.getSingleton();

    private StaticLoggerBinder() {
        defaultLoggerContext.setName(CoreConstants.DEFAULT_CONTEXT_NAME);
    }

    public static StaticLoggerBinder getSingleton() {
        return SINGLETON;
    }

    /**
     * Package access for testing purposes.
     */
    static void reset() {
        SINGLETON = new StaticLoggerBinder();
        SINGLETON.init();
    }

    /**
     * Package access for testing purposes.
     */
    void init() {
        try {
            try {
                new ContextInitializer(defaultLoggerContext).autoConfig();
            } catch (JoranException je) {
                Util.report("Failed to auto configure default logger context", je);
            }
            // logback-292
            if (!StatusUtil.contextHasStatusListener(defaultLoggerContext)) {
                StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext);
            }
            contextSelectorBinder.init(defaultLoggerContext, KEY);
            initialized = true;
        } catch (Exception t) { // see LOGBACK-1159
            Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t);
        }
    }

    public ILoggerFactory getLoggerFactory() {
        if (!initialized) {
            return defaultLoggerContext;
        }

        if (contextSelectorBinder.getContextSelector() == null) {
            throw new IllegalStateException("contextSelector cannot be null. See also " + NULL_CS_URL);
        }
        return contextSelectorBinder.getContextSelector().getLoggerContext();
    }

    public String getLoggerFactoryClassStr() {
        return contextSelectorBinder.getClass().getName();
    }

}

解決方案

  1. 將logback.xml從classpath根目錄移動到非根目錄,然後在application.properties加入參數:logging.config=classpath:/logback/logback.xml
  2. 既然spring boot可以在applicaiton.properties配置,爲何還要單獨使用logback.xml配置呢,於是嘗試使用application.properties配置

新的問題

對於第2中解決方案,詳見:Logging

按照文檔中的解釋,本以爲設置瞭如下3個參數就完事了,期望實現日誌文件達到10M時歸檔壓縮,最多保留10個歸檔文件:

logging.file=/var/log/myapp/info.log
logging.file.max-size=10mb
logging.file.max-history=10

但實際並不與期望相符,google找到如下解釋:Clarify description of “logging.file.max-history” #17566

Spring Boot is using Logback’s SizeAndTimeBasedRollingPolicy, which means log files are rotated when the time limit has been reached - also, the log files are split to make sure that files don’t exceed the maximum size.
In our case, Spring Boot is using the following pattern %d{yyyy-MM-dd}.%i.gz, which infers daily periods.
This means that, for the following configuration, we might get 14 files if the application logs 20MB every 24hours (2x 10MB files per day):

logging.file.max-history=7
logging.file.max-size=10MB

This holds true, unless developers override the default logback configuration provided by Spring Boot.
I think you’re right and we should update the configuration description to reflect that.

簡言之,就是spring boot默認使用的rolling策略是SizeAndTimeBasedRollingPolicy,在這個策略中,logging.file.max-history並不是代表最大文件數,而是最多保留多少天的文件

修改logging.file.max-size=10kb logging.file.max-history=1,並配合修改系統時間,發現果真如此。

總結

  1. logback的org.slf4j.impl.StaticLoggerBinder裏有一個static塊,會在項目啓動的時候讀取classpath根目錄下的logback.xml,並初始化日誌文件;
  2. spring boot的logging配置有很多侷限性,在後續的版本增加了許多logback的參數,比如total-cap-size等等,但還是不能支持logback所有參數和配置方式,這可能和springboot的初衷有關:簡化配置,而不是支持所有配置,如果需要使用logback的複雜特性,建議使用外部xml配置。
發佈了78 篇原創文章 · 獲贊 20 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章