美文网首页
go配置和日志

go配置和日志

作者: 温岭夹糕 | 来源:发表于2024-08-02 23:37 被阅读0次

    目录

    链接地址

    学习目标

    1.如何自己设计一个日志包

    1. zap包和viper包学习

    1.日志包开发

    尽管网上有很多优秀的开源日志包,但在大型应用开发中,很可能仍无法满足我们定制化的需求,因此需要自己开发日志包

    1.1.基础功能

    基础功能指适合一些中小型的项目,具体应包含以下4个:

    1. 支持基本日志信息
      基本日志信息包括时间戳、文件名、行号、日志级别和日志信息。时间戳可以记录日志发生的时间,因为在定位问题时,我们常需要根据时间戳,来复原请求过程,核对相同时间戳下的上下文,从而定位出问题;文件名、行号帮助我们快速定位到打印日志的位置,找到问题代码;日志级别可以知道日志的错误类型,一般而言,Error级别是需要我们立刻处理的;日志信息可以知道错误发生的具体原因

    2. 支持不同的日志级别,个人感觉Debug/info/Warn/Error4种级别够了

    3. 支持自定义配置,如是否开启行号记录等

    4.支持输出到标准输出和文件

    实战:实现一个最小的Log

    type Level uint8 
    type MyLog struct {} //自定义日志
    const (
        DebugLevel Level = iota
        InfoLevel
        WarnLevel
        ErrorLevel
    )
    
    
    var LevelNameMap = map[Level]string {
        DebugLevel:"DEBUG",
        InfoLevel:"INFO",
        WarnLevel:"WARN",
        ErrorLevel:"ERROR",
    }
    

    日志配置设计,这里所谓的配置实际就是指日志的成员对象

    type options struct {
        output        io.Writer //输出位置
        level         Level     //日志级别 小于改级别就不输出
        disableCaller bool      //是否开启行号
    }
    
    func DefaultOptions() *options {
        return &options{
            output: os.Stderr,
        }
    }
    
    
    type MyLog struct {
        ops *options
        l sync.Mutex
    }
    

    我们采用Option设计模式来修改配置

    type Option func(*options)
    
    func WithLevel(level Level) Option {
        return func(ops *options) {
            ops.level = level
        }
    }
    
    func NewLog(ops ...Option) *MyLog {
        res := &MyLog{ops: DefaultOptions()}
        for _, op := range ops {
            op(res.ops)
        }
        return res
    }
    

    最后是支持输出到标准输出和文件,很简单嘛直接调用ops.ouput的write方法往外部提供的地方写嘛

    1.2高级功能

    1.支持多种日志格式。
    一般来说至少支持json和text,前者能方便提供给logstash,filebeat等日志采集工具,后者更使用于日常开发使用

    1. 能够按级别分类输出
      按级别分类能快速定位到需要的日志
      3.支持日志轮转
      大型项目一天会产生几十个G的日志,为防止把磁盘占满,需要确保当其到达一定阈值时,进行切割,压缩和转存
    2. 具备Hook能力
      对日志内容自定义处理,如当某个级别的日志产生时,主动发送邮件进行警告

    实战:补全部分高级功能
    首先是级别打印,我们对每个日志级别定义一个对应的函数;

    func (log *MyLog) Debug(format string, args ...any)
    func (log *MyLog) Warn(format string, args ...any)  {}
    func (log *MyLog) Info(format string, args ...any)  {}
    func (log *MyLog) Error(format string, args ...any) {}
    

    然后是核心的输出打印,以输出下面日志为目标

    2024-08-03T21:45:18+08:00 DEBUG log_test.go:170 hi
    

    参考gorm将一个表抽象成一个Model类,Model中记录着数据表的字段信息,我们也将日志抽象成一个Entry类,保存要记录信息的元数据:时间戳、文件名、行数、函数、格式化信息、参数等,因为还要根据日志类中的配置进行输出的调整,所以也需要一个字段保存日志类

    type Entry struct {
        Log    *MyLog
        Buffer *bytes.Buffer
        Map    map[string]any
        Level  Level
        Time   time.Time
        File   string
        Line   int
        Func   string
        Format string
        Args   []any
    }
    
    func newEntry(logger *MyLog) *Entry {
        return &Entry{
            Log:    logger,
            Buffer: new(bytes.Buffer),
            Map:    make(map[string]any),
        }
    }
    

    写日志就是往Buffer中写入字符流

    func (e *Entry) format() error
    

    输出就是将buffer内容写到mylog.ops.output中

    func (e *Entry) writer() {
        e.Log.l.Lock()
        defer e.Log.l.Unlock()
    
        _, _ = e.Log.ops.output.Write(e.Buffer.Bytes())
    }
    

    然后就是整合所有逻辑进行代码编写

    func (e *Entry) Write(level Level, format string, args ...any) {
        //低于设置的日志级别就不打印
        if e.Log.ops.level > level {
            return
        }
    
        e.Time = time.Now()
        e.Level = level
        e.Format = format
        e.Args = args
    
        //打印行号等信息
        if !e.Log.ops.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()
                // a/b/c   e.func = c
                e.Func = e.Func[strings.LastIndex(e.Func, "/")+1:]
            }
        }
        //todo 往buffer里写数据
        e.format()
        e.writer()
        //重置
        e.release()
    }
    
    func (e *Entry) release() {
        e.Args, e.Line, e.File, e.Format, e.Func = nil, 0, "", "", ""
        e.Buffer.Reset()
    }
    

    暂时只实现Text的格式化输出

    func (e *Entry) format() error {
        return e.textFormat()
    }
    
    func (e *Entry) textFormat() error {
        e.Buffer.WriteString(fmt.Sprintf("%s %s ", e.Time.Format(time.RFC3339),
            LevelNameMap[e.Level],
        ))
        if e.File != "" {
            // a/b/c.go    short = c.go
            short := e.File[strings.LastIndex(e.File, "/")+1:]
            e.Buffer.WriteString(fmt.Sprintf("%s:%d", short, e.Line))
        }
        e.Buffer.WriteString(" ")
        e.Buffer.WriteString(fmt.Sprintf(e.Format, e.Args...))
        e.Buffer.WriteString(" \n")
    
        return nil
    }
    

    最后我们将entry保存到连接池中,随用随取
    修改mylogger

    type MyLog struct {
        ops  *options
        l    sync.Mutex
        pool *sync.Pool
    }
    
    func NewLog(ops ...Option) *MyLog {
        res := &MyLog{ops: DefaultOptions()}
        for _, op := range ops {
            op(res.ops)
        }
        res.pool = &sync.Pool{
            New: func() any { return newEntry(res) },
        }
        return res
    }
    func (log *MyLog) Debug(format string, args ...any) {
        e := log.pool.Get().(*Entry)
        e.Write(DebugLevel, format, args...)
    }
    
    func (e *Entry) release() {
        e.Args, e.Line, e.File, e.Format, e.Func = nil, 0, "", "", ""
        e.Buffer.Reset()
        e.Log.pool.Put(e)
    }
    

    最后测试代码

    func TestLog(t *testing.T) {
        log := NewLog(WithOutput(io.MultiWriter(os.Stdout)))
        log.Debug("%s", "hi")
    }
    

    输出结果就如上
    以上代码编写参考cuslog
    至于hook能力,最简单的办法就是在ops里添加一组map存放分别对应各自的级别函数,当entry里触发时,最后write阶段执行log里注册的对应级别函数即可,至此我们已经手写完成日志类

    2.Zap源码走读

    go get go.uber.org/zap
    

    快速开始

    func TestZap(t *testing.T) {
        logger := zap.NewExample()
        defer logger.Sync()
    
        url := "xxx"
        logger.Info("failed to fetch URL", zap.String("url", url), zap.Int("aa", 3), zap.Duration("back", time.Second))
    
        sugar := logger.Sugar()
        sugar.Info("failed to fetch URL", "url", url, "aa", 3, "back", time.Second)
    
    }
    

    zap库和我们写的日志类相似,先创建logger,然后调用对应级别的日志输出函数

    2.1创建日志

    zap提供了几个快速创建的办法

    zap.NewExample()
    zap.NewDevelopment()
    zap.NewProduction()
    

    分别对应在测试、开发和生产环境中使用
    还有一个New方法是高度定制化

    func New(core zapcore.Core, options ...Option) *Logger {
        if core == nil {
            return NewNop()
        }
        log := &Logger{
            core:        core,
            errorOutput: zapcore.Lock(os.Stderr),
            addStack:    zapcore.FatalLevel + 1,
            clock:       zapcore.DefaultClock,
        }
        return log.WithOptions(options...)
    }
    

    这个withOption我们上面使用过该设计模式(Option模式也叫函数式选择模式)

    func (log *Logger) WithOptions(opts ...Option) *Logger {
        c := log.clone()
        for _, opt := range opts {
            opt.apply(c)
        }
        return c
    }
    
    type Option interface {
        apply(*Logger)
    }
    
    // optionFunc wraps a func so it satisfies the Option interface.
    type optionFunc func(*Logger)
    
    func (f optionFunc) apply(log *Logger) {
        f(log)
    }
    

    生成的logger结构体如下

    type Logger struct {
        core zapcore.Core
    
        development bool
        addCaller   bool
        onPanic     zapcore.CheckWriteHook // default is WriteThenPanic
        onFatal     zapcore.CheckWriteHook // default is WriteThenFatal
    
        name        string
        errorOutput zapcore.WriteSyncer
    
        addStack zapcore.LevelEnabler
    
        callerSkip int
    
        clock zapcore.Clock
    }
    
    • addCaller是否启用行号,函数名和调用方
    • onPanic/onFatal 当遇到这两个级别时执行的钩子函数
    • clock 用来确定当前时间的时钟
      以上是其基本配置

    2.1.1Core

    Core接口是日志的核心接口,LevelEnabler 接口提供用于判断给定的日志级别是否应该打印该条日志的逻辑判断方法

    type Core interface {
        LevelEnabler
        With([]Field) Core
        Check(Entry, *CheckedEntry) *CheckedEntry
        Write(Entry, []Field) error
        Sync() error
    }
    
    type LevelEnabler interface {
        Enabled(Level) bool
    }
    
    • LevelEnabler一般都用于给Check方法检查和判断
    • Write方法负责日志的打印输出
    • With方法说白了就是将field添加到对象成员中

    什么是field?
    就是调用zap进行日志打印输出输入一堆键值对

    zap.String("url", url), 
    
    func String(key string, val string) Field 
    
    type Field struct {
        Key       string
        Type      FieldType
        Integer   int64
        String    string
        Interface interface{}
    }
    

    在zap中,Core的实现有两种

    • nopCore
    • ioCore

    当zap.New方法未传入core时调用一个空的core实例结构体(啥也不能干)

    type nopCore struct{}
    
    func NewNopCore() Core                                        { return nopCore{} }
    func (nopCore) Enabled(Level) bool                            { return false }
    ......
    

    实际上为了方便使用,zap提供了两个全局logger(zap.L()和S()获取)

    var (
      _globalMu sync.RWMutex
      _globalL  = NewNop()
      _globalS  = _globalL.Sugar()
    )
    

    所以从nop函数来看,全局日志默认是不会记录日志,是无效logger(后续可调用repeat函数替换)

    2.1.2 io.Core

    创建logger的时候大部分都是通过该方法来生成Core实例

    type ioCore struct {
        LevelEnabler
        enc Encoder
        out WriteSyncer
    }
    
    NewCore(enc Encoder, ws WriteSyncer, enab LevelEnabler) Core
    

    Encoder是一个数据编码接口,每一个具体的 Encoder 实现都实现了 ObjectEncoder 接口中的一系列根据具体数据类型进行 Add 或者 Append 的方法;
    根据 field 的具体数据类型构造一个 buffer 储存对应的数据格式,例如 json_encoder 在 EncodeEntry 方法中构造出对应的 json 格式保存在 buffer 中返回出来

    type Encoder interface {
        ObjectEncoder
    
        Clone() Encoder
        EncodeEntry(Entry, []Field) (*buffer.Buffer, error)
    }
    

    也就是说这个EnCoder是上文提到的用于支持日志多种输出格式的功能类
    zap内部只提供了两种输出格式,即EnCoder内部的实现有两种

    • consoleEncoder 对应NewConsoleEncoder生成
    • jsonEncoder 对应NewJSONEncoder生成

    此外zap还提供了外部注册Encoder的能力

    RegisterEncoder(name string, constructor func(zapcore.EncoderConfig) (zapcore.Encoder, error)) error
    

    上面这些函数都需要传入EncoderConfig结构体

    image.png

    2.1.3 Entry

    我们观察到Core的很多函数都使用到了Entry

    type Entry struct {
        Level      Level
        Time       time.Time
        LoggerName string
        Message    string
        Caller     EntryCaller
        Stack      string
    }
    

    Entry是日志主体内容结构体,表示一条具体日志,经过检查之后的日志内容结构体(装饰器模式),cores带有具体的打印方式,定义如下

    type CheckedEntry struct {
        Entry
        ErrorOutput WriteSyncer
        dirty       bool // best-effort detection of pool misuse
        should      CheckWriteAction
        cores       []Core
    }
    

    日志内部打印,实际是先Core调用Check生成CheckEntry,然后再用CheckEntry的写入方法来打印输出

    2.1.4Config

    回头再看几个快速创建的函数

    func NewDevelopment(options ...Option) (*Logger, error) {
        return NewDevelopmentConfig().Build(options...)
    }
    
    func NewDevelopmentConfig() Config
    

    Config结构体

    type Options struct {
    Level AtomicLevel `json:"level" yaml:"level"`
    Development bool
    DisableCaller bool
    DisableStacktrace bool
    Sampling *SamplingConfig
    Encoding string 
    EncoderConfig zapcore.EncoderConfig
    OutputPaths []string
    ErrorOutputPaths []string
    }
    
    
    • Development 是否开发者模式
    • Level是用来配置日志级别的,即日志的最低输出级别
    • DisableCaller是否开启行号
    • DisableStacktrace 是否打印堆栈
    • Sampling日志流控制功能
    • Encoding json格式还是Text格式输出
    • EncoderConfig上文分析过了,它确保日志的打印输出格式
    • OutputPaths 日志输出路径
    • ErrorOutputPaths []string 错误日志输出路径

    build方法负责对config参数解析封装,最后调用New方法生成logger

    func (cfg Config) Build(opts ...Option) (*Logger, error)
    

    2.2用New自定义logger

    走读了上面源码,我们就已经学会了如何自定义zap.logger,就是模仿NewDevelopment:

    1. 构造出zap.Config
    2. zap.Config内部需要zap.EncoderConfig,要先于1提前构造好
    3. 调用调用Config.Builder构造logger
    func TestZap(t *testing.T) {
    
        var zapLevel zapcore.Level
        if err := zapLevel.UnmarshalText([]byte("debug")); err != nil {
            zapLevel = zapcore.InfoLevel
        }
    
        enCoderConfig := zapcore.EncoderConfig{
            MessageKey:     "message",
            LevelKey:       "level",
            TimeKey:        "timestamp",
            NameKey:        "logger",
            CallerKey:      "caller",
            StacktraceKey:  "stacktrace",
            LineEnding:     "\n",
            EncodeTime:     zapcore.EpochMillisTimeEncoder,
            EncodeDuration: zapcore.MillisDurationEncoder,
            EncodeCaller:   zapcore.ShortCallerEncoder,
        }
    
        loggerConfig := &zap.Config{
            Level:             zap.NewAtomicLevelAt(zapLevel),
            Development:       true,
            DisableCaller:     false,
            DisableStacktrace: false,
            Encoding:          "console",
            EncoderConfig:     enCoderConfig,
            OutputPaths:       []string{"my.log"},
            ErrorOutputPaths:  []string{"err.log"},
        }
    
        l, err := loggerConfig.Build(zap.Fields(zap.String("path", "v1")))
        if err != nil {
            t.Fatal("log创建失败", err)
        }
    
        l.Named("mylog")
    
        l.Debug("debug测试")
        l.Error("err测试")
    }
    

    2.3日志输出

    以info为例

    func (log *Logger) Info(msg string, fields ...Field) {
        if ce := log.check(InfoLevel, msg); ce != nil {
            ce.Write(fields...)
        }
    }
    

    先调用Core.Check方法进行级别等的检查,check返回CheckedEntry ,调用内部的Write方法进行数据写入,和我们上面写的日志有异曲同工之妙

    3.配置库Viper

    没啥好说的会用就行
    Viper学习
    唯一需要注意的是我们编写项目时,读取配置的优先级是

    启动参数 > 环境变量 > 配置文件 >远程配置
    

    viper常与pflag搭配读取启动参数

    相关文章

      网友评论

          本文标题:go配置和日志

          本文链接:https://www.haomeiwen.com/subject/rjnahjtx.html