
背景
當前的開源日志包有很多,像go中的標準庫log包、glog、logrus、zap。它們每種日志包都有相應的應用場景。四種日志包相關對比如下所示:
標準庫log | 功能簡單,不支持日志級別、日志格式。但是使用簡單,易于快速上手。大型項目較少使用 |
glog | 提供了日志包的基本功能,像日志級別、格式等。適合一些小項目 |
logrus | 功能強大,不僅實現了基本日志功能,還提供了很多高級功能。適合大型項目 |
zap | 功能強大,性能高,適合對日志性能要求高的項目。另外zap的子包zapcore提供了很多底層日志接口,適合二次開發 |
從頭開發一個日志包,可以讓我們了解日志包的底層邏輯,使得我們對日志包有定制需求的時候,可以能夠基于開源的日志包實現我們的功能。所以本文以cuslog為例,看看如何實現我們自己的日志包(代碼:
https://github.com/marmotedu/gopractise-demo/tree/master/log/cuslog)。
代碼結構

cuslog目錄代碼結構

cuslog代碼結構
要實現一個日志包,就需要實現下面三個基本的對象,Entry,Logger,Options。
Entry
代碼:
https://github.com/marmotedu/gopractise-demo/blob/master/log/cuslog/entry.go。
//構造函數
func entry(logger *logger) *Entry {
return &Entry{logger: logger, Buffer: new(bytes.Buffer), Map: make(map[string]interface{}, 5)}
}
/*entry主要方法是write方法,它首先通過e.logger.opt.level 與 level的比較,來判斷
是否要將日志輸出,其中DEBUG最低,FATAL最高,這個從
https://github.com/marmotedu/gopractise-demo/blob/master/log/cuslog/options.go#L19
中可以看到。
*/
func (e *Entry) write(level Level, format string, args ...interface{}) {
if e.logger.opt.level > level {
return
}
e.Time = time.Now()
e.Level = level
e.Format = format
e.Args = args
if !e.logger.opt.disableCaller {
if pc, file, line, ok := runtime.Caller(2); !ok {
e.File = "???"
e.Func = "???"
} else {
e.File, e.Line, e.Func = file, line, runtime.FuncForPC(pc).Name()
e.Func = e.Func[strings.LastIndex(e.Func, "/")+1:]
}
}
e.format()
e.writer()
e.release()
}
Entry的write方法實現了將它Buffer中的數據,寫入到它的logger所配置的output中。
Logger
代碼:
https://github.com/marmotedu/gopractise-demo/blob/master/log/cuslog/logger.go。
/*創建方法,通過sync.Pool緩存對象,提升性能,initOptions是用于初始化logger的options的
各種屬性*/
func New(opts ...Option) *logger {
logger := &logger{opt: initOptions(opts...)}
logger.entryPool = &sync.Pool{New: func() interface{} { return entry(logger) }}
return logger
}
Options
代碼:
https://github.com/marmotedu/gopractise-demo/blob/68e100ee78a3093e6f2434439e7d4b143b9ebf60/log/cuslog/options.go。
/*opts 參數是一系列的用于設置options屬性的函數,比如下面的WithOutput和WithLevel
都是這種函數,initOptions會接收這些函數作為輸入,對o = &options{}進行設置*/
type Option func(*options)
func initOptions(opts ...Option) (o *options) {
o = &options{}
for _, opt := range opts {
opt(o)
}
if o.output == nil {
o.output = os.Stderr
}
if o.formatter == nil {
o.formatter = &TextFormatter{}
}
return
}
func WithOutput(output io.Writer) Option {
return func(o *options) {
o.output = output
}
}
func WithLevel(level Level) Option {
return func(o *options) {
o.level = level
}
}
應用
通過下面的代碼,我們看看整個代碼是如何串起來的。
// 輸出到文件
fd, err := os.OpenFile("test.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatalln("create file test.log failed")
}
defer fd.Close()
l := cuslog.New(cuslog.WithLevel(cuslog.InfoLevel),
cuslog.WithOutput(fd),
cuslog.WithFormatter(&cuslog.JsonFormatter{IgnoreBasicFields: false}),
)
l.Info("custom log with json formatter")
- 上面的整體實現是,將"custom log with json formatter"這段字符串寫入到指定文件里面
- 1到6行創建并打開文件
- 重點是8行,cuslog.WithLevel(cuslog.InfoLevel)、cuslog.WithOutput(fd)、cuslog.WithFormatter(&cuslog.JsonFormatter{IgnoreBasicFields: false})三個函數調用,返回三個函數func(o *options),這些函數都是Option類型,因為type Option func(*options)。
- 然后調用cuslog.New,這個函數上面也給出了,它里面通過initOptions依次調用上面的Option函數,對options對象進行設置,然后把options賦給opt, 并創建logger. logger := &logger{opt: initOptions(opts...)}
- 最后調用l.Info("custom log with json formatter")把字符串輸出到文件中.整個調用鏈是l.info===>通過l的pool獲取entry===>調用entry的write(InfoLevel, FmtEmptySeparate, args...),在這個write函數里面,只有infoLevel比logger.level優先級大或相等,才輸出。并且通過runtime.Caller(2)獲取最上層調用info時的,文件名,行號,函數名等信息。因為這個地方有2層嵌套才調用到entry的write,所以runtime.Caller(2)的參數是2
總結
上面的代碼實現了基本的日志功能,包括日志級別、日志格式配置、輸出文件或標準輸出的設置。但是一些高級的功能,比如按級別分類輸出,Hook能力,結構化日志。這些目前都不支持。
