SpringBoot项目logback日志配置

  • Session 认证和 Token 认证

  • 过滤器和拦截器

  • SpringBoot统一返回和统一异常处理

  • 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. 日志内容

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

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


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


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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/361263.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

配置vite自动按需引入 vant 组件

为什么学 按需加载可以减少包体积,优化加载性能 学习内容 全局注册组件 import 需要的组件import 组件样式使用 app.use 注册组件 Tree Shaking 介绍使用 什么是 tree shaking&#xff1f; Tree shaking是一种优化技术&#xff0c;用于减少JavaScript或其他编程语言中未被使用…

fullcalendar案例

fullcalendar案例 <script srchttps://cdn.jsdelivr.net/npm/fullcalendar6.1.10/index.global.min.js></script><script srchttps://code.jquery.com/jquery-3.6.0.min.js></script> <!-- 引入 jQuery CDN --><script>document.addEventL…

虚拟机VMware vCneter告警:Log DIsk Exhaustion on frvc70,vCenter日志清理

其中frvc70是主机名称 1.告警原因 Troubleshooting vCenter Appliance /storage/log directory is 80% or more ful 当分区/storage/log使用率达到 80% 时&#xff0c;会触发此告警。 2.解决方法 1.通过 SSH 或通过 vCenter 虚拟机控制台连接到 vCenter Server Appliance …

Halcon 几何测量

文章目录 算子Halcon 计算两点之间的距离案例Halcon 计算点到直线的距离Halcon 计算点到区域的距离Halcon 线到区域的距离Halcon 线到线的距离 算子 distance_pp 两点之间的距离算子 distance_pp( : : Row1, Column1, Row2, Column2 : Distance) Row1 点1的行坐标 Column1 点1的…

[ESP32]在Thonny IDE中,如何將MicroPython firmware燒錄到ESP32開發板中?

[ESP32 I MicroPython] Flash Firmware by Thonny(4.1.4) IDE 正常安裝流程&#xff0c;可參考上述影片。然而&#xff0c;本篇文章主要是紀錄安裝過程遇到的bug, 供未來查詢用&#xff0c;也一併供有需要的同好參考。 問題:安裝後&#xff0c;Thonny互動介面顯示一堆亂碼和co…

网安人必看!CISP家族顶流证书攻略

网络安全已成为当今的热门领域&#xff0c;证书在职业发展中的重要性不言而喻。但是&#xff0c;证书市场五花八门&#xff0c;选择适合自己的证书可是个大问题。别担心&#xff0c;今天我们就来聊聊CISP家族的几个热门认证&#xff0c;让你在网络安全领域的发展更加顺利&#…

ADI 配合 USRP 使用的相控阵天线 cn0566

相控阵天线 在这里插入图片描述

IDEA快捷键大全

提示&#xff1a; ① 主要记录我在使用 IDEA 开发的过程中用到的快捷键&#xff0c;可以提高开发速度。 ② 不一定要全部记住&#xff0c;主要是当一个参考文档&#xff0c;大家有一点印象&#xff0c;随时可以查看。 参考博客 > IntelliJ IDEA 快捷键说明大全&#xff08;官…

Springboot+vue的健身房管理系统(有报告)。Javaee项目,springboot vue前后端分离项目

演示视频&#xff1a; Springbootvue的健身房管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot vue前后端分离项目 项目介绍&#xff1a; 本文设计了一个基于Springbootvue的前后端分离的健身房管理系统&#xff0c;采用M&#xff08;model&#xf…

WPF图表库LiveChart异常问题处理-System.ArgumentOutOfRangeException:指定的参数超出了有效值的范围

问题&#xff1a; 在使用liveChart处理一个以时间为X轴的曲线时&#xff0c;遇到一个报错&#xff1a;指定的参数超出了有效值的范围System.ArgumentOutOfRangeException:“Specified argument was out of the range of valid values. Arg_ParamName_Name” 指定的参数超出了有…

YOLOv5源码逐行超详细注释与解读(1)——项目目录结构解析

前言 前面简单介绍了YOLOv5的网络结构和创新点&#xff08;直通车&#xff1a;【YOLO系列】YOLOv5超详细解读&#xff08;网络详解&#xff09;&#xff09; 在接下来我们会进入到YOLOv5更深一步的学习&#xff0c;首先从源码解读开始。 因为我是纯小白&#xff0c;刚开始下…

EXCHANGE PARTITION 方法处理(挽救)大型分区表中的块损坏的步骤

当在巨大的表分区块&#xff08;例如 ORA-01578&#xff09;中发现损坏时&#xff0c;并且我们没有备份&#xff08;例如 RMAN、操作系统级别、导出或任何外部资源&#xff09;来恢复损坏&#xff0c;我们仍然可以尝试挽救使用 10231 事件处理表中的剩余数据&#xff08;由于跳…

扩展学习|商业智能和大数据分析的研究前景(比对分析)

文献来源&#xff1a; Liang T P , Liu Y H .Research Landscape of Business Intelligence and Big Data analytics: A bibliometrics study[J].Expert Systems with Applications, 2018, 111(NOV.):2-10.DOI:10.1016/j.eswa.2018.05.018. 信息和通信技术的快速发展导致了数字…

养老院|基于Springboot的养老院管理系统设计与实现(源码+数据库+文档)

养老院管理系统目录 目录 基于Springboot的养老院管理系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、老人信息管理 2、家属信息管理 3、公告类型管理 4、公告信息管理 四、数据库设计 1、实体ER图 五、核心代码 六、论文参考 七、最新计算机毕设选…

西瓜书读书笔记整理(十二) —— 第十二章 计算学习理论(下)

第十二章 计算学习理论&#xff08;下&#xff09; 12.4 VC 维&#xff08;Vapnik-Chervonenkis dimension&#xff09;12.4.1 什么是 VC 维12.4.2 增长函数&#xff08;growth function&#xff09;、对分&#xff08;dichotomy&#xff09;和打散&#xff08;shattering&…

【Linux系统】文件系统和软硬链接

前言 之前的博客介绍过了打开的文件是如何被操作系统管理起来的&#xff0c;但是绝大多数文件是没有被打开的&#xff0c;静静地躺在磁盘上。 这些文件也应该要被操作系统管理起来&#xff0c;以方便系统快速地在磁盘上查找它们&#xff0c;进而加载到内存。 这套管理方式就…

vue使用json格式化

安装 npm i bin-code-editor -S // Vue2 npm install vue-json-viewer --save 在main.js引用 //引入bin-code-editor相关插件和样式 import CodeEditor from bin-code-editor; import bin-code-editor/lib/styles/index.css; import JsonViewer from vue-json-viewer //vue使用…

golang开源的可嵌入应用程序高性能的MQTT服务

golang开源的可嵌入应用程序高性能的MQTT服务 什么是MQTT&#xff1f; MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;是一种轻量级的、开放的消息传输协议&#xff0c;设计用于在低带宽、高延迟或不可靠的网络环境中进行通信。MQTT最初由IBM开发&#xf…

python webdriver 测试框架数据驱动json文件驱动的方式

简介&#xff1a; 数据驱动excel驱动方式,就是数据配置在excel里面&#xff0c;主程序调用的时候每次用从excel里取出的数据作为参数&#xff0c;进行操作&#xff0c; 需要掌握的地方是对excel的操作&#xff0c;要灵活的找到目标数据 测试数据.xlsx: 路径-D:\test\0627 E…

产品原型图设计规范大全

目前&#xff0c;市场上许多产品经理或设计师都在使用一些优秀的原型设计规范&#xff0c;这些规范几乎涵盖了原型设计的许多方面。一套好的、完整的原型设计规范可以统一产品设计风格&#xff0c;检验产品的可用性&#xff0c;有效提高产品经理绘制原型图的效率&#xff0c;更…