GoLang日志框架:分类存储,容量切割,日志自由配置,日志过滤的实现

初学golang不久,在尝试一个golang的基础组件封装,想着完成一个Java的Logback日志框架的粗略功能的组件
由于是自学且刚刚开始,对golang的很多语法及开发模式不是非常了解,只是为实现如标题所示一样的功能,可以说是不择手段
所以,不管是日志配置文件的配置方式,还是代码的实现逻辑都还有很多需要改善的地方,该示例也只能算作一个随笔一个记录
该文章编写时间为: 2023年12月11日

日志组件选择

日志配置文件说明

配置文件在当前示例中使用config.ini配置文件,可自行修改配置文件名称,对应的也要修改源码中读取的配置文件名称
配置中主要有四大配置项: logging, Logging, Encode, LogRule

  • logging 为日志读取和初始化开始的地方,这里配置了终端输出的日志级别、终端日志输出格式、日志文件保存目录、日志文件的配置,示例配置:
[logging]
# 终端日志级别从小到大为: Debug, Info, Warn, Error, DPanic, Panic, Fatal
# 日志级别配置
# :Info -> 表示小于等于Info级别
# Info: -> 表示大于等于Info级别
# Info ->  表示等于Info级别
# 为空则默认配置为 Debug:
ConsoleLevel = Debug:

# 终端日志格式配置,默认为内置配置,否则必须申明
ConsoleEncode = consoleEncode

# 日志保存目录
# 默认在与应用平级目录下
LogPath = /logs

# 日志文件的配置,可不配置,也可配置多个,但是配置了就必须要在配置文件中找到指定的配置项,日志加载时会进行验证
Logging = stdout, stderr, stdBus
  • Logging 为日志文件的配置,配置了日志文件的名称,日志文件记录的日志级别,日志文件要过滤的GO文件日志,日志文件切割规则和日志内容格式,示例配置:
[stdout]
# 日志文件名称
# 必须申明
# 必须以".log"结尾,只能包含大小写字母、数字、符号"-"和"_"
# 路径为 LogPath/LogFile
LogFile = stdout.log

# 日志级别从小到大为: Debug, Info, Warn, Error, DPanic, Panic, Fatal
# 日志级别配置
# :Info -> 表示小于等于Info级别
# Info: -> 表示大于等于Info级别
# Info ->  表示等于Info级别
# 为空则默认配置为 Debug:
LogLevel = Info

#日志过滤,多个配置,以英文逗号分隔:
# Service -> 表示只记录Service.go文件的日志
# Service: -> 表示以Service开头的Go文件的日志
# :Service -> 表示以Service结尾的Go文件的日志
# :Service: -> 表示包含Service关键字的Go文件的日志
# !Service -> 表示不记录Service.go文件的日志
# !Service: -> 表示不以Service开头的Go文件的日志
# !:Service -> 表示不以Service结尾的Go文件的日志
# !:Service: -> 表示不包含Service关键字的Go文件的日志
# !Ser:vice -> 表示不记录Ser:vice.Go文件日志,中间的 "!" 或 ":" 将当作文件名称处理
# "Service" 这个Go文件名称根据项目中实际需求自行修改,不是固定的只能过滤Service关键字的Go文件名称,这里只是用来做为示例
Filters =

# 日志文件切割规则
# "fileRule"这个名称可以自定义,要保证配置文件中必须有对应名称的配置项
# 这里只是把"fileRule"这个字符串用作示例,并且在下面的示例中可以找到fileRule的配置
# 也可以不写配置名称,将使用默认的切割规则(其实就是示例配置的切割规则),
LogRule = fileRule

# 日志格式配置
# "fileEncode"这个名称可以自定义,要保证配置文件必须有对应名称的配置项
# 这里只是把"fileEncode"这个字符串用作示例,并且在下面的示例中可以找到fileEncode的配置
# 也可以不写日志格式配置名称,将使用默认的日志格式配置(其实就是示例配置的格式配置)
LogEncode = fileEncode
  • Encode 为日志内容的格式配置,为Zap对日志内容的配置,[fileEncode]在上面的[stdout]中配置引用,配置示例:
[fileEncode]
# 日志时间格式
# EpochTimeEncoder -> 时间戳
# EpochMillisTimeEncoder -> 到毫秒
# EpochNanosTimeEncoder -> 到纳秒
# ISO8601TimeEncoder -> UTC时间
# RFC3339TimeEncoder -> RFC3339格式
# RFC3339NanoTimeEncoder -> RFC3339格式到纳秒
# 否则为指定的格式内容,其中YYYY,MM,dd,HH,mm,ss,SSS为重要的格式,其他的可以自定义
# 默认为 ISO8601TimeEncoder
EncodeTime = YYYY-MM-dd HH:mm:ss.SSS

# 默认为Time
TimeKey = Time

# 日志级别字符串的格式:
# CapitalLevelEncoder -> 大写不带颜色
# CapitalColorLevelEncoder -> 大写带颜色
# LowercaseLevelEncoder -> 小写不带颜色
# LowercaseColorLevelEncoder -> 小写带颜色
# 默认为 CapitalLevelEncoder
EncodeLevel = CapitalLevelEncoder

# 日志对于时间类型数据的格式化
# SecondsDurationEncoder -> 格式化到秒
# NanosDurationEncoder -> 格式化到纳秒
# MillisDurationEncoder -> 格式化到毫秒
# StringDurationEncoder -> 使用内置的时间格式化,与EncodeTime没有任何关系
# 默认为 SecondsDurationEncoder
EncodeDuration = SecondsDurationEncoder

# 打印日志的文件和行数配置
# ShortCallerEncoder -> 使用短名称,格式为package/file.go:line
# FullCallerEncoder -> 使用完整的名称,格式为 path/to/package/file.go:line
# 默认为 ShortCallerEncoder
EncodeCaller = ShortCallerEncoder

# 日志内容结构配置
# Console -> 使用ConsoleEncoder
# JSON -> 使用JSONEncoder
# 默认为 Console
Encode = Console
  • LogRulelumberjack的日志切割配置,[fileRule]在上面的[stdout]配置中引用,示例如下:
[fileRule]
# 单个日志文件最大体积,单位为MB,超过该大小会进行日志切割
# 为空默认为100MB
MaxSize = 100

# 日志保留天数
# 默认为 10
MaxAge = 10

# 切割的日志保留的数量
# 默认为 10
MaxBackups = 10

# 确定用于格式化备份文件中的时间戳的时间是否为计算机的本地时间
# 默认为 false,使用UTC时间用作备份文件名称
LocalTime = true

# 备份日志文件是否启用压缩
# 默认为 false,不压缩
Compress = true

必须说明:

  • [logging]这个名称不可自定义,这个是内置的读取名称,如果要进行修改,则需要对源码进行修改
  • [logging] > Logging的值,根据实际需要进行自定义,但是这些自定义的名称必须要在配置文件中能找到对应的配置
  • [logging] > Logging > Filters的值, 根据实际的需求进行一系列自定义Go文件名称过滤
  • [logging] > Logging > LogRule的值,根据实际需要自定义名称,但是定义的名称必须要在配置文件中找到对应的配置,可复用
  • [logging] > Logging > LogEncode的值,根据实际需要自定义名称,但是定义的名称必须要在配置文件中找到对应的配置,可复用

日志配置的加载及功能实现源码

没有注释,自行理解吧~~
在项目启动过程中,在适当的时候调用该go文件的InitLogging()方法进行日志文件解析和功能初始化

package logging

import (
    "fmt"
    "github.com/go-ini/ini"
    "github.com/natefinch/lumberjack"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "os"
    "path/filepath"
    "regexp"
    "runtime"
    "strings"
    "time"
)

type customCore struct {
    zapcore.LevelEnabler
    enc    zapcore.Encoder
    out    zapcore.WriteSyncer
    filter *[]string
}

type logging struct {
    logPath       string
    consoleLevel  string
    consoleEncode *logEncoder
    loggingMap    *map[string]*logSdt
}

type logSdt struct {
    logFile   string
    logLevel  string
    filters   *[]string
    logRule   *logRule
    logEncode *logEncoder
}

type logRule struct {
    maxSize    int
    maxAge     int
    maxBackups int
    localTime  bool
    compress   bool
}

type logEncoder struct {
    encodeTime     string
    timeKey        string
    encodeLevel    string
    encodeDuration string
    encodeCaller   string
    encode         string
}

var logLevelStrArr = []string{"Debug", "Debug:", "Info", ":Info", "Info:", "Warn", ":Warn", "Warn:", "Error", ":Error", "Error:", ":DPanic", "DPanic:", "DPanic", "Panic", ":Panic", "Panic:", "Fatal", ":Fatal"}
var encodeDurationStrArr = []string{"SecondsDurationEncoder", "NanosDurationEncoder", "MillisDurationEncoder", "StringDurationEncoder"}
var encodeLevelStrArr = []string{"CapitalLevelEncoder", "CapitalColorLevelEncoder", "LowercaseLevelEncoder", "LowercaseColorLevelEncoder"}
var encodeCallerStrArr = []string{"ShortCallerEncoder", "FullCallerEncoder"}
var encodeStrArr = []string{"Console", "JSON"}

func InitLogging() {
    fmt.Println("Start Init Logger ...")
    loadLogging(loadLoggingConfig())
}

func loadLogging(loggingCfg *logging) {
    var cores []zapcore.Core
    // console
    consoleWriter := zapcore.AddSync(os.Stdout)
    consoleEncoder := logEncode(loggingCfg.consoleEncode)
    var consoleFilter []string
    consoleCore := newCore(consoleEncoder, &consoleWriter, getLogLevel(loggingCfg.consoleLevel), &consoleFilter)
    cores = append(cores, consoleCore)

    // logFiles
    logPath := loggingCfg.logPath
    loggingMap := *loggingCfg.loggingMap
    for key := range loggingMap {
        std := loggingMap[key]
        stdWriter := logWriter(logPath, std)
        stdEncoder := logEncode(std.logEncode)
        stdCore := newCore(stdEncoder, &stdWriter, getLogLevel(std.logLevel), std.filters)
        cores = append(cores, stdCore)
    }

    core := zapcore.NewTee(cores[:]...)

    logger := zap.New(core, zap.AddCaller())
    defer logger.Sync()

    zap.ReplaceGlobals(logger)
}

func logWriter(logPath string, sdt *logSdt) zapcore.WriteSyncer {
    logRule := sdt.logRule
    logger := &lumberjack.Logger{
        Filename:   filepath.Join(logPath, sdt.logFile),
        MaxSize:    logRule.maxSize,
        MaxAge:     logRule.maxAge,
        MaxBackups: logRule.maxBackups,
        LocalTime:  logRule.localTime,
        Compress:   logRule.compress,
    }
    return zapcore.AddSync(logger)
}

func logEncode(cfg *logEncoder) *zapcore.Encoder {
    config := zap.NewProductionEncoderConfig()
    config.EncodeTime = logEncodeTime(cfg.encodeTime)
    config.TimeKey = cfg.timeKey
    config.EncodeLevel = logEncodeLevel(cfg.encodeLevel)
    config.EncodeDuration = logEncodeDuration(cfg.encodeDuration)
    config.EncodeCaller = logCallerEncoder(cfg.encodeCaller)

    var encoder zapcore.Encoder
    if strings.EqualFold(cfg.encode, "JSON") {
        encoder = zapcore.NewJSONEncoder(config)
    } else {
        encoder = zapcore.NewConsoleEncoder(config)
    }
    return &encoder
}

func logCallerEncoder(encodeCaller string) func(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) {
    switch encodeCaller {
    case "ShortCallerEncoder":
        return zapcore.ShortCallerEncoder
    case "FullCallerEncoder":
        return zapcore.FullCallerEncoder
    default:
        return zapcore.ShortCallerEncoder
    }
}

func logEncodeDuration(encodeDuration string) func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) {
    switch encodeDuration {
    case "SecondsDurationEncoder":
        return zapcore.SecondsDurationEncoder
    case "NanosDurationEncoder":
        return zapcore.NanosDurationEncoder
    case "MillisDurationEncoder":
        return zapcore.MillisDurationEncoder
    case "StringDurationEncoder":
        return zapcore.StringDurationEncoder
    default:
        return zapcore.SecondsDurationEncoder
    }
}

func logEncodeLevel(encodeLevel string) func(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
    switch encodeLevel {
    case "CapitalLevelEncoder":
        return zapcore.CapitalLevelEncoder
    case "CapitalColorLevelEncoder":
        return zapcore.CapitalColorLevelEncoder
    case "LowercaseLevelEncoder":
        return zapcore.LowercaseLevelEncoder
    case "LowercaseColorLevelEncoder":
        return zapcore.LowercaseColorLevelEncoder
    default:
        return zapcore.CapitalLevelEncoder
    }
}

func logEncodeTime(encodeTime string) func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
    switch encodeTime {
    case "EpochTimeEncoder":
        return zapcore.EpochTimeEncoder
    case "EpochMillisTimeEncoder":
        return zapcore.EpochMillisTimeEncoder
    case "EpochNanosTimeEncoder":
        return zapcore.EpochNanosTimeEncoder
    case "ISO8601TimeEncoder":
        return zapcore.ISO8601TimeEncoder
    case "RFC3339TimeEncoder":
        return zapcore.RFC3339TimeEncoder
    case "RFC3339NanoTimeEncoder":
        return zapcore.RFC3339NanoTimeEncoder
    default:
        return customEncodeTime(encodeTime)
    }
}

func customEncodeTime(format string) func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
    // YYYY,MM,dd,HH,mm,ss,SSS
    _format := strings.ReplaceAll(format, "YYYY", "2006")
    _format = strings.ReplaceAll(_format, "MM", "01")
    _format = strings.ReplaceAll(_format, "dd", "02")
    _format = strings.ReplaceAll(_format, "HH", "15")
    _format = strings.ReplaceAll(_format, "mm", "04")
    _format = strings.ReplaceAll(_format, "ss", "05")
    _format = strings.ReplaceAll(_format, "SSS", "000")
    return func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
        enc.AppendString(t.Format(_format))
    }
}

func getLogLevel(l string) zap.LevelEnablerFunc {
    return zap.LevelEnablerFunc(func(level zapcore.Level) bool {
        switch l {
        case "Debug":
            return level == zapcore.DebugLevel
        case "Debug:":
            return level >= zapcore.DebugLevel
        case "Info":
            return level == zapcore.InfoLevel
        case ":Info":
            return level <= zapcore.InfoLevel
        case "Info:":
            return level >= zapcore.InfoLevel
        case "Warn":
            return level == zapcore.WarnLevel
        case ":Warn":
            return level <= zapcore.WarnLevel
        case "Warn:":
            return level >= zapcore.WarnLevel
        case "Error":
            return level == zapcore.ErrorLevel
        case ":Error":
            return level <= zapcore.ErrorLevel
        case "Error:":
            return level >= zapcore.ErrorLevel
        case "DPanic":
            return level == zapcore.DPanicLevel
        case ":DPanic":
            return level <= zapcore.DPanicLevel
        case "DPanic:":
            return level >= zapcore.DPanicLevel
        case "Panic":
            return level == zapcore.PanicLevel
        case ":Panic":
            return level <= zapcore.PanicLevel
        case "Panic:":
            return level >= zapcore.PanicLevel
        case "Fatal":
            return level == zapcore.FatalLevel
        case ":Fatal":
            return level <= zapcore.FatalLevel
        default:
            return level >= zapcore.DebugLevel
        }
    })
}

func loadLoggingConfig() *logging {
    cfg, err := ini.Load("config.ini")
    if err != nil {
        zap.L().Error("Failed to load config", zap.Error(err))
        panic(err)
    }

    logPath := cfg.Section("logging").Key("LogPath").MustString(".")
    if len(strings.TrimSpace(logPath)) == 0 {
        logPath = "."
    }
    logPathClean := filepath.Clean(logPath)

    consoleLevel := cfg.Section("logging").Key("ConsoleLevel").MustString("Debug:")
    if !verifyStr(logLevelStrArr, consoleLevel) {
        panic(fmt.Errorf("logging.ConsoleLevel -> %s Not Support", consoleLevel))
    }

    clsEncode := cfg.Section("logging").Key("ConsoleEncode").String()
    clsEncodeCfg := loggingEncodeCfg(cfg, clsEncode)

    loggingArr := cfg.Section("logging").Key("Logging").Strings(",")
    loggingMap := make(map[string]*logSdt)
    for _, v := range loggingArr {
        loggingMap[v] = logMap(cfg, v)
    }

    return &logging{
        logPath:       logPathClean,
        consoleLevel:  consoleLevel,
        consoleEncode: clsEncodeCfg,
        loggingMap:    &loggingMap,
    }
}

func logMap(cfg *ini.File, pk string) *logSdt {
    if !cfg.HasSection(pk) {
        panic(fmt.Errorf("logging.Logging -> %s Not Found ", pk))
    }
    pkSection := cfg.Section(pk)

    logFile := pkSection.Key("LogFile").String()
    logFileClean := filepath.Clean(logFile)
    filePattern := "^[a-zA-Z0-9\\_-]+.log$"
    matched, _ := regexp.MatchString(filePattern, logFileClean)
    if !matched {
        panic(fmt.Errorf("%s.LogFile -> %s Invalied", pk, logFile))
    }

    logLevel := pkSection.Key("LogLevel").MustString("Debug:")
    if !verifyStr(logLevelStrArr, logLevel) {
        panic(fmt.Errorf("%s.LogLevel -> %s Not Support", pk, logLevel))
    }

    filters := pkSection.Key("Filters").Strings(",")

    logRuleStr := pkSection.Key("LogRule").String()
    logRuleCfg := loggingRuleCfg(cfg, logRuleStr)

    logEncodeStr := pkSection.Key("LogEncode").String()
    logEncoderCfg := loggingEncodeCfg(cfg, logEncodeStr)

    return &logSdt{
        logFile:   logFileClean,
        logLevel:  logLevel,
        filters:   &filters,
        logRule:   logRuleCfg,
        logEncode: logEncoderCfg,
    }
}

func loggingEncodeCfg(cfg *ini.File, pk string) *logEncoder {
    if len(strings.TrimSpace(pk)) == 0 {
        return &logEncoder{
            encodeTime:     "ISO8601TimeEncoder",
            timeKey:        "Time",
            encodeLevel:    "CapitalLevelEncoder",
            encodeDuration: "SecondsDurationEncoder",
            encodeCaller:   "ShortCallerEncoder",
            encode:         "Console",
        }
    }

    if !cfg.HasSection(pk) {
        panic(fmt.Errorf("config %s Not Found", pk))
    }

    pkSection := cfg.Section(pk)

    encodeLevel := pkSection.Key("EncodeLevel").MustString("CapitalLevelEncoder")
    if !verifyStr(encodeLevelStrArr, encodeLevel) {
        panic(fmt.Errorf("%s.EncodeLevel -> %s Not Support", pk, encodeLevel))
    }

    encodeDuration := pkSection.Key("EncodeDuration").MustString("SecondsDurationEncoder")
    if !verifyStr(encodeDurationStrArr, encodeDuration) {
        panic(fmt.Errorf("%s.EncodeDuration -> %s Not Support", pk, encodeDuration))
    }

    encodeCaller := pkSection.Key("EncodeCaller").MustString("ShortCallerEncoder")
    if !verifyStr(encodeCallerStrArr, encodeCaller) {
        panic(fmt.Errorf("%s.EncodeCaller -> %s Not Support", pk, encodeCaller))
    }

    encode := pkSection.Key("Encode").MustString("Console")
    if !verifyStr(encodeStrArr, encode) {
        panic(fmt.Errorf("%s.Encode -> %s Not Support", pk, encode))
    }

    return &logEncoder{
        encodeTime:     pkSection.Key("EncodeTime").MustString("ISO8601TimeEncoder"),
        timeKey:        pkSection.Key("TimeKey").MustString("Time"),
        encodeLevel:    encodeLevel,
        encodeDuration: encodeDuration,
        encodeCaller:   encodeCaller,
        encode:         encode,
    }
}

func loggingRuleCfg(cfg *ini.File, pk string) *logRule {
    if len(strings.TrimSpace(pk)) == 0 {
        return &logRule{
            maxSize:    100,
            maxAge:     10,
            maxBackups: 10,
            localTime:  false,
            compress:   false,
        }
    }

    if !cfg.HasSection(pk) {
        panic(fmt.Errorf("config %s Not Found", pk))
    }
    pkSection := cfg.Section(pk)
    return &logRule{
        maxSize:    pkSection.Key("MaxSize").MustInt(100),
        maxAge:     pkSection.Key("MaxAge").MustInt(10),
        maxBackups: pkSection.Key("MaxBackups").MustInt(10),
        localTime:  pkSection.Key("LocalTime").MustBool(false),
        compress:   pkSection.Key("Compress").MustBool(false),
    }
}

func verifyStr(arr []string, str string) bool {
    for _, v := range arr {
        if strings.EqualFold(v, str) {
            return true
        }
    }
    return false
}

func (c customCore) With(fields []zapcore.Field) zapcore.Core {
    clone := c.clone()
    addFields(clone.enc, fields)
    return clone
}

func (c customCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
    _, file, _, ok := runtime.Caller(4)
    if !ok {
        return nil
    }
    fileName := file[strings.LastIndex(file, "/")+1 : strings.LastIndex(file, ".")]
    flag := false
    if c.filter == nil || len(*c.filter) == 0 {
        flag = true
    }
    for _, str := range *c.filter {
        if len(strings.TrimSpace(str)) == 0 {
            continue
        }

        keyWord := trimFilter(str)
        if strings.HasPrefix(str, "!:") {
            if strings.HasSuffix(str, ":") {
                // !:aa:
                flag = !strings.Contains(fileName, keyWord)
            } else {
                // !:aa
                flag = !strings.HasSuffix(fileName, keyWord)
            }
        } else if strings.HasPrefix(str, "!") {
            if strings.HasSuffix(str, ":") {
                // !aa:
                flag = !strings.HasPrefix(fileName, keyWord)
            } else {
                // !aa
                flag = !strings.EqualFold(fileName, keyWord)
            }
        } else if strings.HasPrefix(str, ":") {
            if strings.HasSuffix(str, ":") {
                // :aa:
                flag = strings.Contains(fileName, keyWord)
            } else {
                // :aa
                flag = strings.HasSuffix(fileName, keyWord)
            }
        } else if strings.HasSuffix(str, ":") {
            // aa:
            flag = strings.HasPrefix(fileName, keyWord)
        } else {
            // aa
            flag = strings.EqualFold(fileName, keyWord)
        }

        if flag {
            break
        }
    }

    if c.Enabled(ent.Level) && flag {
        return ce.AddCore(ent, c)
    }
    return ce
}

func trimFilter(str string) string {
    replace := ""
    if strings.HasPrefix(str, "!:") {
        replace = str[2:]
    } else if strings.HasPrefix(str, ":") {
        replace = str[1:]
    }

    if strings.HasSuffix(replace, ":") {
        replace = replace[:len(replace)-1]
    }

    if len(strings.TrimSpace(replace)) == 0 {
        panic(fmt.Errorf("Logging Filter -> %s Parttern Is Invalid  ", str))
    }
    return replace
}

func (c customCore) Write(ent zapcore.Entry, fields []zapcore.Field) error {
    buf, err := c.enc.EncodeEntry(ent, fields)
    if err != nil {
        return err
    }
    _, err = c.out.Write(buf.Bytes())
    buf.Free()
    if err != nil {
        return err
    }
    if ent.Level > zapcore.ErrorLevel {
        _ = c.Sync()
    }
    return nil
}

func (c customCore) Sync() error {
    return c.out.Sync()
}

func newCore(enc *zapcore.Encoder, ws *zapcore.WriteSyncer, enab zapcore.LevelEnabler, filter *[]string) zapcore.Core {
    return &customCore{
        LevelEnabler: enab,
        enc:          *enc,
        out:          *ws,
        filter:       filter,
    }
}

func addFields(enc zapcore.ObjectEncoder, fields []zapcore.Field) {
    for i := range fields {
        fields[i].AddTo(enc)
    }
}

func (c *customCore) clone() *customCore {
    return &customCore{
        LevelEnabler: c.LevelEnabler,
        enc:          c.enc.Clone(),
        out:          c.out,
        filter:       c.filter,
    }
}

在项目中,需要打印日志的地方的使用:

import (
    "go.uber.org/zap"
)

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

推荐阅读更多精彩内容