读读源码 - 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)
}

在获取writerlevel等配置文件时,均使用了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,它实现了WriterCloser接口,赋值给了下面不同的Log

type concreteWriter struct {
	infoLog   io.WriteCloser
	errorLog  io.WriteCloser
	severeLog io.WriteCloser
	slowLog   io.WriteCloser
	statLog   io.WriteCloser
	stackLog  io.Writer
}

作为logxwriter,需要实现以下接口,如果要自定义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,但是很多第三方日志是有一些基础的时间功能的,这就使得在现有架构下几乎无法使用第三方库。

其次,整体的耦合性感觉偏高,但我看的日志库比较少不好评价,等我多看看再回来锐评。