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"/> <!–<appender-ref ref="ERROR_FILE"/>–> </logger>--> <root level="INFO"> <!--使用非同步列印日誌--> <appender-ref ref="ASYNC"/> <appender-ref ref="ERROR_ASYNC"/> </root> </configuration>
END.