1. 日志分类
日志能够帮助我们很快的定位出生产上的一些问题,因此程序中日志打印的质量,可以从某种程度中反应出开发者的水平。对于Java开发而言,市面上有很多日志框架可以供我们选择,选择多了有时候并不是一件好事,因为很容易混乱,需要开发者理清它们的关系并进行选用。常见的日志框架有如下几种:
- JCL(Jakarta Commons Logging),也称为“Apache Commons Logging”;
- JUL:java.util.logging,JDK自带的日志体系;
- SLF4J:Simple Logging Facade for Java,日志门面
- log4j
- log4j2
- logback
- Jboss-logging
这些日志可以分为两类
- 日志门面,即实际日志框架的抽象,一般只提供接口API。上面列举的JCL、SLF4J和Jboss-logging都属于日志门面;
- 日志实现,日志具体实现框架。
Java开发提倡面向接口编程,所以在我们实际应用当中应该使用日志门面来进行日志编程,这样方便在不同日志实现框架之间进行切换。
Spring框架中,日志默认使用的就是JCL,SpringBoot选用 SLF4j和logback。
2. JCL
使用JCL,只需要引入以下依赖即可
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
JCL中存在默认的日志实现,当classpath中没有其他日志jar即会使用其默认的日志。使用默认的日志实现,只要引入上面的jar包即可。
如果要想使用其他的日志实现,除了commons-logging
包外,还需要引入相关的依赖包,具体如下
log4j
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
log4j2
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.11.0</version>
</dependency>
<!-- log4j2与commons-logging集成包 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jcl</artifactId>
<version>2.11.0</version>
</dependency>
3. SLF4J
官方使用手册:请点这里
3.1. 简单使用
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.8.0-beta2</version>
</dependency>
程序中引用如下
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HelloWorld {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(HelloWorld.class);
logger.info("Hello World");
}
}
与JCL不同,slf4j-api
中没有提供默认的实现,如果仅添加该依赖而不添加日志实现框架的依赖,在控制台会看到如下输出:
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
SLF4J提供一个简单的日志API接口的实现项目slf4j-simple
,不过该实现比较简单,一般不在生产项目中使用它。
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.8.0-beta2</version>
<scope>test</scope>
</dependency>
3.2. SLF4J和其他日志框架
logback由于天然支持SLF4J,所以一般推荐SLF4J+logback的使用组合
应用系统应用SLF4J+具体日志实现框架的关系图如下所示
每一个日志的实现框架都有自己的配置文件。使用slf4j以后,配置文件还是做成日志实现框架自己本身的配置文件;
根据不同的日志系统,你可以按如下规则组织配置文件名,就能被正确加载:
Logback:logback-spring.xml, logback-spring.groovy, logback.xml, logback.groovy
Log4j:log4j-spring.properties, log4j-spring.xml, log4j.properties, log4j.xml
Log4j2:log4j2-spring.xml, log4j2.xml
JDK (Java Util Logging):logging.properties
日志名称如果不按照约定的也是可以的,在spring boot中通过logging.config=classpath:logging-config.xml
进行修改。
SLF4J必须要和其他日志实现框架一起使用,才能正常输出日志。不同日志框架引入的依赖有所不同,具体如下:
loggback
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.0.13</version>
</dependency>
log4j
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.8.0-beta2</version>
</dependency>
java.util.logging
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.8.0-beta2</version>
</dependency>
上面的这些依赖都会自动引入其所对应日志实现框架的jar包和slf4j-api
。
3.3. 系统中统一使用SLF4J
SLF4J官方强烈建议我们,在创建公共的类包和框架时只依赖slf4j-api
,而不依赖具体的日志实现框架。不过依然会存在一些历史原因导致我们应用引入的Jar包引用了具体的日志实现框架,如果这个时候我们依然想在我们的应用当中统一使用SLF4J+具体日志框架,该如何办呢?
考虑到这种情况,SLF4J附带了几个桥接模块,这些模块重定向对log4j,JCL和java.util.logging API的调用,就好像它们是对SLF4J API一样。
1、将系统中其他日志框架先排除出去;
2、用中间包来替换原有的日志框架;
3、我们导入slf4j其他的实现
具体案例可以参考:slf4j+log4j+logback总结
3.4. logback的配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径 /app/sv/logs -->
<property name="LOG_HOME" value="/app/myapp/logs"/>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- SQL-APPENDER 记录所有sql输出日志 -->
<appender name="SQL-APPENDER"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名 -->
<FileNamePattern>${LOG_HOME}/sql.%d{yyyy-MM-dd}-%i.log</FileNamePattern>
<!--日志文件保留天数 -->
<MaxHistory>30</MaxHistory>
<!--日志文件最大的大小 -->
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>500MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<ImmediateFlush>false</ImmediateFlush>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- APPLICATION-APPENDER 当前应用的日志 -->
<appender name="APPLICATION-APPENDER"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名 -->
<FileNamePattern>${LOG_HOME}/application.%d{yyyy-MM-dd}-%i.log</FileNamePattern>
<!--日志文件保留天数 -->
<MaxHistory>30</MaxHistory>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>500MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<ImmediateFlush>false</ImmediateFlush>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<appender name="DUBBO-APPENDER"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名 -->
<FileNamePattern>${LOG_HOME}/dubbo.%d{yyyy-MM-dd}-%i.log</FileNamePattern>
<!--日志文件保留天数 -->
<MaxHistory>30</MaxHistory>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>500MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<ImmediateFlush>false</ImmediateFlush>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<appender name="THIRD-PARTY-APPENDER"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名 -->
<FileNamePattern>${LOG_HOME}/3rd-party.%d{yyyy-MM-dd}-%i.log</FileNamePattern>
<!--日志文件保留天数 -->
<MaxHistory>30</MaxHistory>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>500MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<ImmediateFlush>false</ImmediateFlush>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<appender name="THIRD-PART-C3P0"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名 -->
<FileNamePattern>${LOG_HOME}/3rd-party-c3p0.%d{yyyy-MM-dd}-%i.log</FileNamePattern>
<!--日志文件保留天数 -->
<MaxHistory>30</MaxHistory>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>500MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<ImmediateFlush>false</ImmediateFlush>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 异步输出 -->
<appender name="ASYNC-APPLICATION" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>8096</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="APPLICATION-APPENDER"/>
</appender>
<!-- 异步输出 -->
<appender name="ASYNC-SQL" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>8096</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="SQL-APPENDER"/>
</appender>
<logger name="com.xxx.application" level="INFO" additivity="false">
<appender-ref ref="ASYNC-APPLICATION"/>
</logger>
<logger name="com.xxx.mapper" level="DEBUG" additivity="false">
<appender-ref ref="ASYNC-SQL"/>
</logger>
<logger name="org.springframework" level="INFO" additivity="false">
<appender-ref ref="THIRD-PARTY-APPENDER"/>
</logger>
<logger name="org.apache" level="INFO" additivity="false">
<appender-ref ref="THIRD-PARTY-APPENDER"/>
</logger>
<logger name="com.xxx.dubbo" level="INFO" additivity="false">
<appender-ref ref="DUBBO-APPENDER"/>
</logger>
<logger name="com.mchange" level="TRACE" additivity="false">
<appender-ref ref="ASYNC-C3P0"/>
</logger>
<logger name="com.aliyun" level="INFO" additivity="true">
<appender-ref ref="ASYNC-APPLICATION"/>
</logger>
<!-- 日志输出级别-->
<root level="INFO">
<!-- <appender-ref ref="STDOUT"/> -->
</root>
</configuration>
更多请参考:
logback日志配置
logback 配置详解(一)——logger、root
4. SpringBoot日志关系
SpringBoot中日志starter依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
其底层依赖关系
可以看到
1)、SpringBoot底层是使用slf4j+logback的方式进行日志记录
2)、SpringBoot也把其他的日志都替换成了slf4j;
3)、引入了中间替换包
如果我们在SpringBoot项目中引入其他框架,一定要把这个框架的默认日志依赖移除掉。例如:spring-core
依赖了commons-logging
我们需要将其排除掉。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
5. 日志规约
《阿里巴巴Java开发手册》中关于日志方面的规定如下
1.【强制】应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架
SLF4J中的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
privatestaticfinalLoggerlogger=LoggerFactory.getLogger(Abc.class);
2.【强制】日志文件推荐至少保存15天,因为有些异常具备以“周”为频次发生的特点。
3.【强制】应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:
appName_logType_logName.log。logType:日志类型,推荐分类有
stats/desc/monitor/visit等;logName:日志描述。这种命名的好处:通过文件名就可知
道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。
正例:mppserver应用中单独监控时区转换异常,如:
mppserver_monitor_timeZoneConvert.log
说明:推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于
通过日志对系统进行及时监控。
4.【强制】对trace/debug/info级别的日志输出,必须使用条件输出形式或者使用占位符的方式。
说明:logger.debug("Processing trade with id:"+id+" and symbol:"+symbol);
如果日志级别是warn,上述日志不会打印,但是会执行字符串拼接操作,如果symbol是对象,会执行toString()方法,浪费了系统资源,执行了上述操作,最终日志却没有打印。
正例:(条件)
if(logger.isDebugEnabled()){
logger.debug("Processingtradewithid:"+id+"andsymbol:"+symbol);
}
正例:(占位符)
logger.debug("Processingtradewithid:{}andsymbol:{}",id,symbol);
5.【强制】避免重复打印日志,浪费磁盘空间,务必在log4j.xml中设置additivity=false。
正例:<loggername="com.taobao.dubbo.config"additivity="false">
6.【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字throws往上抛出。
正例:logger.error(各类参数或者对象toString+"_"+e.getMessage(),e);
7.【推荐】谨慎地记录日志。生产环境禁止输出debug日志;有选择地输出info日志;如果使用warn来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。
说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请
思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
8.【参考】可以使用warn日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。注意日志输出的级别,error级别只记录系统逻辑出错、异常等重要的错误信息。如非必要,请不要在此场景打出error级别。
参考文档
Bridging legacy APIs
SLF4J user manual
Spring Boot干货系列:(七)默认日志框架配置
《阿里巴巴Java开发手册》