目录
学习目标
1.如何自己设计一个日志包
- zap包和viper包学习
1.日志包开发
尽管网上有很多优秀的开源日志包,但在大型应用开发中,很可能仍无法满足我们定制化的需求,因此需要自己开发日志包
1.1.基础功能
基础功能指适合一些中小型的项目,具体应包含以下4个:
-
支持基本日志信息
基本日志信息包括时间戳、文件名、行号、日志级别和日志信息。时间戳可以记录日志发生的时间,因为在定位问题时,我们常需要根据时间戳,来复原请求过程,核对相同时间戳下的上下文,从而定位出问题;文件名、行号帮助我们快速定位到打印日志的位置,找到问题代码;日志级别可以知道日志的错误类型,一般而言,Error级别是需要我们立刻处理的;日志信息可以知道错误发生的具体原因 -
支持不同的日志级别,个人感觉Debug/info/Warn/Error4种级别够了
-
支持自定义配置,如是否开启行号记录等
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等日志采集工具,后者更使用于日常开发使用
- 能够按级别分类输出
按级别分类能快速定位到需要的日志
3.支持日志轮转
大型项目一天会产生几十个G的日志,为防止把磁盘占满,需要确保当其到达一定阈值时,进行切割,压缩和转存 - 具备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.png2.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:
- 构造出zap.Config
- zap.Config内部需要zap.EncoderConfig,要先于1提前构造好
- 调用调用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搭配读取启动参数
网友评论