03 基础 go 包开发 - 日志包
1. 日志记录
记录日志通常涉及到以下几个方面:
- 日志记录方式
- 日志记录规范
- 日志保存方式
1.1 日志记录方式
在 Go 项目开发中,通过日志包来记录日志。所以,在项目开发之前,需要准备一个易用、满足需求的 Go 日志包。准备日志包的方式有以下三种:
- 使用开源日志包:使用开源的日志包,例如 log、glog、logrus、zap 等。Docker、 ilium、Tyk 等项目使用了 logrus,etcd 则使用了 log 和 zap;
- 定制化开源日志包:基于开源日志包封装一个满足特定需求的日志包。例如,Kubernetes 使用的 klog 是基于 glog 开发的。有些项目封装的日志包还会兼容多种类别的 Logger;
- 自研日志包:根据需求,从零开发一个日志包。
目前已有许多开源日志包,社区比较受欢迎的开源日志包有 logrus、zap、zerolog、apex/log、log15 等。其中最受欢迎的两个日志包是 logrus 和 zap。k8s 使用的时 klog 进行记录。
一般项目中基于 logrus 或者 zap 两个包进行使用基本足够:
- logrus 功能强大、使用简单,不仅实现了日志包的基本功能,还有很多高级特性,适合一些大型项目,尤其是需要结构化日志记录的项目。因为 logrus 封装了很多能力,性能一般。
- zap 提供了很强大的日志功能,性能高,内存分配次数少,适合对日志性能要求很高的项目。另外,zap 包中的子包 zapcore,提供了很多底层的日志接口,适合用来做二次封装。
logrus、 zap、 klog 对比:
| 特性对比 | logrus (1) | zap (2) | klog (3) |
| 日志级别 | 支持 Debug、Info、Warn、Error、Fatal、Panic | 支持 Debug、Info、Warn、Error、DPanic、Panic、Fatal | 支持 Info、Warning、Error、Fatal、Panic |
| 结构化日志 | 支持,通过 Fields 添加结构化字段 | 支持,性能优化,可直接将复杂类型作为字段 | 支持,但不如 logrus 和 zap 灵活 |
| 日志格式 | 默认文本格式,可自定义为 JSON | 提供 Console 和 JSON 编码器,可灵活配置 | 默认文本格式,可自定义为 JSON |
| 日志输出 | 支持控制台、文件等 | 支持控制台、文件、网络等 | 支持控制台、文件 |
| 调用堆栈 | 支持,可输出堆栈信息 | 支持,可在特定级别输出堆栈 | 支持,可在日志中输出堆栈信息 |
| 插件支持 | 支持,可通过插件扩展功能 | 支持,可通过 Hooks 机制扩展 | 不支持 |
| 性能对比 | logrus (3) | zap (1) | klog (2) |
| 性能表现 | 性能一般,适合中小规模项目 | 性能极高,适合对性能要求极高的项目 | 性能较好,但不如 zap |
| 内存分配 | 内存分配较多,性能瓶颈 | 使用 sync.Pool,内存分配少 |
内存分配适中 |
| 易用性 | logrus (1) | zap (3) | klog (2) |
| 学习成本 | 较低,适合新手 | 较高,功能丰富 | 较低,基于 glog 封装 |
| 使用复杂度 | 使用简单,适合快速开发 | 功能强大但配置复杂 | 使用简单,适合 Kubernetes |
| 适用场景 | logrus | zap | klog |
| 适用项目 | 中小型项目、对结构化日志有需求的项目 | 高性能要求、大规模分布式系统 | Kubernetes 生态 |
| 二次封装 | 不太适合,功能封装较多 | 非常适合,底层接口丰富 | 不适合,主要用于 Kubernetes |
综上:
- logrus:功能强大且灵活,适合中小规模项目,尤其是对结构化日志有需求的场景。
- zap:性能卓越,适合对性能要求极高的项目,尤其是分布式系统。
- klog:适合 Kubernetes 生态,使用简单,但功能相对有限。
如果是 Kubernetes 生态,应该选用 klog,平时开发直接选用 zap 即可。
定制化开源日志包以及自研日志包大概都接触不上,所以暂时不赘述。
1.2 日志记录规范
miniblog 也制定了相应的日志规范,具体规范内容见 docs/devel/zh-CN/conversions/logging.md。该日志规范可以在后续的开发过程中根据需求不断更新和迭代。
在 miniblog 的日志规范中,有以下两点规范需要注意:
- 错误日志应在最初发生错误的位置打印。这样做一方面可以避免上层代码缺失关键的日志信息(因为上层代码可能无法获取错误发生处的详细信息),另一方面可以减少日志漏打的情况(距离错误发生位置越远,越容易忽略错误的存在,从而导致日志未被打印);
- 当调用第三方报函数或放发报错时,需要在错误处打印日志,例如:
1 | |
对于嵌套的 Error,可在 Error 产生的最初位置打印 Error 日志,上层如果不需要添加必要的信息,可以直接返回下层的 Error。例如:
1 | |
在最初产生错误的位置打印日志,可以很方便地追踪到错误产生的根源,并且错误日志只打印一次,可以减少重复的日志打印,减少排障时重复日志干扰,也可以提高代码的简洁度。当然,在开发中也可以根据需要对错误补充一些有用的信息,以记录错误产生的其他影响。
1.3 日志保存方式
我们可以将日志保存到任意需要的位置,常见的保存位置包括以下几种:
- **标准输出:**通常用于开发和测试阶段,主要目的是便于调试和查看;
- **日志文件:**这是生产环境中最常见的日志保存方式。保存的日志通常会被 Filebeat、Fluentd 等日志采集组件收集,并存储到 Elasticsearch 等系统中;
- **消息中间件:**例如 Kafka。日志包会调用 API 接口将日志保存到 Kafka 中。为了提高性能,通常会使用异步任务队列异步保存。然而,在这种情况下,需要开发异步上报逻辑,且服务重启时可能导致日志丢失,因此这种方式较少被采用。
当前比较受欢迎的日志包(如 zap、logrus 等)都支持将日志同时保存到多个位置。例如,miniblog 项目的日志包底层封装了 zap,zap 支持同时将日志输出到标准输出和日志文件中。
如果应用采用容器化部署,建议优先将日志输出到标准输出。容器平台通常具备采集容器日志的能力,采集日志时可以选择从标准输出采集或从容器内的日志文件中采集。如果选择从日志文件采集,则需要配置日志采集路径;而如果选择从标准输出采集,则无需额外配置,可以直接复用容器平台现有的能力,从而实现日志记录与日志采集的完全解耦。在 Kubernetes 最新的日志设计方案中,也建议应用直接将日志输出到标准输出。
2. miniblog 日志包
2.1 miniblog 日志包开发
这里不做过多说明,详情参考文章:10 | 基础 Go 包开发:日志包设计和实现
2.2 miniblog 日志包使用
2.2.1 开箱即用前需要知道的知识点
日志级别和记录方法:
- **日志级别:**在记录日志时,按严重性由低到高通常包括 Debug、Info、Warn、Error、Panic、Fatal 级别。Warn 级别在有些日志包中也叫 Warning 级别;
- **日志记录方法:**每个日志级别,根据记录方式,又包括非格式化记录、格式化记录和结构化记录三种方式。形如 Info(msg string) 的方法为非格式化记录方式。形如 Infof(format string, args …any) 的方法为格式化记录方式。形如 Infow(msg string, kvs …any) 的方法为结构化记录方式。Infow 方法名中的 w 代表“with”,即“带有”额外的上下文信息。这些方法与没有 w 的方法(如 Debug,Info 等)相比,允许你在日志消息后面附加额外的键值对(key-value),从而提供更详细的上下文信息。
miniblog 在设计时,为了满足项目不同日志级别的记录需求,实现了 Debug、Info、Warn、Error、Panic、Fatal 级别的记录方法。
在 Go 项目开发中,建议的日志记录方式为结构化记录方式(带有 w 后缀的,例如 Infow**)**。
格式化记录方式可以通过结构化记录方式来替代,例如:log.Infof(“Failed to create user: %s”, username) 可替换为 log.Infow(“Failed to create user”, “username”, username)。
所以,miniblog 项目为了方便日志记录,降低开发者理解日志记录方法的负担,只实现了结构化记录方法。
1 | |
将日志包 log 放置在 internal/pkg 目录下的原因在于,日志包封装了一些定制化的逻辑,不适合对外暴露,所以不适合放在 pkg/ 目录下。但是日志包又是项目内的共享包,所以需要放在 internal/pkg 目录下。
通过定义 Logger 接口,可以体现接口即规范的编程哲学。这意味着,通过 Logger 接口可以清晰地表明 zapLogger 需要实现哪些方法,并明确日志调用者应调用哪些方法。在 Go 项目中,通常将日志接口命名为 Logger。
2.3 miniblog 日志包开箱即用
2.3.1 初始化日志包
日志配置,实际开发项目中如有特殊配置项再更新改配置:https://github.com/mmungdong/miniblog/blob/master/internal/pkg/log/options.go
1 | |
1 | |
初始化日志包:
1 | |
在 run 函数中,添加了 log.Init(logOptions()) 函数调用,用来在应用运行时,初始化日志实例,并在 miniblog 应用退出时,调用 log.Sync() 将缓存中的日志写入磁盘中。
🤗 总结归纳
- 整理日志记录打印以及保存方式
- miniblog 日志包开箱即用说明方式