Java 日志框架总结

日志门面:

日志门面——SLF4J

优点:

  • 通过依赖配置,在编译期静态绑定真正的日志框架库。

    What has changed in SLF4J version 2.0.0?

    More visibly, slf4j-api now relies on the ServiceLoader mechanism to find its logging backend. SLF4J 1.7.x and earlier versions relied on the static binder mechanism which is no loger honored by slf4j-api version 2.0.x. More specifically, when initializing the LoggerFactory class will no longer search for the StaticLoggerBinder class on the class path.

    Instead of “bindings” now org.slf4j.LoggerFactory searches for “providers”. These ship for example with slf4j-nop-2.0.x.jar, slf4j-simple-2.0.x.jar or slf4j-jdk14-2.0.x.jar.

  • 不需要使用 logger.isDebugEnabled() 来解决日志因为字符拼接产生的性能问题。SLF4J 的方式是使用 {} 作为字符串替换符。

  • 提供了很多桥接方案,以便灵活替换日志库。

缺点:

  • 旧版不支持 Lambda 表达式(slf4j-api-2.0.0-alpha 支持但还未 GA)。但日志框架 Log4j 2.4 及以上版本支持 Java 8 Lambda,参考 12,例如:

    1
    logger.debug("This {} and {} with {} ", () -> this, () -> that, () -> compute());

SLF4J bindings

http://www.slf4j.org/manual.html#swapping

concrete bindings

Logback

https://logback.qos.ch/

推荐使用 SLF4J bound to logback-classic 的经典组合:

1
2
3
4
5
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>

传递依赖如下:

1
2
3
ch.qos.logback:logback-classic:jar:1.2.3:compile
+- ch.qos.logback:logback-core:jar:1.2.3:compile
\- org.slf4j:slf4j-api:jar:1.7.25:compile

Log4j 2.x

https://logging.apache.org/log4j/2.x/

需引入适配层依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.16.0</version>
</dependency>

传递依赖如下:

1
2
3
4
org.apache.logging.log4j:log4j-slf4j-impl:jar:2.16.0:compile
+- org.slf4j:slf4j-api:jar:1.7.25:compile
+- org.apache.logging.log4j:log4j-api:jar:2.16.0:compile
\- org.apache.logging.log4j:log4j-core:jar:2.16.0:runtime

参考:

Log4j 1.x

https://logging.apache.org/log4j/1.2/

On August 5, 2015 the Logging Services Project Management Committee announced that Log4j 1.x had reached end of life.

需引入适配层依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.32</version>
</dependency>

传递依赖如下:

1
2
3
org.slf4j:slf4j-log4j12:jar:1.7.32:compile
+- org.slf4j:slf4j-api:jar:1.7.32:compile
\- log4j:log4j:jar:1.2.17:compile

java.util.logging (JUL)

需引入适配层依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-jdk14 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.32</version>
</dependency>

传递依赖如下:

1
2
org.slf4j:slf4j-jdk14:jar:1.7.32:compile
\- org.slf4j:slf4j-api:jar:1.7.32:compile

日志框架——Logback

Configuration

https://logback.qos.ch/manual/configuration.html

Configuration file syntax

Configuring Appenders

Logback 配置文件样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
<springProperty scope="context" name="LOG_PATH" source="log.path" defaultValue="/data/logs"/>
<springProperty scope="context" name="ACTIVE_PROFILE" source="spring.profiles.active" defaultValue="dev" />
<springProperty scope="context" name="LOG_PATTERN" source="log.pattern" defaultValue="[%d{yyyy-MM-dd HH:mm:ss.SSS}] %5p [%t] %logger{50}:%L [%X{traceId}] --> %m%n" />
<springProperty scope="context" name="MONGO_URI" source="spring.data.mongodb.uri" />

<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>

<!-- 每天归档日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/project-${ACTIVE_PROFILE}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<fileNamePattern>${LOG_PATH}/project-${ACTIVE_PROFILE}-%i.log.%d{yyyyMMdd}</fileNamePattern>
<!--日志文件保留天数-->
<maxHistory>90</maxHistory>
<maxFileSize>100MB</maxFileSize>
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>

<!-- 自定义 MongoDB Appender -->
<appender name="MONGO" class="com.test.MongoDBAppender"></appender>

<logger name="org.springframework" level="WARN"/>

<root level="INFO">
<appender-ref ref="FILE" />
<appender-ref ref="STDOUT" />
<appender-ref ref="MONGO" />
</root>

</configuration>

Appenders

有三种 Appenders:

  1. Logback 官方提供的 Appenders。
  2. 第三方提供的 Appenders(logstash、…)。
  3. 自定义 Appenders。

官方 Appenders

Logback 官方提供的 Appenders 如下:http://logback.qos.ch/manual/appenders.html

Appender

其中,常用的两个 Appender 继承结构如下:

  • ch.qos.logback.core.ConsoleAppender
  • ch.qos.logback.core.rolling.RollingFileAppender

OutputStreamAppender

其中,ch.qos.logback.core.rolling.RollingFileAppender 可选的策略类如下:

Policy

第三方 Appenders

https://github.com/logstash/logstash-logback-encoder

Provides logback encoders, layouts, and appenders to log in JSON and other formats supported by Jackson.

自定义 Appenders

通过自定义 Appenders,可以将日志输出到任意位置,如 MongoDB、Kafka 等服务。

下面演示一个简单的同步 Appender,如需异步 Appender,参考这里

首先,新建 Appender,继承自抽象类:

UnsynchronizedAppenderBase

ILoggingEvent

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDbFactory;

public class MongoDBAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {

private SimpleMongoClientDbFactory mongoDbFactory;
private MongoTemplate mongoTemplate;
private final ReentrantLock lock = new ReentrantLock(false);

@Override
public void start() {
String uri = this.getContext().getProperty("MONGO_URI");
mongoDbFactory = new SimpleMongoClientDbFactory(uri);
mongoTemplate = new MongoTemplate(mongoDbFactory);
super.start();
}

@Override
protected void append(ILoggingEvent eventObject) {
lock.lock();
try {
PayLog log = new PayLog();
log.setTraceId(eventObject.getMDCPropertyMap().getOrDefault(MDCUtils.LOG_TRACE_ID, null));
log.setOper(eventObject.getMDCPropertyMap().getOrDefault(MDCUtils.OPER, null));
log.setLevel(eventObject.getLevel().toString());
log.setLogger(eventObject.getLoggerName());
log.setThread(eventObject.getThreadName());
log.setMessage(eventObject.getFormattedMessage());
log.setTimestamp(eventObject.getTimeStamp());
mongoTemplate.insert(log, "payLog");
} finally {
lock.unlock();
}
}

}

最后,验证 MongoDB 数据。

Encoders & Layouts

https://logback.qos.ch/manual/encoders.html

https://logback.qos.ch/manual/layouts.html

Logback 提供的 ch.qos.logback.core.encoder.Encoderch.qos.logback.core.Layout 的默认实现如下:

Layout

一般有几种使用组合方式:

自定义 Pattern 格式

基于自定义 Pattern 格式,直接使用 ch.qos.logback.classic.encoder.PatternLayoutEncoder 即可:

1
2
3
4
5
6
7
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
...
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>

还可以输出扩展信息,例如 %X{traceId}

预定义格式

基于预定义格式,需要使用 ch.qos.logback.core.encoder.LayoutWrappingEncoder,并搭配相应 Layout 实现。例如输出成 HTML 格式:

1
2
3
4
5
6
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
...
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.classic.html.HTMLLayout" />
</encoder>
</appender>

Mapped Diagnostic Contexts (MDC)

https://logback.qos.ch/manual/mdc.html

To uniquely stamp each request, the user puts contextual information into the MDC, the abbreviation of Mapped Diagnostic Context.

The MDC class contains only static methods. It lets the developer place information in a diagnostic context that can be subsequently retrieved by certain logback components. The MDC manages contextual information on a per thread basis. Typically, while starting to service a new client request, the developer will insert pertinent contextual information, such as the client id, client’s IP address, request parameters etc. into the MDC. Logback components, if appropriately configured, will automatically include this information in each log entry.

Please note that MDC as implemented by logback-classic assumes that values are placed into the MDC with moderate frequency. Also note that a child thread does not automatically inherit a copy of the mapped diagnostic context of its parent.

Logback leverages SLF4J API:

MDC And Managed Threads

https://logback.qos.ch/manual/mdc.html#managedThreads

MDCInsertingServletFilter

https://logback.qos.ch/manual/mdc.html#mis

例子

一、创建 MDCUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
import lombok.experimental.UtilityClass;
import org.slf4j.MDC;

@UtilityClass
public class MDCUtils {

public static final String TRACE_ID = "traceId";

public MDC.MDCCloseable addTraceId(String traceId) {
return MDC.putCloseable(TRACE_ID, traceId);
}

}

二、在合适的位置设置上下文:

  • 在 http 接口的拦截器
  • 在定时任务执行之前

代码如下:

1
2
3
try (MDC.MDCCloseable mdc = MDCUtils.addTraceId(...)) {
// TODO
}

TraceIdInterceptor

三、选择合适的 Appender 并加以配置,参考 Logback 配置文件样例。

参考

https://github.com/logstash/logstash-logback-encoder

细说 Java 主流日志工具库

SpringBoot + MDC 实现全链路调用日志跟踪

Java 中打印日志的正确姿势

实战总结 | 系统日志规范及最佳实践 | 阿里技术