原:http://www.oschina.net/question/12_44624
每次想找都忘了地址,在此轉一下。
本文解釋應用程序中的錯誤和調試日誌的最佳實踐。應用日誌是一個非常重要的“工具”用來幫你查明具問題。如果有一個基礎架構用來記錄應用中的日誌和事件,那麼調試和問題的解決就會變得很簡單。
在開始文章之前,我們要介紹兩個相關的工具:
不同的日誌含義
日誌 Logging 可以代表不同的含義,有着不同的目的,在這篇文章中主要指應用程序的日誌。
應用日誌,或者也叫做調試信息和錯誤日誌,主要的目的是爲了跟蹤應用程序的底層行爲,跟蹤應用內部所執行的過程。還有另外一些日誌,例如是 Google 的 Analytics 是用來統計網站的訪問量等信息,這二者的目的是決然不同的。
爲什麼要做日誌
審計跟蹤
但錯誤發生時,你需要清除知道該如何處理,通過對日誌跟蹤,你可以獲取該錯誤發生的具體環境,你需要確切知道什麼是什麼引起該錯誤,什麼對該錯誤不會造成影響。
跟蹤應用的警告和錯誤
爲了識別錯誤,我們將日誌分爲警告和錯誤信息,這些都是可以跟蹤並予以解決的。例如在 PHP 中 deprecated、strict 和 notices 類別的信息是需要引起注意的。
跟蹤崩潰bug
在開發過程中,日誌可以幫助開發者和軟件測試人員跟蹤程序崩潰的原因。
跟蹤性能下降的問題範圍
產品所反映出來的性能問題,很難在開發過程中暴露出來,這需要進行全方位的測試跟蹤,而通過日誌提供的詳細執行時間記錄可以很方便的找出應用的性能瓶頸。
一些日誌實例
日誌的內容和格式並沒有人做嚴格的規定,但下面是我們給出的一些建議以供參考:
- 日誌發生的日期和時間,包含時區信息和毫秒,建議使用 ISO-8601 時間格式
- 日誌級別,例如 debug、info 和 error
- 相應的會話標識,能知道是哪個客戶端或者是其他請求所產生的日誌,例如在 Web 日誌中 IP 地址就可以用來標識一個客戶端
- 一個有用的日誌內容
下面是一個好日誌的例子
1 |
[2012-03-02T20:20:49.003+01:00][43gg84][info]
Bootstrapping application ( v 2.1b) |
這條日誌中包含:
- 日期、時間,提供毫秒信息和時區
- 會話標識 43gg84,不同的客戶端會有不同的值
- 日誌級別,在這裏是 info
- 日誌詳情:Bootstrapping application
下面是一個不好的日誌例子,光知道錯誤,不知道發生什麼錯誤:
1 |
[Thu,
01 Jan 1970 00:00:00 +0100] Error: "" |
(這是我在實際中遇見過的日誌,真實的問題是 Beanstalkd 服務器沒啓動導致的,但這個錯誤信息中完全沒有提及。)
一旦發生錯誤,使用堆棧的跟蹤是非常有用的,當然這也是一個巨大的開銷,因此要謹慎使用,到處拋出異常並不是一個好主意。
一個成功啓動過程的日誌信息
讓我們繼續看一些好的例子,下面日誌包含了應用程序啓動完整過程:
1 |
[2012-03-02T20:20:49.003+02:00][43gg84][info]
Bootstrapping application ( v 2.1b) |
2 |
[2012-03-02T20:20:49.013+02:00][43gg84][info]
Request cycle startup |
3 |
[2012-03-02T20:20:49.093+02:00][43gg84][info]
User session found, user ID: 13414564 |
4 |
[2012-03-02T20:20:49.123+02:00][43gg84][info]
Requested URI '/fu/bar/index' |
5 |
[2012-03-02T20:20:49.133+02:00][43gg84][info]
Requested forwarded to URI '/fu/bar/overview' |
6 |
[2012-03-02T20:20:49.273+02:00][43gg84][info]
Sending HTTP headers |
7 |
[2012-03-02T20:20:49.283+02:00][43gg84][info]
Sending HTTP Body |
8 |
[2012-03-02T20:20:49.313+02:00][43gg84][info]
Request cycle shutdown |
9 |
[2012-03-02T20:20:49.323+02:00][43gg84][info]
Request cycle completed in 0.320
seconds. |
這個日誌自我解釋的非常清楚,你能知道當前應用的版本,URI 處理等詳細信息。
一個包含堆棧信息的日誌
下面例子展示瞭如何輸出日誌堆棧,你可以看到堆棧信息的級別和錯誤信息是不同的,當然這不是固定的,有時候發生錯誤的時候,你需要記錄詳細的錯誤堆棧信息以便跟蹤錯誤發生的具體位置:
01 |
[2012-03-02T20:20:49.003+02:00][43gg84][info]
Bootstrapping application ( v 2.1b) |
02 |
[2012-03-02T20:20:49.013+02:00][43gg84][info]
Request cycle startup |
03 |
[2012-03-02T20:20:49.123+02:00][43gg84][info]
Requested URI '/fu/bar/index' |
04 |
[2012-03-02T20:20:49.273+02:00][43gg84][info]
Sending HTTP headers |
05 |
[2012-03-02T20:20:49.283+02:00][43gg84][erro]
Cannot modify header information – headers already sent by X on line Y |
06 |
[2012-03-02T20:20:49.293+02:00][43gg84][dbug]
Stack trace: |
07 |
[2012-03-02T20:20:49.293+02:00][43gg84][dbug]
1. {main}() /tmp/fu/bar/httpdocs/index.php:0 |
08 |
[2012-03-02T20:20:49.293+02:00][43gg84][dbug]
2. Zend_Application->run() /tmp/fu/bar/httpdocs/index.php:31 |
09 |
[2012-03-02T20:20:49.293+02:00][43gg84][dbug]
3. Zend_Application_Bootstrap_Bootstrap->run() /tmp/fu/bar/library/Zend/Application.php:366 |
10 |
[2012-03-02T20:20:49.293+02:00][43gg84][dbug]
4. Zend_Controller_Front->dispatch() /tmp/fu/bar/library/Zend/Application/Bootstrap/Bootstrap.php:97 |
11 |
[2012-03-02T20:20:49.293+02:00][43gg84][dbug]
5. Zend_Controller_Dispatcher_Standard->dispatch() /tmp/fu/bar/library/Zend/Controller/Front.php:954 |
12 |
[2012-03-02T20:20:49.293+02:00][43gg84][dbug]
6. Zend_Controller_Action->dispatch() /tmp/fu/bar/library/Zend/Controller/Dispatcher/Standard.php:295 |
13 |
[2012-03-02T20:20:49.293+02:00][43gg84][dbug]
7. IndexController->indexAction() /tmp/fu/bar/library/Zend/Controller/Action.php:513 |
14 |
[2012-03-02T20:20:49.313+02:00][43gg84][info]
Sending HTTP Body |
15 |
[2012-03-02T20:20:49.323+02:00][43gg84][info]
Request cycle shutdown |
16 |
[2012-03-02T20:20:49.333+02:00][43gg84][info]
Request cycle completed in 0.330
seconds. |
幾乎每個 PHP 開發者都會碰到這個 "headers sent" 的錯誤,在這裏堆棧跟蹤信息很小,當你使用 debug_backtrace() 方法可以指定不輸出對象信息(傳遞 FALSE 做參數)。不過對象信息提供了非常有用的信息,但是它也會因此而生成了更多的噪音來干撓你對日誌的閱讀。如果對象的調試正確,那其實就沒必要輸出對象信息了。
要記錄的重要事件
並不是每個事件都有記錄的必要,我們將在下一小節裏描述這個問題,在這裏你需要了解的是日誌應該記錄什麼。
完成某項操作所需的時間
通過它可以跟蹤爲什麼系統響應變慢或者太快
- 完成一個請求處理所需的時間,精確到毫秒
- 什麼時候接收到的請求
- 執行數據庫查詢的時間
- 從磁盤或者存儲系統獲取數據的時間
- 等等
異常和堆棧跟蹤
異常信息和堆棧跟蹤信息是很有價值的,但要處理好相當不易,特別是當你通過重新拋出異常來控制應用流程。在異常發生時記錄詳細的堆棧跟蹤信息對問題的確定是很有幫助的。
會話
知道一個問題是由誰引起的非常重要,因此在日誌中使用會話標識符就變得必不可少。它可以簡單到是一個 IP 地址或者是一個跟複雜的 UUID,只要能區分不同的用戶就足夠。IP 地址有時候不能確認問題,因爲很多公司都會使用同一個 IP 上互聯網。
版本號
我經常碰到這樣的問題,一個 bug 在服務器 A 上沒事,但在服務器 B 上就經常出現,原因肯定就是版本的差別導致的,因此在日誌中你應該記錄當前軟件的版本號。
哪些內容無需記錄
日誌是用來標識問題的好方法,但不可過度使用,不要爲了日誌而日誌。太多的日誌也會爲查找問題帶來困難,只應該繼續應用的關鍵信息,當然這個“關鍵”很難去量化,你必須依照自身系統的情況來決定。
不要包含一些不相干的信息,我看過很多系統把所有的環境變量都記錄到日誌中,包括整個請求的 dump 等等,這些東西沒有任何幫助。
當然,哪些內容無需記錄你可以在監控過程中不斷的優化。
日誌記錄後該幹什麼?
如果你還在閱讀此文,我非常感動 :) 因爲這篇文章可能真的很枯燥
但你記錄了很多日誌信息,這些信息包含大量有用的而且是更新的信息,你必須監控它們,並從中找出存在的問題或者潛在的問題予以解決,那麼這些日誌就成了無用而且佔據着大量的系統資源。
異常處理
異常發生時必須詳細的進行記錄。在異常對象中創建一個日誌信息在語義上是不正確的。一些邏輯和函數不應該放在異常內部去定義。但一個異常被重新拋出時,你應該記錄這個異常的詳細信息,包括:堆棧跟蹤、錯誤碼和描述。
異常不應該改變應用的流程,只是用來指示問題的發生。
下面的代碼很顯然只有開發者自己才能知道什麼情況是異常,什麼情況不是。下面這種情況我傾向於當沒找到用戶時不要拋出異常。
01 |
//
.. |
02 |
03 |
//
Fetching the user by the received ID. |
04 |
try { |
05 |
$user
= $service->findById( $id ); |
06 |
} catch (SpecificServiceException
$e) { |
07 |
//
Only the specific exception "SpecificServiceException" is being caught here. |
08 |
//
Other possible exceptions are caught later, so they are not logged here. |
09 |
$ this ->getLogger()->logError(
$e->getMessage() ); |
10 |
} |
11 |
12 |
13 |
//
Sorry, no data found |
14 |
if (
! $user instanceof User)
{ |
15 |
return false ; |
16 |
} |
17 |
18 |
//
.. |
處理 PHP 錯誤
但發生什麼問題時,PHP 會報錯,從 E_FATAL 到 E_STRICT,或者是用戶自定義的 E_USER_*. 你也可以通過自定義錯誤處理起來捕獲這些錯誤,例如在 php.ini 中通過 error_log = syslog 來將日誌寫到 syslog 中。
在所有環境中,除了開發環境,display_errors 和相關的設置都應該關閉,而在開發環境中必須打開,error_reporting 應該設置爲包含所有項。
貼士: 在 PHP 運行環境之外配置 PHP 的錯誤報告時,例如 Apache或者是SHELL腳本,應使用整數值,這在PHP文檔中是沒有說明的,例如 error_reporting 設置爲 -1 標識 E_ALL。
處理 E_STRICT 和 E_DEPRECATED
在 PHP 中有很多日誌級別,在開發過程中 E_STRICT 和 E_DEPRECATED 很重要,但沒有任何理由在產品環境中記錄這個信息。
最後一點是應該在開發過程中解決所有 E_STRICT violation 的問題。
處理 E_NOTICE
Notices 相當於 PHP 的 Achilles' heel (不知道該怎麼翻譯). 它們並不被記錄,但能指示應用程序的健康狀態。如果你收到一個 notice 信息,說明開發過程並不順利,可能會有不少潛在問題。你應該重視 E_NOTICE ,把它們當成是錯誤來對待,是需要修復的問題。
處理 @ 操作符
錯誤控制操作符 ("@") 是不推薦使用的,我經常看到很多偷懶的開發者使用它,強烈建議不要使用,除非你真的需要。
不同的消息級別以及什麼時候該用
如果你看 Zend Framework 是,你會注意到有 8 個錯誤級別,從 0 ~ 7,0 是最高級別。我個人覺得下面5種就足夠了:
常量名 | 描述說明 |
---|---|
EMERG | 災難 Emergency |
ERR | 錯誤 Error |
WARN | 警告 Warnings |
INFO | 基本信息 Informational |
DEBUG | 調試信息 Debug |
Emergency
但災難發生,系統完全不可用的時候,應該使用 EMERG 常量,要求維護人員立即處理。
例如:
- 後端服務器不可訪問
- 數據庫掛了,無法訪問
Error
當碰到一些例如數據丟失,請求未處理完成等相關嚴重錯誤時使用,要求重視並應該採取行動。
例如:
- 請求未完成,而且沒法恢復
- 請求完成,數據丟失
- 某些情況下保存數據失敗
- 接收到無效的數據
- 超時或者響應時間太長
- 處理異常
- 安全相關問題
Warnings
但系統有些警告信息需要注意時使用 WARN 常量,它表示某些比較奇怪的問題,這些問題應該在下一個版本中解決,系統運行還是正常無誤的,不需要立即採取行動。
例如:
- 接收不到數據,或者計劃外的數據
- 處理過程返回的狀態未定義,但無誤
- E-mails 延遲,但仍可送達
- 響應時間比較長,但不會太嚴重
- 其他非錯誤問題
Informational
應用程序中一些跟錯誤和警告無關的信息記錄應使用 INFO 常量。
例如:
- 事務啓動,事務結束
- 發送某個 email 到某個地址
- 接收到來自 127.0.0.2 的 PUT 類型的 HTTP 請求
- 獲取ID爲42的用戶詳細資料
Debug
調試信息,僅僅用於跟蹤應用運行狀態。
例如:
- 使用 Mailer 適配器的參數值
- 請求參數名和值
- 數據庫查詢執行了多長時間以及返回多少條記錄
- 堆棧跟蹤
聰明的日誌實現
前面我們提到,不應該在產品環境中啓用調試信息,因爲會讓系統佔用更多的資源。
日誌記錄應該使用如下僞碼的做法:
01 |
//
If we have errors, include debug events in our logs. Else don't include them. |
02 |
if ($errorEventCount
> 0 )
{ |
03 |
$allEvents
= array_merge( |
04 |
$criticalEvents,
$errorEvents, $warningEvents, $infoEvents, $debugEvents |
05 |
); |
06 |
} else { |
07 |
$allEvents
= array_merge( |
08 |
$criticalEvents,
$errorEvents, $warningEvents, $infoEvents |
09 |
); |
10 |
} |
結論
在完美世界中,日誌是不需要的,但現實不存在完美。儘管記錄日誌很簡單,但你應該謹慎處之,讓下一個開發者能非常輕鬆的就理解日誌具體的意思。
最後非常感謝您閱讀這篇羅裏羅嗦的文章!
英文原文,OSCHINA 原創翻譯
紅薯注:在翻譯這篇文章時,裏面有不少小節我並沒有領會其中具體的意思,特別是 PHP 相關的,請大家見諒,如果有實在看不懂的可以參考英文原文。