SpringBoot入門到精通(十三)日誌:別小看它,否則吃虧的是自己!學會你也可以設計架構

净重21克發表於2024-10-24

別小看他,當你面對的時候,就會知道,多麼痛的領悟!

  如何在 Spring Boot 中使用 Logback 記錄詳細的日誌?

  整合LogBack,Log4J...等,是不是很多方法!但需要注意,我講的可能和你是一樣的,但也是不一樣的。

常見日誌級別:高 --- 低排列
TRACE:
描述:最詳細的日誌級別,通常用於開發和除錯階段,記錄非常詳細的執行資訊。
示例:log.trace("Entering method: {這裡的資料,後面的引數會自動填充}", methodName);
DEBUG:
描述:用於除錯資訊,記錄程式的詳細執行過程,但比 TRACE 級別略少。
示例:log.debug("Variable value: {}", variableValue);
INFO:
描述:記錄普通的資訊日誌,通常用於記錄應用程式的正常執行狀態。
示例:log.info("User logged in: {}", userId);
WARN:
描述:警告資訊,表示潛在的問題,但應用程式仍可以繼續執行。
示例:log.warn("File not found: {}", fileName);
ERROR:
描述:錯誤資訊,表示應用程式中發生了錯誤,可能會影響功能的正常執行。
示例:log.error("Database connection failed: {}", e.getMessage());  這裡每個字母程式碼,都認真看,有沒有疑問呢?特別,特別注意哦,後面告訴你
FATAL:
描述:嚴重錯誤,通常會導致應用程式崩潰或無法繼續執行。
示例:log.fatal("Critical system failure: {}", e.getMessage());

  實戰檢驗真理!論日誌的重要性。

  在開發企業級應用時,日誌記錄是一項非常重要的功能。良好的日誌記錄可以幫助我們快速定位和解決問題。比如異常排查,介面互動!大多數認為,直接log.info.debug一下就可以了...

  細節很重要:

    通常,生產環境,日誌級別要求是很嚴格的(設定INFO的舉手),企業級開發,基本要求不允許太多日誌,通常不推薦使用DEBUG級別的日誌,因為這會產生大量的日誌輸出,不僅佔用儲存空間,還可能影響系統效能。。

SpringBoot與LogBack日誌(合併)

1. 引入依賴

  在 Spring Boot 專案中,Logback 是預設的日誌框架。Spring Boot 會自動配置 Logback,因此你通常不需要手動新增 Logback 的依賴。但是,為了確保所有必要的依賴都已包含,你可以在 pom.xml 檔案中明確指定這些依賴。

<dependencies>
    <!-- Spring Boot Starter Web (或其他你需要的 Starter) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Boot Starter Logging (包含 SLF4J 和 Logback) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>

    <!-- 如果你已經包含了 spring-boot-starter-web,這個依賴是多餘的,因為 spring-boot-starter-web 已經包含了 spring-boot-starter-logging -->
</dependencies>

2. 配置 Logback

  在 src/main/resources 目錄下建立或編輯 logback-spring.xml 檔案(按照自動裝配機制,檔名和位置,預設)

  檔名:logback-spring.xml (基於自動裝配)
  位置:src/main/resources/ (基於自動裝配)

<configuration>
    <!-- 定義日誌檔案的儲存路徑 -->
    <!-- <property name="LOG_PATH" value="logs" /> 相對路徑,當前專案所在目錄下的logs,如專案在/home/tomcat/project那麼日誌在/home/tomcat/project/logs -->
    <!-- 自定義日誌路徑,可指定日誌儲存位置,logging.file.path指向Boot配置檔案yml、properties檔案配置 -->
    <property name="LOG_PATH" value="${logging.file.path}" />
    <!-- 自定義日誌名稱,可忽略 -->
    <property name="LOG_FILE_NAME" value="application" />

    <!-- 控制檯日誌輸出:常配置使用在開發環境本地 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{uuid}] %logger{36} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 檔案日誌輸出:線上環境測試、UAT、PRE -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 【定義日誌檔案要求】 -->
        <file>${LOG_PATH}/${LOG_FILE_NAME}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 按天滾動
            ${LOG_PATH} 路徑
            ${LOG_FILE_NAME} 名稱
            %d{yyyy-MM-dd} 日期
            %i    序號,0開始
            -->
            <fileNamePattern>${LOG_PATH}/${LOG_FILE_NAME}-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!-- 單個檔案最大500MB -->
            <maxFileSize>500MB</maxFileSize>
            <!-- 保留最近30天的日誌檔案 -->
            <maxHistory>30</maxHistory>
            <!-- 總日誌檔案大小不超過1GB -->
            <!-- <totalSizeCap>1GB</totalSizeCap> -->
        </rollingPolicy>
        <!-- 【定義日誌檔案中記錄日誌的內容格式】
            %d{yyyy-MM-dd HH:mm:ss.SSS}:
            %d:表示日期和時間。
            {yyyy-MM-dd HH:mm:ss.SSS}:日期和時間的格式化模式。
            yyyy:四位年份。
            MM:兩位月份。
            dd:兩位日期。
            HH:兩位小時(24小時制)。
            mm:兩位分鐘。
            ss:兩位秒。
            SSS:三位毫秒。
            [%thread]:
            %thread:表示當前執行緒的名稱。
            []:用於包裹執行緒名稱,使其更易讀。
            %-5level:
            %level:表示日誌級別(如 TRACE, DEBUG, INFO, WARN, ERROR)。
            -5:表示日誌級別的最小寬度為5個字元,如果日誌級別不足5個字元,則左對齊並用空格填充。
            %logger{36}:
            %logger:表示日誌記錄器的名稱。
            {36}:表示日誌記錄器名稱的最大長度為36個字元,如果名稱超過36個字元,則截斷。
            - %msg:
            %msg:表示日誌訊息的內容。
            -:用於分隔日誌記錄器名稱和日誌訊息,使其更易讀。
            %n:
            %n:表示換行符,用於在每條日誌訊息後換行。
        -->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{uuid}] %logger{36} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 非同步日誌輸出
        作用
        提高效能:
        非同步日誌記錄:AsyncAppender 將日誌訊息放入佇列中,然後由單獨的執行緒處理這些訊息。這樣,主執行緒不會因為日誌記錄操作而阻塞,從而提高應用程式的效能。
        減少 I/O 影響:日誌記錄通常涉及磁碟 I/O 操作,這些操作可能會比較慢。透過非同步記錄,可以將這些操作移到後臺執行緒,減少對主執行緒的影響。
        資源管理:
        佇列大小:透過設定 queueSize,可以控制記憶體的使用。較大的佇列可以容納更多的日誌訊息,但會佔用更多的記憶體。
        丟棄策略:透過設定 discardingThreshold,可以在佇列滿時選擇是否丟棄日誌訊息,以防止記憶體溢位。
    -->
    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="FILE" /> <!-- 這裡有指向哦 -->
        <!-- 這定義非同步日誌記錄器的佇列大小。佇列用於暫存日誌訊息,直到它們被處理 -->
        <queueSize>512</queueSize>
        <!-- 定義當佇列滿時是否丟棄日誌訊息。預設值是 0,表示不丟棄任何日誌訊息。大於 0 當佇列中的訊息數量超過這個閾值時,新產生的日誌訊息會被丟棄 -->
        <discardingThreshold>0</discardingThreshold> 
    </appender>
    
    <!-- 開發日誌級別設定 springProfile的啟用 與boot配置檔案yml、properties中的 spring.profiles.active 指向相關聯 -->
    <springProfile name="dev">
        <!-- 設定日誌的根級別為 debug 並指定日誌輸出到控制檯,同時非同步輸出到檔案 -->
        <root level="debug">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="ASYNC" />
        </root>
        <!-- 指定一個其他日誌記錄器為 info 非同步輸出到檔案
            additivity="false":當 com.yourcompany 包中的日誌事件被記錄時,這些日誌事件只會被輸出到 FILE 日誌記錄器,而不會被傳遞到根日誌記錄器。因此,這些日誌事件不會出現在控制檯中。
            additivity="true"(預設):如果 additivity 為 true,com.yourcompany 包中的日誌事件不僅會被輸出到 FILE 日誌記錄器,還會被傳遞到根日誌記錄器,從而也會出現在控制檯中。
            使用場景
            避免重複日誌:如果你希望某個特定的日誌記錄器的日誌事件只輸出到特定的記錄器,而不希望這些日誌事件在其他地方重複出現,可以設定 additivity="false"。
            精細控制日誌輸出:透過設定 additivity="false",可以更精細地控制日誌的輸出,確保日誌資訊的清晰和有序。
            注意:有root,其實就夠了,其他的頂部定義的看你的要求,這是精細化的一種方式
        -->
        <logger name="包名" level="info" additivity="false">
            <appender-ref ref="ASYNC" />
        </logger>
    </springProfile>
    
    <!-- test、UAT日誌級別設定 springProfile的啟用 與boot配置檔案yml、properties中的 spring.profiles.active 指向相關聯 -->
    <springProfile name="test、uat">
        <!-- 設定日誌的根級別為 debug 並指定日誌非同步輸出到檔案 -->
        <root level="debug">
            <appender-ref ref="ASYNC" />
        </root>
        <logger name="包名" level="info" additivity="false">
            <appender-ref ref="ASYNC" />
        </logger>
    </springProfile>

    <springProfile name="pre">
        <root level="info">
            <appender-ref ref="ASYNC" />
        </root>
        <!-- 第三方庫的日誌級別配置 -->
        <logger name="org.driud" level="debug" additivity="false">
            <appender-ref ref="ASYNC" />
        </logger>
    </springProfile>
</configuration>

3. 配置變化量

  你可以在 Spring Boot 的配置檔案中,使用環境變數來設定日誌變數,Spring Boot 支援在 application.properties 或 application.yml 檔案中引用環境變數:

# application.properties
# 啟用配置環境
spring.profiles.active=dev

# 配置日誌檔案生成後,放在哪裡:檔案位置,配置後都可以被引用
logging.file.path=/opt/tomcat/myapp/logs
# 配置根日誌級別
# logging.level.root=info
# 配置三方日誌級別,細化
# logging.level.包名=debug
# 如果日誌配置檔案位置、名稱,自定義了怎麼辦?
# logging.config=classpath:custom-logback.xml

你還可以結合多環境配置檔案,來設定不同的日誌路徑。例如,為開發環境、測試環境和預釋出環境分別設定不同的日誌路徑。
  application-dev.properties
  application-uat.properties
  application-pre.properties

application.properties 檔案啟用特定環境的配置檔案即可

SpringBoot與LogBack日誌(生產細節控)

問題一

  無論是否叢集服務,十個人併發也是併發,發起同一個功能或不通功能屬於併發互動,日誌記錄器列印日誌時也是有交叉列印。那麼如何在百千萬行的交叉日誌記錄中,找到屬於某一個指定功能操作的記錄呢!

  A、B、C都在請求message服務,每個業務的處理時間肯定不一致,併發交叉日誌列印時,如何排查耗時?

  解決方案:每次互動,從日誌的開始就進行標記,直到這個互動結束,只要保證,每個執行緒,每個互動,標記是唯一的,哪就可以了,即使日誌交叉列印,因為保證了唯一標記不同,所以也會很好區分。

  Logback 支援在日誌輸出中插入 MDC(Mapped Diagnostic Context)變數的佔位符。在【定義日誌檔案中記錄日誌的內容格式】時,有一處標紅,那裡就是一個MDC取值的示例,MDC 是一個執行緒上下文相關的鍵值對儲存,可以用來在日誌中新增額外的資訊,如請求的唯一識別符號(UUID)

MDC(Mapped Diagnostic Context)  

  MDC 概述
    MDC 是一個執行緒上下文相關的鍵值對儲存,每個執行緒都有自己的 MDC 例項。MDC 是 Logback 和 Log4j 中提供的一個功能,主要用途是在日誌記錄中新增與當前執行緒相關的上下文資訊,些資訊可以是請求的唯一識別符號、使用者 ID、會話 ID 等,以便在日誌中進行更細粒度的跟蹤和除錯。
  主要功能
    儲存上下文資訊:MDC 允許你在日誌記錄中儲存和訪問與當前執行緒相關的上下文資訊。
    日誌格式化:在日誌輸出模式中使用 MDC 變數,可以在日誌訊息中插入這些上下文資訊。
    執行緒安全:MDC 是執行緒安全的,每個執行緒都有獨立的 MDC 例項,不會互相干擾。

  MDC的使用

  1.在每個日誌列印之前使用(不推薦,不利用統一管理,但凡有人忘,新人,那日誌就沒有了,重複,還太麻煩)

public class MyController {
    public void handleRequest() {
        // 生成一個唯一的請求識別符號
        String uuid = UUID.randomUUID().toString();
        
        // 將 UUID 設定到 MDC 中   在你使用log。。。列印日誌之前放入
        MDC.put("uuid", uuid);
        
        log.info("請求,start:{}",json)
        // 處理請求
        // ...其他業務,等等,其他日誌等等...
        log.info("應答,end:{}",json)
        
        // 請求處理完成後,清除 MDC 中的 UUID
        MDC.remove("uuid");
    }
}

  控制檯輸出

2024-11-01 14:30:00.123 [main] [f0e2c1a0-1b9d-4b7e-8c0a-123456789abc] INFO  MyController - 請求,start:{"name": "John", "age": 30}
------ 若業務中有其他日誌,那麼他們的uuid都是一樣的,非常好定位  ------
2024-11-01 14:30:00.124 [main] [f0e2c1a0-1b9d-4b7e-8c0a-123456789abc] INFO  MyController - 應答,end:{"name": "John", "age": 30}

  2.利用過濾器Filter

  可以自定義實現Filter介面哈,這就比較老了,與時俱進吧,Boot中有了,就用吧。

  OncePerRequestFilter 概述
  OncePerRequestFilter 的主要目的是確保即使在一個請求被多個過濾器鏈中的多個例項處理時,也只會執行一次過濾邏輯。這對於避免重複處理和潛在的效能問題非常重要。

  OncePerRequestFilter 是 Spring Framework 提供的一個過濾器類,用於確保每個請求只被處理一次。它繼承自 org.springframework.web.filter.OncePerRequestFilter 類,並且通常用於需要在每個 HTTP 請求上執行某些操作的場景,例如日誌記錄、效能監控、安全檢查等。

  主要特點
    單次執行:確保每個請求只被處理一次,即使在過濾器鏈中有多個例項。
    執行緒安全:適用於多執行緒環境,確保執行緒安全。
    靈活性:可以透過重寫 doFilterInternal 方法來自定義過濾邏輯。
  使用場景
    日誌記錄:在每個請求開始和結束時記錄日誌。
    效能監控:記錄每個請求的處理時間。
    安全檢查:在請求到達控制器之前進行身份驗證和授權。
    跨域處理:設定響應頭以支援跨域請求。、

  建立自定義過濾器

   建立一個繼承自 OncePerRequestFilter 的自定義過濾器,用於生成唯一請求識別符號並將其設定到 MDC 中:

@Component // 標記為Spring元件,SpringBoot自動裝配機制,掃描到元件後,會自動將其載入。當然也可以選擇手動將過濾器加入到服務(可自行查閱)
@Slf4j
public class RequestLoggingFilter extends OncePerRequestFilter {
    /**
     * 重寫doFilterInternal過濾器方法,每次互動,過濾器都會攔截,並執行次方法
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // 生成一個唯一的請求識別符號 每個請求生成一個
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        // 將 UUID 設定到 MDC 中 每個執行緒一個
        MDC.put("uuid", uuid);
        // 記錄請求開始日誌
        log.info("請求開始,URL: {}", request.getRequestURL());

        // 繼續執行過濾器鏈
        try {
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            // 記錄錯誤日誌
            log.error("請求處理失敗,URL: {}, 錯誤資訊: {}", request.getRequestURL(), ExceptionUtils.getStackTrace(e));
            // 重新丟擲異常,確保後續的統一異常處理 能夠接收到異常,並統一處理異常資訊
            throw e;
        } finally {
            // 記錄請求結束日誌
            log.info("請求結束,URL: {}", request.getRequestURL());
            // 請求處理完成後,清除 MDC 中的 UUID
            // MDC.remove("uuid"); // 清楚指定
            // 請求處理完成後,清除 MDC 中的 UUID
            MDC.clear(); // 清除所有(MDC每個請求、執行緒都有自己的MDC,建議使用清除所有,避免互動結束後的MDC上下文資料殘留,以及潛在的記憶體洩漏或資訊混淆)
        }
    }
}

  備註:關於統一異常處理,請觀看我的”統一異常處理“ ,這些都是架構必須要會的。

  說明:正常情況下,如上操作後,每個流程對應的日誌執行緒資訊,都會被賦予一個UUID流程標記。但-----假設你的流程中有非同步操作,比如,接收到請求後流程:1.2.3.4.5.但是第3步,是一個非同步方法或者新執行緒功能,那麼第3步內的所有操作記錄,日誌列印是不存在UUID標記的。

  原因:在使用 MDC(Mapped Diagnostic Context)記錄日誌時,確實會遇到在非同步操作中丟失上下文資訊的問題。這是因為 MDC 資訊通常是執行緒繫結的,而非同步操作(執行緒切換,一個執行緒,切換到另一個新的執行緒了)是在不同的執行緒中執行,導致 MDC 資訊無法傳遞。

  解決方案:

  1.手動傳遞,在非同步之前當前執行緒方法內,把MDC的標記拿到,然後作為引數,傳遞,非同步業務中接受到你引數後,業務開始前再次MDC設定標記,業務結束後,再次MDC銷燬即可;

  2.利用InheritableThreadLocal + MDC,InheritableThreadLocal支援子執行緒繼承父執行緒中的執行緒變數,也支援重寫父執行緒變數(這個重寫和Java中繼承模式一樣,不會影響父執行緒哦)。簡單說一下,就是設定MDC值得時候,同時設定InheritableThreadLocal,在非同步方法中,就可以透過InheritableThreadLocal獲取值,再放入道MDC中即可。

  最佳化日誌第三步,AOP

  日誌搭建基本完善,但是日誌輸出呢?每個介面都需要手動log.....

  可以利用Spring AOP 對控制器進行環繞通知。在每個控制器的請求處理業務之前,應答返回之前,加入日誌記錄:介面名,控制器名稱,互動的請求報文,應答報文等。

@Component
@Aspect
@Slf4j
public class ControllerAop {
    /**
     * 切入點表示式:篩選所有的控制層,以及控制層介面方法
     * 第一個“*” :任意返回值
     * 第二個“.*” :如果少一個點,就是com.xxx下的任意包,多一個點,就是com.xxx下的任意包以及子包的controller包
     * 第三個“.*” :controller包下任意類
     * 第四個“*” :任意方法
     * 第五個“(..)”:任意引數
     */
    @Pointcut("execution(public * com.xxx..*.controller..*.*(..))")
    public void privilege() {
    }

    /**
     * 定義AOP通知方式:Around環繞,滿足切片的控制器方法,將在這裡先走一次關卡。
     * 
     * @param proceedingJoinPoint 是 AOP(面向切面程式設計)中的一個重要介面,主要用於實現環繞通知(@Around)
     * @return 被攔截方法執行結果
     * @throws Throwable
     */
    @Around("privilege()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // ThreadLocalContext 自己定義的類 是一個用於在當前執行緒中儲存和傳遞上下文資訊的工具類。它利用了 ThreadLocal 機制,確保每個執行緒都有獨立的變數副本,從而避免了多執行緒環境下的資料競爭問題。
        // ThreadLocalContext.set("aaa", "");
        // PageHelper.clearPage():清除分頁資訊。PageHelper 是一個常用的分頁外掛,這個方法用於在每次請求開始時清除之前的分頁設定,確保新的請求不會受到之前分頁設定的影響。

        long start = System.currentTimeMillis();
        // getSignature 獲取被攔截方法的簽名資訊,包括方法名、類名等
        String className = proceedingJoinPoint.getSignature().getDeclaringTypeName();
        String methodName = proceedingJoinPoint.getSignature().getName();
        Object result = null;
        // 獲取被攔截方法的引數陣列
        Object[] args = proceedingJoinPoint.getArgs();
        for (Object object : args) {
            if(object instanceof RequestVo) {
                log.info("{}{}{}" , "【"+className + "." + methodName + "】" , "【前後端互動--請求】" ,  null != object ? JSONObject.toJsonString(object): object);
            }
        }
        // 繼續執行被攔截的方法,並獲取返回報文。
        result = proceedingJoinPoint.proceed();
        long end = System.currentTimeMillis();
        log.info("{}{}{}" , "【"+className + "." + methodName + "】" , "【前後端互動--響應--耗時:"+(end - start)+"】" ,  null != result ? JSONObject.toJsonString(result) : result);
        return result;
    }
}

  ThreadLocal 是 Java 提供的一個執行緒區域性變數儲存機制,每個執行緒都有自己獨立的 ThreadLocal 變數副本,互不干擾。所以,它其實也可以是一個MDC,可以做到和MDC一樣實現(只能再本執行緒中使用),具體我就不提供了,這裡僅給出一些簡單說明。它的主要作用是為每個執行緒提供獨立的變數副本,從而實現執行緒間的隔離和資料的安全性。下面詳細解釋 ThreadLocal 的作用、意義和常見使用場景。  

  另InheritableThreadLocal也可以哦。那麼

  ThreadLocal、MDC、InheritableThreadLocal之間的區別呢?

  作用
    執行緒隔離:
      每個執行緒都有自己獨立的 ThreadLocal 變數副本,確保不同執行緒之間不會相互影響。
      適用於需要線上程內部儲存狀態且不希望其他執行緒訪問這些狀態的場景。
    簡化執行緒間資料傳遞:
      避免在方法呼叫之間傳遞引數,減少方法簽名的複雜性。
      提高程式碼的可讀性和可維護性。

  使用場景
    日誌追蹤:
      在分散式系統中,為了追蹤請求的整個呼叫鏈路,可以在 ThreadLocal 中儲存一個唯一的請求識別符號(如 traceId),並在日誌中輸出這個識別符號,以便於問題定位和除錯。
    事務管理:
      在事務處理中,可以使用 ThreadLocal 儲存事務上下文資訊,確保同一個執行緒中的多個方法呼叫共享同一個事務上下文。
      例如,Spring 的事務管理器 TransactionSynchronizationManager 使用 ThreadLocal 來管理事務上下文。
    使用者會話資訊:
      在 Web 應用中,可以使用 ThreadLocal 儲存使用者的會話資訊(如使用者 ID、角色等),以便在多個方法呼叫中使用這些資訊,而不需要每次都傳遞引數。

  自定義類ThreadLocalContext ,這個不一定用到,看自己的任務需求,其實這個應該單獨介紹的,後來想想還是算了,懶

/**
 * 一個用於在當前執行緒中儲存和傳遞上下文資訊的工具類。它利用了 ThreadLocal 機制,確保每個執行緒都有獨立的變數副本,從而避免了多執行緒環境下的資料競爭問題。.
 */
public class ThreadLocalContext {
    
    private static final ThreadLocal<Map<String, Object>> context = new ThreadLocal<Map<String, Object>>();
    
    /**
     * 清空執行緒上下文中快取的變數.
     */
     public static void clear() {
       context.set(null);
     }

     /**
      * 線上程上下文中快取變數.
      */
     public static void set(String key, Object value) {
       Map<String, Object> map = context.get();
       if (map == null) {
         map = new HashMap<>();
         context.set(map);
       }
       map.put(key, value);
     }

     /**
      * 從執行緒上下文中取出變數.
      */
     public static Object get(String key) {
       Map<String, Object> map = context.get();
       Object value = null;
       if (map != null) {
         value = map.get(key);
       }
       return value;
     }

     /**
      * 將變數移除.
      */
     public static void remove(String key) {
       Map<String, Object> map = context.get();
       if (map != null) {
         map.remove(key);
       }
     }
}

  日誌輸出樣式:(不要看我的日期,我都是手打的)

2024-11-01 05:09:20.788 logback [http-nio-8080-exec-13] INFO  c.x.c.u.RequestLoggingFilter [ed80915251674756adf3d51c7c89bfdb] 請求開始,URL: {http://......}
2023-11-01 05:09:20,789 logback [http-nio-8080-exec-13] INFO  c.e.a.ControllerAop [ed80915251674756adf3d51c7c89bfdb] -【com.xx.controller.CtrollerTest.testLog】【前後端互動--請求】{"param1":"Hello","param2":123}
2023-11-01 05:09:20,790 logback [http-nio-8080-exec-13] INFO  c.e.a.ControllerAop [ed80915251674756adf3d51c7c89bfdb] -【com.xx.controller.CtrollerTest.testLog】【前後端互動--響應--耗時:1ms】{"param1":"Hello","param2":123}
2024-11-01 05:09:20.800 logback [http-nio-8080-exec-13] INFO  c.x.c.u.RequestLoggingFilter [ed80915251674756adf3d51c7c89bfdb] 請求結束,URL: {http://......}

問題二

  日誌log....必須要注意的事項。

  當有異常處理,不想丟擲異常,但需要顯示提示,並日志追蹤,想便於日誌排查時,那麼須使用過載哦。否則,可能你什麼都看不到

try {
    // 某個業務操作
} catch(Exception e) {
    // 列印異常資訊,有用debug,有的用info,有的用error(標準),但,方法使用不對,是沒有什麼作用的,只有很簡單的資訊文字,很難排查
    log.debug("debug異常資訊:" + e.getMessage());
    log.info("info異常資訊:" + e.toString());
    log.error("error異常資訊:" + e);
    /** 
     * 以上無論哪個,也許顯示的都是這樣:xxx異常資訊: / by zero  
     * 然後就沒了,具體那一行報錯,哪個類報錯,都不知道,
     * 甚至某些錯誤可能 getMessage 就是個空字串,那麼你將只看到:xxx異常資訊:
     * 排查問題時,你將無從下手。
     */

    
    // 一定要用這個,方法過載,且過載引數是一個異常物件:預設情況下log不會記錄異常的堆疊跟蹤資訊,只有過載傳遞異常物件,才支援異常顯現堆疊。
    log.debug("異常資訊:" , e);
    log.info("異常資訊:" , e);
    log.error("異常資訊:" , e);
    /**很詳細的日誌資訊,能確定是哪個類,那行,哪個位置。
     * 如上將顯示日誌就很全面,示例:(不要看我的日誌,我都是手打的)
     * 2024-11-01 09:09:00,866 ERROR [Thread-53][com.x.x.x.x.spring.xxxxxxx]異常資訊: java.lang.ArithmeticException: / by zero
     *         at ExampleClass.someMethod(ExampleClass.java:14)
     *         at ExampleClass.main(ExampleClass.java:21)
     *         ...省略N行...
     */
}

SpringBoot與LogBack日誌(動態更新級別)

  在開發和運維過程中,日誌是診斷問題的重要工具。Spring Boot 提供了強大的日誌管理功能,但預設情況下,日誌級別是在啟動時配置的(如上,啟動的時候已經配置了日誌級別為INFO)。

  有時候,我們希望在應用程式執行時動態地調整日誌級別,以便更靈活地進行除錯和監控。比如生產日誌通產為了節省伺服器資源,會選擇INFO級別,但是問題出現後,又想要DEBU詳情,就需要動態更新了。

  問題:更新日誌級別,由 INFO -→ DEBUG || DEBUG -→ INFO 如果是更改專案配置檔案,哪就需要進行服務重啟,生產中在不進行版本迭代的情況下,服務重啟,是禁忌!如何實現動態更新日誌級別,且不重啟應用呢?

  方式一:SpringBoot監控(不推薦)

  先說不推薦原因:生產,對外開放端點,是不明智的選擇。當然,安全管理好了,也不是不可以。

  Spring Boot Actuatorspring boot專案一個監控模組,提供了很多原生的端點,包含了對應用系統的自省和監控的整合功能,比如應用程式上下文裡全部的Bean、r日誌級別、執行狀況檢查、健康指標、環境變數及各類重要度量指標等等。因此可以透過其自帶的日誌監控,實現動態更新日誌級別。

  1. 搭建SpringBoot監控。

    參考閱讀”SpringBoot監控“,裡面有詳細說明介紹。

  2. 注意端點的開啟,指定並開啟loggers

# 只暴露日誌相關的端點
management.endpoints.web.exposure.include=loggers,logfile

# 啟用日誌檔案端點
management.endpoint.logfile.enabled=true

# 配置日誌級別
logging.level.root=INFO
logging.level.com.example=DEBUG

# 配置日誌檔案路徑
logging.file.name=app.log
logging.file.path=/var/log/myapp

  訪問:localhost:8080/actuator/loggers,將獲取到一個JSON資料:

{
    "levels": [
        "OFF",
        "ERROR",
        "WARN",
        "INFO",
        "DEBUG",
        "TRACE"
    ],
    "loggers": { 
        "ROOT": {
            "configuredLevel": "INFO",
            "effectiveLevel": "INFO"
        },
        "com": {
            "configuredLevel": null,
            "effectiveLevel": "INFO"
        },
        "com.alibaba": {
            "configuredLevel": null,
            "effectiveLevel": "INFO"
        },
        "com.alibaba.druid": {
            "configuredLevel": null,
            "effectiveLevel": "INFO"
        }
        ....此處省略N行...
    }
}

  簡單說明:"ROOT"、"com"、"com.alibaba".....這些,就是日誌記錄器的名稱,說白了就是你的包層次....著這個包層次,下的所有類,如果有日誌,那麼其對應的日誌級別。(其中ROOT,最高階)

    configuredLevel:配置級別(希望配置的級別)
    effectiveLevel:有效級別(當前正在使用的級別)

  另:子包路徑的日誌級別,會被父級別傳遞,反之,單獨設定子包日誌級別,不影響父包日誌級別

  如上:若更新 ROOT 日誌級別,其他所有級別會同步更新,更新 com.alibaba 日誌級別,com.alibaba......所有子級層次的,都會被更新,ROOT 、 com 則不受影響!

  3. 動態更新日誌級別

  POST請求:http://localhost:8080/actuator/loggers/日誌記錄器名稱

  請求引數:{"configuredLevel": "DEBUG"}

  重新訪問:http://localhost:8080/actuator/loggers,看下JSON日誌級別,就被更新了。此時,檢視服務日誌即可。

  

  方式二:介面更新LoggerContext(不是強烈推薦,但條件下可以考慮)

  先說不推薦原因:自己寫介面維護,每次更新呼叫介面,不是特別方便,但可以用。

  介面方式更新日誌級別:條件情況可以考慮。如除了對外服務端,還是內部運維管理服務。可以將此介面提供內部服務的管理。由運維部操作使用(本身動態更新日誌級別目的,就是為運維開展的)

  呼叫介面:http://IP:埠/api/levelSetting?levelName=root&level=debug

@Slf4j
@RestController
@RequestMapping("api")
public class HealthExaminationController {
    // 獲取LoggerContext日誌上下文物件
    private LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();

    /**
     * 設定指定日誌記錄器的日誌等級【日誌記錄器名稱,可查詢SpringBoot的監控器,開啟日誌監控,訪問:http://localhost:埠/actuator/loggers】
     *
     * @param levelName 日誌記錄器名稱
     * @param level     日誌等級
     * @return
     * @throws Exception
     */
    @GetMapping(value = "/levelSetting")
    public String levelSetting(@RequestParam("levelName") String levelName, @RequestParam("level") String level) {
        // 根據指定的日誌記錄器名稱獲取 logger
        Logger logger = loggerContext.exists(levelName);
        if (logger == null) {
            return levelName + ": The parameter logger Name not exist!";
        }
        log.info("更新日誌之前:日誌等級【{}】", logger.getLevel());
        // 解析 level 引數,第二個參數列示當 level 引數非法時的預設值
        Level newLevel = Level.toLevel(level, null);
        if (newLevel == null) {
            return level + ": The parameter logger level is not legal!";
        }
        // 重寫設定 logger 的 level
        logger.setLevel(newLevel);
        log.debug("更新日誌之後:日誌等級【{}】", logger.getLevel());
        return "success! update logger Level to:" + logger.getLevel();
    }
}

  方式三:Nacos配置中心(推薦,但不寫)

  先說不寫原因:微服務知識點,既然本次編輯的是SpringBoot的,Cloud的東東放進來不是很好吧。所以分個類,可以看後續SpringCloud 、SpringCloud Alibaba的更新。這裡由Nacos官方文件,也可以參考

  1. 下載 Nacos 並啟動 Nacos server

  2. 引入Nacos配置依賴

  3. 配置整合Nacos

  4. 測試在Nacos控制檯更新配置

相關文章