Back 【後端 Junior 生存事件簿】這樣記 log 才有效!

【後端 Junior 生存事件簿】這樣記 log 才有效!

#後端開發

工作對話
主管

昨晚 production 有個 bug,客戶說不能下單,你幫忙查一下 log。

後端小明

好的,我馬上看!

打開 log,畫面長這樣:


production.log
time=2026-05-02T12:01:00Z level=DEBUG msg="cache miss" key="user:1:profile"
time=2026-05-02T12:01:00Z level=DEBUG msg="DB query" sql="SELECT * FROM users WHERE id=?" duration=10ms
time=2026-05-02T12:01:00Z level=INFO msg="GET /api/users/1" status=200 duration=15ms
time=2026-05-02T12:01:03Z level=INFO msg="Received POST /api/orders" user_id=5
time=2026-05-02T12:01:03Z level=ERROR msg="Invalid email format" email="notanemail" status=400
time=2026-05-02T12:01:04Z level=ERROR msg="User not found" user_id=99 status=404
time=2026-05-02T12:01:05Z level=DEBUG msg="cache miss" key="user:2:profile"
time=2026-05-02T12:01:05Z level=DEBUG msg="DB query" sql="SELECT * FROM users WHERE id=?" duration=8ms
time=2026-05-02T12:01:05Z level=WARN msg="DB query slow" sql="SELECT * FROM orders" duration=520ms
time=2026-05-02T12:01:05Z level=INFO msg="GET /api/users/2" status=200 duration=12ms
time=2026-05-02T12:01:07Z level=INFO msg="POST /api/orders" status=201 duration=230ms user_id=5
time=2026-05-02T12:01:08Z level=INFO msg="User alice logged in"
time=2026-05-02T12:01:08Z level=INFO msg="Fetching profile for alice"
time=2026-05-02T12:01:09Z level=ERROR msg="Unauthorized" reason="token expired" status=401
time=2026-05-02T12:01:09Z level=ERROR msg="File not found"
time=2026-05-02T12:01:10Z level=DEBUG msg="DB query" sql="SELECT * FROM products WHERE id=?" duration=7ms
time=2026-05-02T12:01:11Z level=ERROR msg="DB connection timeout" status=500 duration=3010ms

眼花了嗎?這份 log 犯了 4 個常見的錯誤,我們一個一個來解。

問題一:Production 環境有太多雜訊

第一眼最明顯的問題:一堆 DEBUG

DEBUG level 是開發階段用來看除錯的,出現在 production 環境會讓資訊變得太雜落,而且會佔用額外的儲存空間。

修法很簡單:production 環境的 log level 設為 INFO

production.log(移除 DEBUG 後)
time=2026-05-02T12:01:00Z level=INFO msg="GET /api/users/1" status=200 duration=15ms
time=2026-05-02T12:01:03Z level=INFO msg="Received POST /api/orders" user_id=5
time=2026-05-02T12:01:03Z level=ERROR msg="Invalid email format" email="notanemail" status=400
time=2026-05-02T12:01:04Z level=ERROR msg="User not found" user_id=99 status=404
time=2026-05-02T12:01:05Z level=WARN msg="DB query slow" sql="SELECT * FROM orders" duration=520ms
time=2026-05-02T12:01:05Z level=INFO msg="GET /api/users/2" status=200 duration=12ms
time=2026-05-02T12:01:07Z level=INFO msg="POST /api/orders" status=201 duration=230ms user_id=5
time=2026-05-02T12:01:08Z level=INFO msg="User alice logged in"
time=2026-05-02T12:01:08Z level=INFO msg="Fetching profile for alice"
time=2026-05-02T12:01:09Z level=ERROR msg="Unauthorized" reason="token expired" status=401
time=2026-05-02T12:01:09Z level=ERROR msg="File not found"
time=2026-05-02T12:01:11Z level=ERROR msg="DB connection timeout" status=500 duration=3010ms

調整完後行數總算有減少一下了,但問題還沒完。

問題二:重複的 Log

注意看這兩行:

time=2026-05-02T12:01:03Z level=INFO msg="Received POST /api/orders" user_id=5
time=2026-05-02T12:01:07Z level=INFO msg="POST /api/orders" status=201 duration=230ms user_id=5

同一個 request,記了兩次,一條可能是 handler 開始時記的,一條是結束時記的,可能是在 middleware 跟 handler 各記了一遍。

重複的 log 沒有帶來額外資訊,只會讓 log 量加倍,建議把重複的壓縮成一條有足夠資訊量的 log 。

production.log(移除重複 log 後)
time=2026-05-02T12:01:00Z level=INFO msg="GET /api/users/1" status=200 duration=15ms
time=2026-05-02T12:01:03Z level=ERROR msg="Invalid email format" email="notanemail" status=400
time=2026-05-02T12:01:04Z level=ERROR msg="User not found" user_id=99 status=404
time=2026-05-02T12:01:05Z level=WARN msg="DB query slow" sql="SELECT * FROM orders" duration=520ms
time=2026-05-02T12:01:05Z level=INFO msg="GET /api/users/2" status=200 duration=12ms
time=2026-05-02T12:01:07Z level=INFO msg="POST /api/orders" status=201 duration=230ms user_id=5
time=2026-05-02T12:01:08Z level=INFO msg="User alice logged in"
time=2026-05-02T12:01:08Z level=INFO msg="Fetching profile for alice"
time=2026-05-02T12:01:09Z level=ERROR msg="Unauthorized" reason="token expired" status=401
time=2026-05-02T12:01:09Z level=ERROR msg="File not found"
time=2026-05-02T12:01:11Z level=ERROR msg="DB connection timeout" status=500 duration=3010ms

問題三:INFO、WARN、ERROR 使用時機

去除掉重複的 Log ,接著我們來看最關鍵的 ERROR 部分,卻發現 Invalid email format、User not found、Unauthorized 都被標示成 ERROR

這是新手很常犯的錯誤,把「使用者錯誤」標示成 ERROR ,但實際上它們不應該被視為錯誤,而應該被記為 INFO 並回 4xx 狀態碼。

Note
WARN 介於 INFOERROR 之間的灰色地帶,幾乎不太需要用到它,如果不是錯誤,就記為 INFO 或不記錄。
production.log(修正 log level 後)
time=2026-05-02T12:01:00Z level=INFO msg="GET /api/users/1" status=200 duration=15ms
time=2026-05-02T12:01:03Z level=INFO msg="Invalid email format" email="notanemail" status=400
time=2026-05-02T12:01:04Z level=INFO msg="User not found" user_id=99 status=404
time=2026-05-02T12:01:05Z level=INFO msg="GET /api/users/2" status=200 duration=12ms
time=2026-05-02T12:01:07Z level=INFO msg="POST /api/orders" status=201 duration=230ms user_id=5
time=2026-05-02T12:01:08Z level=INFO msg="User alice logged in"
time=2026-05-02T12:01:08Z level=INFO msg="Fetching profile for alice"
time=2026-05-02T12:01:09Z level=INFO msg="Unauthorized" reason="token expired" status=401
time=2026-05-02T12:01:09Z level=ERROR msg="File not found"
time=2026-05-02T12:01:11Z level=ERROR msg="DB connection timeout" status=500 duration=3010ms

整份 log 只剩兩個 ERROR,而且它們都是真的錯誤。

問題四:同一個事件不要拆成多條 Log

最後一個問題,也是最容易被忽略的。

time=2026-05-02T12:01:08Z level=INFO msg="User alice logged in"
time=2026-05-02T12:01:08Z level=INFO msg="Fetching profile for alice"
time=2026-05-02T12:01:09Z level=ERROR msg="File not found"

這三條是同一個操作:alice 登入後,系統去撈她的 profile,然後 File not found。在低流量的情況下,它們剛好排在一起,看起來還好。

但 production 同時有幾十個 request 在跑,實際的 log 可能長這樣:

time=2026-05-02T12:01:08Z level=INFO msg="User alice logged in"
time=2026-05-02T12:01:08Z level=INFO msg="GET /api/users/1" status=200 duration=15ms
time=2026-05-02T12:01:08Z level=INFO msg="Fetching profile for alice"
time=2026-05-02T12:01:08Z level=INFO msg="POST /api/orders" status=201 duration=230ms user_id=5
time=2026-05-02T12:01:09Z level=INFO msg="User bob logged in"
time=2026-05-02T12:01:09Z level=INFO msg="Fetching profile for bob"
time=2026-05-02T12:01:09Z level=ERROR msg="File not found"

現在這個 File not found 是 alice 的還是 bob 的?完全不知道。

解法是把同一事件的資訊壓成一條 log,把 context 都放在 fields 裡:

time=2026-05-02T12:01:09Z level=ERROR msg="File not found" user="alice" action="fetch_profile" filename="alice_profile.json"

一條 log 包含完整的 context,不管旁邊插了多少其他 request,都能獨立判讀。

總結

  • Production 只留 INFO 以上,DEBUG 不要帶進 production
  • 同一 request 只記一次,避免重複 log 讓量加倍
  • 4xx 是使用者錯誤,記 INFO;ERROR 留給系統真正壞掉的情況
  • 一個事件一條 log,把 context 放在 fields 裡,concurrent request 也能獨立追查
Share