程序运行出现错误时,第一时间想到的是甩锅还是日志?通过查看日志定位出问题的位置,才能更好的甩锅,今天就来学习 springBoot
日志如何配置。
一、日志框架
Java
中的日志框架分为两种,分别为日志抽象/门面、日志实现。
日志门面不负责日志具体实现,它只是为所有日志框架提供一套标准、规范的API
框架。其主要意义在于提供接口,具体的实现可以交由其它日志框架,例如 log4j
和logback
等。
当今主流的的日志门面是SLF4J
,SpringBoot
中推荐使用该门面技术。
1.1、SLF4J
SLF4J
官网地址:https://www.slf4j.org/
SLF4J(Simple Logging Facade For Java)
,即简单日志门面,它用作各种日志框架(例如Java.util.Logging、logback、log4j
)的简单门面或抽象,允许最终用户在部署时插入所需的日志框架。
它和JDBC
差不多,JDBC
不关心具体的数据库实现,同样的,SLF4J
也不关心具体日志框架实现。
application
下面的 SLF4JAPI
表示 SLF4J
的日志门面,包含以下三种情况:
- 如果只是导入
slf4j
日志门面,没有导入对应的日志实现框架,则日志功能默认是关闭的,不会进行日志输出。 - 蓝色图里
Logback、slf4j-simple、slf4j-nop
遵循slf4j
的API
规范,只要导入对应的日志实现框架,来实现开发 - 中间两个日志框架
slf4j-reload4、JUL(slf4j-jdk14)
没有遵循slf4j
的API
规范,所有无法直接使用,中间需要增加一个适配层(Adaptation layer)
,通过对应的适配器来适配具体的日志实现框架。
1.2、日志实现框架
Java
中的日志实现框架,主流的有以下几种:
-
log4j
:老牌日志框架,已经多年不更新了,性能比logback、log4j2
差。 -
logback
:log4j
创始人创建的另一个开源日志框架,SpringBoot
默认的日志框架。 -
log4j2
:Apache
官方项目,传闻性能优于logback
,它是log4j
的新版本。 -
JUL
:(Java.Util.Logging)
,jdk
内置。
在项目中,一般都是日志门面+日志实现框架组合使用,这样更灵活,适配起来更简单。
前面提到logback
作为Spring Boot
默认的日志框架 ,肯定有相应的考量,我司也是使用logback
作为 Spring Boot
项目中的日志实现框架,下面我们就详细说说 logback
。
二、SpringBoot
日志框架 logback
2.1、logback
是什么?
logback
是 log4j
团队创建的开源日志组件。与 log4j
类似,但是比 log4j
更强大,是log4j
的改良版本。
logback
主要包含三个模块:
-
logback-core
:所有logback
模块的基础。 -
logback-classic
:是log4j
的改良版本,完整实现了slf4j API
。 -
logback-access
:访问模块和servlet
容器集成,提供通过http
来访问日志的功能。
2.2、logback
的日志级别有哪些?
日志级别(log level)
:用来控制日志信息的输出,从高到低共分为七个等级。
-
OFF
:最高等级,用于关闭所有信息。 -
FATAL
:灾难级的,系统级别,程序无法打印。 -
ERROR
:错误信息 -
WARN
:告警信息 -
INFO
:普通的打印信息 -
DEBUG
:调试,对调试应用程序有帮助。 -
TRACE
:跟踪
如果项目中日志级别设置为 INFO
,则比它更低级别的日志信息将看不到了,即 DEBUG
日志不会显示。
默认情况下,Spring Boot
会用Logback
来记录日志,并用 INFO
级别输出到控制台。
2.3、SpringBoot
中如何使用日志?
首先新建一个 SpringBoot
项目 log
,我们看到 SpringBoot
默认已经引入 logback
依赖。
启动项目,日志打印如下:
从图中可以看出,输出的日志默认元素如下:
- 时间日期:精确到毫秒。
- 日志级别:默认是
INFO
。 - 进程
Id
- 分隔符:—标识日志开始的地方。
- 线程名称:方括号括起来的。
-
Logger
名称:源代码的类名。 - 日志内容
在业务中输出日志,常见的有两种方式。
方式一:在业务代码里添加如下代码
private final Logger log = LoggerFactory.getLogger(LoginController.class);
package com.duan.controller;
import com.duan.pojo.Result;
import com.duan.pojo.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author db
* @version 1.0
* @description LoginController
* @since 2023/12/19
*/
@RestController
public class LoginController {
private final Logger log = LoggerFactory.getLogger(LoginController.class);
@PostMapping("/login")
public Result login(@RequestBody User user){
log.info("这是正常日志");
if("admin".equals(user.getUsername()) && "123456".equals(user.getPassword())){
return Result.success("ok");
}
return Result.error();
}
}
每个类中都要添加这行代码才能输出日志,这样代码会很冗余。
方式二:使用 lomback
中的 @Slf4j
注解,但是需要在 pom
中引用 lomback
依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
使用时只需要在类上标注一个 @Slf4j
注解即可
package com.duan.controller;
import com.duan.pojo.Result;
import com.duan.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author db
* @version 1.0
* @description LoginController
* @since 2023/12/19
*/
@RestController
@Slf4j
public class LoginController {
@PostMapping("/login")
public Result login(@RequestBody User user){
log.info("这是正常日志");
if("admin".equals(user.getUsername()) && "123456".equals(user.getPassword())){
return Result.success("ok");
}
return Result.error();
}
}
2.4、如何指定具体的日志级别?
前面我们提到, SpringBoot
默认的日志级别是 INFO
,根据需要我们还可以具体的日志级别,如下:
logging:
level:
root: ERROR
将所有的日志级别都改为了 ERROR
,同时 SpringBoot
还支持包级别的日志调整,如下:
logging:
level:
com:
duan:
controller: ERROR
com.duan.controller
是项目包名。
2.5、日志如何输出到指定文件
SpringBoot
默认是把日志输出到控制台,生成环境中是不行的,需要把日志输出到文件中。
其中有两个重要配置如下:
-
logging.file.path
:指定日志文件的路径 -
logging.file.name
:日志的文件名,默认为spring.log
注意:官方文档说这两个属性不能同时配置,否则不生效,因此只需要配置一个即可。
指定日志输出文件存在当前路径的 log
文件夹下,默认生成的文件为 spring.log
logging:
file:
path: ./logs
2.6、自定义日志配置
SpringBoot
官方优先推荐使用带有 -spring
的文件名称作为项目日志配置,所以只需要在 src/resource
文件夹下创建 logback-spring.xml
即可,配置文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<!-- logback默认每60秒扫描该文件一次,如果有变动则用变动后的配置文件。 -->
<configuration scan="false">
<!-- ==============================================开发环境=========================================== -->
<springProfile name="dev">
<!-- 控制台输出 -->
<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{HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 日志输出级别 -->
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</springProfile>
<!-- ==============================================生产环境=========================================== -->
<springProfile name="prod">
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
<property name="LOG_HOME" value="./log"/>
<!-- 控制台输出 -->
<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{HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="INFO_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志名称,如果没有File 属性,那么只会使用FileNamePattern的文件路径规则
如果同时有<File>和<FileNamePattern>,那么当天日志是<File>,明天会自动把今天
的日志改名为今天的日期。即,<File> 的日志都是当天的。
-->
<file>${LOG_HOME}/info.log</file>
<!--滚动策略,按照大小时间滚动 SizeAndTimeBasedRollingPolicy-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${LOG_HOME}/info.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<!--只保留最近30天的日志-->
<MaxHistory>30</MaxHistory>
<!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
<totalSizeCap>1GB</totalSizeCap>
<MaxFileSize>10MB</MaxFileSize>
</rollingPolicy>
<!--日志输出编码格式化-->
<encoder>
<charset>UTF-8</charset>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</encoder>
<!--过滤器,只有过滤到指定级别的日志信息才会输出,如果level为ERROR,那么控制台只会输出ERROR日志-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="ERROR_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志名称,如果没有File 属性,那么只会使用FileNamePattern的文件路径规则
如果同时有<File>和<FileNamePattern>,那么当天日志是<File>,明天会自动把今天
的日志改名为今天的日期。即,<File> 的日志都是当天的。
-->
<file>${LOG_HOME}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${LOG_HOME}/error.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<MaxHistory>30</MaxHistory>
<!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
<totalSizeCap>1GB</totalSizeCap>
<MaxFileSize>10MB</MaxFileSize>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<!--指定最基础的日志输出级别-->
<root level="INFO">
<!--appender将会添加到这个loger-->
<appender-ref ref="STDOUT"/>
<appender-ref ref="INFO_APPENDER"/>
<appender-ref ref="ERROR_APPENDER"/>
</root>
</springProfile>
</configuration>
最基本配置是一个 configuration
里面有零个或多个 appender
,零个或多个 logger
和最多一个 root
标签组成。(logback
对大小写敏感)
configuration
节点:根节点,属性如下:
-
scan
:此属性为true
时,配置文件发生改变,将会被重新加载,默认为true
。 -
scanPeriod
:监测配置文件是否有修改的时间间隔,单位毫秒,当scan
为true
时,此属性生效。默认的时间间隔为1分钟 。 -
debug
:此属性为true
时,打印出logback
内部日志信息,实时查看logback
运行状态,默认false
。
root
节点:必须的节点,用来指定基础的日志级别,只有一个属性。该节点可以包含零个或者多个元素,子节点是 appender-ref
,标记 appender
将会添加到这个 logger
中。
-
level
:默认值DEBUG
contextName
节点:标识一个上下文名称,默认 default
,一般用不到。
property
节点:标记一个上下文变量,属性有 name
和 value
,定义变量之后用 ${}
获取值。
appender
节点:<appender>
是 <configuration>
的子节点,主要用于格式化日志输出节点,属性有 name
和 class
,class
用来指定那种输出策略,常用的就是控制台输出策略和文件输出策略。有几个子节点比较重要。
-
filter
:日志输出拦截器,没特殊要求就使用系统自带的,若要将日志分开,比如将ERROR
级别的日志输出到一个文件中,其他级别的日志输出到另一个文件中,这时候就要用到filter
。 -
encoder
:和pattern
节点组合用于具体输出日志的格式和编码方式。 -
file
:用来指定日志文件输出位置,绝对路径或者相对路径。 -
rollingPolicy
:日志回滚策略,常见的就是按照时间回滚策略(TimeBasedRollingPolicy)
和按照大小时间回滚策略(SizeAndTimeBasedRollingPolicy)
。 -
maxHistory
:可选节点,控制保留日志文件的最大数量,超出数量就删除旧文件。 -
totalSizeCap
:可选节点,指定日志文件的上限大小。
logger
节点:可选节点,用来指定某一个包或者具体某一个类的日志打印级别。
-
name
:指定包名。 -
level
:可选,日志的级别。 -
addtivity
:可选,默认为true
,此logger
的信息向上传递。
springProfile
:多环境输出日志文件,根据配置文件激活参数 (active)
选择性的包含和排查部分配置信息。根据不同环境来定义不同的日志输出。
logback
中一般有三种过滤器 Filter
-
LevelFilter
:级别过滤器,根据日志级别进行过滤,如果日志级别等于配置级别,过滤器会根据onMath
和onMismatch
接受或者拒绝日志。有以下子节点
-
level
:设置过滤级别 -
onMath
:配置符合过滤条件的操作 -
onMismath
:配置不符合过滤条件的操作
<!-- 在文件中出现级别为INFO的日志内容 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 在文件中出现级别为INFO、ERROR的日志内容 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<level>ERROR</level>
</filter>
-
ThresholdFilter
:临界值过滤器,过滤掉低于临界值的日志,当日志级别等于或高于临界值时,过滤器返回NEUTRAL
;当日志级别低于临界值时,日志会被拒绝。
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!-- 过滤掉 TRACE 和 DEBUG 级别的日志-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger{30} - %msg%n
</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
-
EvaluatorFilter
:求值过滤器,评估、鉴别日志是否符合指定条件。
如果不使用 SpringBoot
推荐的名字,想用自己定制的也可以,只需要在配置文件中配置。
logging:
config: logging-config.xml
2.7、异步日志
之前都是用同步去记录日志,这样代码效率会大大降低,logback
提供异步记录日志功能。
原理:
系统会为日志操作单独分配一个线程,原来用来执行当前方法是主线程会继续向下执行,线程1:系统业务代码执行。线程2:打印日志
<!-- 异步输出 -->
<appender name ="async-file-info" class= "ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold >0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>256</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref ="INFO_APPENDER"/>
</appender>
<root level="INFO">
<!-- 引入appender -->
<appender-ref ref="async-file-info"/>
</root>
2.8、如何定制日志格式?
上面我们已经看到默认的日志格式,实际项目代码中的日志格式不会是 logback
默认的格式,要根据项目业务要求,进行修改,下面我们来看如何定制日志格式。
# 常见的日志格式
2023-12-21 10:39:44.631----[应用名|主机ip|客户端ip|用户uuid|traceid]----{}
解释
2023-12-21 10:39:44.631:时间,格式为yyyy-MM-dd HH:mm:ss.SSS
应用名称:标识项目应用名称,一般就是项目名
主机ip:本机IP
客户端ip:请求IP
用户uuid:根据用户uuid可以知道是谁调用的
traceid:追溯当前链路操作日志的一种有效手段
创建自定义格式转换符有两步:
- 首先必须继承
ClassicConverter
类,ClassicConverter
对象负责从ILoggingEvent
提取信息,并产生一个字符串。 - 然后要让
logback
知道新的Converter
,方法是在配置文件里声明新的转换符。
在 config
包中新建 HostIpConfig
类、RequestIpConfig
类、UUIDConfig
类,代码如下:
HostIpConfig.java
package com.duan.config;
import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import com.duan.utils.LocalIP;
/**
* @author db
* @version 1.0
* @description HostIpConfig 获得主机IP地址
* @since 2024/1/9
*/
public class HostIpConfig extends ClassicConverter {
@Override
public String convert(ILoggingEvent event) {
String hostIP = LocalIP.getIpAddress();
return hostIP;
}
}
RequestIpConfig.java
package com.duan.config;
import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import com.duan.utils.IpUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* @author db
* @version 1.0
* @description RequestIpConfig 获得请求IP
* @since 2024/1/9
*/
public class RequestIpConfig extends ClassicConverter {
@Override
public String convert(ILoggingEvent event) {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return "127.0.0.1";
}
HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
String requestIP = IpUtils.getIpAddr(request);
return requestIP;
}
}
UUIDConfig.java
package com.duan.config;
import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
/**
* @author db
* @version 1.0
* @description UUIDConfig
* @since 2024/1/9
*/
public class UUIDConfig extends ClassicConverter {
@Override
public String convert(ILoggingEvent iLoggingEvent) {
// 这里作为演示,直接生成的一个String,实际项目中可以Servlet获得用户信息
return "12344556";
}
}
工具类代码如下:
package com.duan.utils;
import com.google.common.base.Strings;
import javax.servlet.http.HttpServletRequest;
// 请求IP
public class IpUtils {
private IpUtils(){
}
public static String getIpAddr(HttpServletRequest request) {
String xIp = request.getHeader("X-Real-IP");
String xFor = request.getHeader("X-Forwarded-For");
if (!Strings.isNullOrEmpty(xFor) && !"unKnown".equalsIgnoreCase(xFor)) {
//多次反向代理后会有多个ip值,第一个ip才是真实ip
int index = xFor.indexOf(",");
if (index != -1) {
return xFor.substring(0, index);
} else {
return xFor;
}
}
xFor = xIp;
if (!Strings.isNullOrEmpty(xFor) && !"unKnown".equalsIgnoreCase(xFor)) {
return xFor;
}
if (Strings.nullToEmpty(xFor).trim().isEmpty() || "unknown".equalsIgnoreCase(xFor)) {
xFor = request.getHeader("Proxy-Client-IP");
}
if (Strings.nullToEmpty(xFor).trim().isEmpty() || "unknown".equalsIgnoreCase(xFor)) {
xFor = request.getHeader("WL-Proxy-Client-IP");
}
if (Strings.nullToEmpty(xFor).trim().isEmpty() || "unknown".equalsIgnoreCase(xFor)) {
xFor = request.getHeader("HTTP_CLIENT_IP");
}
if (Strings.nullToEmpty(xFor).trim().isEmpty() || "unknown".equalsIgnoreCase(xFor)) {
xFor = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (Strings.nullToEmpty(xFor).trim().isEmpty() || "unknown".equalsIgnoreCase(xFor)) {
xFor = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(xFor) ? "127.0.0.1" : xFor;
}
}
package com.duan.utils;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;
// 获得主机IP
public class LocalIP {
public static InetAddress getLocalHostExactAddress() {
try {
InetAddress candidateAddress = null;
// 从网卡中获取IP
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements()) {
NetworkInterface iface = networkInterfaces.nextElement();
// 该网卡接口下的ip会有多个,也需要一个个的遍历,找到自己所需要的
for (Enumeration<InetAddress> inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements(); ) {
InetAddress inetAddr = inetAddrs.nextElement();
// 排除loopback回环类型地址(不管是IPv4还是IPv6 只要是回环地址都会返回true)
if (!inetAddr.isLoopbackAddress()) {
if (inetAddr.isSiteLocalAddress()) {
// 如果是site-local地址,就是它了 就是我们要找的
// ~~~~~~~~~~~~~绝大部分情况下都会在此处返回你的ip地址值~~~~~~~~~~~~~
return inetAddr;
}
// 若不是site-local地址 那就记录下该地址当作候选
if (candidateAddress == null) {
candidateAddress = inetAddr;
}
}
}
}
// 如果出去loopback回环地之外无其它地址了,那就回退到原始方案吧
return candidateAddress == null ? InetAddress.getLocalHost() : candidateAddress;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static String getIpAddress() {
try {
//从网卡中获取IP
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
InetAddress ip;
while (allNetInterfaces.hasMoreElements()) {
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
//用于排除回送接口,非虚拟网卡,未在使用中的网络接口
if (!netInterface.isLoopback() && !netInterface.isVirtual() && netInterface.isUp()) {
//返回和网络接口绑定的所有IP地址
Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
while (addresses.hasMoreElements()) {
ip = addresses.nextElement();
if (ip instanceof Inet4Address) {
return ip.getHostAddress();
}
}
}
}
} catch (Exception e) {
System.err.println("IP地址获取失败" + e.toString());
}
return "";
}
}
traceId
:用于标识摸一次具体的请求 Id
,通过 traceId
可以把一次用户请求在系统中的调用路径串联起来。
logback
自定义日志格式 traceId
使用 MDC
进行实现。
MDC(Mapped Diagnostic Context)
映射诊断环境,是 log4j
和 logback
提供的一种方便在线多线程条件下记录日志的功能,可以看成是一个与当前线程绑定的 ThreadLocal
。
public class MDC {
// 添加 key-value
public static void put(String key, String val) {...}
// 根据 key 获取 value
public static String get(String key) {...}
// 根据 key 删除映射
public static void remove(String key) {...}
// 清空
public static void clear() {...}
}
用拦截器或者过滤器实现 MDC
,在这里使用拦截器实现,首先在 interceptor
包中创建 TraceInterceptor
类并实现 HandlerInterceptor
方法。
package com.duan.interceptor;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
/**
* @author db
* @version 1.0
* @description TraceInterceptor
* @since 2024/1/9
*/
@Component
public class TraceInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {
MDC.put("traceid", UUID.randomUUID().toString());
return true;
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,Object handler,Exception e) throws Exception {
MDC.remove("traceid");
}
}
在 config
包中新建 WebConfig
类并继承 WebMvcConfigurerAdapter
,把 TraceInterceptor
拦截器注入。
package com.duan.config;
import com.duan.interceptor.TraceInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* @author db
* @version 1.0
* @description WebConfig
* @since 2024/1/9
*/
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
private TraceInterceptor traceInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(traceInterceptor);
}
}
第二步,在 logback-spring.xml
配置文件中进行配置,配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<!-- logback默认每60秒扫描该文件一次,如果有变动则用变动后的配置文件。 -->
<configuration scan="false">
<!-- ==============================================开发环境=========================================== -->
<springProfile name="dev">
<conversionRule conversionWord="hostIp" converterClass="com.duan.config.HostIpConfig"/>
<conversionRule conversionWord="requestIp" converterClass="com.duan.config.RequestIpConfig"/>
<conversionRule conversionWord="uuid" converterClass="com.duan.config.UUIDConfig"/>
<property name="CONSOLE_LOG_PATTERN"
value="%yellow(%date{yyyy-MM-dd HH:mm:ss.SSS})----[%magenta(cxykk)|%magenta(%hostIp)|%magenta(%requestIp)|%magenta(%uuid)|%magenta(%X{traceid})]----%cyan(%msg%n)"/>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出-->
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 日志输出级别 -->
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</springProfile>
<!-- ==============================================生产环境=========================================== -->
<springProfile name="prod">
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
<property name="LOG_HOME" value="./log"/>
<!-- 控制台输出 -->
<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{HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="INFO_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志名称,如果没有File 属性,那么只会使用FileNamePattern的文件路径规则
如果同时有<File>和<FileNamePattern>,那么当天日志是<File>,明天会自动把今天
的日志改名为今天的日期。即,<File> 的日志都是当天的。
-->
<file>${LOG_HOME}/info.log</file>
<!--滚动策略,按照大小时间滚动 SizeAndTimeBasedRollingPolicy-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${LOG_HOME}/info.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<!--只保留最近30天的日志-->
<MaxHistory>30</MaxHistory>
<!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
<totalSizeCap>1GB</totalSizeCap>
<MaxFileSize>10MB</MaxFileSize>
</rollingPolicy>
<!--日志输出编码格式化-->
<encoder>
<charset>UTF-8</charset>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</encoder>
<!--过滤器,只有过滤到指定级别的日志信息才会输出,如果level为ERROR,那么控制台只会输出ERROR日志-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="ERROR_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${LOG_HOME}/error.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<MaxHistory>30</MaxHistory>
<!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
<totalSizeCap>1GB</totalSizeCap>
<MaxFileSize>10MB</MaxFileSize>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<!--指定最基础的日志输出级别-->
<root level="INFO">
<!--appender将会添加到这个loger-->
<appender-ref ref="STDOUT"/>
<appender-ref ref="INFO_APPENDER"/>
<appender-ref ref="ERROR_APPENDER"/>
</root>
</springProfile>
</configuration>
启动项目,通过 postman
调用 login
接口,查看结果输出日志格式。
代码地址:https://gitee.com/duan138/practice-code/tree/dev/logback
三、总结
SpringBoot
中日志讲解就到这里,上面提到的知识点都是项目中常用的,比如日志怎么配置、根据日志级别把日志输出到不同的文件里、或者将 INFO
和 ERROR
级别的日志输出到同一个文件中、或者定制日志格式等等。
下篇文章将学习 spring
事务,后续的文章会使用 AOP
或者拦截器描述在实际项目中怎么去记录日志。