SpringBoot 彻底搞懂日志

刚接触过日志的人可能对日志的一些关键词比较头晕,比如: Apache Commons Logging Slf4j Log4j Log4j2 Logback Java Util Logging 。 搞不清楚他们是什么关系。今天我们就来详细讲解下。

日志门面与日志实现

日志框架主要是分为两大类: 日志门面+日志实现

日志门面

日志门面定义了一组日志的接口规范,它并不提供底层具体的实现逻辑。 Apache Commons Logging Slf4j 就属于这一类。

日志实现

日志实现则是日志具体的实现,包括日志级别控制、日志打印格式、日志输出形式(输出到数据库、输出到文件、输出到控制台等)。 Log4j Log4j2 Logback 以及 Java Util Logging 则属于这一类。

将日志门面和日志实现分离其实是一种典型的门面模式,这种方式可以让具体业务在不同的日志实现框架之间自由切换,而不需要改动任何代码,开发者只需要掌握日志门面的 API 即可。

下面图展示了日志框架:

Spring Boot 默认日志实现

SpringBoot默认使用 Logback 作为日志实现,我们可以断点调试下下面这段代码:

private static final Logger logger = LoggerFactory.getLogger(OrderController.class);

我们可以搜索maven依赖:

发现 日志是通过spring-boot-starter-logging引进来的,日志有log4j 和 logback 。但是默认使用logback 。


Logback 日志配置

由于springboot默认使用Logback,所以我们学习掌握Logback就行了。将 日志配置文件( logback.xml ) 放到项目classpath下就可以自动被读取到。

日志分等级存储不同文件

将所有日志都存储在一个文件中文件大小也随着应用的运行越来越大并且不好排查问题。线上的做法应该是将 error 日志和其他日志分开,并且不同级别的日志根据时间段进行记录存储。如下面配置:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
	<contextName>${APP_NAME}</contextName>
        <!-- 定义变量 -->
	<springProperty name="APP_NAME" scope="context" source="spring.application.name" />
	<springProperty name="LOG_INFO_FILE" scope="context" source="logging.file" defaultValue="/logs/${APP_NAME}/info/stdout.log" />
	<springProperty name="LOG_ERROR_FILE" scope="context" source="logging.file" defaultValue="/logs/${APP_NAME}/error/stdout.log" />
        <!-- console_strategy 策略 -->
	<appender name="console_strategy"
		class="ch.qos.logback.core.ConsoleAppender">
		<layout class="ch.qos.logback.classic.PatternLayout">
			<pattern>[%d{yyyy-MM-dd' 'HH:mm:ss.sss}] [%C] [%t] [%L] [%-5p] %m%n
			</pattern>
		</layout>
	</appender>
	<!-- info_strategy 策略 -->
	<appender name="info_strategy"
		class="ch.qos.logback.core.rolling.RollingFileAppender">
		<filter class="ch.qos.logback.classic.filter.LevelFilter">
			<level>ERROR</level>
			<onMatch>DENY</onMatch>
			<onMismatch>ACCEPT</onMismatch>
		</filter>
		<encoder>
			<pattern>[%d{yyyy-MM-dd' 'HH:mm:ss.sss}] [%C] [%t] [%L] [%-5p] %m%n
			</pattern>
		</encoder>

		<!--滚动策略 -->
		<rollingPolicy
			class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<!--路径 -->
			<fileNamePattern>${LOG_INFO_FILE}</fileNamePattern>
			<maxHistory>30</maxHistory>
		</rollingPolicy>
	</appender>
	
	<!-- error_strategy 策略 -->
	<appender name="error_strategy"
		class="ch.qos.logback.core.rolling.RollingFileAppender">
		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
			<level>ERROR</level>
		</filter>
		<encoder>
			<pattern>[%d{yyyy-MM-dd' 'HH:mm:ss.sss}] [%C] [%t] [%L] [%-5p] %m%n
			</pattern>
		</encoder>
		<!--滚动策略 -->
		<rollingPolicy
			class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<!--路径 -->
			<fileNamePattern>${LOG_ERROR_FILE}</fileNamePattern>
			<maxHistory>30</maxHistory>
		</rollingPolicy>
	</appender>

	<root level="info">
		<appender-ref ref="console_strategy" />
		<appender-ref ref="info_strategy" />
		<appender-ref ref="error_strategy" />
	</root>
</configuration>

几个要点:

  • < springProperty > 定义了变量,后面${变量名}可以取到。
  • <appender> 指定一个日志策略(日志具体规则) 。 name 定义了当前策略名称。上面定义了三个策略: appender-ref,appender-ref,error_strategy。
  • <root> 用来指定最基础的日志输出级别。appender-ref 指定了上面的3个日志策略。

appender 日志策略详解

  • filter

日志输出拦截器,可以自定义拦截器也可以用系统一些定义好的拦截器。

public class MyFilter extends Filter<ILoggingEvent> {

@Override
public FilterReply decide(ILoggingEvent event) {

    if (event.getMessage().contains("hadluo")) {
        return FilterReply.ACCEPT; //允许输入串
    } else {
        return FilterReply.DENY; //不允许输出
    }
}
}

系统定义的拦截器,例如我们用ThresholdFilter来过滤掉ERROR级别以下的日志不输出到文件中:

<filter  class="ch.qos.logback.classic.filter.ThresholdFilter">
   <level>ERROR</level>
</filter>

2. encoder

encoder和pattern节点组合用于具体输出的日志格式。

3. rollingPolicy

rollingPolicy日志回滚策略,在这里我们用了TimeBasedRollingPolicy:

<!--滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
	<!--路径 -->
	<fileNamePattern>${LOG_INFO_FILE}_.%d{yyyy-MM-dd}.zip</fileNamePattern>
	<maxHistory>30</maxHistory>
</rollingPolicy>

fileNamePattern,必要节点,可以用来设置指定时间的日志归档,例如我们上面的例子是每天将日志归档成一个zip包
maxHistory ,可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件,,例如设置为30的话,则30天之后,旧的日志就会被删除
totalSizeCap,可选节点,用来指定日志文件的上限大小,例如设置为3GB的话,那么到了这个值,就会删除旧的日志
除了用TimeBasedRollingPolicy策略,我们还可以用SizeAndTimeBasedRollingPolicy,配置子节点的maxFileSize来指定单个日志文件的大小:

<appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>mylog.txt</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
  <!-- 每天一归档 -->
  <fileNamePattern>mylog-%d{yyyy-MM-dd}.%i.txt</fileNamePattern>
   <!-- 单个日志文件最多 100MB, 60天的日志周期最大不能超过20GB -->
   <maxFileSize>100MB</maxFileSize>    
   <maxHistory>60</maxHistory>
   <totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<encoder>
  <pattern>%msg%n</pattern>
</encoder>
</appender>

logback 异步输出日志

上面日志配置方式是基于同步的,每次日志输出到文件都会进行一次磁盘IO。采用异步写日志的方式而不让此次写日志发生磁盘IO,阻塞线程从而造成不必要的性能损耗。异步输出日志的方式很简单,添加一个基于异步写日志的 appender ,并指向原先配置的 appender 即可:

 <!-- 异步输出 -->
    <appender name="ASYNC-INFO" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACTDEBUGINFO级别的日志 -->
        <discardingThreshold>0</discardingThreshold>
        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
        <queueSize>256</queueSize>
        <!-- 添加附加的appender,最多只能添加一个 -->
        <appender-ref ref="INFO-LOG"/>
    </appender>

    <appender name="ASYNC-ERROR" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACTDEBUGINFO级别的日志 -->
        <discardingThreshold>0</discardingThreshold>
        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
        <queueSize>256</queueSize>
        <!-- 添加附加的appender,最多只能添加一个 -->
        <appender-ref ref="ERROR-LOG"/>
    </appender>


最后奉献一个线上同步方式日志配置(仅供参考):

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<contextName>${APP_NAME}</contextName>
	<springProperty name="APP_NAME" scope="context"
		source="spring.application.name" />
	<springProperty name="LOG_FILE" scope="context"
		source="logging.file"
		defaultValue="/logs/${APP_NAME}/" />
	<!-- <springProperty name="LOG_POINT_FILE" scope="context" source="logging.file" 
		defaultValue="../logs/point"/> -->
	<!-- <springProperty name="LOG_AUDIT_FILE" scope="context" source="logging.file" 
		defaultValue="../logs/audit"/> 
		/usr/local/${APP_NAME}/logs/
		-->

	<!-- 日志文件最大 容量 -->
	<springProperty name="LOG_MAXFILESIZE" scope="context"
		source="logback.filesize" defaultValue="100MB" />

	<!-- 日志保留 天数
	<springProperty name="LOG_FILEMAXDAY" scope="context"
		source="logback.filemaxday" defaultValue="30" />
		 -->
	<springProperty name="ServerIP" scope="context"
		source="spring.cloud.client.ip-address" defaultValue="0.0.0.0" />
	<springProperty name="ServerPort" scope="context"
		source="server.port" defaultValue="0000" />

	<!-- 彩色日志 -->
	<!-- 彩色日志依赖的渲染类 -->
	<conversionRule conversionWord="clr"
		converterClass="org.springframework.boot.logging.logback.ColorConverter" />
	<conversionRule conversionWord="wex"
		converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
	<conversionRule conversionWord="wEx"
		converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />

	<!-- 彩色日志格式 -->
	<property name="CONSOLE_LOG_PATTERN"
		value="[${APP_NAME}:${ServerIP}:${ServerPort}] %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%level){blue} %clr(${PID}){magenta} %clr([%X{traceId}]){yellow} %clr([%thread]){orange} %clr(%logger){cyan} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}" />
	<property name="CONSOLE_LOG_PATTERN_NO_COLOR"
		value="[${APP_NAME}:${ServerIP}:${ServerPort}] %d{yyyy-MM-dd HH:mm:ss.SSS} %level ${PID} [%X{traceId}] [%thread] %logger %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}" />

	<!-- 控制台日志 -->
	<appender name="StdoutAppender"
		class="ch.qos.logback.core.ConsoleAppender">
		<withJansi>true</withJansi>
		<encoder>
			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
			<charset>UTF-8</charset>
		</encoder>
		 <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
        </filter>
	</appender>

	<!-- 按照每天生成常规日志文件 -->
	<appender name="FileAppender"
		class="ch.qos.logback.core.rolling.RollingFileAppender">
		<file>${LOG_FILE}/${APP_NAME}.json</file>
		 <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>  
            <onMismatch>DENY</onMismatch>  
        </filter>
		<!-- <encoder> -->
		<!-- <pattern>${CONSOLE_LOG_PATTERN_NO_COLOR}</pattern> -->
		<!-- <charset>UTF-8</charset> -->
		<!-- </encoder> -->
		<encoder class="net.logstash.logback.encoder.LogstashEncoder">
			<!-- <provider class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.logstash.TraceIdJsonProvider"> 
				</provider> -->
		</encoder>
		<!-- 基于时间的分包策略 -->
		<rollingPolicy
			class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<fileNamePattern>${LOG_FILE}/${APP_NAME}.%d{yyyy-MM-dd}.%i.json
			</fileNamePattern>
			<!--保留时间,单位: -->
			<maxHistory>10</maxHistory>
			<timeBasedFileNamingAndTriggeringPolicy
				class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
				<maxFileSize>${LOG_MAXFILESIZE}</maxFileSize>
			</timeBasedFileNamingAndTriggeringPolicy>
		</rollingPolicy>
	</appender>
	
	<appender name="file_async"
		class="ch.qos.logback.classic.AsyncAppender">
		<discardingThreshold>0</discardingThreshold>
		<appender-ref ref="FileAppender" />
	</appender>
   <root level="INFO">
        <appender-ref ref="StdoutAppender"/>
        <appender-ref ref="file_async"/>
    </root>
	<logger name="com.alibaba.nacos" level="error" />
	<logger name="org.apache.kafka" level="error" />
	<logger name="org.springframework.kafka" level="error" />
</configuration>

推荐一个Java架构师博客,带你一起写架构:

JAVA架构师修炼

支付宝打赏 微信打赏

如果文章对您有帮助,您可以鼓励一下作者