SpringBoot项目logback日志配置

news2024/11/17 15:56:46

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


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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mastercam 2024 下载安装教程,流程简单,小白也能轻松搞定,附安装包和工具

前言 Mastercam是一款高效专业的实用型CAD/CAM设计辅助工具&#xff0c;集二维绘图、三维实体造型、曲面设计、体素拼合、数控编程、刀具路径模拟及真实感模拟等多种功能于一身&#xff0c;能够帮助用户轻松设计各种复杂的曲线、曲面零件、刀具路径等。 准备工作 1、Win10及…

Python爬虫:数据获取requests

1. 基本用法 1.1. 安装requests库 pip3 install requests 1.2. 发送HTTP请求 requests.request(method, url, **kwargs) 1.3. 发送GET请求 requests.get(url, paramsNone, **kwargs) 1.4. 发送POST请求 requests.post(url, dataNone, jsonNone, **kwargs) 1.5. 发送PU…

传感器类总结(一)MPU9250 3-2程序关于IIC的底层程序

关于IIC的逻辑和底层协议可以看之前总结的 #IIC 通信协议 1、读写数据 1.1、写数据 发送N个字节程序的流程: 1、发送起始信号 2、发送从机地址和写 3、等待从机发回应答信号 4、发送第一字节数据 等待应答 5、发送下一字节数据 等带应答或非应答信号 6、发送停止信号停止发送…

Kafka核心概念、数据存储设计及Partition数据文件 生产者负载均衡策略、批量发送技巧、消息压缩手段、消费者设计

关注公众号&#xff0c;发送 “面试题” 即可免费领取一份超全的面试题PDF文件&#xff01;&#xff01;&#xff01;&#xff01; 1、kafka的概念 Kafka 是一个开源的分布式流处理平台&#xff0c;最初由LinkedIn开发&#xff0c;后来成为Apache软件基金会的一个顶级项目。它…

【解决】Unity 工程无法正常打开而崩溃问题

开发平台&#xff1a;Unity 2022.3.17f1c1 一、问题描述 访问 Unity 工程等待 Open Projet&#xff08;busy for 时间&#xff09;&#xff0c;出现崩溃、闪退等情况&#xff0c;导致无法正常进入Unity编辑页面。 二、问题分析 笔者在 URP 渲染管线下处理 Obi Fluid 流体插件 D…

detectron2的read_image方法

在看代码的时候&#xff0c;看到一行注释&#xff1a;use PIL, to be consistent with evaluation 说是用PIL方法加载&#xff0c;却又看见了BGR这种表述&#xff0c;后面的调用也都是cv2格式&#xff1a; 那我就要看下这里面是怎么实现的了&#xff0c;找到了read_image函数&…

知识点积累系列(一)golang语言篇【持续更新】

云原生学习路线导航页&#xff08;持续更新中&#xff09; 本文是 知识点积累 系列文章的第一篇&#xff0c;记录golang语言相关的知识点 1.结构体的mapstructure是什么 mapstructure:"default" mapstructure是一个Go语言的库&#xff0c;用于将一个map中的值映射到…

通过手写简易版RPC理解RPC原理

RPC是什么 所谓的RPC其实是为了不同主机的两个进程间通信而产生的&#xff0c;通常不同的主机之间的进程通信&#xff0c;程序编写需要考虑到网络通信的功能&#xff0c;这样程序的编写将会变得复杂。RPC就来解决这一问题的&#xff0c;一台主机上的进程对另外一台主机的进程发…

【2024程序员必看】鸿蒙应用开发行业分析

鸿蒙操作系统沉浸四年&#xff0c;这次终于迎来了破局的机会&#xff0c;自从2023年华为秋季发布会上宣布鸿蒙 Next操作系统不在兼容Android后&#xff0c;就有不少大厂开始陆续与华为达成了鸿蒙原生应用的开发合作&#xff0c;据1月18日华为官方宣布110多天的产业合力“突进”…

log4j2 无垃圾稳态日志 Garbage-free Steady State Logging

无垃圾稳态日志 垃圾收集暂停是导致延迟峰值的常见原因&#xff0c;对于许多系统来说&#xff0c;需要花费大量精力来控制这些暂停。 许多日志库&#xff0c;包括以前版本的Log4j&#xff0c;在稳态日志记录期间分配临时对象&#xff0c;如日志事件对象、字符串、char数组、字…

RocksDB是如何实现存算分离的

核心参考文献&#xff1a; Dong, S., P, S. S., Pan, S., Ananthabhotla, A., Ekambaram, D., Sharma, A., Dayal, S., Parikh, N. V., Jin, Y., Kim, A., Patil, S., Zhuang, J., Dunster, S., Mahajan, A., Chelluri, A., Datye, C., Santana, L. V., Garg, N., & Gawde,…

基于YOLOv7算法的高精度实时安全帽和背心目标检测系统(PyTorch+Pyside6+YOLOv7)

摘要&#xff1a;基于YOLOv7算法的高精度实时安全帽和背心目标检测系统可用于日常生活中检测与定位安全帽和安全背心&#xff0c;此系统可完成对输入图片、视频、文件夹以及摄像头方式的目标检测与识别&#xff0c;同时本系统还支持检测结果可视化与导出。本系统采用YOLOv7目标…

B样条基函数

​定义&#xff1a;令U{u0,u1,…,um}是一个单调不减的实数序列&#xff0c;即ui≤ui1&#xff0c;i0&#xff0c;1&#xff0c;…&#xff0c;m-1。其中&#xff0c;ui称为节点&#xff0c;U称为节点矢量&#xff0c;用Ni,p(u)表示第i个p次&#xff08;p1阶&#xff09;B样条基…

短视频界的变革者:上海 AI lab 发布 Vlogger,几句话生成分钟级视频

现如今&#xff0c;vlog 已经成为我们日常生活的重要组成部分。无论是看视频学习休闲、记录珍贵瞬间还是分享生活见闻&#xff0c;视频已经成为人们表达创意和观点的独特媒介。 然而&#xff0c;与几秒钟的短视频不同&#xff0c;要创作出引人入胜、生动有趣的长视频&#xff…

十分钟学会用springboot制作微信小程序富文本编辑器

1.1 富文本模型设计 在构建富文本编辑器系统时&#xff0c;首先需要设计一个合适的富文本模型。 CREATE TABLE IF NOT EXISTS rich_texts (id INT PRIMARY KEY AUTO_INCREMENT,title VARCHAR(255),content TEXT,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );这个表包括…

一键转换MOV至MP3:轻松删除原视频,释放存储空间!

你是否曾经有一个MOV格式的视频文件&#xff0c;想要提取其中的音频却苦于没有合适的工具&#xff1f;现在&#xff0c;有了我们的全新视频剪辑工具&#xff0c;这个烦恼全部消失&#xff01;我们为你提供一键式解决方案&#xff0c;将MOV视频文件快速转换为MP3音频格式。 首先…

PyNest 一个可以搭建微服务的 Python 包

PyNest 在构建 Python API 和微服务方面崭露头角&#xff0c;解决了 FastAPI 中存在的关键问题&#xff0c;因此成为卓越的框架。凭借其模块化的架构和先进的特性&#xff0c;PyNest 在 2024 年及以后有望成为 Python 开发者的首选选择。 随着 Python 生态系统的不断成熟&…

YAYI-UIE: 一个用于通用信息提取的聊天增强的指令微调框架

1、写作动机&#xff1a; 最近的研究提出了基于大型语言模型的方法&#xff0c;以统一地建模不同的信息提取任务。然而&#xff0c;这些现有方法在处理英语以外的中文语言的信息提取能力方面存在不足。 2、主要贡献&#xff1a; 提出了YAYI-UIE&#xff0c;一个端到端的聊天…

Prometheus+grafana配置监控系统

使用docker compose安装 方便拓展, 配置信息都放在在 /docker/prometheus 目录下 1.目录结构如下 . ├── conf │ └── prometheus.yml ├── grafana_data ├── prometheus_data └── prometheus_grafana.yaml2.创建目录文件 mkdir /docker/prometheus &&am…

Java面试题之 IO(四)

Java面试题之 IO&#xff08;四&#xff09; 文章目录 Java面试题之 IO&#xff08;四&#xff09;随机访问流 文章来自Java Guide 用于学习如有侵权&#xff0c;立即删除 随机访问流 这里要介绍的随机访问流指的是支持随意跳转到文件的任意位置进行读写的 RandomAccessFile 。…