Stories

Detail Return Return

我發現很多程序員都不會打日誌。。。 - Stories Detail

你是小阿巴,剛入職的低級程序員,正在開發一個批量導入數據的程序。

沒想到,程序剛上線,產品經理就跑過來説:小阿巴,用户反饋你的程序有 Bug,剛導入沒多久就報錯中斷了!

你趕緊打開服務器,看着比你髮量都少的報錯信息:

你一臉懵逼:只有這點兒信息,我咋知道哪裏出了問題啊?!

你只能硬着頭皮讓產品經理找用户要數據,然後一條條測試,看看是哪條數據出了問題……

原本大好的摸魚時光,就這樣無了。

這時,你的導師魚皮走了過來,問道:小阿巴,你是持矢了麼?臉色這麼難看?

你無奈地説:皮哥,剛才線上出了個 bug,我花了 8 個小時才定位到問題……

魚皮皺了皺眉:這麼久?你沒打日誌嗎?

你很是疑惑:誰是日誌?為什麼要打它?

魚皮嘆了口氣:唉,難怪你要花這麼久…… 來,我教你打日誌!

⭐️ 本文對應視頻版:https://bilibili.com/video/BV1K71yBUEDv

 

什麼是日誌?

魚皮打開電腦,給你看了一段代碼:

@Slf4j
public class UserService {
   public void batchImport(List<UserDTOuserList) {
       log.info("開始批量導入用户,總數:{}", userList.size());
       
       int successCount 0;
       int failCount 0;
       
       for (UserDTO userDTO : userList) {
           try {
               log.info("正在導入用户:{}", userDTO.getUsername());
               validateUser(userDTO);
               saveUser(userDTO);
               successCount++;
               log.info("用户 {} 導入成功", userDTO.getUsername());
          } catch (Exception e) {
               failCount++;
               log.error("用户 {} 導入失敗,原因:{}", userDTO.getUsername(), e.getMessage(), e);
          }
      }
       
       log.info("批量導入完成,成功:{},失敗:{}", successCount, failCount);
  }
}

你看着代碼裏的 log.infolog.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 註解,可以自動為當前類生成日誌對象,不用手動定義啦。

@Slf4j
public class MyService {
   public void doSomething() {
       log.info("執行了一些操作");
  }
}

上面的代碼等同於 “自動為當前類生成日誌對象”:

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、把控時機和內容

很快,你給批量導入程序的代碼加上了日誌:

@Slf4j
public class UserService {
   public BatchImportResult batchImport(List<UserDTOuserList) {
       log.info("開始批量導入用户,總數:{}", userList.size());
       int successCount 0;
       int failCount 0;
       for (UserDTO userDTO : userList) {
           try {
               log.info("正在導入用户:{}", userDTO.getUsername());   
               // 校驗用户名
               if (StringUtils.isBlank(userDTO.getUsername())) {
                   throw new BusinessException("用户名不能為空");
              }
               // 保存用户
               saveUser(userDTO);
               successCount++;
               log.info("用户 {} 導入成功", userDTO.getUsername());
          } catch (Exception e) {
               failCount++;
               log.error("用户 {} 導入失敗,原因:{}", userDTO.getUsername(), e.getMessage(), e);
          }
      }
       log.info("批量導入完成,成功:{},失敗:{}", successCount, failCount);
       return new BatchImportResult(successCount, failCount);
  }
}

 

光做這點還不夠,你還翻出了之前的屎山代碼,想給每個文件都打打日誌。

 

但打着打着,你就不耐煩了:每段代碼都要打日誌,好累啊!但是不打日誌又怕出問題,怎麼辦才好?

魚皮笑道:好問題,這就是我要教你的第三招 —— 把握打日誌的時機。

對於重要的業務功能,我建議採用防禦性編程,先多多打日誌。比如在方法代碼的入口和出口記錄參數和返回值、在每個關鍵步驟記錄執行狀態,而不是等出了問題無法排查的時候才追悔莫及。之後可以再慢慢移除掉不需要的日誌。

 

你嘆了口氣:這我知道,但每個方法都打日誌,工作量太大,都影響我摸魚了!

魚皮:別擔心,你可以利用 AOP 切面編程,自動給每個業務方法的執行前後添加日誌,這樣就不會錯過任何一次調用信息了。

 

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

 

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

你拍拍胸脯:必須的!

 

4、控制日誌輸出量

一個星期後,產品經理又來找你了:小阿巴,你的批量導入功能又報錯啦!而且怎麼感覺程序變慢了?

你完全不慌,淡定地打開服務器的日誌文件。結果瞬間呆住了……

好傢伙,滿屏都是密密麻麻的日誌,這可怎麼看啊?!

魚皮看了看你的代碼,搖了搖頭:你現在每導入一條數據都要打一些日誌,如果用户導入 10 萬條數據,那就是幾十萬條日誌!不僅刷屏,還會影響性能。

你有點委屈:不是你讓我多打日誌的麼?那我應該怎麼辦?

魚皮:你需要控制日誌的輸出量。

1)可以添加條件來控制,比如每處理 100 條數據時才記錄一次:

if ((1) 100 == 0) {
   log.info("批量導入進度:{}/{}", 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 設置屬性值:

@PostMapping("/user/import")
public Result importUsers(@RequestBody UserImportRequest request) {
   // 1. 設置 MDC 上下文信息
   MDC.put("requestId", generateRequestId());
   MDC.put("userId", String.valueOf(request.getUserId()));
   try {
       log.info("用户請求處理完成");      
       // 執行具體業務邏輯
       userService.batchImport(request.getUserList());     
       return Result.success();
  } finally {
       // 2. 及時清理MDC(重要!)
       MDC.clear();
  }
}

然後在日誌配置文件中就可以使用這些值了:

<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 你要記住,日誌不是寫給機器看的,是寫給未來的你和你的隊友看的!

你要是以後不打日誌,我就打你!

 

更多編程學習資源

  • Java前端程序員必做項目實戰教程+畢設網站

  • 程序員免費編程學習交流社區(自學必備)

  • 程序員保姆級求職寫簡歷指南(找工作必備)

  • 程序員免費面試刷題網站工具(找工作必備)

  • 最新Java零基礎入門學習路線 + Java教程

  • 最新Python零基礎入門學習路線 + Python教程

  • 最新前端零基礎入門學習路線 + 前端教程

  • 最新數據結構和算法零基礎入門學習路線 + 算法教程

  • 最新C++零基礎入門學習路線、C++教程

  • 最新數據庫零基礎入門學習路線 + 數據庫教程

  • 最新Redis零基礎入門學習路線 + Redis教程

  • 最新計算機基礎入門學習路線 + 計算機基礎教程

  • 最新小程序入門學習路線 + 小程序開發教程

  • 最新SQL零基礎入門學習路線 + SQL教程

  • 最新Linux零基礎入門學習路線 + Linux教程

  • 最新Git/GitHub零基礎入門學習路線 + Git教程

  • 最新操作系統零基礎入門學習路線 + 操作系統教程

  • 最新計算機網絡零基礎入門學習路線 + 計算機網絡教程

  • 最新設計模式零基礎入門學習路線 + 設計模式教程

  • 最新軟件工程零基礎入門學習路線 + 軟件工程教程

Add a new Comments

Some HTML is okay.