Golang 優(yōu)化之路——自己造一個日志輪子

寫在前面

Golang 的log包內(nèi)容不多趋惨,說實(shí)話,直接用來做日志開發(fā)有些簡易惦蚊。主要是缺少一些功能:

  1. 按日志級別打印和控制日志器虾;
  2. 日志文件自動分割讯嫂;
  3. 異步打印日志。

按日志級別打印和控制日志

我們實(shí)現(xiàn)的日志模塊將會支持4個級別:

const (
    LevelError = iota
    LevelWarning
    LevelInformational
    LevelDebug
)

定義一個日志結(jié)構(gòu)體:

type Logger struct {
    level int
    l     *log.Logger
}

func (ll *Logger) Error(format string, v ...interface{}) {
    if LevelError > ll.level {
        return
    }
    msg := fmt.Sprintf("[E] "+format, v...)
    ll.l.Printf(msg)
}

這樣就能實(shí)現(xiàn)日志級別控制輸出兆沙,并且打印的時候追加一個標(biāo)記欧芽,比如上面的例子,Error 級別就會追加[E]葛圃。

這個實(shí)現(xiàn)已經(jīng)可以了千扔,但是還是有優(yōu)化的空間。比如打印追加標(biāo)記[E]的時候库正,用的是字符串加法曲楚。字符串加法會申請新的內(nèi)存,對性能不是很優(yōu)化褥符。我們需要通過字符數(shù)組來優(yōu)化龙誊。

但我不會這么去優(yōu)化了。這個時候看一下 log 包的 API属瓣,可以發(fā)現(xiàn)原生包是支持設(shè)置前綴的:

func (l *Logger) SetPrefix(prefix string)

再去看一下具體的實(shí)現(xiàn):

func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) {
    *buf = append(*buf, l.prefix...)

原生包在寫日志前綴的時候就用到了[]byte來提升性能载迄。既然人家已經(jīng)提供了,我們還是不要自己造了抡蛙。那么問題來了护昧,設(shè)置前綴是初始化的時候就要設(shè)置,打印的時候自動輸出出來粗截。一個log.Logger對象只能有一個前綴惋耙,而我們需要4種級別的前綴,這個如何打有懿绽榛?

type Logger struct {
    level int
    err   *log.Logger
    warn  *log.Logger
    info  *log.Logger
    debug *log.Logger
}

我們直接申請4個日志對象就能解決。為了保證所有級別都打印到同一個文件里面婿屹,初始化的時候設(shè)置成同一個io.Writer即可灭美。

logger := new(LcLogger)

logger.err = log.New(w, "[E] ", flag)
logger.warn = log.New(w, "[W] ", flag)
logger.info = log.New(w, "[I] ", flag)
logger.debug = log.New(w, "[D] ", flag)

設(shè)置日志級別:

func (ll *Logger) SetLevel(l int) {
    ll.level = l
}

打印的時候根據(jù)日志級別控制輸出。(講一個我遇到的坑昂利。之前有一次打印日志打太多了届腐,磁盤都打滿了,就尋思著把日志級別調(diào)高減少打印內(nèi)容蜂奸。把級別調(diào)成 Error 后發(fā)現(xiàn)還是沒有效果犁苏,最后看了看代碼發(fā)現(xiàn)出問題的日志打印的是 Error 級別。扩所。围详。Error級別的日志要盡量少打。)

func (ll *Logger) Error(format string, v ...interface{}) {
    if LevelError > ll.level {
        return
    }
    ll.err.Printf(format, v...)
}

日志文件自動分割

日志文件需要自動分割祖屏。否則一個文件過大助赞,清理磁盤的時候這個文件因為還是打印日志沒辦法清理买羞。

日志分割我覺得簡單的以大小分割就好。

那么日志分割功能如何接入咱們上面實(shí)現(xiàn)的日志模塊呢嫉拐?關(guān)鍵就在io.Writer哩都。

type Writer interface {
    Write(p []byte) (n int, err error)
}

Writer這個接口只有一個方法,如此簡單婉徘。原生包默認(rèn)打印日志會輸出到os.Stderr里面,這是一個os.File類型的變量咐汞,它實(shí)現(xiàn)了Writer這個接口盖呼。

func (f *File) Write(b []byte) (n int, err error)

寫日志的時候,log 包會自動調(diào)用Write方法化撕。我們可以自己實(shí)現(xiàn)一個Writer几晤,在Write的時候計算一下寫入此行日志之后當(dāng)前日志文件大小,如果超過設(shè)定的值植阴,執(zhí)行一次分割蟹瘾。按日子分割日志也是這個時候操作。

推薦用 gopkg.in/natefinch/lumberjack.v2 這個包來做日志分割掠手,功能很強(qiáng)大憾朴。

jack := &lumberjack.Logger{
    Filename: lfn,
    MaxSize:  maxsize, // megabytes
}

使用也很簡單,jack對象就是一個Writer了喷鸽,可以直接復(fù)制給Logger使用众雷。

日志的異步輸出

協(xié)程池也整個包:github.com/ivpusic/grpool。協(xié)程池就不展開說了做祝,有興趣的可以看看這個包的實(shí)現(xiàn)砾省。

日志的結(jié)構(gòu)體再一次升級:

type Logger struct {
    level int
    err   *log.Logger
    warn  *log.Logger
    info  *log.Logger
    debug *log.Logger
    p     *grpool.Pool
}

初始化:

logger.p = grpool.NewPool(numWorkers, jobQueueLen)

日志輸出:

func (ll *Logger) Error(format string, v ...interface{}) {
    if LevelError > ll.level {
        return
    }
    ll.p.JobQueue <- func() {
        ll.err.Printf(format, v...)
    }
}

日志行號

如果你一步一步按上面的做了,打印日志設(shè)置了Lshortfile混槐,展示行號的花编兄,你可能會發(fā)現(xiàn)這個時候打印出來的行號有問題。打印日志的時候用到了runtime里面的堆棧信息声登,因為我們封裝了一層狠鸳,所以打印的堆棧深度會發(fā)生變化。簡單的說就是深了一層捌刮。

原生的日志包提供了func (l *Logger) Output(calldepth int, s string) error來控制日志堆棧深度輸出碰煌,我們再次對代碼進(jìn)行調(diào)整。

type Logger struct {
    level int
    err   *log.Logger
    warn  *log.Logger
    info  *log.Logger
    debug *log.Logger
    p     *grpool.Pool
    depth int
}

func (ll *Logger) Error(format string, v ...interface{}) {
    if LevelError > ll.level {
        return
    }
    ll.p.JobQueue <- func() {
        ll.err.Output(ll.depth, fmt.Sprintf(format, v...))
    }
}

我們只封裝了一層绅作,所以深度設(shè)置成3就可以了芦圾。

線程安全

原生包打印日志是線程安全的:

func (l *Logger) Output(calldepth int, s string) error {
    now := time.Now() // get this early.
    var file string
    var line int
    l.mu.Lock() // 看到這里了么?
    defer l.mu.Unlock()
    if l.flag&(Lshortfile|Llongfile) != 0 {
        // release lock while getting caller info - it's expensive.
        l.mu.Unlock()
        var ok bool
        _, file, line, ok = runtime.Caller(calldepth)
        if !ok {
            file = "???"
            line = 0
        }
        l.mu.Lock()
    }
    l.buf = l.buf[:0]
    l.formatHeader(&l.buf, now, file, line)
    l.buf = append(l.buf, s...)
    if len(s) == 0 || s[len(s)-1] != '\n' {
        l.buf = append(l.buf, '\n')
    }
    _, err := l.out.Write(l.buf)
    return err
}

有它的保證俄认,我們也不需要考慮線程安全的問題了个少。

那么問題來了洪乍,fmt包打印日志是線程安全的么?println安全么夜焦?fmtprintln打印日志都打印到了哪里壳澳?有興趣的可以留言一下一起討論。

最后

日志的打印會用到諸如fmt.Sprintf的東西茫经,這個在實(shí)現(xiàn)的時候?qū)玫椒瓷湎锊ā7瓷鋾π阅苡杏绊懀遣挥梅瓷涞脑挻a過于惡心卸伞。

完整的代碼放到了 GitHub 上面抹镊,地址

上面介紹的日志只是在針對輸出到文件荤傲。如果你想輸出有郵件垮耳、ElasticSearch等其它地方,不要在初始化的時候通過各種復(fù)雜配置參數(shù)來實(shí)現(xiàn)遂黍。

我說的是這樣:

NewLogger("es", ...)
NewLogger("smtp", ...)

這樣做的問題就是终佛,我只能用你提供好的東西,如果想擴(kuò)展只能修改日志包了雾家。如果這個包是第三方的包铃彰,那讓別人怎么擴(kuò)展呢?而且這種實(shí)現(xiàn)也不是 Golang 的實(shí)現(xiàn)風(fēng)格榜贴。

其實(shí)大家看看原生的這些包豌研,很多都是通過接口串聯(lián)起來的。原生的 log 包唬党,你可以認(rèn)為他提供的服務(wù)主要是流程方面的服務(wù)鹃共,拼接好要打印的內(nèi)容,包括行號驶拱、時間等等霜浴,保證線程安全,然后調(diào)用Writer來打印蓝纲。如果我們要把日志打印到 ES 里面阴孟,就實(shí)現(xiàn)一個ESWriter。這才是 Golang 風(fēng)格的代碼税迷。


參考文獻(xiàn)

  • 【1】《Go 語言實(shí)戰(zhàn)》
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末永丝,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子箭养,更是在濱河造成了極大的恐慌慕嚷,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,946評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異喝检,居然都是意外死亡嗅辣,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,336評論 3 399
  • 文/潘曉璐 我一進(jìn)店門挠说,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澡谭,“玉大人,你說我怎么就攤上這事损俭⊥芙保” “怎么了?”我有些...
    開封第一講書人閱讀 169,716評論 0 364
  • 文/不壞的土叔 我叫張陵杆兵,是天一觀的道長外永。 經(jīng)常有香客問我,道長拧咳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,222評論 1 300
  • 正文 為了忘掉前任囚灼,我火速辦了婚禮骆膝,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘灶体。我一直安慰自己阅签,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,223評論 6 398
  • 文/花漫 我一把揭開白布蝎抽。 她就那樣靜靜地躺著政钟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪樟结。 梳的紋絲不亂的頭發(fā)上养交,一...
    開封第一講書人閱讀 52,807評論 1 314
  • 那天,我揣著相機(jī)與錄音瓢宦,去河邊找鬼碎连。 笑死,一個胖子當(dāng)著我的面吹牛驮履,可吹牛的內(nèi)容都是我干的鱼辙。 我是一名探鬼主播,決...
    沈念sama閱讀 41,235評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼玫镐,長吁一口氣:“原來是場噩夢啊……” “哼倒戏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起恐似,我...
    開封第一講書人閱讀 40,189評論 0 277
  • 序言:老撾萬榮一對情侶失蹤杜跷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體葱椭,經(jīng)...
    沈念sama閱讀 46,712評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡捂寿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,775評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了孵运。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秦陋。...
    茶點(diǎn)故事閱讀 40,926評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖治笨,靈堂內(nèi)的尸體忽然破棺而出驳概,到底是詐尸還是另有隱情,我是刑警寧澤旷赖,帶...
    沈念sama閱讀 36,580評論 5 351
  • 正文 年R本政府宣布顺又,位于F島的核電站,受9級特大地震影響等孵,放射性物質(zhì)發(fā)生泄漏稚照。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,259評論 3 336
  • 文/蒙蒙 一俯萌、第九天 我趴在偏房一處隱蔽的房頂上張望果录。 院中可真熱鬧,春花似錦咐熙、人聲如沸弱恒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,750評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽返弹。三九已至,卻和暖如春爪飘,著一層夾襖步出監(jiān)牢的瞬間义起,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,867評論 1 274
  • 我被黑心中介騙來泰國打工悦施, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留并扇,地道東北人。 一個月前我還...
    沈念sama閱讀 49,368評論 3 379
  • 正文 我出身青樓抡诞,卻偏偏與公主長得像穷蛹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子昼汗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,930評論 2 361

推薦閱讀更多精彩內(nèi)容