跳到主要内容

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
}
  1. 前端方法(例如:Info)将所传属性封装为 Record 类型的变量。
  2. 将 Record 类型的变量传递给后端方法(例如:Handle)。
  3. 后端 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库,转而直接使用官方的日志库,毕竟他们的设计哲学一致!