Logrus源碼閱讀(2)--logrus生命周期

上一篇介紹logrus的基本用法, 本篇文章介紹logrus的整個(gè)生命周期

func main() {
    log.Info("hello logrus")
}

從上面這個(gè)簡(jiǎn)單的例子, 追蹤logrus的整個(gè)生命周期

初始化

// exported.go:L108
func Info(args ...interface{}) {
    std.Info(args...)
}

Info函數(shù)的參數(shù)是一個(gè)可變參數(shù), 接收任意類(lèi)型的參數(shù)

// exported.go:L11
var (
    // std is the name of the standard logger in stdlib `log`
    std = New()
)

func StandardLogger() *Logger {
    return std
}

std是一個(gè)全局變量, 是一個(gè)logrus.Logger類(lèi)型. 由于std是包外面無(wú)法訪問(wèn)的, 所以還提供StandardLogger()函數(shù)獲取到std

logrus就是初始化一個(gè)全局變量std, 所有的使用方式都是圍繞著這個(gè)std來(lái)的

上一篇關(guān)于logrus的三種使用方式: logrus.Info, logrus.WithField, Entry(ctx *gin.Context) *logrus.Entry本質(zhì)就是調(diào)用全局變量std

這里留個(gè)思考題, 您是否知道golang的初始化流程呢? std全局變量是什么時(shí)候被初始化完成的?

這里還要引申出另外一個(gè)問(wèn)題: 由于我們維護(hù)一個(gè)全局變量, 但是我們程序是多goroutine的, 當(dāng)程序多個(gè)地方打印日志或者寫(xiě)入文件時(shí), 如何保證日志順序的正確性, 也就是并發(fā)是如何實(shí)現(xiàn)的?

New

初始化那里可以看到std是由New函數(shù)創(chuàng)建出來(lái)的

func New() *Logger {
    return &Logger{
        Out:          os.Stderr,
        Formatter:    new(TextFormatter),
        Hooks:        make(LevelHooks),
        Level:        InfoLevel,
        ExitFunc:     os.Exit,
        ReportCaller: false,
    }
}

logrus由Out, Formatter, Hooks, Level, ExitFunc, ReportCaller組成. 關(guān)于組件的詳細(xì)作用, 下面再具體介紹剖析

其實(shí)還有兩個(gè)重要的字段

  • MutexWrap: 用來(lái)解決并發(fā). // Used to sync writing to the log. Locking is enabled by Default
  • entryPool: 用來(lái)解決Entry gc壓力. // Reusable empty entry

調(diào)用流程

回到log.Info("hello logrus")這個(gè)最簡(jiǎn)單的使用的例子, 追蹤下具體的調(diào)用過(guò)程

image
// exported.go:L107
// Info logs a message at level Info on the standard logger.
func Info(args ...interface{}) {
    std.Info(args...) // <-- 看這里
}

// logger.go:L205
func (logger *Logger) Info(args ...interface{}) {
    logger.Log(InfoLevel, args...) // <-- 看這里
}

// logger.go:L189
func (logger *Logger) Log(level Level, args ...interface{}) {
    if logger.IsLevelEnabled(level) {
        entry := logger.newEntry()
        entry.Log(level, args...) // <-- 看這里
        logger.releaseEntry(entry)
    }
}

// entry.go:L266
func (entry *Entry) Log(level Level, args ...interface{}) {
    if entry.Logger.IsLevelEnabled(level) {
        entry.log(level, fmt.Sprint(args...)) // <-- 看這里
    }
}

// entry.go:L206
func (entry Entry) log(level Level, msg string) {
    ...

    buffer = bufferPool.Get().(*bytes.Buffer)
    buffer.Reset()
    defer bufferPool.Put(buffer)
    entry.Buffer = buffer

    entry.write() // <-- 看這里

    ....
}

// entry.go:L252
func (entry *Entry) write() {
    entry.Logger.mu.Lock()
    defer entry.Logger.mu.Unlock()
    serialized, err := entry.Logger.Formatter.Format(entry)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
    } else {
        _, err = entry.Logger.Out.Write(serialized)
        if err != nil {
            fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
        }
    }
}

雖說(shuō)初始化的時(shí)候, New出來(lái)的是Logger類(lèi)型, 但logrus真正執(zhí)行者卻是Entry. Logger有兩個(gè)比較重要的函數(shù)newEntry, releaseEntry, logrus所有的log函數(shù), 比如: Info, Error.... 最終都會(huì)調(diào)用這兩個(gè)函數(shù)

newEntry && releaseEntry

// logger.go:L90-101
func (logger *Logger) newEntry() *Entry {
    entry, ok := logger.entryPool.Get().(*Entry)
    if ok {
        return entry
    }
    return NewEntry(logger)
}

func (logger *Logger) releaseEntry(entry *Entry) {
    entry.Data = map[string]interface{}{}
    logger.entryPool.Put(entry)
}

Logger使用到了sync.Pool, 用來(lái)解決頻繁創(chuàng)建/釋放Entry對(duì)象造成的gc的壓力. 具體位置就是logger.go:L189

當(dāng)我們使用logrus log相關(guān)函數(shù)時(shí), 必定會(huì)調(diào)用到logger.Log()函數(shù), 該函數(shù)會(huì)調(diào)用newEntry()來(lái)申請(qǐng)Pool內(nèi)存, 調(diào)用完成后會(huì)再調(diào)用releaseEntry()返還給Pool

注意點(diǎn):

  1. 初始化Logger時(shí), New函數(shù)沒(méi)有初始化entryPool, 所以entryPool默認(rèn)返回的是nil
  2. entry, ok := logger.entryPool.Get().(*Entry), 這段代碼是先從Pool獲取內(nèi)存, 然后判斷獲取到的值是否是*Entry類(lèi)型
  3. 在第一次調(diào)用時(shí)由于獲取到的值肯定是nil, 故調(diào)用了NewEntry函數(shù)獲取了一塊內(nèi)存
  4. 不過(guò)在調(diào)用releaseEntry函數(shù)時(shí)將這塊內(nèi)存Put到entryPool里, 后面所有調(diào)用都是從Pool里面獲取

newEntry, releaseEntry這是Sync.Pool的另外一種用法, 可以看這里一個(gè)具體的簡(jiǎn)單的例子

Entry

type Entry struct {
    Logger *Logger // 其實(shí)就是std指針, 后面再說(shuō)具體的作用
    Data Fields    // 就是各種WithXXX所帶的參數(shù)
    Time time.Time // 提供給logrus.WithTime, logrus.WithContext使用
    Level Level    // 日志級(jí)別
    Caller *runtime.Frame  // 當(dāng)設(shè)置SetReportCaller時(shí)使用, 具體后面再說(shuō)
    Message string // 真正打印的日志內(nèi)容
    Buffer *bytes.Buffer // 提供給各種Formatter使用, 其實(shí)就是真正要打印的日志的內(nèi)存地址
    Context context.Context // 提供給logrus.WithTime, logrus.WithContext使用
    err string // 提供一個(gè)能夠包含錯(cuò)誤信息的字段
}
// entry.go:L80-86
func NewEntry(logger *Logger) *Entry {
    return &Entry{
        Logger: logger,
        // Default is three fields, plus one optional.  Give a little extra room.
        Data: make(Fields, 6),
    }
}

注意到到Data其實(shí)就是map[string]interface{}, 其預(yù)先分配了6個(gè)空間(預(yù)先給make函數(shù)?一個(gè)合理元素?cái)?shù)量參數(shù),有助于提升性能。因?yàn)槭孪壬暾?qǐng)?一?大塊內(nèi)存, 可避免后續(xù)操作時(shí)頻繁擴(kuò)張 -- Go 學(xué)習(xí)筆記 第四版. 引申: map是否能用cap函數(shù)計(jì)算其容量? 為什么?)

WithXXX

幾個(gè)比較重要的With函數(shù)

  • WithContext
  • WithField
  • WithFields
  • WithTime
  • WithError

考慮到篇幅過(guò)長(zhǎng), 這個(gè)幾個(gè)函數(shù)具體實(shí)現(xiàn), 下篇介紹logurs高級(jí)用法再說(shuō)

log

不管程序是否調(diào)用WithXXX函數(shù), 最終都會(huì)調(diào)用Entry.log函數(shù). 這是logrus最重要的函數(shù), Hook機(jī)制也就是在這里實(shí)現(xiàn)的

func (entry Entry) log(level Level, msg string) {
    var buffer *bytes.Buffer

    // 判斷時(shí)間是否為空, 如果是空的話, 就設(shè)置entry.Time為當(dāng)前時(shí)間
    if entry.Time.IsZero() {
        entry.Time = time.Now()
    }

    entry.Level = level
    entry.Message = msg
    // 設(shè)置調(diào)用者
    if entry.Logger.ReportCaller {
        entry.Caller = getCaller()
    }

    // 調(diào)用Hook
    entry.fireHooks()

    buffer = bufferPool.Get().(*bytes.Buffer)
    buffer.Reset()
    defer bufferPool.Put(buffer)
    entry.Buffer = buffer

    entry.write()

    entry.Buffer = nil

    // 當(dāng)日志級(jí)別是PanicLevel時(shí), 讓程序直接panic
    if level <= PanicLevel {
        panic(&entry)
    }
}

Hook

logrus提供了一個(gè)很方便的插件功能就是Hook, 其實(shí)現(xiàn)原理很簡(jiǎn)單. 流程調(diào)用的地方就是entry.fireHooks()

func (entry *Entry) fireHooks() {
    entry.Logger.mu.Lock()
    defer entry.Logger.mu.Unlock()
    err := entry.Logger.Hooks.Fire(entry.Level, entry)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
    }
}

我們可以根據(jù)自己的需求自定義Hook, 但需要實(shí)現(xiàn)Hook的interface

type Hook interface {
    Levels() []Level // 用來(lái)確定哪些級(jí)別的日志, 去調(diào)用Hook
    Fire(*Entry) error // 真正執(zhí)行自定義Hook
}

Hook的使用方法

  1. 實(shí)現(xiàn)Levels, Fire函數(shù)
  2. 調(diào)用全局AddHook, 將Hook注冊(cè), ok

github.com/rifflock/lfshook舉例

package main

import (
    rotatelogs "github.com/lestrrat-go/file-rotatelogs"
    "github.com/rifflock/lfshook"
)

func newLfsHook(fileName string, maxRemainCnt uint) log.Hook {
    writer, err := rotatelogs.New(
        fileName+".%Y%m%d%H%M",
        rotatelogs.WithLinkName("ling_nest_log"),
        rotatelogs.WithRotationTime(time.Hour*time.Duration(config.Config.GetInt("log.time"))),
        rotatelogs.WithRotationCount(maxRemainCnt),
    )

    if err != nil {
        log.Errorf("config local file system for logger error: %v", err)
    }

    lfsHook := lfshook.NewHook(lfshook.WriterMap{
        log.DebugLevel: writer,
        log.InfoLevel:  writer,
        log.WarnLevel:  writer,
        log.ErrorLevel: writer,
        log.FatalLevel: writer,
        log.PanicLevel: writer,
    }, &log.JSONFormatter{})

    return lfsHook
}

func main() {
    fileName := "log.txt"
    logrus.AddHook(newLfsHook(fileName, 100))
    logrus.Info("xxxx")
}

值得注意的是: 由于logrus本身并不提供寫(xiě)文件, 并且按照日期自動(dòng)分割, 刪除過(guò)期日志文件的功能. 一般情況下大家都是使用github.com/rifflock/lfshook配合github.com/lestrrat-go/file-rotatelogs來(lái)實(shí)現(xiàn)相關(guān)的功能

原理:

type Logger struct {
    ...

    Hooks LevelHooks

    ...
}
type LevelHooks map[Level][]Hook
func (hooks LevelHooks) Add(hook Hook) {
    for _, level := range hook.Levels() {
        hooks[level] = append(hooks[level], hook)
    }
}

func (hooks LevelHooks) Fire(level Level, entry *Entry) error {
    for _, hook := range hooks[level] {
        if err := hook.Fire(entry); err != nil {
            return err
        }
    }

    return nil
}
  1. 調(diào)用AddHook時(shí), 將Hook加入到LevelHooks map中
  2. 程序打印log, 會(huì)最終執(zhí)行到Entry.log()
  3. Entry.log()會(huì)調(diào)用fireHooks()
  4. fireHooks又會(huì)調(diào)用LevelHooks Fire()函數(shù), 該函數(shù)會(huì)遍歷所有的Hook, 從而執(zhí)行相應(yīng)的Hook

ReportCaller

import (
    log "github.com/sirupsen/logrus"
)

func main() {
    log.SetReportCaller(true)
    log.Info("hello logrus")
}

輸出:

INFO[0000]/Users/haohongfan/goproject/test/logrus_test/main.go:37 main.main() hello logrus

對(duì)比不開(kāi)啟ReportCaller的日志, 多了下面字段:

  1. 日志打印的文件名字
  2. 日志打印的行號(hào)
  3. 日志打印的函數(shù)名字

關(guān)于如何實(shí)現(xiàn)的, 推薦先看鳥(niǎo)窩博客- <<如何在Go的函數(shù)中得到調(diào)用者函數(shù)名?>>

其實(shí)ReportCaller主要是提供給Formatter使用的, 例如: JSONFormatter

// json_formatter.go:L91-103
if entry.HasCaller() {
    funcVal := entry.Caller.Function
    fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
    if f.CallerPrettyfier != nil {
        funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
    }
    if funcVal != "" {
        data[f.FieldMap.resolve(FieldKeyFunc)] = funcVal
    }
    if fileVal != "" {
        data[f.FieldMap.resolve(FieldKeyFile)] = fileVal
    }
}

由于打算結(jié)合logrus的實(shí)現(xiàn), 出一篇介紹golang如何獲取調(diào)用者的文件名/函數(shù)名/行號(hào)等等, 及其實(shí)現(xiàn)的原理的文章, 就不在繼續(xù)擴(kuò)展了

這里簡(jiǎn)單說(shuō)下logrus的實(shí)現(xiàn)過(guò)程.

規(guī)則:

  1. 當(dāng)設(shè)置SetReportCaller(true)時(shí), 會(huì)最終在Entry.log()函數(shù)調(diào)用entry.Caller = getCaller()
  2. getCaller()函數(shù)有個(gè)callerInitOnce sync.Once變量, 在第一次被調(diào)用時(shí)會(huì)獲取logrus的包名字是github.com/sirupsen/logrus
  3. 緊接著調(diào)用runtime.CallersFrames獲取到所有函數(shù)調(diào)用棧
  4. 然后比對(duì)函數(shù)棧的package名字, 與github.com/sirupsen/logrus相比, 如果不相等, 則是去掉logrus包的第一個(gè)調(diào)用者; 否則continue

比如:

func main() {
    log.SetReportCaller(true)
    log.Info("hello logrus")
}

函數(shù)調(diào)用棧的順序是:

  1. github.com/sirupsen/logrus.(*Logger).Log
  2. github.com/sirupsen/logrus.(*Logger).Info
  3. github.com/sirupsen/logrus.Info
  4. main.main

按照上面的規(guī)則, 由于1,2,3獲取到的package包名都是github.com/sirupsen/logrus, 故continue, 最終獲取到的第一個(gè)函數(shù)是main.main的*runtime.Frame. Frame包含著文件名, 函數(shù)名, 行號(hào)等等

我們回過(guò)頭看logrus獲取調(diào)用者的這個(gè)實(shí)現(xiàn). 是靠著完全遍歷匹配package名來(lái)獲取調(diào)用者的. 先拋去runtime.Caller等相關(guān)的函數(shù)是否慢的問(wèn)題, 單說(shuō)這個(gè)完全匹配的過(guò)程已經(jīng)浪費(fèi)了大量時(shí)間處理這個(gè)事情. 所以我們?nèi)罩驹趓elease版本下還是盡量不要開(kāi)啟這個(gè)選項(xiàng), logrus也不建議使用這開(kāi)啟這個(gè)選項(xiàng)

write

logrus另外一個(gè)非常重要的函數(shù)

// entry.go:L252-264
func (entry *Entry) write() {
    entry.Logger.mu.Lock()
    defer entry.Logger.mu.Unlock()
    serialized, err := entry.Logger.Formatter.Format(entry)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
    } else {
        _, err = entry.Logger.Out.Write(serialized)
        if err != nil {
            fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
        }
    }
}

看著很簡(jiǎn)單, 其實(shí)包含的內(nèi)容還是挺多的: Formatter, Out

Formatter

type Formatter interface {
    Format(*Entry) ([]byte, error)
}

由于Formatter是個(gè)接口類(lèi)型, 故可以根據(jù)自己的需求, 實(shí)現(xiàn)自己的Formatter, 只需要實(shí)現(xiàn)對(duì)應(yīng)的Format函數(shù)即可

繼續(xù)查看Formatter的具體調(diào)用過(guò)程(暫且不管Mutex的問(wèn)題)

// 執(zhí)行Formatter的地方
func (entry *Entry) write() {
    ...

    entry.Logger.mu.Lock()
    defer entry.Logger.mu.Unlock()
    serialized, err := entry.Logger.Formatter.Format(entry)

    ...
}
// 設(shè)置Formatter的地方
// SetFormatter sets the standard logger formatter.
func SetFormatter(formatter Formatter) {
    std.SetFormatter(formatter)
}

在調(diào)用logrus.SetFormatter()函數(shù)后, LoggerFormatter字段就被設(shè)置為你想使用的XXXFormatter了, 如果沒(méi)有設(shè)置那么就是默認(rèn)的TextFormatter

程序執(zhí)行到entry.Logger.Formatter.Format(entry)時(shí), 就會(huì)執(zhí)行具體的XXXFormatter的Format函數(shù), 從而執(zhí)行具體的序列化過(guò)程

這里由于篇幅限制只解析比較簡(jiǎn)單的JSONFormatter, 這個(gè)其實(shí)經(jīng)常被用到的Formatter.

JSONFormatter

// json_formatter.go:L24-54
type JSONFormatter struct {
    TimestampFormat string // 設(shè)置Formatter時(shí)間格式
    DisableTimestamp bool  // 控制序列化時(shí)是否顯示時(shí)間
    DataKey string // 配合主要是配合WithFields使用
    FieldMap FieldMap // 其實(shí)用處很小, 就是讓用戶(hù)自定義序列化字段的名字
    CallerPrettyfier func(*runtime.Frame) (function string, file string) // 配合SetReportCaller, 不需要太關(guān)注
    PrettyPrint bool // 讓Json格式化輸出
}

主要字段介紹

1.TimestampFormat

Time的時(shí)間格式, 設(shè)置JSONFormatter TimestampFormat字段時(shí)就可以選擇下面這些常量. 默認(rèn)值:time.RFC3339

ANSIC       = "Mon Jan _2 15:04:05 2006"
UnixDate    = "Mon Jan _2 15:04:05 MST 2006"
RubyDate    = "Mon Jan 02 15:04:05 -0700 2006"
RFC822      = "02 Jan 06 15:04 MST"
RFC822Z     = "02 Jan 06 15:04 -0700" // RFC822 with numeric zone
RFC850      = "Monday, 02-Jan-06 15:04:05 MST"
RFC1123     = "Mon, 02 Jan 2006 15:04:05 MST"
RFC1123Z    = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone
RFC3339     = "2006-01-02T15:04:05Z07:00"
RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
Kitchen     = "3:04PM"
// Handy time stamps.
Stamp      = "Jan _2 15:04:05"
StampMilli = "Jan _2 15:04:05.000"
StampMicro = "Jan _2 15:04:05.000000"
StampNano  = "Jan _2 15:04:05.000000000
2.DataKey
func main() {
    log.SetFormatter(&log.JSONFormatter{
        DataKey: "hhf",
    })
    log.WithFields(log.Fields{"k1": "v1"}).Info("hello logrus")
}

輸出:

{"hhf":{"k1":"v1"},"level":"info","msg":"hello logrus","time":"2019-10-09T13:31:05+08:00"}

當(dāng)沒(méi)有注釋掉DataKey: "hhf"時(shí), 輸出就會(huì)變成下面

{"k1":"v1","level":"info","msg":"hello logrus","time":"2019-10-09T13:32:26+08:00"}

其實(shí)就是用DataKey來(lái)包裝一下WithFields的k-v字段

3.FieldMap
func main() {
    log.SetFormatter(&log.JSONFormatter{
        FieldMap: log.FieldMap{
            log.FieldKeyTime:  "@timestamphhf",
            log.FieldKeyLevel: "@levelhhf",
            log.FieldKeyMsg:   "@messagehhf",
            log.FieldKeyFunc:  "@callerhhf",
        },
    })
    log.WithFields(log.Fields{"k1": "v1"}).Info("hello logrus"
}

輸出:

{"@levelhhf":"info","@messagehhf":"hello logrus","@timestamphhf":"2019-10-09T13:42:09+08:00","k1":"v1"}

主要的key有下面這幾種類(lèi)型

FieldKeyMsg            = "msg"
FieldKeyLevel          = "level"
FieldKeyTime           = "time"
FieldKeyLogrusError    = "logrus_error"
FieldKeyFunc           = "func"
FieldKeyFile           = "file
4.PrettyPrint
func main() {
    log.SetFormatter(&log.JSONFormatter{PrettyPrint: true})
    log.WithFields(log.Fields{"k1": "v1"}).Info("hello logrus")
}

輸出結(jié)果:

{
  "k1": "v1",
  "level": "info",
  "msg": "hello logrus",
  "time": "2019-10-09T13:52:18+08:00"
}

Format

// json_formatter.go:L57-121
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
    // 將Entry的WithFields的kv值遍歷放入到map[string]interface{}類(lèi)型的data中
    data := make(Fields, len(entry.Data)+4)
    for k, v := range entry.Data {
        switch v := v.(type) {
        case error:
            // Otherwise errors are ignored by `encoding/json`
            // https://github.com/sirupsen/logrus/issues/137
            data[k] = v.Error()
        default:
            data[k] = v
        }
    }

    // 判斷是否存在DataKey, 如果是就用DataKey包裝一下data
    if f.DataKey != "" {
        newData := make(Fields, 4)
        newData[f.DataKey] = data
        data = newData
    }

    // 該函數(shù)判斷調(diào)用WithFields時(shí), 當(dāng)用戶(hù)自定義的Key與logrus內(nèi)置的key相同時(shí), 
    // 用戶(hù)自定義的key會(huì)轉(zhuǎn)換成fields.xx. 例如:logrus.WithField("level", 1).Info("hello"), 
    // 由于level跟內(nèi)置的FieldKeyLevel沖突了, 那么輸出就會(huì)變成
    // {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."}
    prefixFieldClashes(data, f.FieldMap, entry.HasCaller())

    // 設(shè)置時(shí)間的序列化方式
    timestampFormat := f.TimestampFormat
    if timestampFormat == "" {
        timestampFormat = defaultTimestampFormat
    }

    // 判斷entry的error是否有值, 進(jìn)行相關(guān)的序列化
    if entry.err != "" {
        data[f.FieldMap.resolve(FieldKeyLogrusError)] = entry.err
    }

    // 判斷是否禁用Timestamp, 如果不禁用, 就將時(shí)間戳按照相應(yīng)的格式序列化. entry.Time在entry.log()函數(shù)里進(jìn)行了初始化
    // if entry.Time.IsZero() {
    //  entry.Time = time.Now()
    // }
    if !f.DisableTimestamp {
        data[f.FieldMap.resolve(FieldKeyTime)] = entry.Time.Format(timestampFormat)
    }

    // 設(shè)置日志的具體內(nèi)容
    data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message
    // 設(shè)置日志級(jí)別
    data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String()
    // 序列化調(diào)用位置
    if entry.HasCaller() {
        funcVal := entry.Caller.Function
        fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
        if f.CallerPrettyfier != nil {
            funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
        }
        if funcVal != "" {
            data[f.FieldMap.resolve(FieldKeyFunc)] = funcVal
        }
        if fileVal != "" {
            data[f.FieldMap.resolve(FieldKeyFile)] = fileVal
        }
    }

    // entry.Buffer是在entry.log()函數(shù)里(entry.go:L226-229)從sync.Pool里獲取到一塊內(nèi)容空間
    // 目的是: 防止JSONFormatter每次調(diào)用都會(huì)去申請(qǐng)空間, 減小GC壓力
    var b *bytes.Buffer
    if entry.Buffer != nil {
        b = entry.Buffer
    } else {
        b = &bytes.Buffer{}
    }

    // 將Buffer提供給json encoder使用
    encoder := json.NewEncoder(b)
    if f.PrettyPrint {
        encoder.SetIndent("", "  ")
    }
    if err := encoder.Encode(data); err != nil {
        return nil, fmt.Errorf("failed to marshal fields to JSON, %v", err)
    }

    // 序列化完成, 將序列化的內(nèi)容返回
    return b.Bytes(), nil
}

Out

Formatter介紹完了, 回到上面write()函數(shù)繼續(xù)剖析Out相關(guān)

// entry.go:L252-264
func (entry *Entry) write() {
    entry.Logger.mu.Lock()
    defer entry.Logger.mu.Unlock()
    serialized, err := entry.Logger.Formatter.Format(entry)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
    } else {
        _, err = entry.Logger.Out.Write(serialized)
        if err != nil {
            fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
        }
    }
}

在沒(méi)有調(diào)用SetOutput時(shí), 默認(rèn)的Out是os.Stderr, 所以默認(rèn)情況下基本都打印到終端里, 沒(méi)有存入文件

即使logrus可以提供io.Writter, 但是還是不建議在這里將日志落盤(pán), 還是使用lfsHook來(lái)做這個(gè)事情

在這里也回答上面一個(gè)問(wèn)題, 為什么在Entry結(jié)構(gòu)體里面會(huì)有Logger指針存在?

答: 以O(shè)ut舉例, 由于我們?cè)谡{(diào)用logrus.SetOutput()函數(shù)時(shí), Out是設(shè)置給Logger的, 但是真正的使用者卻是Entry. 故需要將Logger傳給Entry一份

logrus如何保證并發(fā)的正確性

logrus的并發(fā)控制的正確性是靠著Logger.Mutex來(lái)實(shí)現(xiàn)的. 程序中調(diào)用Logger.Mutex地方有幾處:

  1. fireHooks() entry.go:L243
  2. write() entry.go:L252
  3. AddHook() logger.go:L313
  4. ReplaceHook() logger.go:L345
  5. SetFormatter() logger.go:L235
  6. SetNoLock() logger.go:L294
  7. SetOutput() logger.go:L332
  8. SetReportCaller() logger.go:338

最重要的兩處就是fireHooks(), write()

func (entry *Entry) fireHooks() {
    entry.Logger.mu.Lock()
    defer entry.Logger.mu.Unlock()
    err := entry.Logger.Hooks.Fire(entry.Level, entry)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
    }
}
func (entry *Entry) write() {
    entry.Logger.mu.Lock()
    defer entry.Logger.mu.Unlock()
    serialized, err := entry.Logger.Formatter.Format(entry)
    ....
}

可以觀察到, 不管有多少goroutine在調(diào)用logrus, 都是靠著資源競(jìng)爭(zhēng)來(lái)保證順序的正確性

查看整個(gè)logrus的源碼, logrus只有一個(gè)goroutine順序處理日志數(shù)據(jù), 并且沒(méi)有相關(guān)的buffer來(lái)保存日志信息, 這就造成logrus的整體效率是不高的

后面一篇文章會(huì)專(zhuān)門(mén)對(duì)比zap之間的差別, 敬請(qǐng)期待

總結(jié)

至此, logrus的主體源碼已經(jīng)解析完畢

一句話總結(jié)其生命周期就是: logrus是在編譯期就確定的一個(gè)全局變量, 伴隨著我們程序的整個(gè)生命周期而存在. 最重要的組件是: Formatter, Hook. 良好的序列化機(jī)制, 方便的插件開(kāi)發(fā)是我們選擇logrus的原因

參考文檔

  1. 鳥(niǎo)窩博客 - 如何在Go的函數(shù)中得到調(diào)用者函數(shù)名? https://colobu.com/2018/11/03/get-function-name-in-go/
  2. logrus https://github.com/sirupsen/logrus
  3. file-rotatelogs https://github.com/lestrrat-go/file-rotatelogs
  4. lfshook https://github.com/rifflock/lfshook
  5. dingrus https://github.com/dandans-dan/dingrus
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,494評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,283評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,953評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,714評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,186評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,410評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,940評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,776評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,976評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,210評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,642評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,878評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,654評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,958評(píng)論 2 373