让我们来谈谈Go的日志记录(译文) | Go主题月

1,972 阅读6分钟

这是一篇以线程为灵感的文章 Nate Finch started on the Go Forum

这篇文章的重点是 Go ,但是如果你发现你的方法已经过时了,那么这篇文章提出的想法可能是广泛适用的。

为什么没有爱?

Go的 日志包 没有分类,你必须手动添加前缀,如 debug, info, warn, 和 error。此外,Go 的 logger 类型没有方法根据每个包打开或关闭这些不同级别。通过比较,让我们看看第三方的一些替代品。

Google 的 glog 提供以下级别:

  • Info(信息)
  • Warning(警告)
  • Error(错误)
  • Fatal (致命,终止程序)

我们为 Juju 开发的另一个库 loggo 提供了以下级别:

  • Trace(追踪)
  • Debug(调试)
  • Info(信息)
  • Warning(警告)
  • Error(错误)
  • Critical(严重)

Loggo 还提供了根据每个包调整日志详细程度的功能。

下面是两个例子,显然受到其他语言中其他日志库的影响。事实上,它们的命令行可以追溯到 syslog(3),甚至更早。我觉得他们错了。

我想采取一个矛盾的立场。我认为所有的日志库都不好,因为它提供了太多的特性;一系列令人困惑的选择让程序员眼花缭乱,他们必须清楚地思考如何与未来的读者沟通;谁将消耗他们的日志。

我认为,成功的日志记录包需要的功能要少得多,当然也需要更少的级别。

我们来谈谈 warning

让我们从最简单的开始。没有人需要警告日志级别。

没人看 warning,因为从定义上讲,没有什么大问题。也许将来会出问题,但这听起来像是别人的问题。

此外,如果你正在使用某种级别的日志记录,那么为什么要将级别设置为 warning 呢?你将级别设置为 info 或 error。将级别设置为 warning 标识承认您可能正在 warning 级别记录错误。

消除警告级别,它要么是一条信息性消息,要么是一个错误条件。

将级别设置为 warning 表示承认您可能在 warning 级别记录错误。

消除警告级别,它要么是一条信息性消息,要么是一个错误条件。

我们来谈谈 fatal

Fatal 级别是有效地记录消息,然后调用 os.Exit(1)。 原则上这意味着:

  • 其他 goroutine 中的 defer 语句不运行。
  • 缓冲区未刷新。
  • 不会删除临时文件和目录。

实际上,log.Fatalpanic 更详细,但在语义上相当于 panic

通常认为库不应该使用 panic ,但是如果调用 log.Fatal 有同样的效果,当然这也应该被取缔。

建议通过向日志系统注册关闭处理程序来解决此清理问题,这会在日志系统和发生清理操作的每个位置之间紧密耦合;这也违反了关注点的分离。

不要在致命级别记录日志,而是向调用者返回一个错误。如果错误一直持续到 main.main 那在退出之前,这就是处理任何清理操作的正确位置。

我们来谈谈 error

error 处理和日志记录密切相关,因此从表面上看,error 级别的日志记录应该是合理的。这点我不同意。

在 Go 中,如果函数或方法调用返回错误值,实际上有两个选项:

  • 处理错误。
  • 将错误返回给调用者。你可以选择对错误进行包装,但这对本次讨论并不重要。

如果你选择通过日志记录来处理错误,那么根据定义,它不再是一个错误 — 你已经处理了它。记录错误的行为会处理错误,因此不再适合将其记录为错误。

让我试着用代码来说服你:

err := somethingHard()
if err != nil {
        log.Error("oops, something was too hard", err)
        return err // what is this, Java ?
}

你不应该在错误级别记录任何内容,因为你应该处理错误,或者将其传递回调用者。

要清楚的是,我不是说你不应该记录一个错误发生的条件

if err := planA(); err != nil {
        log.Infof("could't open the foo file, continuing with plan b: %v", err)
        planB()
}

但实际上 log.Info 以及 log.Error 有同样的目的。

我不是说不要记录错误!相反,问题是,最小的日志 API 是什么?当提到错误时,我相信绝大多数记录在错误级别的条目都是简单的,因为它们与错误相关。事实上,它们只是信息性的,因此我们可以从 API 中删除错误级别的日志记录。

还剩下什么?

我们排除了警告,认为不应该在错误级别记录任何内容,并且表明只有 app 的顶层应该有某种类型的 log.Fatal 行为。还剩下什么?

我认为只有两件事你应该记录:

  • 开发人员在开发或调试软件时关心的事情。
  • 用户在使用软件时关心的事情。

显然,它们分别是调试级别和信息级别。

log.Info 只需将该行写入日志输出。不应该选择关闭它,因为只需要告诉用户对他们有用的事情。如果发生了一个无法处理的错误,它就会在程序终止的地方冒出来 main.main。在最后的日志消息前面插入 FATAL 前缀,或者使用 fmt.Fprintf 直接写入 os.Stderr 不足以进行日志记录软件包增长 log.Fatal 方法。

log.Debug ,是完全不同的事情。它由开发人员或支持工程师控制。在开发过程中,调试语句应该是丰富的,而不必求助于 trace 或 debug2 (你知道自己是谁)级别。日志包应该支持细粒度控制,以启用或禁用调试,并且只在包或更精细的范围内启用或禁用调试语句。

总结

如果这是一个推特投票,我会让你在两者之间做出选择

  • 记录很重要
  • 记录很难

但事实是,日志记录两者兼而有之。这个问题的解决方案必须是去构造和无情地配对不必要的干扰。

你怎么认为?这是疯狂到足以工作,还是纯粹的疯狂?

笔记

  1. 有些库可能使用 panic/recover 作为内部控制流机制,但最重要的是它们不能让这些控制流操作泄漏到包边界之外。

  2. 具有讽刺意味的是,虽然它缺乏调试级别的输出,但Go标准日志包同时具有 FatalPanic 功能。在这个软件包中,导致程序突然退出的函数的数目超过那些没有退出的函数。

相关连接:

  1. Talk, then code
  2. Stack traces and the errors package
  3. The package level logger anti pattern
  4. Context is for cancelation

Dave Cheney 发布于2015年11月5日

原文连接:dave.cheney.net/2015/11/05/…