日志

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

这些日志可以分为两类

  1. 日志门面,即实际日志框架的抽象,一般只提供接口API。上面列举的JCL、SLF4J和Jboss-logging都属于日志门面;
  2. 日志实现,日志具体实现框架。

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一样。

桥接到SLF4J

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开发手册》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352