go配置和日志

目录

链接地址

学习目标

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搭配读取启动参数

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

推荐阅读更多精彩内容

  • 本文先介绍了Go语言原生的日志库的使用,然后详细介绍了非常流行的Uber开源的zap日志库,同时介绍了如何搭配Lu...
    Mracale阅读 1,058评论 0 1
  • 本文转载自姜总 golang日志库 golang标准库的日志框架非常简单,仅仅提供了print,panic和fat...
    哆啦在这A梦在哪阅读 7,910评论 4 9
  • 本文详细介绍了非常流行的 Uber 开源的 zap 日志库,同时介绍了如何搭配 Lumberjack、Rotate...
    简凡丶阅读 1,438评论 0 2
  • 一、介绍 在许多Go语言项目中,我们需要一个好的日志记录器能够提供下面这些功能: 1、能够将事件记录到文件中,而不...
    梁帆阅读 715评论 0 1
  • 结构化日志包括定义良好的格式(通常是JSON)生成日志记录,这为应用程序日志添加了一定程度的组织和一致性,使它们更...
    Go语言由浅入深阅读 674评论 0 1