我發現很多程序(xu)員都不會打日志。。。
你是小(xiao)阿(a)巴,剛入職的(de)低級程(cheng)序(xu)員,正在開發(fa)一個批量導(dao)入數據的(de)程(cheng)序(xu)。
沒想到,程(cheng)序剛(gang)上線(xian),產品經理就(jiu)跑(pao)過來說:小阿巴,用戶反饋你的程(cheng)序有 Bug,剛(gang)導入沒多久就(jiu)報錯中斷了!
你趕緊打開服務器,看著比你發量(liang)都少(shao)的報錯信息:

你一(yi)臉懵逼:只有這(zhe)點兒信息,我(wo)咋知道(dao)哪里出了問題啊?!
你只能(neng)硬著頭皮讓產品經理找用戶(hu)要數據(ju),然后一條(tiao)條(tiao)測(ce)試,看(kan)看(kan)是(shi)哪條(tiao)數據(ju)出(chu)了問題……
原(yuan)本大好的摸魚時光(guang),就這樣無了(le)。
這時,你的導師魚皮走了過來(lai),問道:小阿巴(ba),你是持矢了么(me)?臉色這么(me)難看?

你無奈地說:皮哥,剛才線上出(chu)了個 bug,我花了 8 個小時才定位到問題……
魚皮皺(zhou)了(le)皺(zhou)眉(mei):這么久?你沒打日(ri)志嗎(ma)?
你很是疑惑:誰(shui)是日志?為什么要打它(ta)?

魚皮嘆了口(kou)氣(qi):唉,難怪你(ni)(ni)要(yao)花(hua)這么久…… 來,我(wo)教你(ni)(ni)打日志!
?? 本文對應視頻版:
什么是(shi)日(ri)志(zhi)?
魚皮打開(kai)電腦(nao),給你看了一段代碼:
你看著代碼里的 log.info、log.error,疑(yi)惑地問:這些 log 是干什么(me)的?
魚皮:這就(jiu)是打日志(zhi)。日志(zhi)用來(lai)記(ji)錄程序運行時的狀態(tai)和信息,這樣(yang)當系統出(chu)現問題(ti)時,我(wo)們(men)可以(yi)通過日志(zhi)快速(su)定位問題(ti)。

你(ni)若有所思:哦(e)?還可(ke)以這(zhe)樣!如(ru)果當初(chu)我的代碼里有這(zhe)些(xie)日(ri)志(zhi),一眼就(jiu)定位到(dao)問題(ti)了…… 那我應(ying)該怎么打日(ri)志(zhi)?用什么技術(shu)呢?
怎么(me)打日志?
魚皮:每種編程語言都有很(hen)多日志(zhi)(zhi)框架和(he)工具(ju)庫,比如 Java 可以(yi)選(xuan)用(yong)(yong) Log4j 2、Logback 等(deng)等(deng)。咱們公司用(yong)(yong)的(de)是(shi) Spring Boot,它默(mo)認(ren)集(ji)成了(le) Logback 日志(zhi)(zhi)框架,你直接用(yong)(yong)就(jiu)行,不(bu)用(yong)(yong)再引入額外的(de)庫了(le)~

日志框(kuang)架的使用非(fei)常簡單,先獲取到 Logger 日志對象(xiang)。
1)方法(fa) 1:通過 LoggerFactory 手動獲取 Logger 日志對(dui)象(xiang):
public class MyService {
private static final Logger logger = LoggerFactory.getLogger(MyService.class);
}
2)方法 2:使用 this.getClass 獲(huo)取當前類(lei)的類(lei)型,來創建 Logger 對象:
public class MyService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
}
然后調用 logger.xxx(比如 logger.info)就能(neng)輸出(chu)日(ri)志了(le)。
public class MyService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
?
public void doSomething() {
logger.info("執行了一些操作");
}
}
效果如(ru)圖:

小阿巴(ba):啊,每個需要打(da)日志的類都要加上這行代碼么?
魚皮:還有更簡單的方式,使用 Lombok 工具庫提供的 @Slf4j 注解(jie),可以(yi)自動(dong)(dong)為當前類生成日志對象,不用手動(dong)(dong)定義啦。
上面的代碼等同(tong)于 “自動(dong)為當前類生成(cheng)日志對象”:
private static final org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(MyService.class);
你(ni)咧嘴一笑:這(zhe)個好(hao),爽爽爽!

等等,不對,我直接用 Java 自帶的 System.out.println 不也能輸出信息么?何必多(duo)此一(yi)舉?
System.out.println("開始導入用戶" + user.getUsername());
魚皮搖了(le)搖頭:千萬別(bie)這(zhe)么干!
首先,System.out.println 是一個(ge)同步方法,每次調(diao)用(yong)都會導致耗時的 I/O 操作(zuo),頻繁(fan)調(diao)用(yong)會影響(xiang)程序的性能。

而且它只能輸出信息到控制臺,不能靈活控制輸出位置、輸出格式、輸出時機等等。比如你現在想看三天前的日志,System.out.println 的(de)輸(shu)出(chu)早(zao)就被刷沒了(le),你(ni)還得浪費(fei)時間(jian)找半天。

你恍然大悟:原(yuan)來如此!那(nei)使用日(ri)志框(kuang)架就能解決這些問題嗎?
魚皮點點頭:沒錯,日(ri)(ri)志(zhi)(zhi)框架(jia)提供了豐富的打日(ri)(ri)志(zhi)(zhi)方法,還可以通(tong)過修改日(ri)(ri)志(zhi)(zhi)配置文(wen)件(jian)來隨心(xin)所(suo)欲地調教(jiao)日(ri)(ri)志(zhi)(zhi),比如把日(ri)(ri)志(zhi)(zhi)同時輸出到控制臺和文(wen)件(jian)中、設(she)置日(ri)(ri)志(zhi)(zhi)格式、控制日(ri)(ri)志(zhi)(zhi)級別等(deng)(deng)等(deng)(deng)。

在下苦心研究日志多年,沉淀了打日志的 8 大邪修秘法,先(xian)傳(chuan)授(shou)你 2 招最基(ji)礎的吧。
打日志的 8 大(da)最佳實踐
1、合理選擇(ze)日志級(ji)別
第一招,日志分(fen)級。
你(ni)好奇道:日志還有(you)級(ji)別?蘋果日志、安卓日志?
魚皮(pi)給(gei)了你(ni)一(yi)巴(ba)掌(zhang):可不要(yao)亂說,日志的(de)級別是按照重要(yao)程(cheng)度進行劃分的(de)。

其中 DEBUG、INFO、WARN 和 ERROR 用的最多。
-
調(diao)試用(yong)的詳細信(xin)息(xi)用(yong) DEBUG
-
正(zheng)常的業務流程用 INFO
-
可能有問題但不影響主流(liu)程的(de)用(yong) WARN
-
出(chu)現異常或錯誤(wu)的(de)用 ERROR
log.debug("用戶對象的詳細信息:{}", userDTO); // 調試信息
log.info("用戶 {} 開始導入", username); // 正常流程信息
log.warn("用戶 {} 的郵箱格式可疑,但仍然導入", username); // 警告信息
log.error("用戶 {} 導入失敗", username, e); // 錯誤信息
你(ni)撓(nao)了撓(nao)頭:俺(an)直接全用(yong) DEBUG 不行么(me)?
魚皮搖(yao)了搖(yao)頭(tou):如(ru)果所有信息(xi)(xi)都用同(tong)一級別,那出了問題時,你怎么(me)快速找到錯誤信息(xi)(xi)?

在生(sheng)產環(huan)境(jing),我(wo)們通常(chang)會把日志級別調高(比如 INFO 或 WARN),這樣 DEBUG 級別的日志就(jiu)不會輸(shu)出了,防(fang)止重要信息被無用日志淹(yan)沒。

你點點頭:俺明白了,不同(tong)的(de)場景用不同(tong)的(de)級(ji)別!
2、正(zheng)確記錄日志(zhi)信息
魚皮:沒錯,下面教你第二招。你注意到我剛才寫的日志里有一對大括號 {} 嗎(ma)?
log.info("用戶 {} 開始導入", username);
你(ni)回憶了一(yi)下:對哦(e),那是啥啊?
魚皮:這叫參數化日志。{} 是一個(ge)占位符,日(ri)志(zhi)框架會(hui)在(zai)運行時(shi)自動把后面的參(can)數值替換進去。
你撓了撓頭:我直(zhi)接用字符串拼(pin)接不(bu)行嗎?
log.info("用戶 " + username + " 開始導入");
魚(yu)皮搖搖頭:不推薦。因為字符(fu)串拼接是在調(diao)用 log 方法之前就執行的,即使這條日志最終不被(bei)輸出,字符(fu)串拼接操(cao)作還是會執行,白(bai)白(bai)浪(lang)費(fei)性能。

你點點頭:確實,而(er)且參數化(hua)日志(zhi)比(bi)字符串拼接看起(qi)來舒服~

魚(yu)皮(pi):沒錯(cuo)。而(er)且當你要輸(shu)出異常(chang)信息時,也(ye)可(ke)以(yi)使用參數化日志:
try {
// 業務邏輯
} catch (Exception e) {
log.error("用戶 {} 導入失敗", username, e); // 注意這個 e
}
這樣日志(zhi)框架會同時記錄上下文信(xin)(xin)息和完整(zheng)的異常(chang)堆棧信(xin)(xin)息,便于(yu)排查問題。

你抱拳:學會了,我這就去打日志!
3、把控時(shi)機和(he)內容
很快,你給批量導入(ru)程序(xu)的(de)代碼加上了日志:
光做這點還不夠(gou),你還翻(fan)出了之前(qian)的屎山代碼,想給每個文件都(dou)打打日志。

但打著打著,你就(jiu)不(bu)耐煩了:每段代碼都要打日志,好累啊!但是不(bu)打日志又怕出問題(ti),怎么辦才好?
魚皮(pi)笑道:好問題,這就(jiu)是我要教你的第三招 —— 把握打日志的時機(ji)。
對于重要的業務功能,我建議采用防御性編程,先多多打日志。比如在方(fang)法(fa)代碼的(de)入口(kou)和出(chu)口(kou)記錄參數和返回值、在每個關鍵(jian)步(bu)驟記錄執(zhi)行狀態,而(er)不是等(deng)出(chu)了問題無法(fa)排查(cha)的(de)時(shi)候才追(zhui)悔莫及。之后可以再(zai)慢(man)慢(man)移(yi)除掉不需(xu)要(yao)的(de)日志(zhi)。

你(ni)嘆了(le)口氣:這我(wo)(wo)知道,但每個方法都打日志,工(gong)作量太大,都影響我(wo)(wo)摸魚了(le)!
魚皮(pi):別擔心,你可以利(li)用 AOP 切(qie)面編(bian)程,自動給(gei)每個業(ye)務(wu)方法的(de)執行前后添(tian)加(jia)日志,這樣就不會錯過任何一次調(diao)用信息(xi)了。

你(ni)雙眼放(fang)光:這(zhe)個好,爽爽爽!

魚皮:不過(guo)這樣(yang)做也(ye)有一個缺(que)點,注(zhu)意不要在日志(zhi)中記錄了敏(min)感信息,比如用戶(hu)密碼。萬一你的日志(zhi)不小(xiao)心泄露出去,就相當(dang)于泄露了大(da)量用戶(hu)的信息。

你拍拍胸脯(fu):必須的!
4、控制(zhi)日志(zhi)輸出(chu)量
一個星期后(hou),產(chan)品(pin)經(jing)理又來(lai)找你了:小阿巴,你的批(pi)量導入功(gong)能又報錯啦(la)!而且怎么感(gan)覺程序變慢了?
你(ni)完全不慌,淡定地(di)打(da)開服(fu)務器(qi)的日(ri)志文件。結果瞬(shun)間呆住了……
好家伙(huo),滿(man)屏都(dou)是(shi)密密麻麻的日志(zhi),這可怎(zen)么看啊?!

魚(yu)皮(pi)看了看你(ni)的代碼,搖(yao)了搖(yao)頭:你(ni)現(xian)在(zai)每導入一(yi)條(tiao)數據(ju)都要打(da)一(yi)些日(ri)志(zhi),如果用戶導入 10 萬條(tiao)數據(ju),那就是幾十萬條(tiao)日(ri)志(zhi)!不僅刷屏,還會影(ying)響性能。
你(ni)有點委屈:不(bu)是(shi)你(ni)讓我(wo)多(duo)打日志的么?那我(wo)應該怎么辦?
魚(yu)皮:你需要(yao)控制(zhi)日(ri)志的輸出量。
1)可以添加(jia)條件來控制,比如每處理 100 條數據時才記錄一次:
if ((i + 1) % 100 == 0) {
log.info("批量導入進度:{}/{}", i + 1, userList.size());
}
2)或者在循環中利用(yong) StringBuilder 進行字(zi)符串拼接,循環結束(shu)后統一輸出:
StringBuilder logBuilder = new StringBuilder("處理結果:");
for (UserDTO userDTO : userList) {
processUser(userDTO);
logBuilder.append(String.format("成功[ID=%s], ", userDTO.getId()));
}
log.info(logBuilder.toString());
3)還可以通過修改日志配置文(wen)件,過濾掉特定級別的日志,防止(zhi)日志刷屏(ping):
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>logs/app.log</file>
<!-- 只允許 INFO 級別及以上的日志通過 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
5、統(tong)一(yi)日志(zhi)格式
你(ni)開(kai)心(xin)了:好耶,這樣就不會刷屏(ping)了!但是感覺有(you)時候日(ri)(ri)志很雜(za)很亂,尤其(qi)是我想看(kan)某一個(ge)請求(qiu)相關的日(ri)(ri)志時,總(zong)是被(bei)其(qi)他的日(ri)(ri)志干擾,怎么辦?
魚皮:好問題,可以(yi)在日(ri)志配(pei)置文件中定義(yi)統一的日(ri)志格(ge)式,包含時間戳、線程名稱、日(ri)志級別、類(lei)名、方法(fa)名、具體(ti)內容等(deng)關鍵(jian)信息。
<!-- 控制臺日志輸出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 日志格式 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
這(zhe)樣輸出的日志更整齊(qi)易(yi)讀:

此外,你還可以通過(guo) MDC(Mapped Diagnostic Context)給日志(zhi)添加額(e)外的上下文信(xin)息,比如(ru)請求 ID、用戶 ID 等(deng),方便(bian)追蹤。

在 Java 代碼中,可以為 MDC 設置屬(shu)性值:
然后在日志配置文件中就可以使(shi)用這些值了:
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<!-- 包含 MDC 信息 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] [%X{userId}] %msg%n</pattern>
</encoder>
</appender>
這(zhe)樣,每個(ge)請求、每個(ge)用戶的(de)操作一目了然。

6、使用(yong)異步日志
你又(you)開心(xin)了:這樣打(da)出(chu)來的日志(zhi),確實舒(shu)服,爽爽爽!但(dan)是(shi)(shi)我打(da)日志(zhi)越多,是(shi)(shi)不是(shi)(shi)程序就會(hui)更慢呢(ni)?有沒有辦法能(neng)優化一下?
魚皮:當然有,可以使用 異步日志。
正常情況下,你調用 log.info() 打日志(zhi)(zhi)(zhi)時,程(cheng)(cheng)序會(hui)立刻把日志(zhi)(zhi)(zhi)寫入文件,這個過程(cheng)(cheng)是同步的(de),會(hui)阻塞當前(qian)線(xian)程(cheng)(cheng)。而異步日志(zhi)(zhi)(zhi)會(hui)把寫日志(zhi)(zhi)(zhi)的(de)操作放到另一個線(xian)程(cheng)(cheng)里(li)去做(zuo),不會(hui)阻塞主線(xian)程(cheng)(cheng),性(xing)能更好(hao)。
你眼睛一亮:這(zhe)么(me)厲害?怎么(me)開啟?
魚皮:很簡單,只需(xu)要修改(gai)一(yi)下(xia)配置文件:
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>512</queueSize> <!-- 隊列大小 -->
<discardingThreshold>0</discardingThreshold> <!-- 丟棄閾值,0 表示不丟棄 -->
<neverBlock>false</neverBlock> <!-- 隊列滿時是否阻塞,false 表示會阻塞 -->
<appender-ref ref="FILE" /> <!-- 引用實際的日志輸出目標 -->
</appender>
<root level="INFO">
<appender-ref ref="ASYNC" />
</root>
不過異步日(ri)志(zhi)也有缺點(dian),如(ru)果程序(xu)突然崩潰,緩沖區中(zhong)還沒來得及寫入(ru)文件(jian)的日(ri)志(zhi)可(ke)能會丟失。

所以要權衡一下,看(kan)你的(de)系統更注重性能還是日志的(de)完整性。
你想了想:我們的程序對性能要求比(bi)較(jiao)高,偶爾丟幾條日志問題不大,那(nei)我就用異步日志吧。
7、日志管理
接下來的很長一段時間(jian),你混的很舒服,有 Bug 都能很快發現(xian)。
你甚至(zhi)覺得 Bug 太少、工作(zuo)沒什(shen)么激(ji)情,所以沒事(shi)兒就跟新來的實習生阿坤吹吹牛皮:你知道日(ri)志(zhi)么?我可會打它了(le)!

直到有一天,運維小哥(ge)突然(ran)跑過來:阿(a)巴阿(a)巴,服務(wu)器掛了!你快去(qu)看看!
你連忙(mang)登錄服務(wu)器,發現服務(wu)器的硬(ying)盤爆滿了,沒法寫入(ru)新數(shu)據。
你查了一下,發(fa)現日志(zhi)文件(jian)竟然占了 200GB 的空間!

你汗流(liu)浹(jia)背了(le),正(zheng)在考慮怎么甩鍋,結果阿(a)坤突然雞叫起(qi)來:阿(a)巴 giegie,你的日志文件是不是從來沒清理過(guo)?
你尷尬地(di)倒了個(ge)立,這樣(yang)眼淚(lei)就不會留下來(lai)。

魚皮嘆了口氣:這就是我要教你的下一招 —— 日志管理。
你(ni)好奇道:怎么管理?我每天登(deng)服務器刪掉一些歷史(shi)文件?
魚皮:人工操(cao)作也太(tai)麻(ma)煩了,我們可以通過(guo)修(xiu)改日志配置文件(jian),讓框(kuang)架(jia)幫忙管理日志。
首先(xian)設置日(ri)(ri)志的滾動(dong)策略,可以根據文(wen)件(jian)大小和日(ri)(ri)期,自動(dong)對日(ri)(ri)志文(wen)件(jian)進行切分。
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/app-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
這樣配置后,每天會創建一個新的日志文件(比如 app-2025-10-23.0.log),如果日志文件大小超過 10MB 就再創建一個(比如 app-2025-10-23.1.log),并且只保留最近 30 天的日志。

還(huan)可以(yi)開啟日志壓縮(suo)功能,進一步節(jie)省磁盤空間:
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- .gz 后綴會自動壓縮 -->
<fileNamePattern>logs/app-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
</rollingPolicy>

你有些激(ji)動:吼(hou)吼(hou),這樣我們(men)就可以按(an)照天數更(geng)快地查(cha)看日志(zhi),服(fu)務器(qi)硬(ying)盤也(ye)有救啦!
8、集(ji)成(cheng)日志收(shou)集(ji)系統
兩年后,你(ni)負責(ze)的項目已(yi)經發展成了一(yi)個(ge)大(da)型的分布式(shi)系統,有好幾十(shi)個(ge)微服務。
如今,每(mei)次排查問(wen)題你都(dou)要登(deng)錄到不同的服務器上(shang)查看日(ri)志,非常麻煩。而且有些請(qing)(qing)求的調用鏈路(lu)很長,你得登(deng)錄好幾(ji)(ji)臺服務器、看好幾(ji)(ji)個(ge)服務的日(ri)志,才能追(zhui)蹤到一(yi)個(ge)請(qing)(qing)求的完整調用過(guo)程。

你(ni)簡直要(yao)瘋了!
于是(shi)你找到魚皮求助(zhu):現(xian)在(zai)查日志(zhi)太麻煩(fan)了(le),當年你還(huan)有(you)一招(zhao)沒有(you)教我,現(xian)在(zai)是(shi)不是(shi)……
魚皮點(dian)點(dian)頭:嗯,對于分布式(shi)系統(tong),就必(bi)須要用(yong)專業的日志(zhi)收集系統(tong)了,比如很流行的 ELK。
你好奇:ELK 是啥?伊拉克(ke)?
阿坤搶(qiang)答道:我(wo)知道,就是 Elasticsearch + Logstash + Kibana 這套組合(he)。
簡單(dan)來說,Logstash 負責收集各個服務的日志,然后發送給 Elasticsearch 存(cun)儲和(he)索引,最后通過 Kibana 提供一個可視(shi)化(hua)的界面。

這樣一來,我們可以方便地集中搜索、查看、分(fen)析日志。

你驚訝了:原來日志還能這么(me)玩,以后我所有的項目都(dou)要(yao)用 ELK!
魚(yu)皮擺擺手:不過(guo) ELK 的搭(da)建和運(yun)維成本比較高,對于小(xiao)團(tuan)隊來說可能有點重,還是要按需(xu)采用啊。
結局
至此,你已經掌(zhang)握了(le)打日(ri)志的核(he)心(xin)秘法。

只是你(ni)很疑惑,為(wei)何那阿坤竟對日(ri)志系統如(ru)此(ci)熟悉?
阿坤苦笑(xiao)道:我(wo)本來就是日志管理大師,可惜我(wo)上(shang)家公司(si)的同(tong)事從來不打(da)日志,所以我(wo)把他們暴打(da)了一頓后跑路了。
阿巴 giegie 你要記住,日志不是寫給機器看的,是寫給未來的你和你的隊友看的!
你要是(shi)以后不打日志,我就(jiu)打你!

日志不是寫給機器看的,是寫給未來的你和你的隊友看的!