SpringBoot项目logback日志配置

程序运行出现错误时,第一时间想到的是甩锅还是日志?通过查看日志定位出问题的位置,才能更好的甩锅,今天就来学习 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. 日志内容

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

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

java 复制代码
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 依赖

pom 复制代码
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>

使用时只需要在类上标注一个 @Slf4j 注解即可

java 复制代码
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,根据需要我们还可以具体的日志级别,如下:

yaml 复制代码
logging:
  level:
    root: ERROR

将所有的日志级别都改为了 ERROR,同时 SpringBoot 还支持包级别的日志调整,如下:

yaml 复制代码
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

yaml 复制代码
logging:
  file:
    path: ./logs

2.6、自定义日志配置

SpringBoot 官方优先推荐使用带有 -spring 的文件名称作为项目日志配置,所以只需要在 src/resource 文件夹下创建 logback-spring.xml 即可,配置文件内容如下:

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 :配置不符合过滤条件的操作
xml 复制代码
<!-- 在文件中出现级别为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 ;当日志级别低于临界值时,日志会被拒绝。
xml 复制代码
<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 推荐的名字,想用自己定制的也可以,只需要在配置文件中配置。

yaml 复制代码
logging:
  config: logging-config.xml

2.7、异步日志

之前都是用同步去记录日志,这样代码效率会大大降低,logback 提供异步记录日志功能。

原理:

系统会为日志操作单独分配一个线程,原来用来执行当前方法是主线程会继续向下执行,线程1:系统业务代码执行。线程2:打印日志

xml 复制代码
<!-- 异步输出 -->
<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 默认的格式,要根据项目业务要求,进行修改,下面我们来看如何定制日志格式。

shell 复制代码
# 常见的日志格式
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

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

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

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";
    }
}

工具类代码如下:

java 复制代码
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;
    }

}
java 复制代码
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

java 复制代码
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 方法。

java 复制代码
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 拦截器注入。

java 复制代码
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 复制代码
<?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 或者拦截器描述在实际项目中怎么去记录日志。


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

相关推荐
风铃儿~6 分钟前
Spring AI 入门:Java 开发者的生成式 AI 实践之路
java·人工智能·spring
斯普信专业组11 分钟前
Tomcat全方位监控实施方案指南
java·tomcat
忆雾屿22 分钟前
云原生时代 Kafka 深度实践:06原理剖析与源码解读
java·后端·云原生·kafka
武昌库里写JAVA34 分钟前
iview Switch Tabs TabPane 使用提示Maximum call stack size exceeded堆栈溢出
java·开发语言·spring boot·学习·课程设计
gaoliheng00643 分钟前
Redis看门狗机制
java·数据库·redis
我是唐青枫1 小时前
.NET AOT 详解
java·服务器·.net
Su米苏1 小时前
Axios请求超时重发机制
java
Undoom2 小时前
🔥支付宝百宝箱新体验!途韵归旅小帮手,让高铁归途变旅行
后端
不超限2 小时前
Asp.net Core 通过依赖注入的方式获取用户
后端·asp.net