06-日志处理
在日常工作中,我们写程序难免会出现bug,不知屏幕前的你有没有遇到这样的情况:项目上线后半夜出了问题,被领导从睡梦中叫醒,你睡眼朦胧的日来一看日志,卧槽!这是哪个蠢货写的代码不打日志?卧槽!这是哪个蠢货漏打日志?卧槽!是我!!
kratos 框架不但不限制你日志库选型,还能帮团队统一日志库使用姿势,这与 go 官方即将推出的新的日志库 slog 的设计哲学不谋而合!前端统一用户使用姿势、后端对接不同实现!
那么这一节,我们学习如何在 kratos 中接入 slog 日志库!
标准库 log 很痛
思考一个问题:平时你在写 Go 工程时,是否很少直接使用官方标准库 log?
在正式项目中,大多是优先使用几个爆款第三方库,例如:Logrus、Zap、zerolog,毕竟又快又猛。
这问题出在了哪里?主要集中在以下方面:
- 没有日志分级。不便于分类、定位、排查问题,例如:Error、Warn、Info、Debug 等。
- 没有结构化日志。只提供格式化日志,不提供结构化,不便于程序读取、解析,例如:Json 格式。
- 没有扩展性,灵活度差。标准库 log 的日志输出都是固定格式,没有一个 Logger 接口规范,让大家都遵守,以至于现在社区纯自然演进,难互相兼容。
除此之外,在用户场景上,有着不包含上下文(context)信息、性能不够强劲、无法引入自定义插件等扩展诉求,这些基本都用户的痛点之一。
尝鲜 slog
slog 库在笔者写此篇文章时还未正式发布,导入地址是:golang.org/x/exp/slog,推荐大家试用。
package main
import (
"golang.org/x/exp/slog"
"os"
)
func main() {
textHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
ReplaceAttr: nil,
})
logger := slog.New(textHandler).With(slog.Int("x", 23))
logger.Info("Go is the best language!")
logger.Debug("Go is the best language!")
}
程序运行结果如下:
我们看到了日志分级(Level)、自定义字段追加、设置输出地等特性。在输出格式上,新的 slog 库,将会采取与 logfmt[2] 库类似的方式来实现,内置至少两种格式,也可以自定义实现。
slog 设计思路
介绍
包slog提供了结构化日志记录功能,其中日志记录包括消息、严重级别和其他以键值对形式表示的各种属性。
它定义了一种类型[Logger],该类型提供了多个方法(如[Logger.Info]和[Logger.Error])用于报告感兴趣的事件。
每个Logger都与一个[Handler]相关联。Logger的输出方法会根据方法参数创建一个[Record]并将其传递给Handler,由Handler决定如何处理它。可以通过顶级函数(如[Info]和[Error])访问默认的Logger,这些函数调用相应的Logger方法。
日志记录由时间、级别、消息和一组键值对组成,其中键是字符串,值可以是任意类型。例如,
slog.Info("hello", "count", 3)
要更多地控制输出格式,请使用不同的处理程序创建一个Logger。以下语句使用[New]创建一个具有TextHandler的新Logger,该处理程序将结构化记录以文本形式写入标准错误:
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
[TextHandler]的输出是一系列的键值对,易于被机器解析。以下语句:
logger.Info("hello", "count", 3)
会产生如下输出:
time=2022-11-08T15:28:26.000-05:00 level=INFO msg=hello count=3
该包还提供了[JSONHandler],其输出是逐行分隔的JSON:
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("hello", "count", 3)
会产生如下输出:
{"time":"2022-11-08T15:28:26.000000000-05:00","level":"INFO","msg":"hello","count":3}
[TextHandler]和[JSONHandler]都可以使用[HandlerOptions]进行配置。可以设置最低级别(参见下文中的Levels)、显示日志调用的源文件和行号,以及在记录属性被日志记录之前修改它们的选项。
使用以下语句将一个logger设置为默认值:
slog.SetDefault(logger)
将导致[Info]等顶级函数使用它。[SetDefault]还更新了[log]包使用的默认logger,这样已经使用[log.Printf]和相关函数的现有应用程序无需重新编写即可将日志记录发送到logger的处理程序。
某些属性对许多日志调用都是常见的。例如,您可能希望在与服务器请求相关的所有日志事件中包含请求的URL或跟踪标识符。您可以使用[Logger.With]构建包含这些属性的新Logger,而无需在每个日志调用中重复指定属性:
logger2 := logger.With("url", r.URL)
With的参数与[Logger.Info]中使用的键值对相同。结果是一个新的Logger,具有与原始Logger相同的处理程序,但附加的属性将出现在每个调用的输出中。
[Level]是表示日志事件重要性或严重级别的整数。级别越高,事件越严重。该包定义了常见级别的常量,但任何int都可以用作级别。
在应用程序中,您可能希望仅记录某个级别或更高级别的消息。一个常见的配置是仅记录Info级别或更高级别的消息,直到需要调试日志。内置的处理程序可以通过设置[HandlerOptions.Level]来配置输出的最低级别。通常,在程序的main函数中进行此设置。默认值为LevelInfo。
将[HandlerOptions.Level]字段设置为[Level]值会在其生命周期内固定处理程序的最低级别。将其设置为[LevelVar]允许级别动态变化。LevelVar持有一个Level,可以从多个goroutine中安全地读取或写入。要为整个程序动态改变级别,请首先初始化全局LevelVar:
var programLevel = new(slog.LevelVar) // 默认为Info级别
h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel})
slog.SetDefault(slog.New(h))
现在,程序可以通过一条语句更改其日志级别:
programLevel.Set(slog.LevelDebug)
前端与后端
前端,slog 认为你常用且能看得见的 API 都是前端,例如:Info、Debug 、With,规范了用户使用姿势。
后端,slog 认为实际干具体业务逻辑的 Handler 是后端,并将其抽象成了 Handler 接口,只需要实现 Handler 接口,就可以注入自定义 Handler。
type Handler interface {
// 启用记录的日志级别
Enabled(Level) bool
// 具体的处理方法,需要 Enabled 返回 true
Handle(r Record) error
WithAttrs(attrs []Attr) Handler
WithGroup(name string) Handler
}
- 前端方法(例如:Info)将所传属性封装为 Record 类型的变量。
- 将 Record 类型的变量传递给后端方法(例如:Handle)。
- 后端 Handle 方法根据所得 Record,进行对应的格式化、方法调用、日志输出等具体日志动作。
kratos log 设计思路
为了方便使用,Kratos 定义了两个层面的抽象,Logger 统一了日志的接入方式,Helper 接口统一的日志库的调用方式。
在不同的公司、使用不同的基础架构,可能对日志的打印方式、格式、输出的位置等要求各有不同。Kratos 为了更加灵活地适配和迁移到各种环境,把日志组件也进行了抽象,这样就可以把业务代码里日志的使用,和日志底层具体的实现隔离开来,提高整体的可维护性。
Kratos 的日志库主要有如下特性:
- Logger 用于对接各种日志库或日志平台,可以用现成的或者自己实现
- Helper 是在您的项目代码中实际需要调用的,用于在业务代码里打日志
- Filter 用于对输出日志进行过滤或魔改(通常用于日志脱敏)
- Valuer 用于绑定一些全局的固定值或动态值(比如时间戳、traceID 或者实例 id 之类的东西)到输出日志中
Logger:这个是底层日志接口,用于快速适配各种日志库到框架中来,仅需要实现一个最简单的 Log 方法。
type Logger interface {
Log(level Level, keyvals ...interface{}) error
}
这个 Logger 接口在实现完毕后的使用,简单来讲就是如下的样子:
logger.Log(log.LevelInfo, "msg", "hello", "instance_id", 123)
前端与后端
前端,kratos 同样使用 helper 来统一用户调用日志库姿势。
后端,kratos 认为 Logger 可以用来对接任何第三方库。
kratos 接入 slog
package logrus
import (
"context"
"golang.org/x/exp/slog"
"github.com/go-kratos/kratos/v2/log"
)
var _ log.Logger = (*Logger)(nil)
type Logger struct {
log *slog.Logger
level slog.Level
}
func NewLogger(logger *slog.Logger, level slog.Level) log.Logger {
return &Logger{
log: logger,
level: level,
}
}
func (l *Logger) Log(level log.Level, keyvals ...interface{}) (err error) {
var (
slogLevel slog.Level
)
switch level {
case log.LevelDebug:
slogLevel = slog.LevelDebug
case log.LevelInfo:
slogLevel = slog.LevelInfo
case log.LevelWarn:
slogLevel = slog.LevelWarn
case log.LevelError:
slogLevel = slog.LevelError
default:
slogLevel = slog.LevelDebug
}
if slogLevel > l.level {
return
}
if len(keyvals) == 0 {
return nil
}
if len(keyvals)%2 != 0 {
keyvals = append(keyvals, "")
}
l.log.Log(context.Background(), l.level, "xx", keyvals)
return
}
以上仅是简单的接入示例,未来如果官方库 slog 真的进入标准库,大概率 kratos 会废弃自己的 log库,转而直接使用官方的日志库,毕竟他们的设计哲学一致!