解決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又根據外部配置再初始化了一次日誌文件,後續都會使用這個配置文件。
- 刪除classpath和cmdline文件夾,在啓動類的main方法第一行打上斷點,啓動項目;
- 到達main方法第一行時,兩個文件夾均未生成;
- step into,剛進入方法第一行,classpath文件夾生成了,並且文件夾下有一個空的info.log;
- 繼續step into進入
org.springframework.boot.SpringApplication#run(java.lang.String...)
方法後,cmdline文件夾還未生成; - 執行
org.springframework.boot.SpringApplication#prepareEnvironment
方法後發佈了一個ApplicationEnvironmentPreparedEvent
事件; org.springframework.boot.context.logging.LoggingApplicationListener
監聽器響應該事件,執行org.springframework.boot.context.logging.LoggingApplicationListener#initialize
方法初始化了cmdline文件夾
至此整個過程基本明瞭:
- 應該有static屬性或static塊讀取了classpath裏的logback.xml,並初始化了日誌文件;
- 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ülcü</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();
}
}
解決方案
- 將logback.xml從classpath根目錄移動到非根目錄,然後在application.properties加入參數:
logging.config=classpath:/logback/logback.xml
- 既然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
,並配合修改系統時間,發現果真如此。
總結
- logback的
org.slf4j.impl.StaticLoggerBinder
裏有一個static塊,會在項目啓動的時候讀取classpath根目錄下的logback.xml
,並初始化日誌文件; - spring boot的logging配置有很多侷限性,在後續的版本增加了許多logback的參數,比如total-cap-size等等,但還是不能支持logback所有參數和配置方式,這可能和springboot的初衷有關:簡化配置,而不是支持所有配置,如果需要使用logback的複雜特性,建議使用外部xml配置。