目录

背景

slog是Go 实现的一个易于使用的,易扩展、可配置的日志库。我们需要一个有实际项目前经百战的日志库,以及支持开箱即用和自定义,帮我们实现了大部分需要的日志实现。

  • 开箱即用,层次清晰。
  • 支持8种日志级别。
  • 支持任意扩展Handler,即支持同一个日志数据可同时由不同Handler处理。
  • 支持Formatter,即支持对输出模板进行格式定义。

自定义slog

需要自定义内容

  • 开启日志样式颜色,根据不同日志级别,日式数据展示不同颜色。
  • 自定义日志名称。
  • 自定义应用名称。
  • 自定义日志模板。
  • 自定义固定字段。

参考范例

参考官方文档范例:

参考范例追加:

logger.AddProcessor(slog.AddHostname())

范例未生效问题跟踪

但输出的日志并没有 "hostname":"InhereMac" 的输出效果,但文档也没有其他说明。所以看下AddProcessor自定义字段后,在哪里生效,以及生效的条件。

首先我们看AddHostname做了什么?该函数添加了个 hostname,并为其赋值到Record.Fields数据结构上。

// AddHostname to record
func AddHostname() Processor {
	hostname, _ := os.Hostname()
	return ProcessorFunc(func(record *Record) {
		record.AddField("hostname", hostname)
	})
}

//....

// AddField add new field to the record
func (r *Record) AddField(name string, val any) *Record {
	if r.Fields == nil {
		r.Fields = make(M, 8)
	}

	r.Fields[name] = val
	return r
}

通过Formatter的定义,我们找到Format方法,即如何定义日志格式。

go/pkg/mod/github.com/gookit/slog@v0.5.5/formatter_text.go:113 中可以看到:

range所有fields,先处理内置固定的filed,default中处理自定义的filed:

// Format a log record
//
//goland:noinspection GoUnhandledErrorResult
func (f *TextFormatter) Format(r *Record) ([]byte, error) {
	buf := textPool.Get()
	defer textPool.Put(buf)

	for _, field := range f.fields {
		// is not field name. eg: "}}] "
		if field[0] < 'a' || field[0] > 'z' {
			// remove left "}}"
			if len(field) > 1 && field[0:2] == "}}" {
				buf.WriteString(field[2:])
			} else {
				buf.WriteString(field)
			}
			continue
		}

		switch {
		case field == FieldKeyDatetime:
			buf.B = r.Time.AppendFormat(buf.B, f.TimeFormat)
		case field == FieldKeyTimestamp:
			buf.WriteString(r.timestamp())
		case field == FieldKeyCaller && r.Caller != nil:
			var callerLog string
			if f.CallerFormatFunc != nil {
				callerLog = f.CallerFormatFunc(r.Caller)
			} else {
				callerLog = formatCaller(r.Caller, r.CallerFlag)
			}
			buf.WriteString(callerLog)
		case field == FieldKeyLevel:
			// output colored logs for console
			if f.EnableColor {
				buf.WriteString(f.renderColorByLevel(r.LevelName(), r.Level))
			} else {
				buf.WriteString(r.LevelName())
			}
		case field == FieldKeyChannel:
			buf.WriteString(r.Channel)
		case field == FieldKeyMessage:
			// output colored logs for console
			if f.EnableColor {
				buf.WriteString(f.renderColorByLevel(r.Message, r.Level))
			} else {
				buf.WriteString(r.Message)
			}
		case field == FieldKeyData:
			if f.FullDisplay || len(r.Data) > 0 {
				buf.WriteString(f.EncodeFunc(r.Data))
			}
		case field == FieldKeyExtra:
			if f.FullDisplay || len(r.Extra) > 0 {
				buf.WriteString(f.EncodeFunc(r.Extra))
			}
		default:
			if _, ok := r.Fields[field]; ok {
				buf.WriteString(f.EncodeFunc(r.Fields[field]))
			} else {
				buf.WriteString(field)
			}
		}
	}

	// return buf.Bytes(), nil
	return buf.B, nil
}

那如果我们执行能了 AddHostname 了,Format方法也看起来照顾到所有fileds,那问题就可以确定是range 的f.fields 并没有包含由AddHostname添加的 hostname ,反查f.fields的定义来源,我们找到 go/pkg/mod/github.com/gookit/slog@v0.5.5/formatter_text.go:81 :

这里我们发现 f.fields 根据template解析得到:

// SetTemplate set the log format template and update field-map
func (f *TextFormatter) SetTemplate(fmtTpl string) {
	f.template = fmtTpl
	f.fields = parseTemplateToFields(fmtTpl)
}

而 TextFormatter 内置了两个template,默认使用DefaultTemplate:

// there are built in text log template
const (
	DefaultTemplate = "[{{datetime}}] [{{channel}}] [{{level}}] [{{caller}}] {{message}} {{data}} {{extra}}\n"
	NamedTemplate   = "{{datetime}} channel={{channel}} level={{level}} [file={{caller}}] message={{message}} data={{data}}\n"
)

所以通过AddProcessor添加自定义字段的话,未提前将自定义hostname加入DefaultTemplate的话,在Format方法里遍历了f.fields输出日志时,将不会遍历到自定义的hostname,即官方的AddHostName范例也没能生效,因为hostname没有定义在DefaultTemplate里。

解决自定义字段未生效问题

除了可以通过范例内置的logger.AddProcessor(slog.AddHostname()),添加自定义字段外,还可以通过slog.ProcessorFunc添加自定义字段:

// 追加新字段platform,值为name的值。
//logger.AddProcessor(slog.AddHostname())
logger.AddProcessors(func() slog.Processor {
	return slog.ProcessorFunc(func(record *slog.Record) {
		record = record.AddField("platform", name)
	})
}())

通过上一节的跟踪,我们知道为了输出自定义field信息,我们还需要自定义template,最终先自定义template,之后在添加自定义字段固定输出:

func loggerConfig(name string) func(logger *slog.SugaredLogger) {
	return func(logger *slog.SugaredLogger) {

		// 启用输出样式。
		f := logger.Formatter.(*slog.TextFormatter)
		f.EnableColor = true

		// 定义输出日志模板。注意processor添加的filed需要在模板里有对应定义才会输出。
		template := "[{{datetime}}] [{{channel}}] [{{level}}] [{{caller}}] [{{platform}}] {{message}} {{data}} {{extra}}\n"
		f.SetTemplate(template)

		logger.SetName(name)
		logger.ChannelName = ApplicationName

		// 追加新字段platform,值为name的值。
		logger.AddProcessor(slog.AddHostname())
		logger.AddProcessors(func() slog.Processor {
			return slog.ProcessorFunc(func(record *slog.Record) {
				record = record.AddField("platform", name)
			})
		}())
	}
}

参考

slog/README.md at master · gookit/slog