掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
歡迎在新的一年來到我的博客。在一個(gè)巴黎devops maillist上回復(fù)了一個(gè)關(guān)于監(jiān)控和日志監(jiān)控之后,我想起了很久以前我的一個(gè)博客計(jì)劃。

創(chuàng)新互聯(lián)-專業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設(shè)、高性價(jià)比淮安網(wǎng)站開發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫(kù),直接使用。一站式淮安網(wǎng)站制作公司更省心,省錢,快速模板網(wǎng)站建設(shè)找我們,業(yè)務(wù)覆蓋淮安地區(qū)。費(fèi)用合理售后完善,十余年實(shí)體公司更值得信賴。
盡管在寫這篇博文的時(shí)候,我是在負(fù)責(zé)運(yùn)維工作,不過本文主要是寫給開發(fā)者的。
對(duì)我來說,明白如何記錄日志和記錄什么,是軟件工程師必須明了的最艱巨的任務(wù)之一。之所以這么說,是因?yàn)檫@項(xiàng)任務(wù)與預(yù)測(cè)(divination)類 似,你不知道當(dāng)你要調(diào)試的時(shí)候需要些什么信息……我希望這10個(gè)建議能幫助你更好地在應(yīng)用程序中記錄日志,讓運(yùn)維工程師們受益。:)
1. 你不應(yīng)自己寫log
絕對(duì)不要,即便是用printf或者是自己寫入到log文件,又或自己處理logrotate。請(qǐng)給你的運(yùn)維同志們省省心,調(diào)用標(biāo)準(zhǔn)庫(kù)或者系統(tǒng)API來完成它。
這樣,你可以保證程序的運(yùn)行與其他系統(tǒng)組件好好相處,把log寫到正確的位置或者網(wǎng)絡(luò)服務(wù)上,而不需要專門的系統(tǒng)配置。
假如你要使用系統(tǒng)API,也就是syslog(3),學(xué)習(xí)好怎么用它。
如果你更喜歡用logging庫(kù),在Java里面你有很多選擇,例如Log4j,JCL,slf4j和logback。我最喜歡用slf4j和logback的組合,因?yàn)樗鼈兲貏e給力,而且相對(duì)地容易配置(還允許使用JMX進(jìn)行配置或者重載配置文件)。
slf4j最好的是你可以修改logging控制臺(tái)的位置。如果你在編寫一個(gè)庫(kù),這會(huì)變得非常重要,因?yàn)檫@可以讓庫(kù)的使用者使用自己的logging控制臺(tái)而不需要修改你的庫(kù)。
其他語(yǔ)言當(dāng)然也有多種logging庫(kù),例如ruby的Log4r,stdlib logger,和幾近完美的Jordan Sissel’s Ruby-cabin。
如果你想糾結(jié)CPU占用問題,那么你不用看這篇文章了。還有,不要把log語(yǔ)句放在緊內(nèi)部循環(huán)體內(nèi),否則你永遠(yuǎn)看不出區(qū)別來。
2. 你應(yīng)在適當(dāng)級(jí)別上進(jìn)行l(wèi)og
如果你遵循了上述第一點(diǎn)的做法,接下來你要對(duì)你程序中每一個(gè)log語(yǔ)句使用不同的log級(jí)別。其中最困難的一個(gè)任務(wù)是找出這個(gè)log應(yīng)該是什么級(jí)別
以下是我的一些建議:
記住,在你的程序中,默認(rèn)的運(yùn)行級(jí)別是高度可變的。例如我通常用INFO運(yùn)行我的服務(wù)端代碼,但是我的桌面程序用的是DEBUG。這是因?yàn)槟愫茈y在 一臺(tái)你沒有接入權(quán)限的機(jī)器上進(jìn)行調(diào)試,但你在做用戶服務(wù)時(shí),比起教他們?cè)趺葱薷膌og level再把生成的log發(fā)給你,我的做法可以讓你輕松得多。當(dāng)然你可以有其他的做法:)
3. honor the log category
我在第一點(diǎn)中提到的大部分logging庫(kù)允許指定一個(gè)logging類別。它可以分類log信息,并基于logging框架的配置,在最后以某一形式進(jìn)行l(wèi)og或是不進(jìn)行。
通常,Java開發(fā)者在log語(yǔ)句處使用完整,合格的類名作為類別名。如果你的程序遵循單一職責(zé)原則(Single responsibility principle,原文有誤),這種模式還不錯(cuò)。
在Java的logging庫(kù)中,Log類別是按等級(jí)劃分的,例如在 com.daysofwonder.ranking.ELORankingComputation會(huì)匹配到頂級(jí)的 com.daysofwonder.ranking。這可以讓運(yùn)營(yíng)工程師配置一個(gè)對(duì)此類別下指定的所有ranking子系統(tǒng)作用的logging。如果需 要的話,還可以同時(shí)生成子類別的logging配置。
拓展開來,我們講解一下特定情況下的調(diào)試。假設(shè)你在做一個(gè)應(yīng)答用戶請(qǐng)求的服務(wù)端軟件(如REST API)。它正在對(duì)my.service.api.
4. 你應(yīng)該寫有意義的log
這可能是最重要的建議了。沒有什么比你深刻理解程序內(nèi)部,卻寫出含糊的log更糟了。
在你寫日志信息之前,總要提醒自己,有突發(fā)事件的時(shí)候,你唯一擁有的只有來自log文件,你必須從中明白發(fā)生了什么。這可能就是被開除和升職之間的微妙的差距。
當(dāng)開發(fā)者寫log的時(shí)候,它(log語(yǔ)句)是直接寫在代碼環(huán)境中的,在各種條件中我們應(yīng)該寫入基于當(dāng)前環(huán)境的信息。不幸的是,在log文件中并沒有這些環(huán)境,這可能導(dǎo)致這些信息無法被理解。
解決這個(gè)情況(在寫warn和error level時(shí)尤為重要)的一個(gè)方法是,添加輔助信息到log信息中,如果做不到,那么改為把這個(gè)操作的作用寫下。
還有,不要讓一個(gè)log信息的內(nèi)容基于上一個(gè)。這是因?yàn)榍懊娴男畔⒖赡苡捎?與當(dāng)前信息)處于不同的類別或者level而沒被寫入。更壞的情況是,它因多線程或異步操作,在另一個(gè)地方(或是以另一方式)出現(xiàn)。
5. 日志信息應(yīng)該用英語(yǔ)
這個(gè)建議可能有點(diǎn)奇怪,尤其是對(duì)法國(guó)佬(French guy)來說。我還是認(rèn)為英語(yǔ)遠(yuǎn)比法語(yǔ)更簡(jiǎn)煉,更適應(yīng)技術(shù)語(yǔ)言。如果一個(gè)信息里面包含超過50%的英語(yǔ)單詞,你有什么理由去用法語(yǔ)寫log呢
把英法之爭(zhēng)丟一邊,下面是這個(gè)建議背后的原因:
#p#
6. 你應(yīng)該給log帶上上下文
沒有什么比這樣的log信息更糟的了
- Transaction failed
或是
- User operation succeeds
又或是API異常時(shí):
- java.lang.IndexOutOfBoundsException
沒有相應(yīng)的上下文,這些信息不過是噪音,它們不會(huì)對(duì)調(diào)試過程中有意義的數(shù)值或是空間起作用(add value and consume space)。
帶上上下文的信息要有價(jià)值得多,例如:
- Transaction 234632 failed: cc number checksum incorrect
或是
- User 54543 successfully registered e-mail[email protected]
又或是
- IndexOutOfBoundsException: index 12 is greater than collection size 10
在上面這一例子中的異常,如果你想把它傳播開, 確保在處理的時(shí)候帶上與當(dāng)前l(fā)evel相應(yīng)的上下文,讓調(diào)試更簡(jiǎn)單,如下一個(gè)java的例子:
- public void storeUserRank(int userId,int rank,String game) {
- try {
- ...deal database ...
- } catch (DatabaseException de) {
- throw new RankingException("Can't store ranking for user "+userId+" in game "+ game + " because " + de.getMessage() );
- }
- }
這樣,rank API的上層客戶端就可以有足夠的上下文信息log這個(gè)error。更好的做法是讓上下文成為exception的參數(shù),而不是信息,如果需要的話,上層可以對(duì)它進(jìn)行修正(use remediation)。
保留上下文的一個(gè)簡(jiǎn)單方法是使用一些java logging庫(kù)的MDC實(shí)現(xiàn)。MDC是一個(gè)每線程關(guān)聯(lián)數(shù)組(per thread associative array)??梢孕薷膌ogger設(shè)置,讓每一行l(wèi)og總是輸出MDC內(nèi)容。如果你的程序使用每線程模式,這可以幫助解決保留上下文的問題。這個(gè) java的例子對(duì)給定的請(qǐng)求,使用MDC記錄每用戶的信息:
- class UserRequest {
- ...
- public void execute(int userid) {
- MDC.put("user",userid);
- // ... all logged message now will display the user=
for this thread context ... - log.info("Successful execution of request");
- // user request processing is now finished,no need to log our current user anymore
- MDC.remote("user");
- }
- }
提示,MDC系統(tǒng)在異步logging模式中的表現(xiàn)并不好,例如Akka的logging系統(tǒng)。因?yàn)镸DC是保存在一個(gè)每線程存儲(chǔ)區(qū)域的,而且在異 步系統(tǒng)中你無法保證在寫入log的線程是有MDC的那一個(gè)。在這種情況下,你需要手動(dòng)地使用每一個(gè)log語(yǔ)句來log這些上下文。
7. 你應(yīng)該用機(jī)器可解析的格式來打日志
Log信息對(duì)人很友善,但是對(duì)機(jī)器就慘了。有時(shí)人工地讀這些log文件并不足夠,你需要進(jìn)行一些自動(dòng)化過程(例如通過警報(bào)和審查)?;蚴悄阆爰写鎯?chǔ)你的log,以進(jìn)行搜索。
如下,如果你把log的上下文嵌在string中會(huì)發(fā)生什么:
- log.info("User {} plays {} in game {}",userId,card,gameId);
這會(huì)生成這樣的文本:
- 2013-01-1217:49:37,656[T1]INFOc.d.g.UserRequestUser1334563plays4ofspadesingame23425656
現(xiàn)在,如果你想使它可解析,你需要下面這個(gè)(未測(cè)試過的)正則表達(dá)式:
- /User(\d+)plays(.+)ingame(\d+)$/
好了,這并不輕松而且容易出錯(cuò),把它接入到你代碼中已有的string參數(shù)中。
這個(gè)方法怎么樣,我相信Jordan Sissel在他的ruby-cabin庫(kù)中第一次介紹的: 在你的log里面加入機(jī)器可解析格式的上下文。我們上述的例子中這樣可以使用JSON:
- 2013-01-1217:49:37,656[T1]INFOc.d.g.UserRequestUserplays{'user':1334563,'card':'4ofspade','game':23425656}
現(xiàn)在你的log分析器可以更容易地寫入,更直接地索引,而且你可以釋放logstash所有的威力。
8. 日志不宜太多或太少
這聽著貌似很愚蠢。log的數(shù)量是有一個(gè)合適的平衡的。
太多的log會(huì)使從中獲得有價(jià)值的東西變得困難。當(dāng)人工地瀏覽這種十分混亂的log,嘗試調(diào)試產(chǎn)品在早上3點(diǎn)的一個(gè)問題可不是一個(gè)好事。
太少的log,你可能無法調(diào)試問題: 調(diào)試就像在拼一個(gè)困難的拼圖,你需要得到足夠的拼塊。
不幸的是,這沒有魔法般的規(guī)則去知道應(yīng)該log些什么。所以需要嚴(yán)格地遵從第一第二點(diǎn),程序可以變得很靈活,輕松地增減log的長(zhǎng)度(verbosity)。
解決這個(gè)問題的一個(gè)方法是,在開發(fā)過程中盡可能多地進(jìn)行l(wèi)og(不要被加入用于程序調(diào)試的log所迷惑)。當(dāng)應(yīng)用程序進(jìn)入生產(chǎn)過程時(shí),對(duì)生成的 log進(jìn)行一次分析,根據(jù)所發(fā)現(xiàn)的問題增減log語(yǔ)句。尤其是在調(diào)試時(shí),在你需要的部分,你可以有更多的上下文或logging,確保在下一個(gè)版本中加入 這些語(yǔ)句(可以的話,同時(shí)解決它來讓這個(gè)問題在記憶中保持新鮮)。當(dāng)然,這需要運(yùn)維人員和開發(fā)者之間大量的交流。
這是一個(gè)復(fù)雜的任務(wù),但是我推薦你重構(gòu)logging語(yǔ)句,如你重構(gòu)代碼一樣多。這樣可以在產(chǎn)品的log和它的log語(yǔ)句的修改中有一個(gè)緊密的反饋循環(huán)。如果你的組織有一個(gè)連續(xù)的交付進(jìn)程的話,它會(huì)十分有效,正如持續(xù)的重構(gòu)。
Logging語(yǔ)句是與代碼注釋同級(jí)的代碼元數(shù)據(jù)。保持logging語(yǔ)句與代碼相同步是很重要的。沒什么比調(diào)試時(shí)獲得與所運(yùn)行的代碼毫無關(guān)系的信息更糟了。
#p#
9. 你應(yīng)該考慮閱讀者
為什么要對(duì)應(yīng)用程序做log
唯一的答案是,在某一天會(huì)有人去讀它(或是它的意義)。更重要的是,猜猜誰(shuí)會(huì)讀它,這是很有趣的事。對(duì)于不同的”誰(shuí)”,你將要寫下的log信息的內(nèi)容,上下文,類別和level會(huì)大不同。
這些”誰(shuí)”包括:
開發(fā)者了解程序內(nèi)部,所以給他的log信息可以比給終端用戶的復(fù)雜得多。為你的目標(biāo)閱讀者調(diào)整你的表達(dá)方式,乃至為此加入額外的類別(dedicate separate catagories)。
10. 你不應(yīng)該只為調(diào)試而log
正如log會(huì)有不同的閱讀者,它也有不同的使用理由。即便調(diào)試是最顯而易見的閱讀log的目的,你同樣可以有效地把log用在:
總結(jié)
我希望這可以幫助你生成更多有用的log。如果我忘記了一些必須的(對(duì)你而言)建議,請(qǐng)諒解。對(duì)了,如果你看了這篇博客之后并不能更好地進(jìn)行l(wèi)og,我并不負(fù)責(zé)
如果這10個(gè)建議還不夠的話,盡管在評(píng)論中補(bǔ)充更多有用的建議。

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流