目录

日志 Log

SLF4J

Java 的简单日志外观 (SLF4J - The Simple Logging Facade for Java) 用作各种日志框架(例如 java.util.logging、logback、log4j)的简单外观或抽象,允许最终用户在部署时插入所需的日志框架。请注意,启用 SLF4J 的库/应用程序意味着仅添加一个强制依赖项,即 slf4j-api-1.7.36.jar。

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.3</version>
</dependency>
1
2
3
4
5
6
7
8
9
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");
  }
}

在部署时与日志框架绑定

SLF4J providers (or bindings)

  • slf4j-log4j12-2.0.3.jar log4j 1.2 版的绑定/提供程序,一个广泛使用的日志框架。鉴于 log4j 1.x 已在 2015 年和 2022 年宣布 EOL,从 SLF4J 1.7.35 开始,slf4j-log4j模块在构建时自动重定向到 slf4j-reload4j模块。 假设您希望继续使用 log4j 1.x 框架,我们强烈建议您改用 slf4j-reload4j。
  • slf4j-reload4j-2.0.3.jar 自 1.7.33 以来 RELOAD4J 框架 的绑定/提供程序 。Reload4j 是 log4j 版本 1.2.7 的直接替代品。您还需要将 reload4j.jar 放在您的类路径上。
  • slf4j-jdk14-2.0.3.jar java.util.logging 的绑定/提供程序,也称为 JDK 1.4 日志记录
  • slf4j-nop-2.0.3.jar NOP 的绑定/提供者,默默地丢弃所有日志记录。
  • slf4j-simple-2.0.3.jar 简单 实现的绑定/提供程序,它将所有事件输出到 System.err。只打印级别 INFO 和更高级别的消息。此绑定在小型应用程序的上下文中可能很有用。
  • slf4j-jcl-2.0.3.jar Jakarta Commons Logging的绑定/提供者。此绑定会将所有 SLF4J 日志记录委托给 JCL。
  • logback-classic-1.4.2.jar 用于 Jakarta EE,需要 logback-core-1.4.2.jar 或 logback-classic-1.3.2.jar 用于 Javax EE,需要 logback-core-1.3.2。logback 是 SLF4J 的 NATIVE IMPLEMENTATION,不需要 SLF4J bindings/providers。

从 2.0.0 版本开始,SLF4J 绑定被称为提供者。SLF4J API 版本 2.0.0 依赖 ServiceLoader 机制来查找其日志记录后端。

/images/tool/slf4j-concrete-bindings.png
SLF4J 提供者

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
<!-- LOGBACK-CLASSIC 1.3.X (JAVAX EE) -->
<dependency> 
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.3.2</version>
</dependency>
<!-- LOGBACK-CLASSIC 1.4.X (JAKARTA EE) -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.4</version>
</dependency>

Log4j 2 SLF4J Binding

  • log4j-slf4j-impl 应与 SLF4J 1.7.x 版本或更早版本一起使用。
  • log4j-slf4j2-impl 应该与 SLF4J 2.0.x 版本或更新版本一起使用。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>2.0.3</version>
</dependency><!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-api</artifactId>
  <version>2.19.0</version>
</dependency><!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-core</artifactId>
  <version>2.19.0</version>
</dependency><!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j2-impl -->
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-slf4j2-impl</artifactId>
  <version>2.19.0</version>
</dependency>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
    <Properties>
        <Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1} - %m%n</Property>
    </Properties>

    <Appenders>
        <Console name="console" target="SYSTEM_OUT" follow="true">
            <PatternLayout pattern="${LOG_PATTERN}"/>
        </Console>
    </Appenders>

    <Loggers>
        <Root level="info">
            <AppenderRef ref="console"/>
        </Root>
    </Loggers>
</Configuration>

MDC 映射调试上下文

MDC (Mapped Diagnostic Context,映射调试上下文) logback 及log4j2 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。

当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

 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
public class LogInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果有上层调用就用上层的ID
        String traceId = request.getHeader(Constants.TRACE_ID);
        if (traceId == null) {
            traceId = TraceIdUtil.getTraceId();
        }

        MDC.put(Constants.TRACE_ID, traceId);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        //调用结束后删除
        MDC.remove(Constants.TRACE_ID);
    }
}
1
<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>

MDC 存在的问题,丢失traceId的情况,来一个再解决一个,绝不提前优化。

  • 子线程中打印日志丢失 traceId
  • HTTP 调用丢失 traceId

子线程在打印日志的过程中traceId将丢失,解决方式为重写线程池。

 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class ThreadMdcUtil {
    public static void setTraceIdIfAbsent() {
        if (MDC.get(Constants.TRACE_ID) == null) {
            MDC.put(Constants.TRACE_ID, TraceIdUtil.getTraceId());
        }
    }

    public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                return callable.call();
            } finally {
                MDC.clear();
            }
        };
    }

    public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}
public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                        BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                        BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }

    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                        BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }

    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                        BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
                                        RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    @Override
    public void execute(Runnable task) {
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public <T> Future<T> submit(Runnable task, T result) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public Future<?> submit(Runnable task) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
}

在使用HTTP调用第三方服务接口时traceId将丢失,需要对HTTP调用工具进行改造,在发送时在request header中添加traceId,在下层被调用方添加拦截器获取header中的traceId添加到MDC中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class RestTemplateTraceIdInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
        String traceId = MDC.get(Constants.TRACE_ID);
        if (traceId != null) {
            httpRequest.getHeaders().add(Constants.TRACE_ID, traceId);
        }

        return clientHttpRequestExecution.execute(httpRequest, bytes);
    }
}

restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor()));

Log4J 配置

Log4j.appender.FILE.layout.ConversionPattern=%d{yyyy-MM-dd}-%t-%x-%-5p-%-10c:%m%n

PatternLayout

如果您希望基于某种模式生成特定格式的日志信息,可使用 org.apache.Log4j.PatternLayout 格式化您的日志信息。

PatternLayout 继承自抽象类 org.apache.Log4j.Layout,覆盖了其 format() 方法,通过提供的模式,来格式化日志信息。

PatternLayout 是一个简单的 Layout 对象,提供了如下属性,该属性可通过配置文件更改:

序号 属性 & 描述
1 conversionPattern设置转换模式,默认为 %r [%t] %p %c %x - %m%n。

模式转换字符

下面的表格解释了上面模式中用到的字符,以及所有定制模式时能用到的字符:

转换字符 含义
c 使用它为输出的日志事件分类,比如对于分类 “a.b.c”,模式 %c{2} 会输出 “b.c” 。
C 使用它输出发起记录日志请求的类的全名。比如对于类 “org.apache.xyz.SomeClass”,模式 %C{1} 会输出 “SomeClass”。
d 使用它输出记录日志的日期,比如 %d{HH:mm:ss,SSS} 或 %d{dd MMM yyyy HH:mm:ss,SSS}。
F 在记录日志时,使用它输出文件名。
l 用它输出生成日志的调用者的地域信息。
L 使用它输出发起日志请求的行号。
m 使用它输出和日志事件关联的,由应用提供的信息。
M 使用它输出发起日志请求的方法名。
n 输出平台相关的换行符。
p 输出日志事件的优先级。
r 使用它输出从构建布局到生成日志事件所花费的时间,以毫秒为单位。
t 输出生成日志事件的线程名。
x 输出和生成日志事件线程相关的 NDC (嵌套诊断上下文)。
X 该字符后跟 MDC 键,比如 X{clientIP} 会输出保存在 MDC 中键 clientIP 对应的值。
% 百分号, %% 会输出一个 %。

格式修饰符

缺省情况下,信息保持原样输出。但是借助格式修饰符的帮助,就可调整最小列宽、最大列宽以及对齐。

下面的表格涵盖了各种修饰符:

格式修饰符 左对齐 最小宽度 最大宽度 注释
%20c 20 如果列名少于 20 个字符,左边使用空格补齐。
%-20c 20 如果列名少于 20 个字符,右边使用空格补齐。
%.30c 不适用 30 如果列名长于 30 个字符,从开头剪除。
%20.30c 20 30 如果列名少于 20 个字符,左边使用空格补齐,如果列名长于 30 个字符,从开头剪除。
%-20.30c 20 30 如果列名少于 20 个字符,右边使用空格补齐,如果列名长于 30 个字符,从开头剪除。

附录