淺談程序日誌
如果世界上有一個人能夠保證一次寫出來的代碼是百分之百正確的,那麼毫無疑問,他一定是世界上最優秀的程序員,沒有之一。為什麼要求代碼寫好過後要進行充分的自測 ( 包括單元測試和集成測試 ) ?就因為是人皆會犯錯,使程序就會有 bug 。作為一名軟件開發人員,必須要學會對程序進行測試,也就是要學會程序的調試。
代碼調試方法
一般而言,對代碼的調試有以下幾種方法:
第一,憑肉眼看 。在開發階段,我們編寫的每一行代碼都需要用我們的“火眼金睛”多審查幾遍。如果要問,最好的代碼調試工具是什麼?我認為是人眼。不管是代碼還是文檔,在用工具檢查之前,都需要先過了我們眼睛這一關。
第二,對代碼進行編譯,以發現語法錯誤 。編譯器能夠幫助我們發現代碼中存在的語法錯誤,但對於那些隱蔽性的錯誤 ( 如邏輯錯誤等 ) 無能為力。
第三,用代碼檢查工具 ( 如 Pclint 等 ) 來走查代碼 。如果代碼編譯通過,並不表示它就沒有問題了。在學校的時候,我們一般認為隻要程序能夠運行就可以了。但在實際的軟件開發項目中,程序能夠跑起來,隻是“萬裏長征走完了第一步”。用代碼檢查工具可以發現很多編譯器無法發現的錯誤,如變量定義了未引用、不同數據類型之間相互賦值、函數未聲明便被調用等。
第四,對代碼進行調試 。對於運行正常而輸出結果不正確的程序,我們可以用設置斷點並進行單步跟蹤調試的方法來發現其中存在的問題。例如,在 VC++ 6.0 裏麵,可實現對代碼的單步調試,並輸出變量在某一步產生的值,可據此判斷程序的邏輯的正確與否。
第五,對程序的日誌文件進行分析 。對代碼的單步調試隻在代碼行數較少的時候比較適用,如學校教材上麵的程序。但在實際的軟件項目中,代碼少則幾千行,多則數萬行,用單步調試的方法顯然不恰當。為了跟蹤某一變量值的變化,用該方法可能要花費幾個小時,這對工作效率產生了嚴重影響。為了解決大程序文件代碼調試問題,日誌係統應運而生。在程序中的重要地方打印日誌,之後對產生的日誌進行分析,可找到對應代碼的問題。因此,日誌文件分析成了大型軟件項目中代碼調試的主要手段。
什麼是日誌文件?
看過電視劇《神探狄仁傑》的朋友可能都會對狄仁傑的斷案能力極為歎服,他會將一個外人看來非常詭異的案件查得水落石出,連武則天都不得不說他是“神乎其技”。實際上,狄仁傑也是凡人,他隻是通過極為細小的線索來順藤摸瓜發現幕後的黑手。對應到軟件開發上,很多厲害的程序員都是通過分析程序運行過程中產生的少量的異常日誌來發現軟件問題的。因此,程序日誌就像斷案線索一樣重要。
在業務軟件係統中大量使用日誌,日誌能夠起到“按圖索驥”的作用,它對於故障定位和係統正常運行維護具有舉足輕重的作用。
日誌文件是程序中寫日誌函數產生的記錄程序執行情況的文件。寫日誌函數可以用多種編程語言編寫,可以像普通的函數一樣被調用。在恰當的地方調用該函數,可對整個程序的運行狀況有一個全麵的了解,方便對程序的跟蹤調試。
日誌等級
事有輕重緩急,日誌信息也有重要與不重要之分。一般按照重要程度,將日誌等級分為幾類。在作者參與過的軟件開發項目中,共有 7 個等級,用宏定義表示如下:
// 日誌等級定義
#define LOG_FATAL (int)1 // 嚴重錯誤
#define LOG_ERROR (int)2 // 一般錯誤
#define LOG_WARN (int)3 // 警告信息
#define LOG_INFO (int)4 // 一般信息
#define LOG_TRACE (int)5 // 跟蹤信息
#define LOG_DEBUG (int)6 // 調試信息
#define LOG_ALL (int)7 // 全部
開發人員根據所要打印的日誌的具體情況采用不同的日誌等級。
日誌配置
由於不同產品程序行數、部署情況、實現功能等的差別,對日誌打印的要求也不盡相同,因此需要有配置來控製日誌的產生數量和顯示情況。
例如,在筆者所參與的開發項目的配置文件中,有一個專門的 [LOG] 配置段,其中的配置項如下:
[ LOG ]
; 日誌等級 , 0-Fatal 1-Error 2-Warn 3-Info 4-Trace 5-Debug 6-All
LogLevel =
; 每個日誌文件的最大容量
LogMaxSize =
; 是否輸出該條日誌在代碼中的行數 , 1-Yes 0-No
LogPosition =
其中, LogLevel 用於控製打印日誌的等級,代碼中日誌等級比配置值大的日誌信息均不在日誌文件中顯示; LogMaxSize 用於控製生成一個日誌文件的大小的上限,超過該值後,便重新生成文件; LogPosition 用於控製是否在日誌文件中顯示代碼行數,方便將日誌與代碼對應起來。
日誌函數的調用
日誌函數的調用遵循一般函數的調用規則。例如,在筆者所參與的開發項目中,有兩類寫日誌函數,如下所示:
(1) 第一類形如: WriteLog(LogLevel, LogInfo) 。其中,參數 LogLevel 指日誌等級 ( 見第 2 節中的說明 ) ;參數 LogInfo 是具體要打印的日誌信息,我們據此信息來檢查程序的運行情況。該函數的調用示例如: WriteLog(LOG_INFO, "The value of this integer is 3.") ,日誌等級為 LOG_INFO ,日誌信息為“ The value of this integer is 3. ” ( 該信息會輸出到日誌文件中 ) 。
(2) 第二類形如: WriteLogEx(LogLevel, LogInfo, ParaInfo) 。這是擴展的日誌函數,不但能夠輸出日誌信息,還能夠在日誌信息中顯示變量的值。該函數的調用示例如: WriteLogEx(LOG_INFO, "The value of integer iInt is %d.", iInt) ,該日誌要輸出整型變量 iInt 的值,可以將該函數的調用與 printf 函數的調用比較起來看 ( 可以認為 WriteLogEx 函數隻是在 printf 函數中增加了一個日誌等級參數 ) 。
日誌編寫基本原則
1) 顯式輸出,關鍵信息必須輸出;
2) 在編碼時使用正確的日誌級別, error 錯誤和 warning 錯誤必須反應出實在的含義,不是特別嚴重的問題不能將日誌等級定義為 LOG_FATAL ;
3) 在寫日誌描述時,要使用正常簡單易懂的語言,不能使用晦澀難懂的語言或某些專業術語;
4) 在極少數特殊情況不希望用戶知道時,可使用特殊日誌標記;
5) 為了寫出優美的代碼,在自己修改或添加代碼的地方,都要正確的打上標記 ( 包括作者、日期信息等 ) ,方便追蹤版本的演進情況。
日誌編寫基本要求
1) 分多條信息分別輸出,不要企圖一次將所有信息打印出來;
2) 分時輸出;
3) 必須分日誌級別,這樣可根據等級迅速對日誌進行分析;
4) 控製日誌信息的條數,不重要的信息盡量不要打印日誌。
輸出日誌位置要求
1) 所有的輸入輸出,包括收消息和發消息都要求輸出日誌;
2) 關鍵控製點必須輸出日誌;
3) 調用底層或第三方軟件,必須輸出日誌,而且對不可靠底層,必須加上 begin/end 兩行日誌;
4) 對方係統處理時間必須輸出日誌,以利以後維護時快速定位性能問題。
一些注意事項
1) 在編寫日誌時需要注重日誌細節,目標是為了方便以後維護,在遇到問題時,可以快速定位問題;
2) 不要在同一行中寫意思重複的日誌;
3) 日誌需要足夠的精簡,不要隨意換行;
4) 日誌中字段之間可以用空格或其它符號分斷,不能將日誌一直連續而不將其分斷,盡量使日誌本身具備進行“識文斷句”的能力;
5) 對於日誌中的特殊信息 (如會話號、 IP 地址等) ,用特殊的符號進行標識,其主要目的是為了便於搜索。
總結
日誌係統在軟件程序中占有非常重要的地位,日誌文件是排查程序問題的主要工具,是程序調試的利器。日誌編寫的總體原則是簡單清晰、便於排查問題。作為一名合格的軟件開發工程師,一定要學會日誌函數的靈活調用及準確通過日誌文件來定位程序問題。
“實踐出真知”,隻有通過不斷的積累和總結,才會對日誌有更全麵的認識。
最後更新:2017-09-23 22:32:43