有点标题党了,以下是个人对于日志的一些理解
日志基础
日志大致分为两类
- 日志门面(接口)
- 日志实现
日志门面
提供了标准的日志输出API,其底层如何实现,或者说采用什么日志组件,由开发者进行选择。当然,选择的日志组件实现了对应日志门面
的接口,以下是市场上常用的日志框架
日志门面 | 日志实现 |
---|---|
JCL(Jakarta Commons Logging) 、slf4j(Simple Logging Facade for Java) 、jboss-logging | log4j JUL(java.util.logging) 、log4j2 、logback |
slf4j、logback、log4j是出自同一人之手,log4j由于性能问题,作者觉得修改起来改动太大,干脆就直接写了logback。由于是出自同一人之后,slf4j和logback的契合度很高,springboot默认的日志选择也是slf4j+logback
log4j2出自apache,性能据说非常强悍,但出过一些漏洞
之前项目导入了很多关于日志的jar包,但是由于对这些jar包不是很了解,导致出了日志相关的问题不知道如何排查,下面简单介绍一下

这是slf4j官网的一张图:
- 假如我们项目中只导入了slf4j-api.jar:上面已经讲过,slf4j只是日志门面,相当于接口,没有实现的情况下,它是没有什么作用的,所以打印出null
- 假如我们项目中导入了slf4j-api.jar,logback-classic.jar,logback-core.jar:我们既导入了接口,也导入了实现类,那么我们就可以完美打印出日志了
- 假如我们项目中用的日志组件是log4j(reload4j是log4j是升级版,因为之前log4j出现了重大漏洞),那么就很尴尬了,log4j是很早之前就出来了,都不知道之后会有slf4j的出现,怎么可能实现了slf4j。不用怕,slf4j也有解决方案,它使用了一个桥接jar,slf4j-reload4j.jar,看名字就知道,将slf4j转换成了reload4j,我们再导入一个reload4j.jar就可以完美解决了
看这个描述,跟JDBC是很像的,都是利用Java的SPI机制实现的,实现SPI主要分三步
- 定义一个接口
- 提供方的
META-INF/services
目录下新建一个名称为接口全限定名的文本文件,内容为接口实现类的全限定名 - 调用方通过
ServiceLoader#load
方法加载接口的实现类实例
这是slf4j github 上的项目地址 github.com/qos-ch/slf4...
我们使用slf4j的时候,一般是使用如下的代码
java
@SpringBootTest
class LogDemoApplicationTests {
@Test
void test() {
Logger logger = LoggerFactory.getLogger(LogDemoApplication.class);
logger.debug("hello");
}
}
进入LoggerFactory.getLogger方法,会发现是通过工厂模式获得ILoggerFactory的实现类,通过实现类获取Logger
scss
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
进入getILoggerFactory()方法,代码较为简易,普通的switch case,是通过获取SLF4JServiceProvider的实现类来获取ILoggerFactory的实现类
csharp
public static ILoggerFactory getILoggerFactory() {
return getProvider().getLoggerFactory();
}
static SLF4JServiceProvider getProvider() {
// 如果是未初始化状态,进行初始化
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
// 初始化之前,先将状态改为初始化中
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
// 初始化
performInitialization();
}
}
}
switch (INITIALIZATION_STATE) {
// 初始化成功,返回对应的provider
case SUCCESSFUL_INITIALIZATION:
return PROVIDER;
// 没有找到日志门面的实现,返回NOP_FALLBACK_SERVICE_PROVIDER
case NOP_FALLBACK_INITIALIZATION:
return NOP_FALLBACK_SERVICE_PROVIDER;
case FAILED_INITIALIZATION:
// 初始化失败,抛出异常
throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
case ONGOING_INITIALIZATION:
// support re-entrant behavior.
// See also http://jira.qos.ch/browse/SLF4J-97
return SUBST_PROVIDER;
}
throw new IllegalStateException("Unreachable code");
}
顺便看看performInitialization()方法中的核心代码,主要是处理三种情况
- 无引入日志门面的实现
- 引入多种日志门面的实现
- 引入一种日志门面的实现
scss
private final static void bind() {
try {
List<SLF4JServiceProvider> providersList = findServiceProviders();
// 如果出现多个SLF4JServiceProvider实例,则打印警告信息
reportMultipleBindingAmbiguity(providersList);
// 判断是否扫描到对应的SLF4JServiceProvider实例
if (providersList != null && !providersList.isEmpty()) {
// 假如有多个日志门面的实现,这里是随机选择一个
PROVIDER = providersList.get(0);
// SLF4JServiceProvider.initialize()仅在此处调用,其它地方不会调用
PROVIDER.initialize();
// 标记初始化成功
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
// 打印实际绑定的SLF4JServiceProvider
reportActualBinding(providersList);
} else {
// 标记未找到日志门面的实现
INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
// 打印警告信息
Reporter.warn("No SLF4J providers were found.");
Reporter.warn("Defaulting to no-operation (NOP) logger implementation");
Reporter.warn("See " + NO_PROVIDERS_URL + " for further details.");
Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportIgnoredStaticLoggerBinders(staticLoggerBinderPathSet);
}
postBindCleanUp();
} catch (Exception e) {
failedBinding(e);
throw new IllegalStateException("Unexpected initialization failure", e);
}
}
于是我们知道了,SPI机制中定义的那个接口是如下的这个接口
csharp
public interface SLF4JServiceProvider {
// 返回ILoggerFactory,用于LoggerFactory类的绑定
public ILoggerFactory getLoggerFactory();
// 初始化日志框架的实现
public void initialize();
}
至于加载接口的实现类实例是通过这个LazyIterator,有兴趣的同学可以看看
看完slf4j,再看看logback的实现
这是logback github 上的项目地址 github.com/qos-ch/logb...
可以清晰看到META-INF/services
目录下关于SLF4JServiceProvider
的实现

简单讲下代码,打印日志到控制台的代码路径为
ch.qos.logback.classic.Logger#filterAndLog_2
ch.qos.logback.classic.Logger#buildLoggingEventAndAppend
ch.qos.logback.classic.Logger#callAppenders
ch.qos.logback.classic.Logger#appendLoopOnAppenders
ch.qos.logback.core.spi.AppenderAttachableImpl#appendLoopOnAppenders
ch.qos.logback.core.UnsynchronizedAppenderBase#doAppend
ch.qos.logback.core.OutputStreamAppender#append
ch.qos.logback.core.OutputStreamAppender#subAppend
ch.qos.logback.core.OutputStreamAppender#writeOut
ch.qos.logback.core.OutputStreamAppender#writeBytes
ch.qos.logback.core.OutputStreamAppender#writeByteArrayToOutputStreamWithPossibleFlush
关键代码
csharp
public void callAppenders(ILoggingEvent event) {
int writes = 0;
// ILoggingEvent包装了日志相关的信息
// 通知logger的appender处理日志事件,如果想要自定义对日志的处理,可以继承
// UnsynchronizedAppenderBase<ILoggingEvent>
for (Logger l = this; l != null; l = l.parent) {
writes += l.appendLoopOnAppenders(event);
if (!l.additive) {
break;
}
}
// 当前logger未设置appender,在控制台打印提醒
if (writes == 0) {
loggerContext.noAppenderDefinedWarning(this);
}
}
SpringBoot项目中如何使用?
引入下面的依赖就可以,默认是使用slf4j+logback
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
如何打印日志
合理使用日志级别
- 日志级别并不是一成不变的,系统、功能上线初期,为了定位问题,可以打多点日志并且将日志级别定为info,系统、功能稳定后,可以适当将日志调整为debug,减少日志量不仅可以降低成本,更可以精准地定位问题,所以日志也是需要维护的
优雅打印日志
- Json格式打印方法入参
scss
public void method(Req req) {
log.info("method,req:{}", JsonUtils.toJSONString(req));
action();
}
系统、功能上线初期,入参日志的打印是必须的,系统、功能稳定后,可以适当去掉或将日志调整为debug,但个人认为这部分日志非常重要,会一直保留
- 使用订单号等信息串联关键if,注意是关键if
c
public void method(Req req) {
if (a) {
log.info("a : {}", orderCode);
} else if (b) {
log.info("b : {}", orderCode);
} else {
log.info("c : {}", orderCode);
}
}
进入不同的if,代码逻辑完全不一样,数据处理的结果、返回值也会不一样,很有利于排查问题
- 批量处理数据,打印开始、结束及处理进度
ini
public void dataJob() {
log.info("job start");
Long maxId = 0L;
List<Entity> entitylist = dao.list(maxId);
while (!entitylist.isEmpty()) {
for(;;) {
action();
}
log.info("最大id:{}", maxId);
entitylist = dao.list(maxId);
}
log.info("job end");
}
数据量太大,没有日志的话,无法预估处理时间,任务中断了都不知道
- 杜绝
麻木日志
,无辨识度,排查费时费力
php
try {
} catch(Exception e) {
// 千篇一律,最好包含信息
log.info("message: {}",e)
}
// 无格式化,难定位问题
e.printStackTrace();
- 避免打印敏感信息,例如手机号、账号密码,密钥,这个说起来简单,做起来很难,比如手机号有时候是排查问题的关键,只能尽量
-
进行日志聚合,一目了然,常见的有
- 请求外部系统,聚合请求体、返回值
- 统计方法耗时,聚合开始时间、结束时间、耗时
perl
@Override
public void httpRequest() {
log("request ---> %s, body ---> %s, response ---> %s, elapsedTime ---> %s", requestMsg, bodyMsg, responseMsg, elapsedTime);
}
- 监控日志需要设置阈值,麻木打印跟没打一样
perl
@Override
public void httpRequest() {
if (time > 1000) {
log("request ---> %s, body ---> %s, response ---> %s, elapsedTime ---> %s", requestMsg, bodyMsg, responseMsg, elapsedTime);
}
}
动态日志追踪
在项目中,可以适当加多一点debug类型的代码,如果线上出现了问题,可以通过配置日志级别来迅速排查问题,而不需要发版,在公司工作的都知道,发版的流程太长
ini
# 设置某个类的日志打印级别
logging.level.类的全限定名 = debug
logging.level.类的全限定名 = info
logging.level.类的全限定名 = warn
logging.level.类的全限定名 = error
# 该类不打日志
logging.level.类的全限定名 = off
# 设置整个项目的日志打印级别
logging.level.root = DEBUG
日志告警
以下是日志告警的方式,系统的监控不仅仅是日志的告警
- error日志实时告警
现在一般是使用ELk处理日志,但是我们也不可能时时刻刻盯着,所以将error日志实时发送到生产告警群是较好的方式

这是logback的一个机制,通过继承UnsynchronizedAppenderBase<ILoggingEvent>
接口可以对日志进行处理
scala
public class TestAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
public TestAppender() {
}
@Override
public void append(ILoggingEvent event) {
// 组装异常,告警到可以实时关注到的地方,比如生产告警群
}
}
- 统计一段时间内大量出现的异常,比如一些正常的400异常,在一段时间内大量增长,纵然是预期的异常,也有可能是代码判断错了导致的