laitimes

Configure the logback log of a SpringBoot project

author:Speed Starry Sky 4DO
Configure the logback log of a SpringBoot project
  • Session Authentication and Token Authentication
  • Filters and interceptors
  • SpringBoot Unified Returns and Unified Exception Handling
  • Configure the logback log of a SpringBoot project

When there is an error in the program running, the first thing that comes to mind is the dump pot or the log? By looking at the logs to locate the location of the problem, you can better shake the pot, today to learn how to configure the springBoot log.

1. Log framework

There are two types of logging frameworks in Java, namely log abstraction/façade and log implementation.

The log façade is not responsible for the specific implementation of logs, it only provides a set of standard and standardized API frameworks for all log frameworks. The main point is to provide an interface that can be implemented by other logging frameworks, such as log4j and logback.

Today's mainstream logging façade is SLF4J, which is recommended in SpringBoot.

1.1、SLF4J

SLF4J official website address: https://www.slf4j.org/

SLF4J (Simple Logging Facade For Java), i.e. Simple Logging Facade For Java, which serves as a simple façade or abstraction for various logging frameworks (e.g., Java.util.Logging, logback, log4j), allows end users to insert the required logging frameworks at the time of deployment.

It's similar to JDBC, which doesn't care about specific database implementations, and similarly, SLF4J doesn't care about specific logging framework implementations.

Configure the logback log of a SpringBoot project

The SLF4JAPI under the application represents the log façade of SLF4J and contains the following three scenarios:

  1. If you only import the SLF4J log façade and do not import the corresponding log implementation framework, the log function is disabled by default and no log output is performed.
  2. Logback, slf4j-simple, and slf4j-nop in the blue figure follow the API specification of slf4j, and you only need to import the corresponding log implementation framework to achieve development
  3. The middle two logging frameworks, slf4j-reload4 and JUL (slf4j-jdk14), do not follow the API specifications of slf4j and cannot be used directly.

1.2. Log implementation framework

The following are the most popular logging implementation frameworks in Java:

  1. log4j: The old logging framework has not been updated for many years, and its performance is worse than that of logback and log4j2.
  2. logback: Another open-source logging framework created by the founders of log4j, the default logging framework of SpringBoot.
  3. log4j2 : The official Apache project, rumored to outperform logback, is a new version of log4j.
  4. JUL :(Java.Util.Logging), jdk 内置。

In a project, the log façade + log implementation framework is generally used in combination, which is more flexible and easier to adapt.

As mentioned earlier, logback is the default logging framework of Spring Boot, there must be corresponding considerations, and our company also uses logback as a logging implementation framework in Spring Boot projects, so let's talk about logback in detail.

2. SpringBoot Logging Framework logback

2.1. What is logback?

Logback is an open-source logging component created by the Log4j team. Similar to Log4J, but more powerful than Log4J, and an improved version of Log4J.

Logback consists of three main modules:

  1. logback-core: The basis for all logback modules.
  2. logback-classic: is an improved version of log4j with a full implementation of the slf4j API.
  3. logback-access: The access module is integrated with the servlet container to provide the ability to access logs via HTTP.

2.2. What are the log levels of logback?

Log level: It is used to control the output of log information, which is divided into seven levels from high to low.

  • OFF: The highest level, which is used to turn off all information.
  • FATAL: Disaster-grade, system-level, program cannot be printed.
  • ERROR: Error message
  • WARN: Alarm information
  • INFO: Ordinary printed information
  • DEBUG: Debugging, which is helpful for debugging applications.
  • TRACE: Trace

If the log level in the project is set to INFO, the log information below it will not be visible, i.e. the DEBUG log will not be displayed. By default, Spring Boot logs with a Logback and outputs to the console at the INFO level.

2.3 How do I use logs in SpringBoot?

First, create a new SpringBoot project log, and we see that SpringBoot has introduced logback dependencies by default.

Configure the logback log of a SpringBoot project

Start the project and print the log as follows:

Configure the logback log of a SpringBoot project

As you can see from the diagram, the default elements of the output log are as follows:

  1. Date and time: accurate to the millisecond.
  2. Log level: The default is INFO.
  3. Process ID
  4. Separator: --- identifies where the log begins.
  5. Thread name: in square brackets.
  6. Logger Name: The class name of the source code.
  7. Log content

There are two common ways to output logs in a business.

Method 1: Add the following code to the service code:

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();
    }
}
           

This line of code needs to be added to each class to output the log, which makes the code redundant.

Method 2: Use the @Slf4j annotation in the lomback, but you need to reference the lomback dependency in the pom

<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>
           

To use it, you only need to annotate a @Slf4j annotation on the class

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. How to specify a specific log level?

As we mentioned earlier, the default log level of SpringBoot is INFO, and we can also specify the log level as needed, as follows:

logging:
  level:
    root: ERROR
           

All log levels have been changed to ERROR, and SpringBoot also supports package-level log adjustments, as follows:

logging:
  level:
    com:
      duan:
        controller: ERROR
           

com.duan.controller is the project package name.

2.5. How to output logs to a specified file

By default, SpringBoot outputs logs to the console, but not in the build environment, so you need to output logs to a file. Two of the most important configurations are as follows:

  1. logging.file.path: Specifies the path of the log file
  2. logging.file.name: The file name of the log, which is spring.log by default Note: The official documentation says that these two properties cannot be configured at the same time, otherwise they will not take effect, so only one needs to be configured.

Specify that the log output file exists in the log folder of the current path, and the generated file is spring.log by default

logging:
  file:
    path: ./logs
           

2.6. Custom log configuration

SpringBoot officially recommends using the file name with -spring as the project log configuration, so you only need to create logback-spring.xml under the src/resource folder, and the configuration file content is as follows:

<?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>
           

The most basic configuration is a configuration that consists of zero or more appenders, zero or more loggers, and at most one root tag. (logback is case-sensitive)

Configure the logback log of a SpringBoot project

configuration 节点:根节点,属性如下:

  • scan: When this property is true, the configuration file will be reloaded and will be reloaded, and it will be true by default.
  • scanPeriod: Monitors whether the configuration file has been modified at a time interval in milliseconds, and this property takes effect when scan is true. The default interval is 1 minute.
  • debug: When this property is true, the internal log information of the logback will be printed to view the running status of the logback in real time, and the default value is false.

root node: A required node that specifies the base log level and has only one attribute. The node can contain zero or more elements, the child node is appender-ref, and the tag appender will be added to the logger.

  • level :默认值 DEBUG

contextName node: identifies a context name, default by default, generally not used.

property node: mark a context variable with a property with name and value, and use ${} to get the value after defining the variable.

appender node: is<appender> <configuration> a child node of , mainly used to format log output nodes, the properties have name and class, class is used to specify the kind of output strategy, commonly used is the console output strategy and file output policy. It is important to have several child nodes.

Configure the logback log of a SpringBoot project
  • filter: Log output interceptor, no special requirements to use the system's own use, if you want to separate the logs, for example, the ERROR level of the log output to one file, other levels of the log output to another file, then you need to use filter.
  • Encoder: Used in combination with the pattern node to specify the format and encoding of the output log.
  • file: Used to specify the output location of the log file, absolute path or relative path.
  • rollingPolicy: Log rollback policy, the common ones are time-based rollbackPolicy (TimeBasedRollingPolicy) and size-based time-based rollbackPolicy (SizeAndTimeBasedRollingPolicy).
  • maxHistory: An optional node that controls the maximum number of log files to be retained, and deletes old files if the number is exceeded.
  • totalSizeCap: An optional node that specifies the maximum size of the log file.

logger node: An optional node that specifies the log printing level of a package or a specific class.

  • name: specifies the package name.
  • level :可选,日志的级别。
  • addtivity: Optional, defaults to true, and information is passed up for this logger.

springProfile: A log file is output for multiple environments, which selectively includes and troubleshoots some configuration information based on the active parameter of the profile. Define different log outputs for different environments.

logback 中一般有三种过滤器 Filter

  1. LevelFilter: A level filter that filters based on the log level, and if the log level is equal to the configured level, the filter accepts or rejects the logs based on onMath and onMismatch. There are the following child nodes
  • level: Set the filter level
  • onMath: Configure actions that match the filter conditions
  • onMismath: Configure actions that do not meet the filter criteria
<!-- 在文件中出现级别为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: A threshold filter that filters out logs below the threshold and returns NEUTRAL when the log level is equal to or higher than the threshold, and rejects logs when the log level is below the threshold.
<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: evaluates and identifies whether the log meets the specified conditions.

If you don't use the name recommended by SpringBoot, you can also use your own custom one, just need to configure it in the configuration file.

logging:
  config: logging-config.xml
           

2.7. Asynchronous logs

Previously, synchronous logging was used, which greatly reduced the efficiency of the code, and logback provided asynchronous logging function.

Principle:

The system will assign a separate thread for log operations, and the original method used to execute the current method is that the main thread will continue to execute downward, and thread 1: system service code execution. Thread 2: Print logs

<!-- 异步输出 -->
<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. How to customize the log format?

We have seen the default log format above, the log format in the actual project code will not be the default format of logback, and it should be modified according to the business requirements of the project, let's see how to customize the log format.

# 常见的日志格式
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:追溯当前链路操作日志的一种有效手段
           

There are two steps to creating a custom format converter:

  • The first thing you have to inherit from is the ClassicConverter class, which is responsible for extracting information from ILoggingEvent and producing a string.
  • Then let the logback know about the new converter by declaring the new converter in the configuration file.

Create the HostIpConfig class, RequestIpConfig class, and UUIDConfig class in the config package, and the code is as follows:

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

The code for the utility class is as follows:

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: It is used to identify a specific request ID, through which the call path of a user request in the system can be concatenated.

logback custom log format traceId is implemented using MDC.

MDC (Mapped Diagnostic Context) is a convenient function provided by log4j and logback to record logs under online multi-threaded conditions, which can be regarded as a ThreadLocal bound to the current thread.

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() {...}
}
           

Implement MDC with an interceptor or filter, in this case with an interceptor, first create a TraceInterceptor class in the interceptor package and implement the HandlerInterceptor method.

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

Create a new WebConfig class in the config package and inherit WebMvcConfigurerAdapter to inject the TraceInterceptor interceptor.

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

The second step is to configure it in the logback-spring .xml configuration file, which is as follows:

<?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>
           

Start the project, call the login API through Postman, and view the output log format.

Configure the logback log of a SpringBoot project

Code address: https://gitee.com/duan138/practice-code/tree/dev/logback

3. Summary

That's it for the explanation of logs in SpringBoot, and the knowledge points mentioned above are commonly used in projects, such as how to configure logs, output logs to different files according to log levels, or output logs at INFO and ERROR levels to the same file, or customize log formats, etc.

Author: Programmer Kang Kang

Link: https://juejin.cn/post/7330104253824729097