0.前言

该日志的实现主要是参考自Go日志包logurs。

实现的日志包主要有以下几个功能:

  • 支持文件名和行号。
  • 支持多日志级别。
  • 支持输出到本地文件和标准输出。
  • 支持JSON和TEXT格式的日志输出,支持自定义日志格式。
  • 支持自定义配置。
  • 支持选项模式。

未来可能实现:支持结构化输出, 支持Hook.................

日志包名称为sulog。代码保存在https://github.com/liwook/Go-projects/tree/main/log

1.定义日志等级和日志选项

那首先是会有日志等级,一共6个等级。

在日志输出时候,是会判别输出的等级是否符合设定的日志等级,那就需要比较,所以日志类型用数值类型就好。

在输出中,也要输出日志等级,那肯定不是输出数字的,那是要输出例如"DEBUG"这种的,可以用数组或者切片来映射。

//option.go
type Level uint8

const (
	DebugLevel Level = iota
	InfoLevel
	WarnLevel
	ErrorLevel
	PanicLevel
	FatalLevel
)

var LevelNameMapping = []string{
	DebugLevel: "DEBUG",
	InfoLevel:  "INFO",
	WarnLevel:  "WARN",
	ErrorLevel: "ERROR",
	PanicLevel: "PAINC",
	FatalLevel: "FATAL",
}

那接着来看日志选项,一个日志中会有很多选项,比如输出地方,输出格式,日志等级,是否输出行号等等。这里我们就把这些选项打包封装到一个结构体options中。

// 日志选项结构体
type options struct {
	output        io.Writer
	level         Level
	formatter     Formatter //格式,比如json格式
	disableCaller bool      //设置是否打印文件名和行号
}

//格式定义,在formatter.go文件中
//Formatter 作为接口
type Formatter interface {
	Format(entry *Entry) error
}

有了选项后,那就需要进行设置选项。提供了initOptions函数来初始化日志选项,参数是传入函数。

默认输出是os.Stderr,输出格式是text文本格式,默认日志等级是0(即是DebugLevel)。

type Option func(*options)

func initOptions(opts ...Option) (o *options) {
	o = &options{}

	for _, opt := range opts {
		opt(o)
	}
	if o.output == nil {
		o.output = os.Stderr
	}
	if o.formatter == nil {
		o.formatter = &TextFormatter{}
	}
	return
}

对每一个日志选项创建设置函数 WithXXXX 。该日志支持如下选项设置

  • WithOutput(output io.Writer):设置输出位置。
  • WithLevel(level Level):设置输出级别。
  • WithFormatter(formatter Formatter):设置输出格式。
  • WithDisableCaller(caller bool):设置是否打印文件名和行号。
func WithOutput(output io.Writer) Option {
	return func(o *options) {
		o.output = output
	}
}

func WithLevel(levle Level) Option {
	return func(o *options) {
		o.level = levle
	}
}

func WithFormatter(formatter Formatter) Option {
	return func(o *options) {
		o.formatter = formatter
	}
}

func WithDisableCaller(caller bool) Option {
	return func(o *options) {
		o.disableCaller = caller
	}
}

 结合initOptions函数用法如下:(比如使用WithOutput)

initOptions(WithOutput(os.Stderr))

这里还没有实现logger结构体,所以还没有到使用设置的地方,讲到logger结构体的时候会再讲解。

2.创建Logger

有了前面的铺垫,可以快速定义结构体logger。

logger中有options类型变量opt。那接着来看看创建Logger的函数New,其内部使用initOptions来初始化logger的opt变量。

而std是日志包的默认使用对象,没有设置参数。

日志包会有一个默认的全局Logger,通过 var std = New() 创建了一个全局的默认Logger。

sulog.Debug、sulog.Info和sulog.Warnf等函数,则是通过调用std Logger所提供的方法来打印日志的。

//logger.go
type logger struct {
	opt       *options
    mu        sync.Mutex//为了可以同步多协程写日志
}

var std = New()

func New(opts ...Option) *logger {
	logger := &logger{opt: initOptions(opts...)}
	return logger
}

func SetOptions(opts ...Option) {
	std.SetOptions(opts...)
}

func (l *logger) SetOptions(opts ...Option) {
	l.mu.Lock()
	defer l.mu.Unlock()
    //这里加锁也是为了可以多协程设置,写日志时刻就不能修改选项
	for _, opt := range opts {
		opt(l.opt)
	}
}

 这时修改日志的选项,就可以使用

sulog.SetOptions(sulog.WithLevel(sulog.DebugLevel))

定义了一个Logger之后,还需要给该Logger添加最核心的日志打印方法,要提供所有支持级别的日志打印方法。

如果日志级别是xxx,则通常会提供两类方法,分别是非格式化方法xxx(args ...any)格式化方法xxxf(format string, args ...any),例如:

func (l *logger) Debug(args ...any) {
	l.log(DebugLevel, args...)
}

func (l *logger) Debugf(format string, args ...any) {
	l.logf(DebugLevel, format, args...)
}

//Debug()调用log()
func (l *logger) log(level Level, args ...any) {
	if l.opt.level > level { //日志等级不符合
		return
	}
	newEntry := l.entry()
	defer l.releaseEntry(newEntry)
	newEntry.Log(level, args...)
}

//Debugf()调用logf()
func (l *logger) logf(level Level, fomat string, args ...any) {
	if l.opt.level > level { //日志等级不符合
		return
	}
	newEntry := l.entry()
	defer l.releaseEntry(newEntry)
	newEntry.Logf(level, fomat, args...)
}

本日志也实现了如下方法:Debug、Debugf、Info、Infof、Warn、Warnf、Error、Errorf、Panic、Panicf、Fatal、Fatalf。这里没有详细的展示。

需要注意的是,Panic、Panicf要调用panic()函数,Fatal、Fatalf函数要调用 os.Exit(1) 函数。

3.日志内容和输出

日志内容

我们定义一个Entry结构体类型。其日志内容数据和日志配置,都保存在Entry对象中。

日志内容对象Entry,主要功能是存储日志内容以及进行日志内容写入。

type Entry struct {
	logger  *logger
	Buffer  *bytes.Buffer    //日志内容的存储地
	DataMap map[string]any //为了日志是json格式使用的
	Level   Level
	Time    time.Time
	File    string
	Line    int
	Func    string
	Message string    //日志数据

}

//创建entry
func entry(logger *logger) *Entry {
	return &Entry{
		logger:  logger,
		Buffer:  new(bytes.Buffer),
		DataMap: make(map[string]any, 5)}
}

为什么要有Entry类型呢?首先这样可以简化了logger结构体的打印方法代码(如上面的代码),也方便管理日志内容,而且这样也方便做日志内容结构化

输出

用Entry的Log或Logf方法来完成日志的写入,自带格式的使用logf,其余使用Log,最终是使用log方法。

log方法中,还会判断是否需要记录文件名和行号,如果需要则调用 runtime.Caller() 来获取文件名和行号,调用 runtime.Caller() 时,要注意传入正确的栈深度。

func (e *Entry) Log(level Level, args ...any) {
	e.log(level, fmt.Sprint(args...))
}

func (e *Entry) Logf(level Level, format string, args ...any) {
	e.log(level, fmt.Sprintf(format, args...))
}

func (e *Entry) log(level Level, msg string) {
	e.Time = time.Now()
	e.Level = level
	e.Message = msg

	if !e.logger.opt.disableCaller {
		if pc, file, line, ok := runtime.Caller(4); !ok {
			e.File = "???"
			e.Func = "???"
		} else {
			e.File, e.Line, e.Func = file, line, runtime.FuncForPC(pc).Name()
			e.Func = e.Func[strings.LastIndex(e.Func, "/")+1:]
		}
	}

	e.write()
}

log方法中调用 e.write来格式化日志(格式化后,会把内容写到e.Buffer中),并写入日志。

output类型为 io.Writer,在e.writeh中,调用e.logger.opt.output.Write(e.Buffer.Bytes())即可将日志写入到指定的位置中。

func (e *Entry) write() {
	e.logger.mu.Lock()
	defer e.logger.mu.Unlock()
    e.logger.opt.formatter.Format(e)
	e.logger.opt.output.Write(e.Buffer.Bytes())
}

看回Debug方法中,其实是只要写一次日志,就需要创建一个Entry结构体(logger.entry()就是创建entry),写完后就释放,这是比较耗性能的。

所以,这块在创建一个新的Entry对象时,使用sync.Pool对象 entryPool,主要保存Entry指针空对象,使用完后再放回sync.Pool中,防止在记录日志的时候大量开辟内存空间,触发GC操作,从而提高日志记录的速度。

使用sync.Pool的关键就是对象的复用,避免重复创建、销毁,而且其是协程安全的,这对于使用者来说是极其方便的

sync.Pool的用法:使用前,设置好对象的 New 函数,用于在 Pool 里没有缓存的对象时,创建一个。之后,在程序的任何地方、任何时候仅通过 Get()Put() 方法就可以取、还对象了。

所以需要再logger结构体中添加sync.Pool类型变量entryPool。在New函数中也需要添加对entryPool的初始化。

type logger struct {
    //..........
	entryPool *sync.Pool  //新添加,存放临时的Entry对象,减少GC对Entry对象的内存回收,提高Entry对象复用,提高效率
}

func New(opts ...Option) *logger {
	logger := &logger{opt: initOptions(opts...)}
	logger.entryPool = &sync.Pool{New: func() any { return entry(logger) }}    //新添加的
	return logger
}

//获取entry对象
func (l *logger) entry() *Entry {
	return l.entryPool.Get().(*Entry)
}
func (l *logger) releaseEntry(e *Entry) {
    e.DataMap = map[string]any{}
	e.Line, e.File, e.Func = 0, "", ""
	e.Buffer.Reset()
	l.entryPool.Put(e)
}

4.自定义日志输出格式

前面的(Entry).format方法使用了自定义格式输出。有多种格式类型,我们可以定义成接口类型Formatter。

将Entry中的数据内容,如DataMap字段存储的有结构的数据键值对,Message中存储的无结构数据,以及Time存储的日志记录时间等等,格式化成我们想要的数据格式

//formatter.go
type Formatter interface {
	Format(entry *Entry) error
}

那么接着实现两种格式:JSON和TEXT格式。

JSON格式

json的编解码使用字节开发的"github.com/bytedance/sonic"。

通过sonic.ConfigDefault.NewEncoder(e.Buffer).Encode(e.DataMap)来把日志内容写到e.Buffer。

type JsonFormatter struct {
	DisableTimestamp bool
	TimestampFormat  string
}

func (f *JsonFormatter) Format(e *Entry) error {
	if !f.DisableTimestamp {
		if f.TimestampFormat == "" {
			f.TimestampFormat = time.RFC3339
		}
		e.DataMap["time"] = e.Time.Format(f.TimestampFormat)
	}

	e.DataMap["level"] = LevelNameMapping[e.Level]

	if e.File != "" {
		e.DataMap["file"] = e.File + ":" + strconv.Itoa(e.Line)
		e.DataMap["func"] = e.Func
	}

	e.DataMap["message"] = e.Message

	return sonic.ConfigDefault.NewEncoder(e.Buffer).Encode(e.DataMap)
}

TEXT格式的就不展示了。

5.测试

func main() {
	//使用默认全局变量
	sulog.Info("std info log")
	sulog.SetOptions(sulog.WithLevel(sulog.ErrorLevel))
	sulog.Info("std can not info") //设置了ErrorLevel等级,那InfoLevle就输出不了

	sulog.SetOptions(sulog.WithFormatter(&sulog.JsonFormatter{}))
	sulog.Error("bad error")
	sulog.Errorf("%s %d", "myhome", 111) //用户自定义message输出格式

	//输出到文件
	file, err := os.OpenFile("./test.log", os.O_CREATE|os.O_APPEND, 0666)
	if err != nil {
		log.Fatal("create file test.log failed")
	}
	defer file.Close()

	//自定义logger变量,New函数中设置选项
	l := sulog.New(sulog.WithLevel(sulog.InfoLevel),
		sulog.WithOutput(file))
	l.SetOptions(sulog.WithFormatter(&sulog.JsonFormatter{IgnoreBasicFields: true}))
	l.Info("log with json")
}

Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐