读读源码 - go-zero logx
整体结构
go-zero
日志系统主要在logx
模块中,其中核心是logs.go
,写入方法主要依靠logs.go
中的writer
实现。
源码
当用户调用类似logx.Info
的方法后,程序会进入logs.go
的这一段:
func Info(v ...any) {
if shallLog(InfoLevel) {
writeInfo(fmt.Sprint(v...))
}
}
在这里,shallLog
会检测当前的日志等级情况,并过滤低于预期等级的日志。日志等级使用SetLevel
设置,并会存储在内存中。
func SetLevel(level uint32) {
atomic.StoreUint32(&logLevel, level)
}
在获取writer
,level
等配置文件时,均使用了atomic,防止出现并发问题。
之后,程序进入writeInfo
:
func writeInfo(val any, fields ...LogField) {
getWriter().Info(val, addCaller(fields...)...)
}
Fields
是一个键值对结构,作为参数传入进去后根据不同的writer
实现会有不同的输出。
如果用户调用了普通的logx.Info
,这里的fields
不会有任何值。添加caller
进入fields
后,就会进入getWriter
阶段。
在getWriter
中,先尝试获取现有的writer
,如果为空的话就会提供go-zero
默认的writer
func getWriter() Writer {
w := writer.Load()
if w == nil {
w = writer.StoreIfNil(newConsoleWriter())
}
return w
}
StoreIfNil
也是加锁的函数:
func (w *atomicWriter) StoreIfNil(v Writer) Writer {
w.lock.Lock()
defer w.lock.Unlock()
if w.writer == nil {
w.writer = v
}
return w.writer
}
newConsoleWriter
会返回一个wrtier
:
func newConsoleWriter() Writer {
outLog := newLogWriter(log.New(fatihcolor.Output, "", flags))
errLog := newLogWriter(log.New(fatihcolor.Error, "", flags))
return &concreteWriter{
infoLog: outLog,
errorLog: errLog,
severeLog: errLog,
slowLog: errLog,
stackLog: newLessWriter(errLog, options.logStackCooldownMills),
statLog: outLog,
}
}
这里的东西比较多,一个个来看。
首先是newLogWriter
,它实现了Writer
和Closer
接口,赋值给了下面不同的Log
type concreteWriter struct {
infoLog io.WriteCloser
errorLog io.WriteCloser
severeLog io.WriteCloser
slowLog io.WriteCloser
statLog io.WriteCloser
stackLog io.Writer
}
作为logx
的writer
,需要实现以下接口,如果要自定义writer
或者引入第三方模块,这里是关键。实现了writer
后,调用setWriter
即可替换原有的writer
。
type Writer interface {
Alert(v any)
Close() error
Debug(v any, fields ...LogField)
Error(v any, fields ...LogField)
Info(v any, fields ...LogField)
Severe(v any)
Slow(v any, fields ...LogField)
Stack(v any)
Stat(v any, fields ...LogField)
}
go-zero
实现的newLogWriter
在这里:
func (w *concreteWriter) Alert(v any) {
output(w.errorLog, levelAlert, v)
}
func (w *concreteWriter) Close() error {
if err := w.infoLog.Close(); err != nil {
return err
}
if err := w.errorLog.Close(); err != nil {
return err
}
if err := w.severeLog.Close(); err != nil {
return err
}
if err := w.slowLog.Close(); err != nil {
return err
}
return w.statLog.Close()
}
func (w *concreteWriter) Debug(v any, fields ...LogField) {
output(w.infoLog, levelDebug, v, fields...)
}
func (w *concreteWriter) Error(v any, fields ...LogField) {
output(w.errorLog, levelError, v, fields...)
}
func (w *concreteWriter) Info(v any, fields ...LogField) {
output(w.infoLog, levelInfo, v, fields...)
}
func (w *concreteWriter) Severe(v any) {
output(w.severeLog, levelFatal, v)
}
func (w *concreteWriter) Slow(v any, fields ...LogField) {
output(w.slowLog, levelSlow, v, fields...)
}
func (w *concreteWriter) Stack(v any) {
output(w.stackLog, levelError, v)
}
func (w *concreteWriter) Stat(v any, fields ...LogField) {
output(w.statLog, levelStat, v, fields...)
}
可以看到所有的函数都走到了output
这条线上,点开output
我们可以看见:
func output(writer io.Writer, level string, val any, fields ...LogField) {
// only truncate string content, don't know how to truncate the values of other types.
if v, ok := val.(string); ok {
maxLen := atomic.LoadUint32(&maxContentLength)
if maxLen > 0 && len(v) > int(maxLen) {
val = v[:maxLen]
fields = append(fields, truncatedField)
}
}
fields = combineGlobalFields(fields)
// +3 for timestamp, level and content
entry := make(logEntry, len(fields)+3)
for _, field := range fields {
entry[field.Key] = field.Value
}
switch atomic.LoadUint32(&encoding) {
case plainEncodingType:
plainFields := buildPlainFields(entry)
writePlainAny(writer, level, val, plainFields...)
default:
entry[timestampKey] = getTimestamp()
entry[levelKey] = level
entry[contentKey] = val
writeJson(writer, entry)
}
}
这里放进来了之前的writer
,设置的日志等级,还有打印参数。首先是在末尾放了个截断,然后调用了combineGlobalFields
放入了全局的Fields
,这个全局Fields
一样可配置,在这就不展开写了。然后就是调用打印方法了,具体的由writer
自己配置了。
这里有一个问题就是,自定义的writer
必须绑死output
这个函数,不然就无法使用全局Fields
,但是很多第三方日志是有一些基础的时间功能的,这就使得在现有架构下几乎无法使用第三方库。
其次,整体的耦合性感觉偏高,但我看的日志库比较少不好评价,等我多看看再回来锐评。