昨晚 production 有個 bug,客戶說不能下單,你幫忙查一下 log。
好的,我馬上看!
打開 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=10mstime=2026-05-02T12:01:00Z level=INFO msg="GET /api/users/1" status=200 duration=15mstime=2026-05-02T12:01:03Z level=INFO msg="Received POST /api/orders" user_id=5time=2026-05-02T12:01:03Z level=ERROR msg="Invalid email format" email="notanemail" status=400time=2026-05-02T12:01:04Z level=ERROR msg="User not found" user_id=99 status=404time=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=8mstime=2026-05-02T12:01:05Z level=WARN msg="DB query slow" sql="SELECT * FROM orders" duration=520mstime=2026-05-02T12:01:05Z level=INFO msg="GET /api/users/2" status=200 duration=12mstime=2026-05-02T12:01:07Z level=INFO msg="POST /api/orders" status=201 duration=230ms user_id=5time=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=401time=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=7mstime=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。
time=2026-05-02T12:01:00Z level=INFO msg="GET /api/users/1" status=200 duration=15mstime=2026-05-02T12:01:03Z level=INFO msg="Received POST /api/orders" user_id=5time=2026-05-02T12:01:03Z level=ERROR msg="Invalid email format" email="notanemail" status=400time=2026-05-02T12:01:04Z level=ERROR msg="User not found" user_id=99 status=404time=2026-05-02T12:01:05Z level=WARN msg="DB query slow" sql="SELECT * FROM orders" duration=520mstime=2026-05-02T12:01:05Z level=INFO msg="GET /api/users/2" status=200 duration=12mstime=2026-05-02T12:01:07Z level=INFO msg="POST /api/orders" status=201 duration=230ms user_id=5time=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=401time=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=5time=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 。
time=2026-05-02T12:01:00Z level=INFO msg="GET /api/users/1" status=200 duration=15mstime=2026-05-02T12:01:03Z level=ERROR msg="Invalid email format" email="notanemail" status=400time=2026-05-02T12:01:04Z level=ERROR msg="User not found" user_id=99 status=404time=2026-05-02T12:01:05Z level=WARN msg="DB query slow" sql="SELECT * FROM orders" duration=520mstime=2026-05-02T12:01:05Z level=INFO msg="GET /api/users/2" status=200 duration=12mstime=2026-05-02T12:01:07Z level=INFO msg="POST /api/orders" status=201 duration=230ms user_id=5time=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=401time=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 狀態碼。
WARN 介於 INFO 和 ERROR 之間的灰色地帶,幾乎不太需要用到它,如果不是錯誤,就記為 INFO 或不記錄。time=2026-05-02T12:01:00Z level=INFO msg="GET /api/users/1" status=200 duration=15mstime=2026-05-02T12:01:03Z level=INFO msg="Invalid email format" email="notanemail" status=400time=2026-05-02T12:01:04Z level=INFO msg="User not found" user_id=99 status=404time=2026-05-02T12:01:05Z level=INFO msg="GET /api/users/2" status=200 duration=12mstime=2026-05-02T12:01:07Z level=INFO msg="POST /api/orders" status=201 duration=230ms user_id=5time=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=401time=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=15mstime=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=5time=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 也能獨立追查
