1. 程式人生 > 其它 >Java日誌規範

Java日誌規範

前言

  寫好程式的日誌可以幫助我們大大減輕後期維護壓力,開發人員應在一開始就養成良好的日誌撰寫習慣

  日誌可以幫我們解決以下問題:
    ①、程式是不是按預期執行
    ②、程式哪裡出現了BUG
    ③、使用者在系統上幹了什麼
    ④、問題是誰造成的,是依賴的業務系統還是本身系統

 

一、日誌框架選型

  logback、log4j2

    logback:推薦,Springboot預設的日誌框架
    log4j2:版本大於2.15.0

 

二、日誌規約

  【強制】應用中不可直接使用日誌系統(Log4j、Logback)中的 API,而應依賴使用日誌框架 (SLF4J、JCL--Jakarta Commons Logging)中的 API,使用門面模式的日誌框架,有利於維護和 各個類的日誌處理方式統一

    日誌框架(SLF4J、JCL--Jakarta Commons Logging)的使用方式(推薦使用 SLF4J)

// 使用 SLF4J: 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 private static final Logger logger = LoggerFactory.getLogger(Test.class); 
 
 // 使用 JCL:
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory; 
 
private static final Log log = LogFactory.getLog(Test.class);

 

  【強制】所有日誌檔案至少儲存15天,因為有些異常具備以“周”為頻次發生的特點。對於 當天日誌,以“info.log/error.log”來儲存,儲存在/home/admin/應用名/logs/目錄下,過往日誌 格式為: {logname}.log.{儲存日期},日期格式:yyyy-MM-dd

    示例: 以 aap 應用為例,日誌儲存在/home/admin/aap/logs/info.log,歷史日誌名稱為 info.log.2016-08-01 

  【強制】在日誌輸出時,字串變數之間的拼接使用佔位符的方式

    說明:因為 String 字串的拼接會使用 StringBuilder 的 append()方式,有一定的效能損耗。使用佔位符僅 是替換動作,可以有效提升效能

    示例:logger.debug("Processing trade with id: {} and symbol: {}", id, symbol); 

  【強制】對於 trace/debug/info 級別的日誌輸出,必須進行日誌級別的開關判斷

    說明:雖然在 debug(引數)的方法體內第一行程式碼 isDisabled(Level.DEBUG_INT)為真時(Slf4j 的常見實現Log4j 和 Logback),就直接 return,但是引數可能會進行字串拼接運算。此外,如果 debug(getName())這種引數內有 getName()方法呼叫,無謂浪費方法呼叫的開銷

// 如果判斷為真,那麼可以輸出 trace 和 debug 級別的日誌 
 if (logger.isDebugEnabled()) {
 logger.debug("Current ID is: {} and name is: {}", id, getName()); 
 } 

  【強制】避免重複列印日誌,浪費磁碟空間,務必在日誌配置檔案中設定 additivity=false

<logger name="com.taobao.dubbo.config" additivity="false"> 

  【強制】生產環境禁止直接使用 System.out 或 System.err 輸出日誌或使用 e.printStackTrace()列印異常堆疊

    說明:標準日誌輸出與標準錯誤輸出檔案每次 Jboss 重啟時才滾動,如果大量輸出送往這兩個檔案,容易 造成檔案大小超過作業系統大小限制

  【強制】異常資訊應該包括兩類資訊:案發現場資訊異常堆疊資訊。如果不處理,那麼通過 關鍵字 throws 往上丟擲

logger.error("inputParams:{} and errorMessage:{}", 各類引數或者物件 toString(), e.getMessage(), e);

  【強制】日誌列印時禁止直接用 JSON 工具將物件轉換成 String  

    說明:如果物件裡某些 get 方法被覆寫,存在丟擲異常的情況,則可能會因為列印日誌而影響正常業務流 程的執行

    正例:列印日誌時僅打印出業務相關屬性值或者呼叫其物件的 toString()方法

  【強制】我們在寫日誌的時候,需要注意輸出適當的內容。首先,儘量使用業務相關的描述

    說明:我們的程式是實現某種業務的,那麼就最好能描述清楚這個時候走到了業務過程的哪一步

    其次,避免在日誌中輸出一些敏感資訊,例如使用者名稱和密碼。以及,要保持編碼的一致。如果不能保證就儘量使用英文而不是中文。這樣當我們拿到日誌之後就不會因為看到一堆亂碼而不知所云了

    

三、打日誌的正確方式

  1、日誌需要列印哪些資訊

    開發人員只需關注日誌內容和異常堆疊即可,其餘資訊在日誌配置檔案中會自動輸出

    ①、日誌時間:yyyy-MM-dd HH:mm:ss.SSS

    ②、日誌級別:DEBUG、INFO、WARN、ERROR

    ③、記錄器名稱(類名)

      日誌的記錄器名稱一般是宣告日誌記錄器例項的類名,通過記錄器名稱可以快速定位到日誌輸出的類是哪個

    ④、方法名,列印日誌的方法

    ⑤、產生行數

      即產生日誌的所在類的原始碼行號  

    ⑥、日誌內容

      開發人員輸出業務相關的日誌

    ⑦、tracing 標識

      通過 AOP 切面,日誌框架 MDC 等技術結合,在日誌上新增一些鏈路追蹤的擴充套件元素,將會很大程度方便通過日誌進行程式請求呼叫的鏈路追蹤,這在分散式系統中尤其重要

    ⑧、異常堆疊

      堆疊異常資訊有助於程式異常的排查定位,ERROR級別必須列印異常堆疊,其他日誌級別不必列印

 

  2、什麼時候應該列印日誌

    ①、當你遇到問題時,只能通過debug功能來確定問題,你應該考慮打日誌,良好的系統,是可以通過日誌進行問題定位的

    ②、當你使用if...else或者swith分支時,要在分支的首行列印日誌,用來確定進入了哪個分支

    ③、經常以功能為核心進行開發,應該在提交程式碼前,可以確定通過日誌可以看到整個流程

 

  3、日誌基本格式

    ①、必須使用引數化資訊的方式:logger.debug("Processing trade with id:[{}] and symbol : [{}] ", id, symbol);

      不要進行字串拼接,那樣會產生很多String物件,佔用空間,影響效能

    ②、使用[] 進行引數變數隔離

       logger.debug("Processing trade with id:[{}] and symbol : [{}] ", id, symbol);這樣的格式寫法,可讀性更好,對於排查問題更有幫助

 

四、日誌追蹤

  1、為什麼要進行日誌追蹤?

    日誌追蹤可以根據traceId追蹤到一個請求鏈路中的所有日誌資訊,便於排查問題

  2、如何進行日誌追蹤?

    通過 AOP 切面,日誌框架 MDC 等技術結合,在日誌中列印traceId,而後根據traceId可檢索整個請求鏈路的日誌

    1)AOP切面結合MDC方式

      ①、日誌配置檔案中配置traceId佔位符

 <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}|%t|%-5level|%X{traceId}|%X{xiaomiId}|%C{0}#%M:%L|%msg%n</pattern>

      ②、AOP切入要增強traceId的方法

      ③、在增強類中往MDC中新增traceId欄位,並賦值,並在finally塊移除MDC中traceId欄位

@Aspect
@Component
public class LogAspect {

    private static final Logger LOGGER = LoggerFactory.getLogger(LogAspect.class);

    @Pointcut(value = "@annotation(org.springframework.web.bind.annotation.RequestMapping)||@annotation(org.springframework.web.bind.annotation.PostMapping)||@annotation(org.springframework.web.bind.annotation.GetMapping)")
    private void webPointcut() {
        // doNothing
    }

    /**
     * 為所有的HTTP請求新增traceId
     *
     * @param joinPoint
     * @throws Throwable
     */
    @Around(value = "webPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws IllegalAccessException, InstantiationException {
        // 方法執行前加上執行緒號,並將執行緒號放到執行緒本地變數中
        String uuid = StringUtil.getUUID();
        MDC.put("traceId", uuid);
        //...此處省略其他業務處理程式碼
        // 執行切點方法
        Object result = joinPoint.proceed();
        } finally {
            // 方法執行結束移除執行緒號,並移除執行緒本地變數,防止記憶體洩漏
            MDC.remove("traceId");
        }
        return result;
    }
}

    2)直接使用MDC方式

      ①、日誌配置檔案中配置traceId佔位符,同上

      ②、在方法執行業務邏輯前往MDC中新增traceId欄位,並賦值

      ③、在方法返回前移除MDC中的traceId欄位

MDC.put("traceId", uuid);
try {
    // 業務邏輯處理
} finally {
    MDC.remove("traceId");
}

 

 

五、推薦日誌配置

  ERROR級別單獨列印在error.log中,INFO及以下級別日誌列印在info.log中

  日誌檔案按天滾動輸出

  日誌非同步列印,提高系統性能

 

  logback.xml:  

<?xml version="1.0" encoding="UTF-8"?>
<!--預設每隔一分鐘掃描此配置檔案的修改並重新載入-->
<configuration>
    <!--定義日誌檔案的儲存地址 勿在LogBack的配置中使用相對路徑-->
    <property name="LOG_HOME" value="/home/work/log/${artifactId}"/>
    <!--輸出日誌到檔案中-->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/info.log</file>
        <!--不輸出ERROR級別的日誌-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>DENY</onMatch>
            <onMismatch>ACCEPT</onMismatch>
        </filter>
        <!--根據日期滾動輸出日誌策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/info.log.%d{yyyy-MM-dd}</fileNamePattern>
            <!--儲存的存檔日誌檔案的數量-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}|%t|%-5level|%X{traceId}|%X{xiaomiId}|%C{0}#%M:%L|%msg%n</pattern>
        </encoder>
    </appender>
    <!--錯誤日誌輸出檔案-->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/error.log</file>
        <!--只輸出ERROR級別的日誌-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <!--根據日期滾動輸出日誌策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/error.log.%d{yyyy-MM-dd}</fileNamePattern>
            <!--儲存的存檔日誌檔案的數量-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}|%t|%-5level|%X{traceId}|%X{xiaomiId}|%C{0}#%M:%L|%msg%n</pattern>
        </encoder>
    </appender>
    <!--非同步列印日誌,任務放在阻塞佇列中,如果佇列達到80%,將會丟棄TRACE,DEBUG,INFO級別的日誌任務,對效能要求不是太高的話不用啟用-->
    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <!--佇列的深度,該值會影響效能,預設256-->
        <queueSize>512</queueSize>
        <!--設為0表示佇列達到80%,也不丟棄任務-->
        <discardingThreshold>0</discardingThreshold>
        <!--日誌上下文關閉後,AsyncAppender繼續執行寫任務的時間,單位毫秒-->
        <maxFlushTime>1000</maxFlushTime>
        <!--佇列滿了直接丟棄要寫的訊息-->
        <neverBlock>true</neverBlock>
        <!--是否包含呼叫方的資訊,false則無法列印類名方法名行號等-->
        <includeCallerData>true</includeCallerData>
        <!--One and only one appender may be attached to AsyncAppender,新增多個的話後面的會被忽略-->
        <appender-ref ref="FILE"/>
    </appender>
    <appender name="ERROR_ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <queueSize>256</queueSize>
        <!--設為0表示佇列達到80%,也不丟棄任務-->
        <discardingThreshold>0</discardingThreshold>
        <!--日誌上下文關閉後,AsyncAppender繼續執行寫任務的時間,單位毫秒-->
        <maxFlushTime>1000</maxFlushTime>
        <!--佇列滿了直接丟棄要寫的訊息,不阻塞寫入佇列-->
        <neverBlock>true</neverBlock>
        <!--是否包含呼叫方的資訊,false則無法列印類名方法名行號等-->
        <includeCallerData>true</includeCallerData>
        <!--One and only one appender may be attached to AsyncAppender,新增多個的話後面的會被忽略-->
        <appender-ref ref="ERROR_FILE"/>
    </appender>

    <!--指定一些依賴包的日誌輸出級別,所有的logger會繼承root,為了避免日誌重複列印,需指定additivity="false",將不會繼承root的append-ref-->
<!--    <logger name="com.xiaomi.mitv.outgoing" level="ERROR" additivity="false">
        <appender-ref ref="STDOUT"/>
        &lt;!&ndash;<appender-ref ref="ERROR_FILE"/>&ndash;&gt;
    </logger>-->

    <root level="INFO">
        <!--使用非同步列印日誌-->
        <appender-ref ref="ASYNC"/>
        <appender-ref ref="ERROR_ASYNC"/>
    </root>
</configuration>

  
   

 

END.