寫(xiě)在前面
Golang 的log包內(nèi)容不多,說(shuō)實(shí)話,直接用來(lái)做日志開(kāi)發(fā)有些簡(jiǎn)易。主要是缺少一些功能:
- 按日志級(jí)別打印和控制日志;
- 日志文件自動(dòng)分割;
- 異步打印日志。
按日志級(jí)別打印和控制日志
我們實(shí)現(xiàn)的日志模塊將會(huì)支持4個(gè)級(jí)別:
const (
LevelError = iota
LevelWarning
LevelInformational
LevelDebug
)
定義一個(gè)日志結(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)日志級(jí)別控制輸出,并且打印的時(shí)候追加一個(gè)標(biāo)記,比如上面的例子,Error 級(jí)別就會(huì)追加[E]。
這個(gè)實(shí)現(xiàn)已經(jīng)可以了,但是還是有優(yōu)化的空間。比如打印追加標(biāo)記[E]的時(shí)候,用的是字符串加法。字符串加法會(huì)申請(qǐng)新的內(nèi)存,對(duì)性能不是很優(yōu)化。我們需要通過(guò)字符數(shù)組來(lái)優(yōu)化。
但我不會(huì)這么去優(yōu)化了。這個(gè)時(shí)候看一下 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...)
原生包在寫(xiě)日志前綴的時(shí)候就用到了[]byte
來(lái)提升性能。既然人家已經(jīng)提供了,我們還是不要自己造了。那么問(wèn)題來(lái)了,設(shè)置前綴是初始化的時(shí)候就要設(shè)置,打印的時(shí)候自動(dòng)輸出出來(lái)。一個(gè)log.Logger
對(duì)象只能有一個(gè)前綴,而我們需要4種級(jí)別的前綴,這個(gè)如何打印?
type Logger struct {
level int
err *log.Logger
warn *log.Logger
info *log.Logger
debug *log.Logger
}
我們直接申請(qǐng)4個(gè)日志對(duì)象就能解決。為了保證所有級(jí)別都打印到同一個(gè)文件里面,初始化的時(shí)候設(shè)置成同一個(gè)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è)置日志級(jí)別:
func (ll *Logger) SetLevel(l int) {
ll.level = l
}
打印的時(shí)候根據(jù)日志級(jí)別控制輸出。(講一個(gè)我遇到的坑。之前有一次打印日志打太多了,磁盤(pán)都打滿了,就尋思著把日志級(jí)別調(diào)高減少打印內(nèi)容。把級(jí)別調(diào)成 Error 后發(fā)現(xiàn)還是沒(méi)有效果,最后看了看代碼發(fā)現(xiàn)出問(wèn)題的日志打印的是 Error 級(jí)別。。。Error級(jí)別的日志要盡量少打。)
func (ll *Logger) Error(format string, v ...interface{}) {
if LevelError > ll.level {
return
}
ll.err.Printf(format, v...)
}
日志文件自動(dòng)分割
日志文件需要自動(dòng)分割。否則一個(gè)文件過(guò)大,清理磁盤(pán)的時(shí)候這個(gè)文件因?yàn)檫€是打印日志沒(méi)辦法清理。
日志分割我覺(jué)得簡(jiǎn)單的以大小分割就好。
那么日志分割功能如何接入咱們上面實(shí)現(xiàn)的日志模塊呢?關(guān)鍵就在io.Writer
。
type Writer interface {
Write(p []byte) (n int, err error)
}
Writer
這個(gè)接口只有一個(gè)方法,如此簡(jiǎn)單。原生包默認(rèn)打印日志會(huì)輸出到os.Stderr
里面,這是一個(gè)os.File
類(lèi)型的變量,它實(shí)現(xiàn)了Writer
這個(gè)接口。
func (f *File) Write(b []byte) (n int, err error)
寫(xiě)日志的時(shí)候,log 包會(huì)自動(dòng)調(diào)用Write
方法。我們可以自己實(shí)現(xiàn)一個(gè)Writer
,在Write
的時(shí)候計(jì)算一下寫(xiě)入此行日志之后當(dāng)前日志文件大小,如果超過(guò)設(shè)定的值,執(zhí)行一次分割。按日子分割日志也是這個(gè)時(shí)候操作。
推薦用 gopkg.in/natefinch/lumberjack.v2 這個(gè)包來(lái)做日志分割,功能很強(qiáng)大。
jack := &lumberjack.Logger{
Filename: lfn,
MaxSize: maxsize, // megabytes
}
使用也很簡(jiǎn)單,jack
對(duì)象就是一個(gè)Writer
了,可以直接復(fù)制給Logger
使用。
日志的異步輸出
協(xié)程池也整個(gè)包:github.com/ivpusic/grpool。協(xié)程池就不展開(kāi)說(shuō)了,有興趣的可以看看這個(gè)包的實(shí)現(xiàn)。
日志的結(jié)構(gòu)體再一次升級(jí):
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...)
}
}
日志行號(hào)
如果你一步一步按上面的做了,打印日志設(shè)置了Lshortfile
,展示行號(hào)的花,你可能會(huì)發(fā)現(xiàn)這個(gè)時(shí)候打印出來(lái)的行號(hào)有問(wèn)題。打印日志的時(shí)候用到了runtime
里面的堆棧信息,因?yàn)槲覀兎庋b了一層,所以打印的堆棧深度會(huì)發(fā)生變化。簡(jiǎn)單的說(shuō)就是深了一層。
原生的日志包提供了func (l *Logger) Output(calldepth int, s string) error
來(lái)控制日志堆棧深度輸出,我們?cè)俅螌?duì)代碼進(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
}
有它的保證,我們也不需要考慮線程安全的問(wèn)題了。
那么問(wèn)題來(lái)了,fmt
包打印日志是線程安全的么?println
安全么?fmt
和println
打印日志都打印到了哪里?有興趣的可以留言一下一起討論。
最后
日志的打印會(huì)用到諸如fmt.Sprintf
的東西,這個(gè)在實(shí)現(xiàn)的時(shí)候?qū)?huì)用到反射。反射會(huì)對(duì)性能有影響,但是不用反射的話代碼過(guò)于惡心。
完整的代碼放到了 GitHub 上面,地址。
上面介紹的日志只是在針對(duì)輸出到文件。如果你想輸出有郵件、ElasticSearch等其它地方,不要在初始化的時(shí)候通過(guò)各種復(fù)雜配置參數(shù)來(lái)實(shí)現(xiàn)。
我說(shuō)的是這樣:
NewLogger("es", ...)
NewLogger("smtp", ...)
這樣做的問(wèn)題就是,我只能用你提供好的東西,如果想擴(kuò)展只能修改日志包了。如果這個(gè)包是第三方的包,那讓別人怎么擴(kuò)展呢?而且這種實(shí)現(xiàn)也不是 Golang 的實(shí)現(xiàn)風(fēng)格。
其實(shí)大家看看原生的這些包,很多都是通過(guò)接口串聯(lián)起來(lái)的。原生的 log 包,你可以認(rèn)為他提供的服務(wù)主要是流程方面的服務(wù),拼接好要打印的內(nèi)容,包括行號(hào)、時(shí)間等等,保證線程安全,然后調(diào)用Writer
來(lái)打印。如果我們要把日志打印到 ES 里面,就實(shí)現(xiàn)一個(gè)ESWriter
。這才是 Golang 風(fēng)格的代碼。
參考文獻(xiàn)
- 【1】《Go 語(yǔ)言實(shí)戰(zhàn)》