寫在前面
Golang 的log包內(nèi)容不多趋惨,說實(shí)話,直接用來做日志開發(fā)有些簡易惦蚊。主要是缺少一些功能:
- 按日志級別打印和控制日志器虾;
- 日志文件自動分割讯嫂;
- 異步打印日志。
按日志級別打印和控制日志
我們實(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
安全么夜焦?fmt
和println
打印日志都打印到了哪里壳澳?有興趣的可以留言一下一起討論。
最后
日志的打印會用到諸如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)》