正如大家所听说的那样,随着 Go 语言的发展,越来越多的公司(人)采用 Go 语言来开发他的应用,尤其在微服务实践中比例尤其高。伴随着 Go 应用程序的使用,当然,对于其他语言的应用程序也一样,如果你想让你的代码运行得更有维护性一些,日志是你离不开的一个重要主题,一个良好的日志处理可以帮助你快速得定位问题,解决 BUG,监控应用和提升性能,甚至于可以帮助你了解客户的习惯行为。

在这篇文章中,我将向您展示一些管理 Go 日志的工具和技术。我将从对于不同类型的需求该使用哪个 Go 语言包开始,然后,我将解释一些使您的日志更易于搜索和可靠的技术,减少日志记录设置的资源占用空间,并标准化日志消息。

选择正确的Log 包

虽然 Go 语言的 log 包很多,但是其实都大同小异,下面我就以两种类型为例进行一个总结,其实就是结构型日志和非结构型日志。

简单的日志使用

在 Go 语言中,内置了 log 模块,可以格式化得输出我们的日志内容,并且还内置了日志级别,甚至还可以将对应的错误都打印出来,这里就给出一个简单的日志错误的示例:

这里的输出结果就是:

结构性日志

但是,前面这个 Go 语言自带的日志不够清晰,并且等级划分也不够用于工业化,所以,我这里推荐一个流行的结构化日志包:logrus,通过 logrus,我们可以将日志以 json 的形式打印出来。同时,因为我们的日志格式是 json 的,所以对于很多工具,我们都可以直接得通过解析 json 来获取有价值的信息,而且增建字段也是很方便的。

使用 logrus,我们可以通过方法 WithFields 直接添加字段,并且可以很简单得使用 Debug/Info/Error 这样的方法来控制日志级别,然后 logrus 就会将日志以 json 的形式打印出来了,下面就给一个例子看看效果:

这一段的输出结果如下,我做了一个格式化,这样更方便看一些:

  1. {
  2. "appname":"personal-blog",
  3. "hostname":"liqiang.io",
  4. "level":"info",
  5. "msg":"GET \/",
  6. "session":"1ce3f6v",
  7. "string":"foo",
  8. "time":"2019-04-13T14:05:25+08:00"
  9. }

非结构性日志

虽然,json 形式的结构化日志很好,但是这个好是相对于工具来说的,对于人来说阅读起来还是会比较难的,当然,对于现在的分布式系统来说,越来越少上机器看日志了,但是这多少也是有需求的。所以,当你想找一个非结构化的日志包时,我是推荐使用 hashicorp 开源的 logutils,这个使用也是很简单,而且功能也很强大,下面给出一个例子:

它其实是扩展了内置的 log 模块,然后可以自定义 Log 的等级以及日志写到那里去,这一段的输出是这样的:

不过这个例子里面不太方便的一点就是日志级别需要自己以字符串的形式来写出,但是,这无妨,我们可以很简单得自己再封装一层 Utils,然后就可以了。

日志的级别

这里参考 log4j 的日志级别进行一个总结,log4j 定义了 8 个级别的 log(除去 OFF 和 ALL,可以说分为 6 个级别),优先级从高到低依次为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、 ALL。

写和存储日志的最佳实践

虽然在代码中写日志的功能已经解决了,但是,还有如何存储这些日志和处理这些日志又是一个问题,这里我给出我的几条最佳实践:

直接在主进程中写日志,不要另开 goroutine 用来写出日志

为什么要在主进程中直接写日志,而不要开启 goroutine 写日志,这里主要有两个原因。一个是可能会产生竞态问题,因为他们都共用同一个 io.Writer;另外一个原因就是现在很多 log 包都默认使用了 goroutine,你自己再用就有点多余了。

updated at 2023-09-04

这里描述的意思是不要使用这种方式写日志:

  1. go log.Error("failed to request rest API")

并且这个前提是你的日志库是一个高性能的日志库,对于一些没有特别为高性能设计的日志库,出于性能的考虑,可以考虑专门开设一个单独的 goroutine 进行日志的写出,从而让业务代码不阻塞,例如:

  1. go func() {
  2. for {
  3. data <- ch
  4. switch data.Level {
  5. case "DEBUG":
  6. log.Debug(data.Content)
  7. case "INFO":
  8. log.Info(data.Content)
  9. ... ...
  10. }
  11. }
  12. }
  13. func bizProcessor() {
  14. ... ...
  15. ch <- log.Data{
  16. Level: log.INFO,
  17. Content: ...
  18. }
  19. }

把日志写到本地文件中,不要直接写到远程日志平台

直接把日志写到本地文件中,不要直接写到远程日志平台,即使你会把日志搬到远程平台。因为写到远程平台无论方式如何,都会有远程连接的问题,当远程连接出现问题的时候,你没法知道你的日志是否写成功,这样会存在很大的隐患;同时,远程平台的存在,会导致你的应用程序维护和远程平台的连接,这样很不划算。

保持你日志的格式一致

在打印日志的时候,因为日志的格式是非常松散的,没有一个固定的形式,所以不同的人对于同一个错误或者异常打印的日志可能都是不一样的,这样很不利于日志的查看和工具使用,例如下面这段:

虽然他们都是同样的意思,但是大家打印的字符方式都是不一样的,这样对于日志的使用和查看者来说就很不好了。所以,一个更好的实践就是定义好通用的日志打印接口,这样,对于同种类型的错误,我们可以保证打印出来的日志的结构是一样的,这样很有利于我们过滤和查找日志的真实错误原因。例如这一段:

将日志集中放到日志平台中,方便用于日志管理

虽然在分布式的应用架构下,我们还是可以通过 SSH 登录到目标机器然后查看目标机器的日志,但是,这样在定位问题的时候会非常麻烦。一个解决办法是你可以使用 Go 语言自带的 syslog 包将日志都导入到同一个 syslog 的服务器中,这样你就可以在这台 syslog 服务器中查看日志了。

但是,我个人更推荐的一个方式是使用 ELK 搭配,当然,现在也渐渐变成了 EFK 搭配了,但是无妨,它的使用方式无非就是通过一个日志收集器将日志文件格式化得收集到 ElasticSearch 中,然后 Kibana 来查看日志的内容,这其实也是为什么前面推荐格式化日志的一个原因,因为这样更有利于工具化处理。通过 Kibana,我们就不用登录到服务器去查看日志了,直接从一个 WebUI 上就可以看到日志的内容了。

在微服务中跟踪你的日志

当我们在实践微服务的时候,一个很烦的问题就是当发生错误的时候,你需要跟踪这个错误的根本原因,但是,那么多的服务,需要从哪个服务开始跟踪起呢?所以这个又是个问题了,这个时候日志可以帮我们,第一步,我们可以先查看我们的调用链路断在哪里,先定位到出错的最后一个服务,第二步,打开我们的日志跟踪平台,查看这个调用的日志,这个时候我们就需要这次调用的标示是什么,所以,一个推荐的做法就是在每个调用中都使用唯一的调用 ID,每个服务对于这次调用都使用这个 ID 进行跟踪,这样的话,我们就可以很清晰得知道问题了,应该实践微服务的同学对 opentracing 这个项目都会有所了解,使用它之后,我们大概会得到这样的一个效果:

清理和压缩日志

日志虽然是好东西,但是你不能无穷无尽得往死里打啊,所以我们需要有一个机制来控制日志的数量,例如单个日志文件超过多少大小的时候,会进行截断,并且把以前打印的日志压缩备份起来,这样可以保证我们需要日志的时候是可以找到日志内容的,并且应用程序也可以根据自己的情况一直打日志下去。

但是,不管如何,有一条是必须得到满足的:无论什么时候,我们想查看日志的时候,都应该能给我们查看到需要的日志内容。这其实也可以认为,如果你硬盘够大,或者你可以无限得扩充,使得日志所在的文件系统足够大,那么你完全无需压缩,至于截不截断就看你看日志的时候方不方便了。

小结

本文参考文章:How to collect, standardize, and centralize Golang logs 而来,但是并非直接翻译,而是带个人主义得根据理解重写一遍,请辨证地阅读。