slog自定义字段与模板
目录
背景
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)
})
}())
}
}