天天看點

SpringBoot項目logback日志配置

SpringBoot項目logback日志配置
  • Session 認證和 Token 認證
  • 過濾器和攔截器
  • SpringBoot統一傳回和統一異常處理
  • SpringBoot項目logback日志配置

程式運作出現錯誤時,第一時間想到的是甩鍋還是日志?通過檢視日志定位出問題的位置,才能更好的甩鍋,今天就來學習 springBoot 日志如何配置。

一、日志架構

Java 中的日志架構分為兩種,分别為日志抽象/門面、日志實作。

日志門面不負責日志具體實作,它隻是為所有日志架構提供一套标準、規範的API架構。其主要意義在于提供接口,具體的實作可以交由其它日志架構,例如 log4j和logback等。

當今主流的的日志門面是SLF4J,SpringBoot 中推薦使用該門面技術。

1.1、SLF4J

SLF4J官網位址:https://www.slf4j.org/

SLF4J(Simple Logging Facade For Java),即簡單日志門面,它用作各種日志架構(例如Java.util.Logging、logback、log4j)的簡單門面或抽象,允許最終使用者在部署時插入所需的日志架構。

它和JDBC 差不多,JDBC 不關心具體的資料庫實作,同樣的,SLF4J 也不關心具體日志架構實作。

SpringBoot項目logback日志配置

application 下面的 SLF4JAPI 表示 SLF4J 的日志門面,包含以下三種情況:

  1. 如果隻是導入 slf4j 日志門面,沒有導入對應的日志實作架構,則日志功能預設是關閉的,不會進行日志輸出。
  2. 藍色圖裡 Logback、slf4j-simple、slf4j-nop 遵循 slf4j 的 API 規範,隻要導入對應的日志實作架構,來實作開發
  3. 中間兩個日志架構 slf4j-reload4、JUL(slf4j-jdk14) 沒有遵循 slf4j 的 API 規範,所有無法直接使用,中間需要增加一個适配層 (Adaptation layer),通過對應的擴充卡來适配具體的日志實作架構。

1.2、日志實作架構

Java 中的日志實作架構,主流的有以下幾種:

  1. log4j :老牌日志架構,已經多年不更新了,性能比 logback、log4j2 差。
  2. logback :log4j 創始人建立的另一個開源日志架構,SpringBoot 預設的日志架構。
  3. log4j2 :Apache 官方項目,傳聞性能優于 logback,它是 log4j 的新版本。
  4. JUL :(Java.Util.Logging), jdk 内置。

在項目中,一般都是日志門面+日志實作架構組合使用,這樣更靈活,适配起來更簡單。

前面提到logback作為Spring Boot預設的日志架構 ,肯定有相應的考量,我司也是使用logback 作為 Spring Boot 項目中的日志實作架構,下面我們就詳細說說 logback。

二、SpringBoot 日志架構 logback

2.1、logback 是什麼?

logback 是 log4j 團隊建立的開源日志元件。與 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 依賴。

SpringBoot項目logback日志配置

啟動項目,日志列印如下:

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 對大小寫敏感)

SpringBoot項目logback日志配置

configuration 節點:根節點,屬性如下:

  • scan :此屬性為 true 時,配置檔案發生改變,将會被重新加載,預設為true。
  • scanPeriod :監測配置檔案是否有修改的時間間隔,機關毫秒,當 scan 為 true 時,此屬性生效。預設的時間間隔為1分鐘 。
  • debug :此屬性為 true 時,列印出 logback 内部日志資訊,實時檢視 logback 運作狀态,預設 false。

root 節點:必須的節點,用來指定基礎的日志級别,隻有一個屬性。該節點可以包含零個或者多個元素,子節點是 appender-ref ,标記 appender 将會添加到這個 logger 中。

  • level :預設值 DEBUG

contextName 節點:辨別一個上下文名稱,預設 default ,一般用不到。

property 節點:标記一個上下文變量,屬性有 name 和 value,定義變量之後用 ${} 擷取值。

appender 節點:<appender> 是 <configuration> 的子節點,主要用于格式化日志輸出節點,屬性有 name 和 class,class 用來指定那種輸出政策,常用的就是控制台輸出政策和檔案輸出政策。有幾個子節點比較重要。

SpringBoot項目logback日志配置
  • filter :日志輸出攔截器,沒特殊要求就使用系統自帶的,若要将日志分開,比如将 ERROR 級别的日志輸出到一個檔案中,其他級别的日志輸出到另一個檔案中,這時候就要用到 filter 。
  • encoder :和 pattern 節點組合用于具體輸出日志的格式和編碼方式。
  • file :用來指定日志檔案輸出位置,絕對路徑或者相對路徑。
  • rollingPolicy :日志復原政策,常見的就是按照時間復原政策(TimeBasedRollingPolicy) 和按照大小時間復原政策 (SizeAndTimeBasedRollingPolicy)。
  • maxHistory :可選節點,控制保留日志檔案的最大數量,超出數量就删除舊檔案。
  • totalSizeCap :可選節點,指定日志檔案的上限大小。

logger 節點:可選節點,用來指定某一個包或者具體某一個類的日志列印級别。

  • name :指定包名。
  • level :可選,日志的級别。
  • addtivity :可選,預設為 true,此 logger 的資訊向上傳遞。

springProfile :多環境輸出日志檔案,根據配置檔案激活參數 (active) 選擇性的包含和排查部配置設定置資訊。根據不同環境來定義不同的日志輸出。

logback 中一般有三種過濾器 Filter

  1. LevelFilter :級别過濾器,根據日志級别進行過濾,如果日志級别等于配置級别,過濾器會根據onMath 和 onMismatch 接受或者拒絕日志。有以下子節點
  • 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) 映射診斷環境,是 log4j 和 logback 提供的一種友善線上多線程條件下記錄日志的功能,可以看成是一個與目前線程綁定的 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 接口,檢視結果輸出日志格式。

SpringBoot項目logback日志配置

代碼位址:https://gitee.com/duan138/practice-code/tree/dev/logback

三、總結

SpringBoot 中日志講解就到這裡,上面提到的知識點都是項目中常用的,比如日志怎麼配置、根據日志級别把日志輸出到不同的檔案裡、或者将 INFO 和 ERROR 級别的日志輸出到同一個檔案中、或者定制日志格式等等。

作者:程式員康康

連結:https://juejin.cn/post/7330104253824729097