SpringBoot项目logback日志配置

541 阅读10分钟

程序运行出现错误时,第一时间想到的是甩锅还是日志?通过查看日志定位出问题的位置,才能更好的甩锅,今天就来学习 springBoot 日志如何配置。

一、日志框架

Java 中的日志框架分为两种,分别为日志抽象/门面、日志实现。

日志门面不负责日志具体实现,它只是为所有日志框架提供一套标准、规范的API框架。其主要意义在于提供接口,具体的实现可以交由其它日志框架,例如 log4jlogback等。

当今主流的的日志门面是SLF4JSpringBoot 中推荐使用该门面技术。

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 的日志门面,包含以下三种情况:

  1. 如果只是导入 slf4j 日志门面,没有导入对应的日志实现框架,则日志功能默认是关闭的,不会进行日志输出。
  2. 蓝色图里 Logback、slf4j-simple、slf4j-nop 遵循 slf4jAPI 规范,只要导入对应的日志实现框架,来实现开发
  3. 中间两个日志框架 slf4j-reload4、JUL(slf4j-jdk14) 没有遵循 slf4jAPI 规范,所有无法直接使用,中间需要增加一个适配层 (Adaptation layer),通过对应的适配器来适配具体的日志实现框架。

1.2、日志实现框架

Java 中的日志实现框架,主流的有以下几种:

  1. log4j :老牌日志框架,已经多年不更新了,性能比 logback、log4j2 差。
  2. logbacklog4j 创始人创建的另一个开源日志框架,SpringBoot 默认的日志框架。
  3. log4j2Apache 官方项目,传闻性能优于 logback,它是 log4j 的新版本。
  4. JUL(Java.Util.Logging), jdk 内置。

在项目中,一般都是日志门面+日志实现框架组合使用,这样更灵活,适配起来更简单。

前面提到logback作为Spring Boot默认的日志框架 ,肯定有相应的考量,我司也是使用logback 作为 Spring Boot 项目中的日志实现框架,下面我们就详细说说 logback

二、SpringBoot 日志框架 logback

2.1、logback 是什么?

logbacklog4j 团队创建的开源日志组件。与 log4j 类似,但是比 log4j 更强大,是log4j 的改良版本。

logback 主要包含三个模块:

  1. logback-core :所有 logback 模块的基础。
  2. logback-classic :是 log4j 的改良版本,完整实现了slf4j API
  3. 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 依赖。

启动项目,日志打印如下:

从图中可以看出,输出的日志默认元素如下:

  1. 时间日期:精确到毫秒。
  2. 日志级别:默认是 INFO
  3. 进程 Id
  4. 分隔符:---标识日志开始的地方。
  5. 线程名称:方括号括起来的。
  6. Logger 名称:源代码的类名。
  7. 日志内容

在业务中输出日志,常见的有两种方式。

方式一:在业务代码里添加如下代码


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 默认是把日志输出到控制台,生成环境中是不行的,需要把日志输出到文件中。 其中有两个重要配置如下:

  1. logging.file.path :指定日志文件的路径
  2. 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 :监测配置文件是否有修改的时间间隔,单位毫秒,当 scantrue 时,此属性生效。默认的时间间隔为1分钟 。
  • debug :此属性为 true 时,打印出 logback 内部日志信息,实时查看 logback 运行状态,默认 false

root 节点:必须的节点,用来指定基础的日志级别,只有一个属性。该节点可以包含零个或者多个元素,子节点是 appender-ref ,标记 appender 将会添加到这个 logger 中。

  • level :默认值 DEBUG

contextName 节点:标识一个上下文名称,默认 default ,一般用不到。

property 节点:标记一个上下文变量,属性有 namevalue,定义变量之后用 ${} 获取值。

appender 节点:<appender><configuration> 的子节点,主要用于格式化日志输出节点,属性有 nameclassclass 用来指定那种输出策略,常用的就是控制台输出策略和文件输出策略。有几个子节点比较重要。

  • filter :日志输出拦截器,没特殊要求就使用系统自带的,若要将日志分开,比如将 ERROR 级别的日志输出到一个文件中,其他级别的日志输出到另一个文件中,这时候就要用到 filter
  • encoder :和 pattern 节点组合用于具体输出日志的格式和编码方式。
  • file :用来指定日志文件输出位置,绝对路径或者相对路径。
  • rollingPolicy :日志回滚策略,常见的就是按照时间回滚策略(TimeBasedRollingPolicy) 和按照大小时间回滚策略 (SizeAndTimeBasedRollingPolicy)
  • maxHistory :可选节点,控制保留日志文件的最大数量,超出数量就删除旧文件。
  • totalSizeCap :可选节点,指定日志文件的上限大小。

logger 节点:可选节点,用来指定某一个包或者具体某一个类的日志打印级别。

  • name :指定包名。
  • level :可选,日志的级别。
  • addtivity :可选,默认为 true,此 logger 的信息向上传递。

springProfile :多环境输出日志文件,根据配置文件激活参数 (active) 选择性的包含和排查部分配置信息。根据不同环境来定义不同的日志输出。

logback 中一般有三种过滤器 Filter

  1. LevelFilter :级别过滤器,根据日志级别进行过滤,如果日志级别等于配置级别,过滤器会根据onMathonMismatch 接受或者拒绝日志。有以下子节点
  • 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> 
  1. 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>
  1. 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) 映射诊断环境,是 log4jlogback 提供的一种方便在线多线程条件下记录日志的功能,可以看成是一个与当前线程绑定的 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 中日志讲解就到这里,上面提到的知识点都是项目中常用的,比如日志怎么配置、根据日志级别把日志输出到不同的文件里、或者将 INFOERROR 级别的日志输出到同一个文件中、或者定制日志格式等等。

下篇文章将学习 spring 事务,后续的文章会使用 AOP 或者拦截器描述在实际项目中怎么去记录日志。


改变你能改变的,接受你不能改变的,关注公众号:程序员康康,一起成长,共同进步。