你是小阿巴,剛入職的低級程序員,正在開發一個批量導入數據的程序。
沒想到,程序剛上線,產品經理就跑過來説:小阿巴,用户反饋你的程序有 Bug,剛導入沒多久就報錯中斷了!
你趕緊打開服務器,看着比你髮量都少的報錯信息:

你一臉懵逼:只有這點兒信息,我咋知道哪裏出了問題啊?!
你只能硬着頭皮讓產品經理找用户要數據,然後一條條測試,看看是哪條數據出了問題……
原本大好的摸魚時光,就這樣無了。
這時,你的導師魚皮走了過來,問道:小阿巴,你是持矢了麼?臉色這麼難看?

你無奈地説:皮哥,剛才線上出了個 bug,我花了 8 個小時才定位到問題……
魚皮皺了皺眉:這麼久?你沒打日誌嗎?
你很是疑惑:誰是日誌?為什麼要打它?

魚皮嘆了口氣:唉,難怪你要花這麼久…… 來,我教你打日誌!
⭐️ 本文對應視頻版:
什麼是日誌?
魚皮打開電腦,給你看了一段代碼:
你看着代碼裏的 log.info、log.error,疑惑地問:這些 log 是幹什麼的?
魚皮:這就是打日誌。日誌用來記錄程序運行時的狀態和信息,這樣當系統出現問題時,我們可以通過日誌快速定位問題。

你若有所思:哦?還可以這樣!如果當初我的代碼裏有這些日誌,一眼就定位到問題了…… 那我應該怎麼打日誌?用什麼技術呢?
怎麼打日誌?
魚皮:每種編程語言都有很多日誌框架和工具庫,比如 Java 可以選用 Log4j 2、Logback 等等。咱們公司用的是 Spring Boot,它默認集成了 Logback 日誌框架,你直接用就行,不用再引入額外的庫了~

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

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

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

而且它只能輸出信息到控制枱,不能靈活控制輸出位置、輸出格式、輸出時機等等。比如你現在想看三天前的日誌,System.out.println 的輸出早就被刷沒了,你還得浪費時間找半天。

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

在下苦心研究日誌多年,沉澱了打日誌的 8 大邪修秘法,先傳授你 2 招最基礎的吧。
打日誌的 8 大最佳實踐
1、合理選擇日誌級別
第一招,日誌分級。
你好奇道:日誌還有級別?蘋果日誌、安卓日誌?
魚皮給了你一巴掌:可不要亂説,日誌的級別是按照重要程度進行劃分的。

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

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

你點點頭:俺明白了,不同的場景用不同的級別!
2、正確記錄日誌信息
魚皮:沒錯,下面教你第二招。你注意到我剛才寫的日誌裏有一對大括號 {} 嗎?
log.info("用户 {} 開始導入", username);
你回憶了一下:對哦,那是啥啊?
魚皮:這叫參數化日誌。{} 是一個佔位符,日誌框架會在運行時自動把後面的參數值替換進去。
你撓了撓頭:我直接用字符串拼接不行嗎?
log.info("用户 " + username + " 開始導入");
魚皮搖搖頭:不推薦。因為字符串拼接是在調用 log 方法之前就執行的,即使這條日誌最終不被輸出,字符串拼接操作還是會執行,白白浪費性能。

你點點頭:確實,而且參數化日誌比字符串拼接看起來舒服~

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

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

但打着打着,你就不耐煩了:每段代碼都要打日誌,好累啊!但是不打日誌又怕出問題,怎麼辦才好?
魚皮笑道:好問題,這就是我要教你的第三招 —— 把握打日誌的時機。
對於重要的業務功能,我建議採用防禦性編程,先多多打日誌。比如在方法代碼的入口和出口記錄參數和返回值、在每個關鍵步驟記錄執行狀態,而不是等出了問題無法排查的時候才追悔莫及。之後可以再慢慢移除掉不需要的日誌。

你嘆了口氣:這我知道,但每個方法都打日誌,工作量太大,都影響我摸魚了!
魚皮:別擔心,你可以利用 AOP 切面編程,自動給每個業務方法的執行前後添加日誌,這樣就不會錯過任何一次調用信息了。

你雙眼放光:這個好,爽爽爽!

魚皮:不過這樣做也有一個缺點,注意不要在日誌中記錄了敏感信息,比如用户密碼。萬一你的日誌不小心泄露出去,就相當於泄露了大量用户的信息。

你拍拍胸脯:必須的!
4、控制日誌輸出量
一個星期後,產品經理又來找你了:小阿巴,你的批量導入功能又報錯啦!而且怎麼感覺程序變慢了?
你完全不慌,淡定地打開服務器的日誌文件。結果瞬間呆住了……
好傢伙,滿屏都是密密麻麻的日誌,這可怎麼看啊?!

魚皮看了看你的代碼,搖了搖頭:你現在每導入一條數據都要打一些日誌,如果用户導入 10 萬條數據,那就是幾十萬條日誌!不僅刷屏,還會影響性能。
你有點委屈:不是你讓我多打日誌的麼?那我應該怎麼辦?
魚皮:你需要控制日誌的輸出量。
1)可以添加條件來控制,比如每處理 100 條數據時才記錄一次:
if ((i + 1) % 100 == 0) {
log.info("批量導入進度:{}/{}", i + 1, userList.size());
}
2)或者在循環中利用 StringBuilder 進行字符串拼接,循環結束後統一輸出:
StringBuilder logBuilder = new StringBuilder("處理結果:");
for (UserDTO userDTO : userList) {
processUser(userDTO);
logBuilder.append(String.format("成功[ID=%s], ", userDTO.getId()));
}
log.info(logBuilder.toString());
3)還可以通過修改日誌配置文件,過濾掉特定級別的日誌,防止日誌刷屏:
<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、統一日誌格式
你開心了:好耶,這樣就不會刷屏了!但是感覺有時候日誌很雜很亂,尤其是我想看某一個請求相關的日誌時,總是被其他的日誌干擾,怎麼辦?
魚皮:好問題,可以在日誌配置文件中定義統一的日誌格式,包含時間戳、線程名稱、日誌級別、類名、方法名、具體內容等關鍵信息。
<!-- 控制枱日誌輸出 -->
<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>
這樣輸出的日誌更整齊易讀:

此外,你還可以通過 MDC(Mapped Diagnostic Context)給日誌添加額外的上下文信息,比如請求 ID、用户 ID 等,方便追蹤。

在 Java 代碼中,可以為 MDC 設置屬性值:
然後在日誌配置文件中就可以使用這些值了:
<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>
這樣,每個請求、每個用户的操作一目瞭然。

6、使用異步日誌
你又開心了:這樣打出來的日誌,確實舒服,爽爽爽!但是我打日誌越多,是不是程序就會更慢呢?有沒有辦法能優化一下?
魚皮:當然有,可以使用 異步日誌。
正常情況下,你調用 log.info() 打日誌時,程序會立刻把日誌寫入文件,這個過程是同步的,會阻塞當前線程。而異步日誌會把寫日誌的操作放到另一個線程裏去做,不會阻塞主線程,性能更好。
你眼睛一亮:這麼厲害?怎麼開啓?
魚皮:很簡單,只需要修改一下配置文件:
<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>
不過異步日誌也有缺點,如果程序突然崩潰,緩衝區中還沒來得及寫入文件的日誌可能會丟失。

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

直到有一天,運維小哥突然跑過來:阿巴阿巴,服務器掛了!你快去看看!
你連忙登錄服務器,發現服務器的硬盤爆滿了,沒法寫入新數據。
你查了一下,發現日誌文件竟然佔了 200GB 的空間!

你汗流浹背了,正在考慮怎麼甩鍋,結果阿坤突然雞叫起來:阿巴 giegie,你的日誌文件是不是從來沒清理過?
你尷尬地倒了個立,這樣眼淚就不會留下來。

魚皮嘆了口氣:這就是我要教你的下一招 —— 日誌管理。
你好奇道:怎麼管理?我每天登服務器刪掉一些歷史文件?
魚皮:人工操作也太麻煩了,我們可以通過修改日誌配置文件,讓框架幫忙管理日誌。
首先設置日誌的滾動策略,可以根據文件大小和日期,自動對日誌文件進行切分。
<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 天的日誌。

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

你有些激動:吼吼,這樣我們就可以按照天數更快地查看日誌,服務器硬盤也有救啦!
8、集成日誌收集系統
兩年後,你負責的項目已經發展成了一個大型的分佈式系統,有好幾十個微服務。
如今,每次排查問題你都要登錄到不同的服務器上查看日誌,非常麻煩。而且有些請求的調用鏈路很長,你得登錄好幾台服務器、看好幾個服務的日誌,才能追蹤到一個請求的完整調用過程。

你簡直要瘋了!
於是你找到魚皮求助:現在查日誌太麻煩了,當年你還有一招沒有教我,現在是不是……
魚皮點點頭:嗯,對於分佈式系統,就必須要用專業的日誌收集系統了,比如很流行的 ELK。
你好奇:ELK 是啥?伊拉克?
阿坤搶答道:我知道,就是 Elasticsearch + Logstash + Kibana 這套組合。
簡單來説,Logstash 負責收集各個服務的日誌,然後發送給 Elasticsearch 存儲和索引,最後通過 Kibana 提供一個可視化的界面。

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

你驚訝了:原來日誌還能這麼玩,以後我所有的項目都要用 ELK!
魚皮擺擺手:不過 ELK 的搭建和運維成本比較高,對於小團隊來説可能有點重,還是要按需採用啊。
結局
至此,你已經掌握了打日誌的核心秘法。

只是你很疑惑,為何那阿坤竟對日誌系統如此熟悉?
阿坤苦笑道:我本來就是日誌管理大師,可惜我上家公司的同事從來不打日誌,所以我把他們暴打了一頓後跑路了。
阿巴 giegie 你要記住,日誌不是寫給機器看的,是寫給未來的你和你的隊友看的!
你要是以後不打日誌,我就打你!