日志的概念

1、日志简介

1、日志是什么

日志:记录程序的运行轨迹,方便查找关键信息,也方便快速定位解决问题

通常,Java程序员在开发项目时都是依赖Eclipse/IDEA等集成开发工具的Debug 调试功能来跟踪解决Bug,但项目发布到了测试、生产环境怎么办?你有可能会说可以使用远程调试,但实际并不能允许让你这么做。

所以,日志的作用就是在测试、生产环境没有 Debug 调试工具时开发和测试人员定位问题的手段。日志打得好,就能根据日志的轨迹快速定位并解决线上问题,反之,日志输出不好,不仅无法辅助定位问题反而可能会影响到程序的运行性能和稳定性。

很多介绍 AOP 的地方都采用日志来作为介绍,实际上日志要采用切面的话是极其不科学的!对于日志来说,只是在方法开始、结束、异常时输出一些什么,那是绝对不够的,这样的日志对于日志分析没有任何意义。如果在方法的开始和结束整个日志,那方法中呢?如果方法中没有日志的话,那就完全失去了日志的意义!如果应用出现问题要查找由什么原因造成的,也没有什么作用。这样的日志还不如不用!

2、日志的作用

不管是使用何种编程语言,日志输出几乎无处不再。总结起来,日志大致有以下几种用途:

  • 「问题追踪」:辅助排查和定位线上问题,优化程序运行性能
  • 「状态监控」:通过日志分析,可以监控系统的运行状态
  • 「安全审计」:审计主要体现在安全上,可以发现非授权的操作

总结:日志在应用程序中是非常非常重要的,好的日志信息能有助于我们在程序出现BUG时能快速进行定位,并能找出其中的原因。作为一个有修养的程序猿,对日志这个东西应当引起足够的重视。

2、常见日志框架

1、日志门面

  • JCL(Apache Commons Logging,原名 Jakarta Commons Logging):commons-logging 是日志的门面接口,它也是Apache 最早提供的日志门面接口,用户可以根据喜好选择不同的日志实现框架,而不必改动日志定义,这就是日志门面的好处,符合面对接口抽象编程。现在已经不太流行了,了解一下就行
  • SLF4j( Simple Logging Facade for Java):为Java提供的简单日志Facade。Facade门面,更底层一点说就是接口。它允许用户以自己的喜好,在工程中通过slf4j接入不同的日志系统。因此slf4j入口就是众多接口的集合,它不负责具体的日志实现,只在编译时负责寻找合适的日志系统进行绑定。具体有哪些接口,全部都定义在slf4j-api中。查看slf4j-api源码就可以发现,里面除了public final class LoggerFactory类之外,都是接口定义。因此slf4j-api本质就是一个接口定义。
  • jboss-logging(诞生就不是为了服务大众)

2、日志实现:JUL(java.util.logging)、Log4j、Log4j2、Logback

  • JUL:这是 Java 自带的日志工具类,在 JDK 1.5 开始就已经有了,在 java.util.logging 包下。通常情况下,这个基本没什么人用了,了解一下就行
  • Log4j:是 Apache 的一个开源日志框架,也是市场占有率最多的一个框架。注意:log4j 在 2015.08.05 这一天被 Apache 宣布停止维护了,用户需要切换到 Log4j2上面去
  • Log4j2:Apache Log4j 2是apache开发的一款Log4j的升级产品。Log4j2与Log4j1发生了很大的变化,log4j2不兼容log4j1
  • Logback:是 Slf4j 的原生实现框架,同样也是出自 Log4j 一个人之手,但拥有比 log4j 更多的优点、特性和更做强的性能,现在基本都用来代替 log4j 成为主流

SLF4J、Log4j、Logback都是同一个人Ceki写的。为什么写三个呢?那是因为Ceki先写了Log4j,但后面觉得Log4j太烂了,他已经不想改了。所以后面写了logback。Log4j2呢,从设计上来说很优秀,但太优秀了,太先进了,很多开源框架对其支持有限(Log4j2性能优于logback,但为了有高性能,设计得过于复杂,没必要,用Logback就够了)

如下是日志结构框架:

┌───────────────────┐                  ┌────────────────────────────────────┐
│    日志适配器      │    »»»»»»»»»     │              日志门面               │
│                   │    »»»»»»»»»     │        slf4j     commons-logging   │
│                   │                  └────────────────────────────────────┘
│   jul-to-slf4j    │                  ┌────────────────────────────────────┐
│  log4j-over-slf4j │                  │            日志门面的适配器          │
│   jcl-to-slf4j    │                  │ slf4j-jdk  slf4j-jcl slf4j-log4j12 |
└───────────────────┘                  └────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────┐
│                           日志库的具体实现                                  │
│      log4j       log4j2       logback       log-jdk       otherLogging    │
└───────────────────────────────────────────────────────────────────────────┘

3、日志框架怎么选

选项太多了的后果就是选择困难症,我的看法是没有最好的,只有最合适的:

  • commons-loggin、slf4j 只是一种日志抽象门面,不是具体的日志框架。log4j、logback 是具体的日志实现框架。
  • 在关注性能的地方,选择Logback或自己实现高性能Logging API可能更合适。推荐:slf4j + logback.
  • 在非常关注异步性能或已经使用了Log4j的项目中,如果没有发现问题,继续使用可能是更合适的方式:推荐组合为:slf4j + Log4j2
  • 如果不想有依赖则使用java.util.logging或框架容器已经提供的日志接口

  • SLF4J实现机制决定SLF4J限制较少,使用范围更广。由于SLF4J在编译期间,静态绑定本地的LOG库使得通用性要比Commons Logging要好
  • Commons Logging开销更高,所以一般日志门面都会选择SLF4J
  • Logback拥有更好的性能。Logback声称:某些关键操作,比如判定是否记录一条日志语句的操作,其性能得到了显著的提高。这个操作在Logback中需要3纳秒,而在Log4J中则需要30纳秒。LogBack创建记录器(logger)的速度也更快:13毫秒,而在Log4J中需要23毫秒。更重要的是,它获取已存在的记录器只需94纳秒,而Log4J需要2234纳秒,时间减少到了1/23。跟JUL相比的性能提高也是显著的(这只是在Log4j2出现之前)
  • Logback文档免费。Logback的所有文档是全面免费提供的,不象Log4J那样只提供部分免费文档而需要用户去购买付费文档
  • Log4j2的出现改变的Logback的地位,他重写了Log4j1.x并修复了Logback中的不足,在性能方面不管是同步还是异步功能都吊打所有的日志框架
  • 总结:如果是使用SpringBoot开发,正常没有特殊性能要求的话,使用Logback或Log4j2都行,建议使用Logback,SpringBoot自带可以减少引入依赖,如果对性能有要求,并且对功能要求较高,可以使用Log4j2(在功能和性能上都比Logback丰富)

4、日志发展历史

1、阶段一(System)

2001年以前,Java是没有日志库的,打印日志全凭:System.out 和 System.err。缺点:

  1. 产生大量的IO操作同时在生产环境中无法合理的控制是否需要输出
  2. 输出的内容不能保存到文件
  3. 只打印在控制台,打印完就过去了,也就是说除非你一直盯着程序跑
  4. 无法定制化,且日志粒度不够细

2、阶段二(Log4j)

2001年,一个叫Ceki Gulcü的大佬搞了一个日志框架 log4j后来(Log4j成为Apache项目),Ceki加入Apache组织Apache还曾经建议Sun引入Log4j到Java的标准库中,但Sun拒绝了。

3、阶段三(JUL)

Sun有自己的小心思,2002年2月JDK1.4发布,Sun推出了自己的日志标准库JUL(Java Util Logging),其实是照着Log4j抄的,而且还没抄好,还是在JDK1.5以后性能和可用性才有所提升。由于Log4j比JUL好用,并且成熟,所以Log4j在选择上占据了一定的优势。

4、阶段四(JCL)

2002年8月Apache推出了JCL(Jakarta Commons Logging),也就是日志抽象层,支持运行时动态加载日志组件的实现,当然也提供一个默认实现Simple Log(在ClassLoader中进行查找,如果能找到Log4j则默认使用Log4j实现,如果没有则使用JUL实现,再没有则使用JCL内部提供的Simple Log实现)

项目
                                       ⬇
                                      JCL
                                       ⬇
     ┌────────────────────┬───────────────────────────┬──────────────────────────────┐
Log4JLogger          JDK14Logger              Jdk13LumberjackLogger              SimpleLog

JUL有三个缺点:

1.效率较低
2.容易引发混乱
3.使用了自定义ClassLoader的程序中,使用JCL会引发内存泄露

5、阶段五(SLF4J)

SLF4J的由来与背景:在Log4j开发出来出来之后,Log4j就受到了广大开发者的爱好,纷纷开始使用Log4j,但是后来Log4j的创始人跟Apache因为一些矛盾从Apache辞职自己去创业了,创始人为了给自己的公司打出一点名声,所以就基于Log4j又开发出了一个新的日志框架Logback,Logback不管是性能还是功能都比Log4j强,但是却很少人使用, 因为JCL门面不支持Logback,所以这个创世人又设计出了slf4j,slf4j支持市面上所有主流的日志框架;所以目前为止,使用最多的就是SLF4J了; 后来Apache又基于Logback的源码设计出了Log4j2日志,性能上Log4j2比Logback更胜一筹,并且Log4j2既是日志框架,也是门面技术,但是Log4j2的门面技术很少人使用,大多还是使用SLF4J。

2006年巨佬Ceki(Log4j的作者)因为一些原因离开了Apache组织,之后Ceki觉得JCL不好用,自己重写了一套新的日志标准接口规范Slf4j(Simple Logging Facacfor Java),也可以称为日志门面,很明显Slf4j是对标JCL,后面也证明了Slf4j比JCL更优秀。

大佬Ceki提供了一系列的桥接包来帮助Slf4j接口与其他日志库建立关系,这种方式称桥接设计模式。代码使用Slf4j接口,就可以实现日志的统一标准化,后续如果想要更换日志实现,只需引入Slf4j与相关的桥接包,再引入具体的日志标准库即可。

6、阶段六(Logback)

Ceki巨佬觉得市场上的日志标准库都是间接实现Slf4j接口,也就是说每次都需要配合桥接包,因此在2006年,Ceki巨佬基于Slf4j接口写出了Logback日志标准库,做为Slf4j接口的默认实现,Logback也十分给力,在功能完整度和性能上超越了所有已有的日志标准库。

  • 根本原因还在于,随着用户体量的提升,Log4j无法满足高性能的要求,成为应用的性能瓶颈
  • 通过SLF4j桥接到具体的日志框架实现。日志框架的绑定(Binding):绑定其他日志的实现
  • 通过其他日志框架桥接到SLF4j。日志框架的桥接(Bridging):桥接旧的日志框架

7、阶段七(Log4j2)

产生背景:Logback算是JAVA 里一个老牌的日志框架,从06年开始第一个版本,迭代至今也十几年了。不过logback最近一个最稳定版本还停留在 2017 年,好几年都没有更新大版本了Llogback的兄弟SLF4J最近一个稳定版也是2017年。而且Logback的异步性能实在拉跨,功能简陋,配置又繁琐,远不及Apache的新一代日志框架:Log4j2。目前来看,Log4j2就是王者,其他日志框架都不是对手。

2012年,Apache直接推出新项目Log4j2(不兼容Log4j),Log4j2全面借鉴Slf4j+Logback 。Log4j2不仅仅具有Logback的所有特性,还做了分离设计,分为log4j-api和log4j-core,log4j-api是日志接口,log4j-core是日志标准库,并且Apache也为Log4j2提供了各种桥接包。而且log4j2 的性能提升很大,而且支持异步日志打印。增加很多新的特性

5、日志打印的建议

1、打日志建议简单版

  1. 应用中不可直接使用日志系统中的API (Log4j,Logback),而应依赖使用日志框架SLF4J中的API。使用门面模式有利于维护和各个类的日志处理方法统一
  2. 日志文件推荐至少保存15天,因为有些异常具备以“周”为频次发生的特点
  3. 应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:appName_logType_logName.log。logType为日志类型,推荐分类有stats/monitor/visit 等
  4. logName为日志描述。这种命名的好处:通过文件名就可以知道日志文件属于哪个应用,哪种类型,有什么目的,这也有利于归类查找
  5. 对trace、debug、info级别的日志输出,必须使用条件输出形式或者占位符的方式
  6. 避免重复打印日志,否则会浪费磁盘空间。务必在日志配置文件中设置additivity=false
  7. 异常信息应该包括两类:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字向上抛出
  8. 谨慎地记录日志。生产环境禁止输出debug日志;有选择地输出info日志;如果使用warn记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免吧服务器磁盘撑爆,并及时删除这些观察日志
  9. 可以使用warn日志级别记录用户输入参数错误的情况,避免当用户投诉时无所适从

2、打日志建议详情版

工作总结:日志打印的15个建议 (qq.com)

日志是快速定位问题的好帮手,是撕逼和甩锅的利器!打印好日志非常重要。今天我们来聊聊日志打印的15个好建议

1、选择恰当的日志级别

常见的日志级别有5种,分别是error、warn、info、debug、trace。日常开发中,我们需要选择恰当的日志级别,不要直接打印info

  • error:错误日志,指比较严重的错误,对正常业务有影响,需要运维配置监控的
  • warn:警告日志,一般的错误,对业务影响不大,但是需要开发关注
  • info:信息日志,记录排查问题的关键信息,如调用时间、出参入参等等;
  • debug:用于开发DEBUG的,关键逻辑里面的运行时数据;
  • trace:最详细的信息,一般这些信息只记录到日志文件中。

2、日志要打印出方法的入参和出参

我们并不需要打印很多很多日志,只需要打印可以快速定位问题的有效日志。有效的日志,才能有效的查到问题!哪些算得的上有效关键的日志呢?比如说,方法进来的时候,打印入参。再然后呢,在方法返回的时候,就是打印出参,返回值。入参的话,一般就是userId或者bizSeq这些关键信息。正例如下:

public String testLogMethod(Document doc, Mode mode){
    log.debug(“method enter param:{}”,userId);
    String id = "666";
    log.debug(“method exit param:{}”,id);
    return id;
}

3、选择合适的日志格式

理想的日志格式,应当包括这些最基本的信息:如当前时间戳(一般毫秒精确度)、日志级别线程名字等等。在logback日志里可以这么配置:

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <pattern>%d{HH:mm:ss.SSS} %-5level [%thread][%logger{0}] %m%n</pattern>
    </encoder>
</appender>

4、遇到if…else…时,每个分支首行都尽量打印日志

当你碰到if…else…或者switch这样的条件时,可以在分支的首行就打印日志,这样排查问题时,就可以通过日志,确定进入了哪个分支,代码逻辑更清晰,也更方便排查问题了。

if(user.isVip()){
    log.info("该用户是会员,Id:{},开始处理会员逻辑",user,getUserId());
    // 会员逻辑
}else{
    log.info("该用户是非会员,Id:{},开始处理非会员逻辑",user,getUserId());
    // 非会员逻辑
}

5、日志级别比较低时,进行日志开关判断

对于trace/debug这些比较低的日志级别,必须进行日志级别的开关判断。

User user = new User(666L, "公众号", "捡田螺的小男孩");
if (log.isDebugEnabled()) {
    log.debug("userId is: {}", user.getId());
}

因为当前有如下的日志代码:

logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);

如果配置的日志级别是warn的话,上述日志不会打印,但是会执行字符串拼接操作,如果symbol是对象, 还会执行toString()方法,浪费了系统资源,执行了上述操作,最终日志却没有打印,因此建议加日志开关判断。

6、不能直接使用日志系统(Log4j、Logback)中的 API,而是使用日志框架SLF4J中的API

SLF4J 是门面模式的日志框架,有利于维护和各个类的日志处理方式统一,并且可以在保证不修改代码的情况下,很方便的实现底层日志框架的更换。

import org.slf4j.Logger; 
import org.slf4j.LoggerFactory;

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

7、建议使用参数占位{},而不是用+拼接

反例:

logger.info("Processing trade with id: " + id + " and symbol: " + symbol);

上面的例子中,使用+操作符进行字符串的拼接,有一定的性能损耗

正例:

logger.info("Processing trade with id: {} and symbol : {} ", id, symbol);

我们使用了大括号{}来作为日志中的占位符,比于使用+操作符,更加优雅简洁。并且,相对于反例,使用占位符仅是替换动作,可以有效提升性能。

8、建议使用异步的方式来输出日志。

  • 日志最终会输出到文件或者其它输出流中的,IO性能会有要求的。如果异步,就可以显著提升IO性能。
  • 除非有特殊要求,要不然建议使用异步的方式来输出日志。以logback为例吧,要配置异步很简单,使用AsyncAppender就行
<appender name="FILE_ASYNC" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="ASYNC"/>
</appender>

9、不要使用e.printStackTrace()

反例:

try {
    // 业务代码处理
} catch(Exception e) {
    e.printStackTrace();
}

正例:

try {
    // 业务代码处理
} catch(Exception e) {
    log.error("你的程序有异常啦",e);
}

理由:

  • e.printStackTrace()打印出的堆栈日志跟业务代码日志是交错混合在一起的,通常排查异常日志不太方便。
  • e.printStackTrace()语句产生的字符串记录的是堆栈信息,如果信息太长太多,字符串常量池所在的内存块没有空间了,即内存满了,那么,用户的请求就卡住啦~

10、异常日志不要只打一半,要输出全部错误信息

反例1:

try {
    //业务代码处理
} catch (Exception e) {
    // 错误
    LOG.error('你的程序有异常啦');
}

异常e都没有打印出来,所以压根不知道出了什么类型的异常。

反例2:

try {
    //业务代码处理
} catch (Exception e) {
    // 错误
    LOG.error('你的程序有异常啦', e.getMessage());
}

e.getMessage()不会记录详细的堆栈异常信息,只会记录错误基本描述信息,不利于排查问题。

正例:

try {
    //业务代码处理
} catch (Exception e) {
    // 错误
    LOG.error('你的程序有异常啦', e);
}

11、禁止在线上环境开启 debug

禁止在线上环境开启debug,这一点非常重要。因为一般系统的debug日志会很多,并且各种框架中也大量使用 debug的日志,线上开启debug不久可能会打满磁盘,影响业务系统的正常运行。

12、不要记录了异常,又抛出异常

反例如下:

log.error("IO exception", e);
throw new MyException(e);
  • 这样实现的话,通常会把栈信息打印两次。这是因为捕获了MyException异常的地方,还会再打印一次。
  • 这样的日志记录,或者包装后再抛出去,不要同时使用!否则你的日志看起来会让人很迷惑。

13、避免重复打印日志

避免重复打印日志,酱紫会浪费磁盘空间。如果你已经有一行日志清楚表达了意思,避免再冗余打印,反例如下:

if (user.isVip()) {
    log.info("该用户是会员,Id:{}",user,getUserId());
    // 冗余,可以跟前面的日志合并一起
    log.info("开始处理会员逻辑,id:{}",user,getUserId());
    // 会员逻辑
} else {
    // 非会员逻辑
}

如果你是使用log4j日志框架,务必在log4j.xml中设置 additivity=false,因为可以避免重复打印日志。正例:

<logger name="com.taobao.dubbo.config" additivity="false">

14、日志文件分离

  • 我们可以把不同类型的日志分离出去,比如access.log,或者error级别error.log,都可以单独打印到一个文件里面。
  • 当然,也可以根据不同的业务模块,打印到不同的日志文件里,这样我们排查问题和做数据统计的时候,都会比较方便啦。

15、核心功能模块,建议打印较完整的日志

  • 我们日常开发中,如果核心或者逻辑复杂的代码,建议添加详细的注释,以及较详细的日志。
  • 日志要多详细呢?脑洞一下,如果你的核心程序哪一步出错了,通过日志可以定位到,那就可以啦。

6、日志分析工具汇总

运维利器:9个Java日志分析工具:https://mp.weixin.qq.com/s/cvnA_HiglxAd5x6XtmnOIQ

  1. Apache Log4j:Java领域资格最老,应用最广的日志工具
  • 官网:https://logging.apache.org/log4j/2.x/
  • Log4j是apache的一个开源项目,创始人Ceki Gulcu。Log4j 从诞生之日到现在一直广受业界欢迎。Log4j是高度可配置的,并可通过在运行时的外部文件配置。它根据记录的优先级别,并提供机制,以指示记录信息到许多的目的地,诸如:数据库,文件,控制台,UNIX系统日志等。
  1. Graylog:适合扩展角色和权限管理的开源聚合器
  • 官网:https://www.graylog.org/
  • Graylog是强大的日志管理、分析工具。它基于 Elasticsearch, Java和MongoDB。Graylog可以收集监控多种不同应用的日志。但是为了示范说明,我只收集syslog。并且,我将会把用到的组件全部安装到一个单独的服务器上。对于大型、生产系统你可以把组件分开安装在不同的服务器上,这样可以提高效率
  1. Kibana:分析和可视化日志文件。 某些功能需要付款
  • 官网:https://www.elastic.co/products/kibana
  • Kibana是一个开源的分析和可视化平台,设计用于和Elasticsearch一起工作。你用Kibana来搜索,查看,并和存储在Elasticsearch索引中的数据进行交互。你可以轻松地执行高级数据分析,并且以各种图标、表格和地图的形式可视化数据。可谓「一张图片胜过千万行日志」
  1. Logback:通过Groovy有趣的配置选项的强大的日志库
  • 官网:https://logback.qos.ch/
  • Logback是由log4j创始人Ceki Gulcu设计的又一个开源日记组件,目标是替代log4j
  1. Logbook:可扩展的开源库,用于HTTP请求和响应日志记录
  • 官网:https://github.com/zalando/logbook
  • Logbook是一个可扩展的Java库,可为不同的客户端和服务器端技术提供完整的请求和响应日志记录。 它满足了以下特殊需求:a)允许Web应用程序开发人员记录应用程序接收或发送的任何HTTP流量;b),以便以后易于持久化和分析。 这对于传统的日志分析,满足审计要求或调查各个历史流量问题非常有用
  1. Logstash:用于管理日志文件的工具
  • 官网:https://www.elastic.co/products/logstash
  1. SLF4J:与实现一起使用的抽象层
  • 官网:https://www.slf4j.org/
  • 作者又是 Ceki Gulcu!这位大神写了Log4j、Logback和slf4j,专注日志组件开发五百年,一直只能超越自己。用作各种日志框架(例如java.util.logging,logback,log4j)的简单外观或抽象,允许最终用户在部署时插入所需的日志记录框架
  1. tinylog:具有静态记录器类的轻量级日志框架
  • 官网:https://tinylog.org/
  • tinylog是一个用于Java和Android的轻量级开源日志记录框架,针对易用性进行了优化
  1. Tracer:在分布式系统中调用跟踪和日志关联
  • 官网:https://github.com/zalando/tracer
  • Tracer是一个管理自定义跟踪标识符并通过分布式系统进行管理的库