一、簡介
在軟件開發(fā)中奈籽,我們出于各種目的饥侵,需要將程序運行中的一些狀態(tài)記錄在日志中。
日志記錄衣屏,并不是越多越好躏升,也不是記錄的越頻繁越好,而是需要我們精心設(shè)計記錄日志的時機(jī)狼忱、內(nèi)容膨疏、格式(以方便后續(xù)解析、查詢?nèi)罩荆┑鹊取?br>
本文簡單介紹了記錄日志的一些基本原則和注意事項钻弄,更具體的記日志的技巧佃却、最佳實踐、日志框架等請參考子文檔 Go如何記錄日志 窘俺。
二饲帅、記錄日志的目的(why)
開發(fā)調(diào)試
目的是開發(fā)期調(diào)試程序使用,這種日志量比較大瘤泪,且沒有什么實質(zhì)性的意義灶泵,只應(yīng)該出現(xiàn)在開發(fā)期,而不應(yīng)該在項目上線之后輸出均芽。
記錄用戶行為
這種類型的日志丘逸,記錄用戶的操作行為单鹿,用于大數(shù)據(jù)分析掀宋,比如監(jiān)控、風(fēng)控仲锄、推薦等等劲妙。這種日志,一般是給其他團(tuán)隊分析使用儒喊,而且可能是多個團(tuán)隊镣奋,因此一般會有一定的格式要求,開發(fā)者應(yīng)該按照這個格式來記錄怀愧,便于其他團(tuán)隊的使用侨颈。當(dāng)然余赢,要記錄哪些行為、操作哈垢,一般也是約定好的妻柒,因此,開發(fā)者主要是執(zhí)行的角色耘分。
程序運行狀況
記錄程序的運行狀況举塔,特別是非預(yù)期的行為、異常情況求泰,這種日志央渣,主要是給開發(fā)、維護(hù)人員使用渴频。什么時候記錄芽丹,記錄什么內(nèi)容,完全取決于開發(fā)人員卜朗,開發(fā)者具有高度自主性志衍。本文討論的主要也是指這種類型的日志,因為作為一個服務(wù)端開發(fā)聊替、運維人員楼肪,程序運行日志往往是解決線上問題的救命稻草。
系統(tǒng)惹悄、機(jī)器狀況
比如網(wǎng)絡(luò)請求春叫、系統(tǒng)CPU、內(nèi)存泣港、IO使用情況等等暂殖,這種日志主要是給運維人員使用,生成各種更直觀的展現(xiàn)形式当纱,在系統(tǒng)出問題的時候報警呛每。
三、日志的要素(what)
每條日志都可以被當(dāng)作一個事件(event)坡氯,紀(jì)錄了該事件發(fā)生時各個信息:
時間
日志的時間可以包含多種含義晨横,不同含義的時間傳遞不同的信息:
- 指事件發(fā)生的時間,而不是日志被打印的時間箫柳。該時間附近范圍內(nèi)手形,結(jié)合該事件及服務(wù)器的網(wǎng)絡(luò)、CPU悯恍、IO等狀況库糠,可以了解他們之間的相關(guān)性,有助于分析事件發(fā)生的原因涮毫。
- 持續(xù)時間瞬欧。例如網(wǎng)絡(luò)請求的耗時贷屎、處理請求的各個階段的耗時等。
- 事件發(fā)生的順序艘虎。通過時間戳的順序豫尽,了解到一系列事件的發(fā)生順序。對多進(jìn)程顷帖、多線程美旧、分布式系統(tǒng)有幫助。
位置
事件發(fā)生在哪個模塊贬墩、哪個文件榴嗅、哪個函數(shù)、哪一行代碼里陶舞。
級別
日志的重要程度嗽测。用于:
- 不同的環(huán)境(測試、生產(chǎn))下肿孵,打印不同級別的日志
- 不同級別的日志產(chǎn)生不同級別的監(jiān)控報警
內(nèi)容
簡明扼要的描述發(fā)生了什么樣的事情唠粥。目的是通過日志本身,而不是重新閱讀相關(guān)代碼來搞清楚發(fā)生了什么事情停做。
例如:logger.warn('user_login failed due to unvalid_username')
唯一標(biāo)識
不管是面向用戶的服務(wù)晤愧、面向機(jī)器集群的服務(wù),都需要一個唯一標(biāo)識作為日志的主體蛉腌,以方便查找該事件主體的其他信息官份。(很多元數(shù)據(jù)是不會記在日志里的,只會記一個唯一標(biāo)識)
例如:logger.warn('user_login failed due to password, username %s', username)
事件上下文
除了時間烙丛、位置舅巷、級別、內(nèi)容河咽,其他的一些有用的信息钠右。
例如:
- logger.warn('user_login failed due to wrong password, username %s', username)
- logger.warn('user_login failed due to invalid password, username %s', username)
- logger.warn('user_login failed due to empty password, username %s', username)
為了獲取更豐富的上下文,有些數(shù)據(jù)需要從 Nginx 那里獲取并傳給本服務(wù)忘蟹,然后打印飒房。參考:和日志相關(guān)的Nginx設(shè)置
格式化
將上述的各個信息按照固定的順序打印出來,不僅方便查找(例如使用grep寒瓦,sed情屹,awk等),也方便收集日志的程序(ES)解析日志杂腰。
其他
根據(jù)各自的業(yè)務(wù)特點,還可以在日志中記錄(包括椅文、不限于):
- 錯誤次數(shù)
- 當(dāng)前正在處理的請求數(shù)
- 處理的進(jìn)度(33%喂很,50%惜颇,78%。少辣。凌摄。)
- IP
- 問題出現(xiàn)時的請求鏈接
四、記錄日志的一些原則和技巧
使用框架或模塊
Java : Log4j漓帅、Log4j2锨亏、Commons Logging、Slf4j忙干、Logback
Python 內(nèi)置的 logging 模塊
beego 框架里的 github.com/astaxie/beego/logs 模塊
其他的一些Go的框架:
- Logrus: github.com/Sirupsen/logrus (Docker use this)
- github.com/op/go-logging
- github.com/golang/glog (from Google, implementation of their C++ glog library in Go)
- github.com/cihub/seelog
不能出錯
記錄日志的目的是為了方便發(fā)現(xiàn)問題器予,解決問題,那么就需要保證記錄日志本身不能出錯捐迫。
尤其是對于 Error乾翔、Fatal級別的日志,出現(xiàn)的概率低施戴,所以要做好單元測試反浓,以保證記錄日志本身是沒問題的,是不會影響正常業(yè)務(wù)的赞哗。
避免敏感信息
- 避免記錄用戶密碼雷则。
- 避免記錄用戶個人信息,如身份證號肪笋、手機(jī)號等巧婶。應(yīng)該只在日志里記錄該用戶的唯一標(biāo)識,然后根據(jù)該唯一標(biāo)識去其他的系統(tǒng)(例如數(shù)據(jù)庫)查看詳細(xì)信息涂乌。
記錄“不可能發(fā)生”的事件
雖然正常邏輯下艺栈,某些情況是永遠(yuǎn)都不可能發(fā)生的,但是還是需要給這些不可能發(fā)生的情況打印一條日志湾盒。
例如 條件語句里的 else湿右,switch 里的 default,都需要進(jìn)行防御式的編程罚勾,同時記錄日志毅人。
Lazy logging
日志本身是一個字符串,可以通過多種方式拼接而成尖殃。如果根據(jù)log level丈莺,該條日志不應(yīng)被打印,那么就應(yīng)該避免拼接這個操作送丰,也應(yīng)該避免字符串拼接里的函數(shù)的調(diào)用缔俄。
例如:
#coding=utf-8
import logging
logger = logging.getLogger('LazyLogging')
logger.setLevel(logging.DEBUG)
hander = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
hander.setFormatter(formatter)
logger.addHandler(hander)
def getUserCount():
logger.info('getUserCount is called')
return 1
logger.debug("There are " + str(getUserCount()) + " users logged in now.")
- log level 設(shè)置為 DEBUG(第5行),那么會先打印第12行,再打印第15行
- log level 設(shè)置為 INFO俐载,依然會打印第12行蟹略,說明:
- 問題1: 即便沒有打印第15行,依然生成了一個字符串
- 問題2: 而且調(diào)用了 getUserCount 這個函數(shù)
- 解決上述的問題1遏佣,可以把第15行修改為:
logger.debug("There are %s users logged in now.", getUserCount())
- 為了解決上述的問題2挖炬,一個辦法是:
class lazyEval:
def __init__(self, f, *args):
self.func = f
self.args = args
def __str__(self):
return str(self.func(*self.args))
logger.debug("There are %s users logged in now.", lazyEval(getUserCount))
Java里的方式:
logger.debug(``"There are {} users logged in now."``, () -> getUserCount());
目前Golang里也存在這樣的問題,但是還沒找到怎么做到 lazyLogging状婶。
但是意敛,可以看出上面的方式也不優(yōu)雅,所以應(yīng)當(dāng)避免在打印日志時調(diào)用函數(shù)膛虫。
異步打印日志
互聯(lián)網(wǎng)應(yīng)用程序中草姻,高并發(fā)下的寫日志會帶來大量的IO操作,從而影響正常服務(wù)的性能走敌。這時候的記日志就需要專門的服務(wù)去做碴倾,例如把日志打到消息隊列里,然后再寫到磁盤上掉丽。
設(shè)置緩存
默認(rèn)的日志是隨時 flush 到console跌榔、文件里的,通過設(shè)置緩存進(jìn)行批量操作捶障,可以一定程度上優(yōu)化服務(wù)性能僧须。
對日志歸檔、分類
打印到一個文件里项炼,會使得該文件越來越大担平,不方便查找。
按日志屬性分類:access log锭部,error log暂论,
按日期歸檔:lathspell-api.2018-01-17.log,lathspell-api.2018-01-18.log
按大小切分:lathspell-api.2018-01-17.log.1拌禾,lathspell-api.2018-01-17.log.2取胎,lathspell-api.2018-01-17.log.3 (避免文件太大)