Go 十年了,终于想起要统一 log 库了!

本文参与了思否技术征文,欢迎正在阅读的你也加入。

大家好,我是煎鱼。

在日常工作中,打日志是很常见的动作。毕竟不打日志,从内部来讲,一旦出问题,定位、排查都会变的非常困难。谁也不想大半夜在那靠猜解决问题。

在其他方面,对日志的存储的内容、时长、安全均有不同程度的合规要求,应对客户诉求和提单上门的事件。

日志好不好用,就成了重要的诉求了。

标准库 log 很痛

思考一个问题:平时你在写 Go 工程时,是否很少直接使用官方标准库 log?

在正式项目中,大多是优先使用几个爆款第三方库,例如:Logrus、Zap、zerolog。而标准库 log,在临时调试,屏幕输出的场景居多,占比较少。

这问题出在了哪里?主要集中在以下方面:

  • 没有日志分级。不便于分类、定位、排查问题,例如:Error、Warn、Info、Debug 等。
  • 没有结构化日志。只提供格式化日志,不提供结构化,不便于程序读取、解析,例如:Json 格式。
  • 没有扩展性,灵活度差。标准库 log 的日志输出都是固定格式,没有一个 Logger 接口规范,让大家都遵守,以至于现在社区纯自然演进,难互相兼容。

除此之外,在用户场景上,有着不包含上下文(context)信息、性能不够强劲、无法引入自定义插件等扩展诉求。基本上第三方库均有实现的,基本都用户的痛点之一。

为什么不早点解决

你可能会想,标准库 log 作为 Go 生态里的核心库,为什么不早点解决?

实际上在 2017 年时,有在社区进行了大规模讨论,可惜放弃了。原因是:“我们还没有找到足够多的导入和使用具体 Logger 的 Go 库,因此没有理由继续开展这项工作”。

如下图:

g/golang-dev/c/F3l9Iz1JX4g/m/t0J0loRaDQAJ

继续摆烂。

救星 slog 库诞生

讨论和目标

在 2022 年 8 月,Go 团队的 @
Jonathan Amsterdam 发起了 discussion: structured, leveled logging 的讨论,试图与这个乱象再度一决雌雄。

discussions/54763

提案(含讨论)的目标是:

  • 使用方便。对现有 Logger 库的调查说明,开发人员更想要一个简洁且易懂的日志 API。
  • 高性能。新的 API 希望做到最大限度的减少内存分配和锁定。
  • 与运行时跟踪集成。Go 团队正在开发和改进运行时跟踪系统,基于新 Logger 库的日志将可以无缝衔接到这个跟踪系统中,开发人员能够实现程序操作与运行时的行为相关联。

目标涵盖了前文背景中提到的痛点。我关注到上述的第三点,来自 Go 团队自己的需求,果然最优先要做的需求都是自己想要 PUSH 的需求?雾了雾了。

毕竟已经 10 年了,本讨论中得到了许多人的建议和推进,成功孵化。

快速 Demo

该库目前已经经过 “石锤” 阶段,进入了实验库,导入地址是:golang.org/x/exp/slog。

我们先上手新日志库 slog 的快速 Demo,便于大家快速了解和熟悉。

如下代码:

import "log/slog"

func main() {
    slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr)))
    slog.Info("hello", "name", "Al")
    slog.Error("oops", net.ErrClosed, "status", 500)
    slog.LogAttrs(slog.ErrorLevel, "oops",
        slog.Int("status", 500), slog.Any("err", net.ErrClosed))
}

如果不设置 slog.SetDefault 将会默认输出到标准输出。由于上述程序设置了 os.Stderr,因此会在此输出。

程序结果如下:

time=2022-10-24T16:05:48.054-04:00 level=INFO msg=hello name=Al
time=2022-10-24T16:05:48.054-04:00 level=ERROR msg=oops status=500 err="use of closed network connection"
time=2022-10-24T16:05:48.054-04:00 level=ERROR msg=oops status=500 err="use of closed network connection"

我们已经看到了日志分级(Level)、自定义字段追加、设置输出地等特性。在输出格式上,新的 slog 库,将会采取与 logfmt 库类似的方式来实现,内置至少两种格式。

默认的 logfmt 消息格式:

foo=bar a=14 baz="hello kitty" cool%story=bro f %^asdf

如果想调整为 JSON 格式,可进行设置:

slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr)))

会使用 JSON 格式输出:

{ "foo": "bar", "a": 14, "baz": "hello kitty", "cool%story": "bro", "f": true, "%^asdf": true }

设计思路

作者将 slog 库的设计分为:前端、后端。

前端,slog 认为你常用且能看得见的 API 都是前端,例如:Info、Debug 等日志分级的,设置上下文内容的 Context 和自定义字段注入等都包含在前端的范畴内。

如下方法:

后端,slog 认为实际干具体业务逻辑的 Handler 是后端,并将其抽象成了 Handler 接口,只需要实现 Handler 接口,就可以注入自定义 Handler。

如下 Handler 接口:

type Handler interface {
  // 启用记录的日志级别
    Enabled(Level) bool
  // 具体的处理方法,需要 Enabled 返回 true
    Handle(r Record) error

    WithAttrs(attrs []Attr) Handler

    WithGroup(name string) Handler
}

其中你可以看到 Handle 函数有一个 Record 属性,它是一个核心的数据结构。

如下代码:

type Record struct {
    Time time.Time

    Message string

    Level Level

    Context context.Context
}

新的 slog 的内部流程如下:

  1. 前端方法(例如:Info)将所传属性封装为 Record 类型的变量。
  2. 将 Record 类型的变量传递给后端方法(例如:Handle)。
  3. 后端 Handle 方法根据所得 Record,进行对应的格式化、方法调用、日志输出。

与其他 Logger 交互

那回到最开始的问题?

如果我们现在要写一个私有的 Logger,或是复用 Zap。要怎么做?

后端方法,有两条路(同一条路):

  1. 要不走 Record,调用 NewRecord 将其包装成 Record 类型的变量,再往下传。
  2. 要不走 Handle,将处理逻辑写到自定义 Handle 中去完成。

如果是想在前端方法来处理,很遗憾,Go 没有计划将 slog 前端开放。确保了前端稳态,后端可变可扩展的灵活性。

如果有兴趣了解如何实现自定义 Handle,可以查看 TextHandlerJSONHandler 即可,是官方最佳实践。

上下文注入

经典的 context 场景,slog 库直接内置了相关的函数进行支持。

如下代码:

func FromContext(ctx context.Context) Logger
    FromContext returns the Logger stored in ctx by NewContext, or the default
    Logger if there is none.

func NewContext(ctx context.Context, l Logger) context.Context
    NewContext returns a context that contains the given Logger. Use FromContext
    to retrieve the Logger.

具体的 Demo:

func handle(w http.ResponseWriter, r *http.Request) {
    rlogger := slog.FromContext(r.Context()).With(
        "method", r.Method,
        "url", r.URL,
        "traceID", getTraceID(r),
    )
    ctx := slog.NewContext(r.Context(), rlogger)
    // ... use slog.FromContext(ctx) ...
}

还是比较方便的。

总结

在此刻,Go 社区中的 log 库们已经基本成熟,格局已定的 7788。此时 Go 官方的 slog 库推出,很明显吸取了前者的大量丰富经验(提案有声明)。

我相信在未来 slog 库,会和更多的 Go 生态的工具链打通,提供更丰富的关联场景。解决 Go 没有一个靠谱 log 库的痛点。

你觉得这个新库对你有帮助吗?欢迎一起交流。


煎鱼的清汤锅
今天写代码了吗 :-) 博客地址:[链接]
8.2k 声望
12.7k 粉丝
0 条评论
推荐阅读
Go 错误处理:100+ 提案全部被拒绝,为何现阶段仍用 if err != nil?
在 2018 年 8 月,现任 Go 核心团队负责人 Russ Cox 给 Go2 的错误处理画了一个大大的蓝图,并介绍了一个未实现的设计草案。

煎鱼阅读 441

Spring事务传播行为详解
Spring在TransactionDefinition接口中规定了7种类型的事务传播行为。事务传播行为是Spring框架独有的事务增强特性,他不属于的事务实际提供方数据库行为。这是Spring为我们提供的强大的工具箱,使用事务传播行可...

JerryTse243阅读 122.8k评论 97

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木75阅读 7.1k评论 16

Java12的新特性
Java语言特性系列Java5的新特性Java6的新特性Java7的新特性Java8的新特性Java9的新特性Java10的新特性Java11的新特性Java12的新特性Java13的新特性Java14的新特性Java15的新特性Java16的新特性Java17的新特性Java...

codecraft63阅读 11.9k

Java8的新特性
Java语言特性系列Java5的新特性Java6的新特性Java7的新特性Java8的新特性Java9的新特性Java10的新特性Java11的新特性Java12的新特性Java13的新特性Java14的新特性Java15的新特性Java16的新特性Java17的新特性Java...

codecraft32阅读 24.6k评论 1

从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木45阅读 8.6k评论 6

从零搭建 Node.js 企业级 Web 服务器(二):校验
校验就是对输入条件的约束,避免无效的输入引起异常。Web 系统的用户输入主要为编辑与提交各类表单,一方面校验要做在编辑表单字段与提交的时候,另一方面接收表单的接口也要做足校验行为,通过前后端共同控制输...

乌柏木35阅读 6.7k评论 10

8.2k 声望
12.7k 粉丝
宣传栏