背景
从毕业到现在参加工作已经有两年了,在这两年期间,经历的几个大项目和一些小项目,每个项目都涉及到日志这个模块。存在的意义无非就是两个:
- 记录业务日志,方便后期排查问题
- 以日志上的形式记录业务数据,并将日志文件推送第三方
每次接手一个新项目,就会先问有没有配置日志,如果没有配置日志模块,那么排查问题就是一个很恼火的问题,不清楚请求参数是什么,走了哪个判断,调用第三方接口的参数和返回结果是什么,一切的问题都需要一步一步的调试且耗时耗力。自从意识到这个问题后,每次写代码都会加入日志,并将请求参数,返回结果,耗时的数据写入日志,方便后期排查问题。
目前接触比较多Java 开源框架是SpringBoot,默认的日志实现是LogBack,通过配置xml文件就可以输出日志。之前在配置LogBack时,总是无脑的将其他项目的配置复制过来,也不懂为什么要这样配置。通过这几天的学习,把自己对logback的日志记录如下。
基础知识
日志级别
日志输出级别从大高到低为:
- OFF
- FATAL
- ERROR
- WARN
- INFO
- DEBUG
- TRACE
- ALL
日常开发中常用的是:
- ERROR:用于输出影响系统正常运行的信息
- WARN:用于输出错误信息,但不影响系统正常运行
- INFO:用于记录正常业务日志信息,方便后期的问题排查
- DEBUG:用于开发环境下的信息输出
方法打印
推荐使用Slf4j
记录日志,在LogBack不被允许的情况下可以方便的切换到其他日志系统。实例化logger对象:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class XXX {
public static final Logger log = LoggerFactory.getLogger(XXX.class);
}
如果使用了lombok
,则可以使用@Slf4j
注解实例化logger对象
- 输出ERROR日志
log.error("this is error log;time:{}", System.currentTimeMillis());
- 输出WARN日志
log.warn("this is warn log;time:{}", System.currentTimeMillis());
- 输出INFO日志
log.info("this is info log;time:{}", System.currentTimeMillis());
- 输出DEBUG日志
if (log.isDebugEnabled()) {
log.debug("this is debug log;time:{}", System.currentTimeMillis());
}
在开发中,使用参数化日志而不是字符串拼接,可以调高代码可读性并提高性能,在记录debug日志时,先判断目前所处环境是否支持debug输出,因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅 是替换动作,可以有效提升性能。(摘自:阿里巴巴Java开发手册嵩山版)
日志规约
[Java开发手册(泰山版)]
Java开发手册(泰山版)
配置LogBack
在SpringBoot框架中使用LogBack只需在resources
目录下添加配置文件即可,可以项目启动日志设置为debug查看其初始化步骤:
23:42:43,896 |-INFO in ch.qos.logback.classic.LoggerContext[heron] - Could NOT find resource [logback-test.xml]
23:42:43,897 |-INFO in ch.qos.logback.classic.LoggerContext[heron] - Could NOT find resource [logback.groovy]
23:42:43,897 |-INFO in ch.qos.logback.classic.LoggerContext[heron] - Found resource [logback.xml] at [file:/E:/heron/target/classes/logback.xml]
这里使用logback.xml
方式配置LogBack。
配置文件的组成
<configuration scan="true" scanPeriod="5 seconds">
<property name="appName" value="heron"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{36} [%file:%line] - %msg%n</pattern>
</encoder>
</appender>
<logger name="webiste.lhc.heron" level="debug" additivity="false">
<appender-ref ref="rollingFileAppender"/>
</logger>
<root level="debug">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
<configuration>
整个XML文件由<configuration></configuration>
标签包裹,属性含义:
- scan: 扫描配置文件,查看配置文件是否有改动
- scanPeriod:扫描时间间隔
<property>
用于定义变量,例如:
<!--定义项目名称-->
<property name="appName" value="heron"/>
<!--定义日志路径-->
<property name="logPath" value="/data/logs"/>
<appender>
输出日志的核心,定义了日志的数据格式、日志类型、日志文件大小、日志保存时间和轮转策略
属性含义:
- name:定义logger所用应用的名称,名称随意
- class:定义日志输出实现类,常用的输出包括:控制台,文件和消息中间件
<encoder>
决定通过哪种方式写日志
<pattern>
定义日志输出格式,例如:年月日时分秒和毫秒信息,线程名称,类名,日志级别
<logger>
用于设置一个包或一个类的日志打印级别
属性含义:
- name:指定包名或类名
- level:定义当前包或类应输出那个范围的日志
- additivity:是否继承<root>标签的日志级别定义
在<logger>标签中应用已定义的<appender> ,可以指定一个类或一个包如何输出日志;<logger>可以包含零个或多个<appender-ref>
<root>
<root>也是一个<logger>,但是它是根logger,且只有一个属性,即:level
日志输出至控制台
在没有日志配置文件之前,SpringBoot框架就会将所有日志输出至控制台,这里是通过BasicConfigurator
类来实现的
package ch.qos.logback.classic;
import ch.qos.logback.classic.layout.TTLLLayout;
import ch.qos.logback.classic.spi.Configurator;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.encoder.LayoutWrappingEncoder;
import ch.qos.logback.core.spi.ContextAwareBase;
/**
* BasicConfigurator configures logback-classic by attaching a
* {@link ConsoleAppender} to the root logger. The console appender's layout
* is set to a {@link ch.qos.logback.classic.layout.TTLLLayout TTLLLayout}.
*
* @author Ceki Gülcü
*/
public class BasicConfigurator extends ContextAwareBase implements Configurator {
public BasicConfigurator() {
}
public void configure(LoggerContext lc) {
addInfo("Setting up default configuration.");
ConsoleAppender<ILoggingEvent> ca = new ConsoleAppender<ILoggingEvent>();
ca.setContext(lc);
ca.setName("console");
LayoutWrappingEncoder<ILoggingEvent> encoder = new LayoutWrappingEncoder<ILoggingEvent>();
encoder.setContext(lc);
// same as
// PatternLayout layout = new PatternLayout();
// layout.setPattern("%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n");
TTLLLayout layout = new TTLLLayout();
layout.setContext(lc);
layout.start();
encoder.setLayout(layout);
ca.setEncoder(encoder);
ca.start();
Logger rootLogger = lc.getLogger(Logger.ROOT_LOGGER_NAME);
rootLogger.addAppender(ca);
}
}
查看TTLLLayout
中的doLayout()
方法完成日志格式的拼接。
可以在配置文件中自定义控制台日志的输出格式,并在<root>
标签中引用即可
<configuration scan="true" scanPeriod="5 seconds">
<!--控制台日志-->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!--默认配置-->
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{36} [%file:%line] - %msg%n</pattern>
<!--只输出内容-->
<pattern>%msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
控制台日志的实现类是ch.qos.logback.core.ConsoleAppender
,在服务器上使用nohup java -jar xxx.jar
命令启动服务时,项目的所有日志会保存在当前目录下的nohup.out
文件中,也可以使用nohup java -jar xxx.jar > start.log
将日志输出至指定目录下的指定文件中。这样做的缺点是:项目的启动日志和业务日志保存在同一个文件中,没有单独的划分。
日志输出至文件
为了解决上面存在的问题,就需要将启动日志和业务日志分隔开来,并输出至两个不同的文件中,且启动日志在项目正常启动之后并不需要经常查看。所以可以使用LogBack提供的ch.qos.logback.core.FileAppender
,将业务日志以指定的格式作为输出。
<configuration scan="true" scanPeriod="5 seconds">
<property name="appName" value="heron"/>
<property name="logPath" value="/tmp/logs"/>
<!--控制台日志-->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{36} [%file:%line] - %msg%n</pattern>
</encoder>
</appender>
<!--业务日志-->
<appender name="file" class="ch.qos.logback.core.FileAppender">
<file>${logPath}/${appName}.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{36} [%file:%line] - %msg%n</pattern>
</encoder>
</appender>
<!--配置FileAppender引用-->
<logger name="webiste.lhc.heron" level="debug" additivity="false">
<appender-ref ref="file"/>
</logger>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
上面的配置中使用了两个变量appName
和logPath
,使用${}
的方式定义了业务日志要输出的路径和文件名称。
在使用nohup java -jar xxx.jar > start.log 2>&1 &
命令启动服务时,就会出现两个文件:当前目录下的start.log和/tmp/logs/heron.log文件。在查看start.log文件没有明显的报错时,就可以查看heron.log文件中所存储的内容。若要将日志文件按月,按天,按小时存储,则可以使用<timestamp>
做划分。
<timestamp key="bySecond" datePattern="yyyy-MM-dd"/>
<appender name="file" class="ch.qos.logback.core.FileAppender">
<file>${logPath}/${bySecond}/${appName}.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{36} [%file:%line] - %msg%n</pattern>
</encoder>
</appender>
这里使用yyyy-MM-dd
的方式按天存储日志,并在<file>
标签以变量的方式应用接口,有了这个配置,业务日志的存储路径为/tmp/logs/2020-10-01/heron.log
。每个文件夹下就存在一个heron.log文件。由此,又出现了一个问题。若该系统用户访问量少,日志文件体积小,只生成一个log是说的过去的。但在一个大量用户访问的系统中,每天都会产生大量的业务日志且日志文件的体积也比较大。有些项目的日志需要在服务器保存特定的时间以备相关部门的检查。
日志轮转
为了解决上述问题,logback提供了ch.qos.logback.core.rolling.RollingFileAppender
。规定日志以某种方式轮转,解决了单个日志文件体积过大,且需要手动删除早期日志的问题。
ch.qos.logback.core.rolling.RollingFileAppender
存在两个重要的子组件:
- RollingPolicy:负责日志轮转的功能(怎么做)
- TriggeringPolicy:负责日志轮转的时机(何时做)
RollingPolicy
上图为RollingPolicy
接口的实现类,每个实现类完成不同的功能。
- TimeBasedRollingPolicy:基于时间的轮转策略。即负责怎么做,又决定何时做。以时间维度轮转。
- SizeAndTimeBasedRollingPolicy: 是
TimeBasedRollingPolicy
子类,基于文件大小和时间的轮转策略,即可以显示单个日志文件的大小,也可以限制总文件的大小 - FixedWindowRollingPolicy:根据固定窗口算法重命名文件
TriggeringPolicy
触发策略可以分为两类:
- 基于时间的触发(常用)
- 基于文件尺寸的触发
TimeBasedRollingPolicy即负责轮转的行为,又负责触发轮转
ConsoleAppender、FileAppender和RollingFileAppender三者关系
日志输出格式
以默认日志输出格式为例,每个参数的意义
%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{36} [%file:%line] - %msg%n
%d{yyyy-MM-dd HH:mm:ss}
:格式化输出当前时间,pattern语法与SimpleDateFormat相同。[%-5level]
:输出日志记录事件的级别%logger{36}
:logger名称[%file:%line]
:输出类名称和行号%m
:内容%n
:换行
更多配置信息,点击查看
logback配置模板
几个拿来即用的配置模板,方便你我他
- 启动日志和业务日志分离,业务日志基于文件尺寸和时间轮转,单个文件最大10M,保留15天,日志总计5GB
<configuration scan="true" scanPeriod="5 seconds">
<contextName>heron</contextName>
<property name="appName" value="项目名称"/>
<property name="logPath" value="日志保存路径"/>
<!--控制台日志-->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{16} [%file:%line] - %msg%n</pattern>
</encoder>
</appender>
<!--基于时间和文件尺寸的轮转-->
<appender name="rollingFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logPath}/${appName}/${appName}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${logPath}/${appName}/%d{yyyy-MM-dd}/${appName}-%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>15</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="项目名称" level="debug" additivity="false">
<appender-ref ref="rollingFileAppender"/>
</logger>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
- 分别输出error info debug日志到不同的文件,并以时间和文件尺寸轮转
<configuration scan="true" scanPeriod="5 seconds">
<contextName>heron</contextName>
<property name="appName" value="app"/>
<property name="logPath" value="/mntlogs"/>
<!--控制台日志-->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{36} [%file:%line] - %msg%n</pattern>
</encoder>
</appender>
<!--error级别-->
<appender name="errorRolling" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logPath}/${appName}/error.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${logPath}/${appName}/%d{yyyy-MM-dd}/${appName}-error-%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>15</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{15} - %msg%n</pattern>
</encoder>
</appender>
<!--info级别-->
<appender name="infoRolling" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logPath}/${appName}/info.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${logPath}/${appName}/%d{yyyy-MM-dd}/${appName}-info-%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>15</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{15} - %msg%n</pattern>
</encoder>
</appender>
<!--debug级别-->
<appender name="debugRolling" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logPath}/${appName}/debug.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${logPath}/${appName}/%d{yyyy-MM-dd}/${appName}-debug-%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>15</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{15} - %msg%n</pattern>
</encoder>
</appender>
<logger name="项目类名" level="debug" additivity="false">
<appender-ref ref="errorRolling"/>
<appender-ref ref="infoRolling"/>
<appender-ref ref="debugRolling"/>
</logger>
<!--spring data mongo输出查询语句-->
<logger name="org.springframework.data.mongodb.core" level="debug" additivity="false">
<appender-ref ref="debugRolling"/>
</logger>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。