beego源码学习-日志模块

安装说明

这是一个用来处理日志的库,它的设计思路来自于 database/sql,目前支持的引擎有 file、console、net、smtp、es、slack,可以通过如下方式进行安装:

go get github.com/astaxie/beego/logs

使用需要导入包:

import github.com/astaxie/beego/logs

添加输出引擎(log 支持同时输出到多个引擎):

logs.SetLogger("console")

//添加输出引擎也支持第二个参数,用来表示配置信息,不同引擎,配置不同
logs.SetLogger(logs.AdapterFile,`{"filename":"project.log","level":7,"maxlines":0,"maxsize":0,"daily":true,"maxdays":10,"color":true}`)

日志适配器注册

  • 日志适配器的注册入口
//=======================log.go==========================
// Name for adapter with beego official support
const (
    AdapterConsole   = "console"
    AdapterFile      = "file"
    AdapterMultiFile = "multifile"
    AdapterMail      = "smtp"
    AdapterConn      = "conn"
    AdapterEs        = "es"
    AdapterJianLiao  = "jianliao"
    AdapterSlack     = "slack"
    AdapterAliLS     = "alils"
)
  • Logger接口定义了每一种适配器需要实现的四个功能:
  1. 初始化
  2. 写入消息
  3. 销毁
  4. 刷新
// Logger defines the behavior of a log provider.
type Logger interface {
    Init(config string) error
    WriteMsg(when time.Time, msg string, level int) error
    Destroy()
    Flush()
}
  • 全局变量adapters
//全局变量adapters是一个map,关联了每个适配器的名称以及对应的创建方法
type newLoggerFunc func() Logger
var adapters = make(map[string]newLoggerFunc)
  • 注册方法
//Register 根据名称进行日志注册,名称重复注册或 nil 会 panics
func Register(name string, log newLoggerFunc) {
    ......
    adapters[name] = log
}
  • 示例:注册file适配器
//=======================log.go==========================
type fileLogWriter struct {......}

func init() {
    Register(AdapterFile, newFileWriter)
}

//创建一个 FileLogWriter 对象作为 LoggerInterface 的返回
func newFileWriter() Logger {
    w := &fileLogWriter{
        Daily:      true,
        MaxDays:    7,
        ......
    }
    return w
}

日志模块主体

  • 主要结构体
//=======================log.go==========================
// BeeLogger 结构体声明
// BeeLogger is default logger in beego application.
type BeeLogger struct {
    lock                sync.Mutex
    level               int
    init                bool
    enableFuncCallDepth bool
    loggerFuncCallDepth int
    asynchronous        bool
    prefix              string
    msgChanLen          int64
    msgChan             chan *logMsg
    signalChan          chan string
    wg                  sync.WaitGroup
    outputs             []*nameLogger
}

//结构体:日志名
type nameLogger struct {
    Logger
    name string
}

//结构体:日志信息
type logMsg struct {
    level int
    msg   string
    when  time.Time
}
  • 日志模块创建了一个全局变量beeLogger,默认使用控制台输出日志
// NewLogger 返回一个 BeeLogger 实例
// channelLen means the number of messages in chan(used where asynchronous is true).
// if the buffering chan is full, logger adapters write to file or other way.
func NewLogger(channelLens ...int64) *BeeLogger {
    bl := new(BeeLogger)
    ......
    bl.setLogger(AdapterConsole)
    return bl
}

// beeLogger 引用使用的应用 logger
var beeLogger = NewLogger()
  • 使用

通过创建NewLogger实例(也就是初始化了一个BeeLogger),然后调用SetLogger()设置使用的日志适配器。

import "github.com/astaxie/beego/logs"

log := NewLogger(10000)
log.SetLogger("console", "")

log.Trace("trace")
log.Info("info")
log.Warn("warning")
log.Debug("debug")
log.Critical("critical")

默认设置

  • 默认日志等级 LevelDebug
//=======================log.go==========================
// RFC5424 log message levels.
const (
    LevelEmergency = iota
    LevelAlert
    LevelCritical
    LevelError
    LevelWarning
    LevelNotice
    LevelInformational
    LevelDebug
)

//创建 Logger 实例时,默认 LevelDebug 等级
func NewLogger(channelLens ...int64) *BeeLogger {
    bl := new(BeeLogger)
    bl.level = LevelDebug
    ......
    return bl
}
  • 默认适配器 console控制台
//=======================console.go=======================
// consoleWriter 实现了 LoggerInterface 的4个方法:Init、WriteMsg、Destroy、Flush
type consoleWriter struct {
    lg       *logWriter          //继承自log.go
    Level    int  `json:"level"`
    Colorful bool `json:"color"` //this filed is useful only when system's terminal supports color
}

// NewConsole 创建一个 ConsoleWriter 实例作为 LoggerInterface 的返回
func NewConsole() Logger {
    cw := &consoleWriter{
        lg:       newLogWriter(ansicolor.NewAnsiColorWriter(os.Stdout)),
        Level:    LevelDebug,
        Colorful: true,
    }
    return cw
}

//=======================log.go==========================
type logWriter struct {
    sync.Mutex
    writer io.Writer
}

func newLogWriter(wr io.Writer) *logWriter {
    return &logWriter{writer: wr}
}

提供的功能

  • 设置日志等级
//=======================log.go==========================
func (bl *BeeLogger) SetLevel(l int) {
    bl.level = l
}

func SetLevel(l int) {
    beeLogger.SetLevel(l)
}
  • 记录日志文件和行数
//=======================log.go==========================
//方法:设置在日志中记录文件名
func EnableFuncCallDepth(b bool) {
    beeLogger.enableFuncCallDepth = b
}
//方法:设置在日志中记录行号
func SetLogFuncCallDepth(d int) {
    beeLogger.loggerFuncCallDepth = d
}

//示例:app.go文件 214行
//2020/11/30 17:01:22.282 [I] [app.go:214]  http server Running on http://:7001
  • 设置日志颜色
//=======================console.go==========================
var colors = []brush{
    newBrush("1;37"), // Emergency          white
    newBrush("1;36"), // Alert              cyan
    newBrush("1;35"), // Critical           magenta
    newBrush("1;31"), // Error              red
    newBrush("1;33"), // Warning            yellow
    newBrush("1;32"), // Notice             green
    newBrush("1;34"), // Informational      blue
    newBrush("1;44"), // Debug              Background blue
}

// brush is a color join function
type brush func(string) string

/**
创建一个颜色刷子
格式:\033[显示方式;前景色;背景色m
参数说明:
前景色            背景色           颜色
---------------------------------------
30                40              黑色
31                41              红色
32                42              绿色
33                43              黃色
34                44              蓝色
35                45              紫红色
36                46              青蓝色
37                47              白色
显示方式           意义
-------------------------
0                终端默认设置
1                高亮显示
4                使用下划线
5                闪烁
7                反白显示
8                不可见
*/
func newBrush(color string) brush {
    pre := "\033["
    reset := "\033[0m"
    return func(text string) string {
        return pre + color + "m" + text + reset
    }
}

//在 console 的 WriteMsg 方法中调用颜色渲染
func (c *consoleWriter) WriteMsg(when time.Time, msg string, level int) error {
    ......
    if c.Colorful {
        msg = strings.Replace(msg, levelPrefix[level], colors[level](levelPrefix[level]), 1)
    }
    ......
}
  • 业务中记录日志
log.Trace("trace")
log.Info("info")
log.Warn("warning")
log.Debug("debug")
log.Critical("critical")

生成日志

  • 生成日志内容 writeMsg
//=======================log.go==========================
//变量:日志等级前缀
var levelPrefix = [LevelDebug + 1]string{"[M]", "[A]", "[C]", "[E]", "[W]", "[N]", "[I]", "[D]"}

//方法:写入日志
func (bl *BeeLogger) writeMsg(logLevel int, msg string, v ...interface{}) error {
    //未初始化进行加锁,设置默认日志适配器
    if !bl.init {
        bl.lock.Lock()
        bl.setLogger(AdapterConsole)
        bl.lock.Unlock()
    }
    //日志长度>0,进行格式化
    if len(v) > 0 {
        msg = fmt.Sprintf(msg, v...)
    }

    msg = bl.prefix + " " + msg
    //设置当前时间
    when := time.Now()
    if bl.enableFuncCallDepth {
        //获取文件名
        _, file, line, ok := runtime.Caller(bl.loggerFuncCallDepth)
        if !ok {
            file = "???"
            line = 0
        }
        _, filename := path.Split(file)
        //获取行号
        msg = "[" + filename + ":" + strconv.Itoa(line) + "] " + msg
    }

    //在文件名信前增加,日志等级标志,如:【D】debug、【I】info
    if logLevel == levelLoggerImpl {
        // set to emergency to ensure all log will be print out correctly
        logLevel = LevelEmergency
    } else {
        msg = levelPrefix[logLevel] + " " + msg
    }

    //异步操作通过日志信息池获取消息并发送给消息管道,同步直接写入
    if bl.asynchronous {
        lm := logMsgPool.Get().(*logMsg)
        lm.level = logLevel
        lm.msg = msg
        lm.when = when
        if bl.outputs != nil {
            bl.msgChan <- lm
        } else {
            logMsgPool.Put(lm)
        }
    } else {
        bl.writeToLoggers(when, msg, logLevel)
    }
    return nil
}
  • 日志时间格式化
//=======================logger.go==========================
//日志日期格式化
func (lg *logWriter) writeln(when time.Time, msg string) {
    lg.Lock()
    //将时间格式化成"yyyy/mm/dd hh:mm:ss "格式
    h, _, _ := formatTimeHeader(when)
    lg.writer.Write(append(append(h, msg...), '\n'))
    lg.Unlock()
}

//=======================console.go==========================
//在控制台调用WriteMsg写入日志时触发
func (c *consoleWriter) WriteMsg(when time.Time, msg string, level int) error {
    ......
    c.lg.writeln(when, msg)
    return nil
}

文件日志适配器

  • 文件适配器结构体和构造函数
type fileLogWriter struct {......}

func newFileWriter() Logger {
    w := &fileLogWriter{
        Daily:      true,       //按日分割
        MaxDays:    7,          //最大保留天数
        Hourly:     false,      //按小时分割
        MaxHours:   168,        //保持小时数
        // DoRotate means it need to write file in new file.
        // new file name like xx.2013-01-01.log (daily) or xx.001.log (by line or size)
        Rotate:     true,   
        RotatePerm: "0440",
        Level:      LevelTrace, //默认日志等级
        Perm:       "0660",
        MaxLines:   10000000,   //最大行数
        MaxFiles:   999,        //最大文件数
        MaxSize:    1 << 28,    //文件大小
    }
    return w
}
  • 初始化
// 配置文件json
//  {
//  "filename":"logs/beego.log",
//  "maxLines":10000,
//  "maxsize":1024,
//  "daily":true,
//  "maxDays":15,
//  "rotate":true,
//      "perm":"0600"
//  }
func (w *fileLogWriter) Init(jsonConfig string) error {
    //将json格式的配置解析为 fileLogWriter 对象
    err := json.Unmarshal([]byte(jsonConfig), w)
    if err != nil {
        return err
    }
    if len(w.Filename) == 0 {
        return errors.New("jsonconfig must have filename")
    }
    //处理.log后缀
    w.suffix = filepath.Ext(w.Filename)
    w.fileNameOnly = strings.TrimSuffix(w.Filename, w.suffix)
    if w.suffix == "" {
        w.suffix = ".log"
    }
    err = w.startLogger()
    return err
}
  • 初始化文件属性
func (w *fileLogWriter) initFd() error {
    fd := w.fileWriter
    fInfo, err := fd.Stat()
    if err != nil {
        return fmt.Errorf("get stat err: %s", err)
    }
    //获取文件大小
    w.maxSizeCurSize = int(fInfo.Size())
    //获取当前时间
    w.dailyOpenTime = time.Now()
    w.dailyOpenDate = w.dailyOpenTime.Day()
    //获取文件打开时间
    w.hourlyOpenTime = time.Now()
    w.hourlyOpenDate = w.hourlyOpenTime.Hour()
    w.maxLinesCurLines = 0
    //启动新的线程并发处理文件分割
    if w.Hourly {
        go w.hourlyRotate(w.hourlyOpenTime)
    } else if w.Daily {
        go w.dailyRotate(w.dailyOpenTime)
    }
    //文件行数
    if fInfo.Size() > 0 && w.MaxLines > 0 {
        count, err := w.lines()
        if err != nil {
            return err
        }
        w.maxLinesCurLines = count
    }
    return nil
}
  • 文件切割
func (w *fileLogWriter) dailyRotate(openTime time.Time) {
    // 设置定时器,定时器间隔每24小时,精度纳秒
    y, m, d := openTime.Add(24 * time.Hour).Date()
    nextDay := time.Date(y, m, d, 0, 0, 0, 0, openTime.Location())
    tm := time.NewTimer(time.Duration(nextDay.UnixNano() - openTime.UnixNano() + 100))
    //以定时间隔从渠道获取数据
    <-tm.C
    //竞争锁
    w.Lock()
    if w.needRotateDaily(0, time.Now().Day()) {
        if err := w.doRotate(time.Now()); err != nil {
            fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err)
        }
    }
    w.Unlock()
}
  • 创建日志文件
func (w *fileLogWriter) createLogFile() (*os.File, error) {
    //打开文件并格式化未int
    perm, err := strconv.ParseInt(w.Perm, 8, 64)
    if err != nil {
        return nil, err
    }

    filepath := path.Dir(w.Filename)
    os.MkdirAll(filepath, os.FileMode(perm))

    //打开文件,如果不存在则创建,操作权限:只写并追加写权限
    fd, err := os.OpenFile(w.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(perm))
    if err == nil {
        //确认操作权限是用户设置的
        os.Chmod(w.Filename, os.FileMode(perm))
    }
    return fd, err
}
  • 处理过期日志文件
func (w *fileLogWriter) deleteOldLog() {
    //处理文件路径
    dir := filepath.Dir(w.Filename)
    absolutePath, err := filepath.EvalSymlinks(w.Filename)
    if err == nil {
        dir = filepath.Dir(absolutePath)
    }
    //遍历路径下的文件
    filepath.Walk(dir, func(path string, info os.FileInfo, err error) (returnErr error) {
        //如果执行中有 recover,延迟到方法结束返回错误
        defer func() {
            if r := recover(); r != nil {
                fmt.Fprintf(os.Stderr, "Unable to delete old log '%s', error: %v\n", path, r)
            }
        }()

        if info == nil {
            return
        }
        if w.Hourly {
            //如果不是目录 && 超过最大保留期限 && 文件名一致 && 文件后缀一致,那么就删除文件
            if !info.IsDir() && info.ModTime().Add(1 * time.Hour * time.Duration(w.MaxHours)).Before(time.Now()) {
                if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) &&
                strings.HasSuffix(filepath.Base(path), w.suffix) {
                    os.Remove(path)
                }
            }
        } else if w.Daily {
            if !info.IsDir() && info.ModTime().Add(24 * time.Hour * time.Duration(w.MaxDays)).Before(time.Now()) {
                if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) &&
                strings.HasSuffix(filepath.Base(path), w.suffix) {
                    os.Remove(path)
                }
            }
        }
        return
    })
}

异步日志

beego日志默认使用同步日志写入的方式,因为写入文件前需要进行加锁,并发下有明显的性能影响,所以采用异步日志写入,由不同的 goroutine 来处理日志输出、发送日志给渠道、从渠道接收日志。

func NewLogger(channelLens ...int64) *BeeLogger {
    bl := new(BeeLogger)
    .....
    bl.msgChanLen = append(channelLens, 0)[0]
    if bl.msgChanLen <= 0 {
        bl.msgChanLen = defaultAsyncMsgLen
    }
    bl.signalChan = make(chan string, 1)
    ......
}
  • 设置渠道大小(默认1000)
func Async(msgLen ...int64) *BeeLogger {
    return beeLogger.Async(msgLen...)
}
func (bl *BeeLogger) Async(msgLen ...int64) *BeeLogger {
    bl.lock.Lock()
    defer bl.lock.Unlock()
    if bl.asynchronous {
        return bl
    }
    //设置日志为异步
    bl.asynchronous = true
    if len(msgLen) > 0 && msgLen[0] > 0 {
        bl.msgChanLen = msgLen[0]
    }
    //设置渠道大小
    bl.msgChan = make(chan *logMsg, bl.msgChanLen)
    //从消息池中取消息,如果为空时,返回哟1个 logMsg 空接口
    logMsgPool = &sync.Pool{
        New: func() interface{} {
            return &logMsg{}
        },
    }
    //同步计数+1
    bl.wg.Add(1)
    //启动新线程开始日志处理
    go bl.startLogger()
    return bl
}
  • 独立线程处理异步日志输出
// start logger chan reading.
// when chan is not empty, write logs.
func (bl *BeeLogger) startLogger() {
    gameOver := false
    for {
        select {
        //从渠道获取数据,并写入
        case bm := <-bl.msgChan:
            bl.writeToLoggers(bm.when, bm.msg, bm.level)
            logMsgPool.Put(bm)
        //给渠道发送close信息
        case sg := <-bl.signalChan:
            // Now should only send "flush" or "close" to bl.signalChan
            bl.flush()
            if sg == "close" {
                for _, l := range bl.outputs {
                    l.Destroy()
                }
                bl.outputs = nil
                gameOver = true
            }
            bl.wg.Done()
        }
        if gameOver {
            break
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容