Go语言 日志组件编写

在使用Go语言的时候,发现标准库的日志log功能很初级。

log.Println(i)
log.Printf(format, i...)
log.Fatalln(i)
log.Panic()
...

基本也就围绕上面几个方法来做日志处理。于是我上Github查找第三方库。在看了几个log库之后,觉得不是满意。我打算自己尝试实现log日志组件。

最后实现log的使用如下:

// 标准日志
l := logs.NewLogger()
l.SetLogLevel(logs.WARN)  //设置日志输出等级
l.Debug("Debug")
l.Info("Info")
l.Notice("Notice")
l.Warn(100)
l.Error("Error")
l.Critical("Critical")
l.Fatal("Fatal")
// 保存到本地log文件的日志
l := logs.NewRotateFileLogger("/Users/xxx/logs")   // 设置log日志文件存放路径
l.SetLogLevel(logs.INFO)                             //设置日志输出等级
l.SetNewFileGapTime(time.Hour)           // 设置每个小时创建新的log文件
l.Notice("Notice")
l.Warn(100)
l.Error("Error")
l.Critical("Critical")
l.Fatal("Fatal")

性能上通过Benchmark分析输出一个Hello

平均每次执行时间:4177ns/p
内存使用情况:168 B/op 和 7 allocs/op

完全可以应对每秒上万的输出。性能达到要求了。

设计思考

我对组件一直有这样的思路,

  1. 组件要易于上手,减少学习成本。尽量减少不必要的概念和抽象。

  2. 初始的组件能满足最常用的使用场景,开箱即可用。满足80%的情况。同时又易于拓展,特别需求可以简单通过组件的拓展来封装。

  3. 组件API精简,设计上更符合人们对组件直观的认知。

开发前

对日志组件的需求,大部分情况无非一几点

  1. 带有Info、Warn、Error等等的不同等级的输出。
  2. 可以设置输出等级。
  3. 输出内容格式可以自定义。
  4. 支持log文件的写入。
  5. 最好也支持过滤Filter输出的功能。
设计API

初定的API

l.SetLogLevel(level)  //设置日志输出等级
l.Debug(...)
l.Info(...)
l.Notice(...)
l.Warn(...)
...
l.SetFormat(...)  //设置输出格式

前面的几个API比较好理解,但是在设计输出格式SetFormat的时候,我查看了几个第三方库,特别容易在这里引入概念和抽象层。设置还有封装出格式化结构的struct来定义格式的。这样的设计明显不符合我之前说的设计思考的几点,于是我查看了官方的log。官方的log很简朴但是很好理解,一个format字符串+对应format字符串格式的内容。例如格式%s-%s 对应2017/11/26 和Hello。这种方式开发者都熟悉,比较适合。所以我现在了官方的方式来自定义格式。

可是用一个SetFormat来传入参数定义格式其实很难封装得好,因为格式和前面的Info(data)有点不好封装,格式和data可以对应,但是扩展很弱。
我想输出是2017/11/26 Hello,定义好格式后,data不能简单的是Hello,Info(data) 这里的data就要自己再加入时间2017/11/26。

于是我选择了用传入函数来定义格式输出。

logFormatFunc func(logType LogType, i interface{}) (string, []interface{}, bool)

logType标记输出的等级,i表示输出的日志,
函数返回(string, []interface{},bool)
string是格式 例如%s-%s。 []interface{}表示返回格式的内容。
bool主要是满足做Filter过滤器

好了 先看看Logger默认的格式定义函数

func (l *Logger) DefaultLogFormatFunc(logType LogType, i interface{})  (string, []interface{}, bool){
    format := "\033["+ logTypesColors[logType] + "m%s [%s] %s \033[0m\n"
    layout :="2006-01-02 15:04:05.999"
    formatTime := time.Now().Format(layout)
    if len(formatTime) != len(layout) {
        // 可能出现结尾是0被省略 如 2006-01-02 15:04:05.9 2006-01-02 15:04:05.99,补上
        formatTime += ".000"[4 - (len(layout) - len(formatTime)):4]
    }

    values := make([]interface{}, 3)
    values[0] = logTypeStrings[logType]
    values[1] = formatTime
    values[2] = fmt.Sprint(i)

    return format, values, true
}

其实就是格式定义是%s [%s] %s
返回[]interface{} 对应格式的 3个%s。

编写Logger

提供Info(...) Warn(...) Error(...) 和 SetLogLevel(level) 再提供了SetLoggerFormat设置自定义格式输出等等功能。
接口少但易于拓展。

通过SetLoggerFormat 我拓展实现了log文件的RotateFileLogger

RotateFileLogger的格式函数如下:


func (l *RotateFileLogger) DefaultLogFormatFunc(logType LogType, i interface{})  (string, []interface{}, bool){
    format := "%s [%s] %s\n"
    now := time.Now()
    gapTime := now.Sub(l.lastFileTime)
    if gapTime > l.newFileGapTime && l.newFileGapTime > 0{
        l.file.Close()

        rate := int(int64(gapTime) / int64(l.newFileGapTime))
        l.lastFileTime = l.lastFileTime.Add(l.newFileGapTime * time.Duration(rate))
        file, err := l.createLogFile(l.fileNameFormatFunc(l.lastFileTime))
        if err != nil{
            file.Close()
            panic(err)
        }

        l.file = file
        l.out = file
    }

    layout :="2006-01-02 15:04:05.999"
    formatTime := now.Format(layout)
    defer func() {
        e := recover()
        if e!=nil{
            panic(debug.Stack())
        }
    }()
    if len(formatTime) != len(layout) {
        // 对于如果是 出现2006-01-02 15:04:05.99  适配处理 成2006-01-02 15:04:05.990
        formatTime += ".000"[4 - (len(layout) - len(formatTime)):4]
    }

    values := make([]interface{}, 3)
    values[0] = GetLogTypeString(logType)
    values[1] = formatTime
    values[2] = fmt.Sprint(i)

    return format, values, true
}

源码 (2个文件)

logger.go

package logs

import (
    "sync"
    "io"
    "fmt"
    "time"
    "os"
    "strings"
)

type LogType int

const (
    DEBUG =     LogType(0)
    INFO =      LogType(1)
    NOTICE =    LogType(2)
    WARN =      LogType(3)
    ERROR =     LogType(4)
    CRITICAL =  LogType(5)
    FATAL =     LogType(6)
)

var logTypeStrings = func() []string{
    // log 类型对应的 名称字符串,用于输出,所以统一了长度,故 DEBUG 为 "DEBUG..." 和 "CRITICAL"等长
    types :=[]string{"DEBUG", "INFO", "NOTICE", "WARN", "ERROR", "CRITICAL", "FATAL"}
    maxTypeLen := 0
    for _, t := range types{
        if len(t) > maxTypeLen {
            maxTypeLen = len(t)
        }
    }
    for index, t := range types{
        typeLen := len(t)
        if typeLen < maxTypeLen{
            types[index] += strings.Repeat(".", maxTypeLen - typeLen)
        }
    }
    return types
}()

var logTypesColors = []string{"0;35", "1;36", "1;37", "0;33", "1;31", "1;31", "1;31"}

/*
    创建 Logger
 */
func NewLogger()  *Logger{
    logger := &Logger{}
    logger.Init()
    return logger
}

func GetLogTypeString(t LogType)  string{
    return logTypeStrings[t]
}

/*
    Logger 日志

    l := NewLogger()
    l.Info("hello")
    l.Warn(1)

    输入格式可以通过 SetLoggerFormat 设置。默认输出格式定义在 见Logger的DefaultLogFormatFunc

    可以通过 SetLogLevel 设置输出等级。
 */
type Logger struct {
    mu              sync.Mutex
    out             io.Writer

    logFormatFunc   func(logType LogType, i interface{}) (string, []interface{}, bool)
    logLevel        LogType
}

func (l *Logger) Init() {
    l.mu.Lock()
    defer l.mu.Unlock()
    l.logFormatFunc = l.DefaultLogFormatFunc
    l.out = os.Stdout
    l.logLevel = DEBUG
}

func (l* Logger) SetLogLevel(logType LogType) {
    l.mu.Lock()
    defer l.mu.Unlock()
    l.logLevel = logType
}

func (l* Logger) GetLogLevel() LogType{
    l.mu.Lock()
    defer l.mu.Unlock()
    return l.logLevel
}

// 设置格式化log输出函数
// 函数返回 format 和 对应格式 []interface{}
func (l *Logger) SetLoggerFormat(formatFunc func(logType LogType, i interface{}) (string, []interface{}, bool))  {
    l.mu.Lock()
    defer l.mu.Unlock()
    l.logFormatFunc = formatFunc
}

// 输出信息
func (l *Logger) Debug(i interface{})  {
    l.log(DEBUG, i)
}

func (l *Logger) Info(i interface{}){
    l.log(INFO, i)
}

func (l *Logger) Notice(i interface{}){
    l.log(NOTICE, i)
}

func (l *Logger) Warn(i interface{})  {
    l.log(WARN, i)
}

func (l *Logger) Error(i interface{})  {
    l.log(ERROR, i)
}

func (l *Logger) Critical(i interface{}) {
    l.log(CRITICAL, i)
}

func (l *Logger) Fatal(i interface{}) {
    l.log(FATAL, i)
}



func (l *Logger) DefaultLogFormatFunc(logType LogType, i interface{})  (string, []interface{}, bool){
    format := "\033["+ logTypesColors[logType] + "m%s [%s] %s \033[0m\n"
    layout :="2006-01-02 15:04:05.999"
    formatTime := time.Now().Format(layout)
    if len(formatTime) != len(layout) {
        // 可能出现结尾是0被省略 如 2006-01-02 15:04:05.9 2006-01-02 15:04:05.99,补上
        formatTime += ".000"[4 - (len(layout) - len(formatTime)):4]
    }

    values := make([]interface{}, 3)
    values[0] = logTypeStrings[logType]
    values[1] = formatTime
    values[2] = fmt.Sprint(i)

    return format, values, true
}

func (l *Logger) log(logType LogType, i interface{})  {
    l.mu.Lock()
    defer l.mu.Unlock()

    if l.logLevel > logType{
        return
    }

    format, data, isLog := l.logFormatFunc(logType, i)
    if !isLog{ return}

    _, err := fmt.Fprintf(l.out, format, data...)
    if err !=nil{
        panic(err)
    }
}

file_logger.go

package logs

import (
    "os"
    "time"
    "fmt"
    "runtime/debug"
)

/*
 *
 */
func NewRotateFileLogger(dir string)  *RotateFileLogger{
    logger := &RotateFileLogger{}
    logger.Init(dir)
    return logger
}



// 一段时间自动创建新的log文件
// 如果在间隔时间无日志,此间隔不会创建log文件
type RotateFileLogger struct {
    Logger

    file                    *os.File                // 正在操作文件
    dirPath                 string                  // logs 文件所在文件夹
    fileNameFormatFunc      func(t time.Time) string    // 获取文件名称格式

    newFileGapTime          time.Duration       // 创建新log的间隔时间
    lastFileTime            time.Time           // 上次创建文件,文件对应时间(依据间隔时间,不是真实创建时间)
}

func (l * RotateFileLogger) Init(dir string) {
    l.Logger.Init()
    l.mu.Lock()
    defer l.mu.Unlock()

    l.fileNameFormatFunc = l.DefaultFileNameFormat
    l.logFormatFunc = l.DefaultLogFormatFunc
    l.newFileGapTime = 0
    l.lastFileTime = time.Now()
    l.dirPath = dir

    file, err := l.createLogFile(l.fileNameFormatFunc(l.lastFileTime))
    if err != nil{
        panic(err)
        return
    }

    l.file = file
    l.out = file
}

func (l * RotateFileLogger) SetNewFileGapTime(gapTime time.Duration)  {
    l.mu.Lock()
    defer l.mu.Unlock()
    l.newFileGapTime = gapTime
}
// 默认 创建格式化的文件名. fileTime 不一定是创建文件的时间
func (l * RotateFileLogger) DefaultFileNameFormat(fileTime time.Time) string{
    // tips: linux 不支持 2006-01-02 15:04:05.999 ":"名称
    // tips: os x  2006/01-02 15-04-05-999 带有"/"报no such file or directory
    layout :="2006-01-02 15-04-05-999"
    formatTime := fileTime.Format(layout)
    if len(formatTime) != len(layout) {
        // 对于如果是 出现2006-01-02 15:04:05.99  适配处理 成2006-01-02 15:04:05.990
        formatTime += ".000"[4 - (len(layout) - len(formatTime)):4]
    }
    return formatTime + ".log"
}

func (l * RotateFileLogger) createLogFile(filename string)  (*os.File, error){

    if len(l.dirPath) != 0{
        filename = l.dirPath + "/" + filename
        err:=os.MkdirAll(l.dirPath, 0777)
        if err != nil{
            panic(err)
        }
    }

    file, err := os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
    if err != nil{
        return nil, err
    }
    return file, nil
}

func (l *RotateFileLogger) DefaultLogFormatFunc(logType LogType, i interface{})  (string, []interface{}, bool){
    format := "%s [%s] %s\n"
    now := time.Now()
    gapTime := now.Sub(l.lastFileTime)
    if gapTime > l.newFileGapTime && l.newFileGapTime > 0{
        l.file.Close()

        rate := int(int64(gapTime) / int64(l.newFileGapTime))
        l.lastFileTime = l.lastFileTime.Add(l.newFileGapTime * time.Duration(rate))
        file, err := l.createLogFile(l.fileNameFormatFunc(l.lastFileTime))
        if err != nil{
            file.Close()
            panic(err)
        }

        l.file = file
        l.out = file
    }

    layout :="2006-01-02 15:04:05.999"
    formatTime := now.Format(layout)
    defer func() {
        e := recover()
        if e!=nil{
            panic(debug.Stack())
        }
    }()
    if len(formatTime) != len(layout) {
        // 对于如果是 出现2006-01-02 15:04:05.99  适配处理 成2006-01-02 15:04:05.990
        formatTime += ".000"[4 - (len(layout) - len(formatTime)):4]
    }

    values := make([]interface{}, 3)
    values[0] = GetLogTypeString(logType)
    values[1] = formatTime
    values[2] = fmt.Sprint(i)

    return format, values, true
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • 在应用程序中添加日志记录总的来说基于三个目的:监视代码中变量的变化情况,周期性的记录到文件中供其他应用进行统计分析...
    时待吾阅读 4,945评论 1 13
  • 在应用程序中添加日志记录总的来说基于三个目的:监视代码中变量的变化情况,周期性的记录到文件中供其他应用进行统计分析...
    时待吾阅读 4,961评论 0 6
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,486评论 25 707