日志规范
在项目开发中,记录日志是一个非常高频的操作。日志包提供了各种接口,可以让我们根据需要记录日志,但是在记录日志时也需要遵循一定的规范。
如果在阅读日志时,你遇到以下问题,说明你的日志打印需要规范起来:
- **过多或冗余的日志,干扰排障:**有时候,系统可能会记录过多的日志信息,包括一些无关紧要的或冗余的信息。这会导致日志文件过大,不易于查找和分析关键的日志记录;
- 缺乏一致性和标准化: 在多个模块或组件中,日志格式和结构可能不一致,导致日志的解析和分析困难。缺乏统一的标准化规范,使得日志的可读性和可维护性下降;
- **缺乏上下文信息:**打印日志时,只把原始的错误打印出来,没有补充打印上下文信息,例如:请求参数、关键变量值等,导致排障困难,需要重新复现;
- **同一个错误层层打印:**例如在 Go 简洁架构中,同一个错误日志分别在 Use Case 层和 Service 层分别打印,导致在排障时带来冗余日志干扰,还会导致程序性能下降、浪费存储空间等;
- **根因丢失,无法快速定位故障点:**有时候,错误日志在向上传递过程中,如果做了包装,要附带一些信息,最原始的报错日志可能就会丢失,导致很难定位出错误根因。
日志规范是为了提高日志的可读性、可维护性和可搜索性而制定的一系列规则和约定。将日志打印规范化,可以带来以下好处:
- **可读性:**日志规范可以定义日志的格式、结构和语义,使日志信息更易于理解和解读。统一的日志格式可以让开发人员、运维人员和其他团队成员更容易阅读和理解日志,从而更快地定位和解决问题;
- **可维护性:**日志规范可以定义日志的级别、分类和命名规则,使日志更易于管理和维护。通过规范化的日志级别和分类,可以更好地组织和过滤日志,只关注关键的日志信息,减少冗余和无用的日志记录;
- **可搜索性:**日志规范可以定义日志的关键字、标签和结构,使日志更易于搜索和过滤。通过定义一致的日志结构和关键字,可以使用日志分析工具或搜索引擎来快速搜索和过滤日志,以便查找特定的事件、错误或异常;
- **故障排查:**日志规范可以帮助定位和排查故障。规范的日志格式和结构可以提供更多的上下文信息,包括时间戳、请求参数、异常堆栈等,有助于分析和理解故障现象,加快故障排查的速度和准确性;
- **性能优化:**日志规范可以帮助识别和优化性能问题。通过规范化的日志记录和度量指标,可以更好地监控和分析系统的性能表现,发现潜在的性能瓶颈和优化机会。
可以看到,日志规范是提高日志质量和效用的重要工具。通过制定和遵守日志规范,可以提升团队协作效率,加快故障排查和问题解决的速度,提高系统的可靠性和性能。
本文就来介绍下 OneX 项目日志记录需要遵循的规范。这些日志规范分为以下 2 类:
- 必须遵循的:这类规范是所有组件记录日志时都要遵循的规范;
- 建议遵循的:这些规范是根据需要选择性需要遵循的规范。
日志打印规范
【强制】必须遵循的规范
- 所有日志均使用英文进行记录;
- 记录日志时,要明确日志级别,选择正确的日志级别;
- 打印结构化的日志,不要拼接字符串, 采用 KV 模式;
- 日志均以大写开头,结尾不跟 .(可以接受问号和感叹号,但不推荐),例如:log.Errorw(err, "Failed to create lru cache");
- 使用过去时,例如:Could not delete B 而不是 Cannot delete B;
- 日志信息应使用主语进行记录,当有执行主体时使用完整句子 (A could not do B),如果主体是程序本身则省略主语(Could not do B);
- 日志要脱敏,禁止输出敏感的信息,例如:密码、密钥、手机号、IP 等信息;
- 为了方便阅读日志,日志禁止换行;
- 日志中不要记录无用信息,防止无用日志淹没重要信息;
- 日志信息要准确全面,努力做到仅凭日志就可以定位问题;
- Error 日志必须记录完整的上下文信息,例如:完整输入和输出、关键变量的值等;
- 使用 Warn 级别记录用户输入参数错误导致的程序错误。因为我们 Error 和 Warn 级别的日志告警策略不同,在 Warn 级别打印,可以避免频繁告警;
- 确保日志打印语句不 Panic,例如:klog.V(4).Infof("Connection error: %s %s: %v", t.Op, t.URL, t.Err),如果 t 是 nil 就会导致日志调用时发生 panic,会大大加大排障难度;
- 日志信息禁用字符串拼接,而要使用占位符。使用占位符,格式更清晰,性能更优。例如:klog.V(4).Infof("Get login token: %s", rp.Token);
- 所有 Operator、Controller、Kube APIServer Style 的组件为了跟 K8S 生态保持兼容,统一使用 k8s.io/klog/v2 包。所有非 Operator、Controller、Kube APIServer Style 的组件统一使用 github.com/superproj/onex/pkg/log 包;
- 当时用 k8s.io/klog/v2 记录日志时,需要遵循以下规范:
- 要使用结构化的日志记录方式:klog.InfoS, klog.ErrorS。Example: klog.InfoS("Received HTTP request", "method", "GET", "URL", "/metrics", "latency", time.Second);
- 日志级别:
- Error 级别日志使用:klog.ErrorS;
- Warning 级别日志使用:klog.V(1).InfoS;
- Info 级别日志使用:klog.V(2).InfoS;
- Debug 级别日志使用:klog.V(4).InfoS;
- Trace 级别日志使用:klog.V(5).InfoS。
- 日志键值对,值规范如下:
- 优先使用klog.KObj 或 klog.KObjSlice来记录 Kubernetes 对象;
- 当日志记录对象不是一个标准的 Kubernetes 资源对象时,使用klog.KRef;
- 当日志记录对象是单个 Kubernetes 资源对象时(例如:*v1.Pod),使用klog.KObj;
- 当日志记录对象是 Kubernetes 资源对象数组时(例如[]*v1.Pod),使用klog.KObjSlice。
- 优先传递结构化的对象,而非object.String();
- 当期望将[]byte类型的对象作为string类型记录时,需要明确使用string(<byte array>)进行转换;
- 优先使用klog.KObj 或 klog.KObjSlice来记录 Kubernetes 对象;
- 如果使用 github.com/superproj/onex/pkg/log日志包:
- 不要使用 Fatal 级别的日志,因为 Fatal 级别的日志会调用 os.Exit(255) 导致日志退出。如果确实需要退出,请先打印 Error 级别的日志,在调用 os.Exit(255) 显示退出程序;
- 不要使用 Panic 级别的日志,这会导致程序 Panic,造成服务不稳定。如果程序需要 Panic,可以通过返回 error,并处理改 error 来达到相同的目的;
- 线上日志至少要保留 15 天,因为异常日志具有以 **周 **为频次发生的特点,保留 15 天,可以帮助你在排障时,有日志可以查询;
- 在 Debug、排障过程中,持续不断优化日志输出,定期对代码日志进行 review。如果定位问题时间过长则说明日志需要优化。
提示:
- 任何日志事件都可以简单归为错误日志和非错误日志,所以在使用klog记录日志时,只使用了klog.ErrorS 和klog.InfoS;
- 在日志消息中,关于开头字母大小写的惯例因开发团队而异。一些团队更喜欢使用大写字母开头,这有助于强调重要性,以及使日志更易读,尤其是在较长的日志行中。而其他团队更倾向于使用小写字母,因为这样的日志看起来更加紧凑和一致;
- 无用日志常见情况:
- 能够放在一条日志中的东西放在多条日志中输出;
- 预期会发生且能够正常处理的异常,打印一堆无用的堆栈;
- 为了开发调试方便而加入的“临时”日志;
- 日志过少的情况有:
- 请求出错时不能通过日志直接定位问题,需要添加临时日志并重新请求才能定位问题;
- 无法确定服务中的后台任务是否按照期望执行;
- 无法确定服务的内存数据结构的状态;
- 无法确定服务的异常处理逻辑(如重试)是否正常执行;
- 无法确定服务启动时配置是否正确加载。
- ...
【建议】建议遵循的规范
- 请遵循日志打印基本原则:日志信息要简明扼要、易理解、易搜索,并包含排障所需的上下文
- 失败日志建议格式为 Failed to <动词> + <一些事>,例如:log.Errorw(err, "Failed to initialize casbin adapter");
- 成功日志建议格式为 <动词> + <一些事>,例如:log.Infow("Initialize idempotent success")。
- 共享库,例如:github.com/superproj/onex/pkg/db 只返回错误,不记录日志。因为共享库可能会用在命令行工具、其他项目中,如果记录日志,势必会造成命令行工具有日志输出,影响使用体验、共享库的日志输出跟其他项目的日志输出格式不一致等问题;
- 日志包名字统一为 github.com/superproj/onex/pkg/log 或 k8s.io/klog/v2,如果同一个文件中有其他同名的日志包,需要将其他日志包重命名,而且重命名的名字要在 OneX 项目中保持一致,例如:kratoslog "github.com/go-kratos/kratos/v2/log";
- Error 日志应该在最原始的报错位置打印,一是避免上层代码缺失部分入参,二是避免漏打;
- 服务初始化时,成功信息和失败信息都需要打印,影响启动的错误需要 panic,并打印 FATAL 日志;
- 打印参数类型已知的情况下,建议按照对应类型格式化方式打印参数;不确定类型可采用 %v;结构体打印可使用 %+v,可将变量名和变量值都打印出来,但需要注意结构体包含指针类型变量,那打印的只是地址信息,因此需要单独打印。
选择合适的日志级别
不同级别的日志,具有不同的意义,能实现不同的功能,在开发中,我们应该根据目的,在合适的级别记录日志,这里我同样给你一些建议。具体如下表所示:
日志级别 | 描述 | 告警级别 |
---|---|---|
Debug | 为了获取足够的信息进行 Debug,通常会在 Debug 级别打印很多日志。例如,可以打印整个 HTTP 请求的请求 Body 或者响应 Body。 | |
Debug 级别需要打印大量的日志,这会严重拖累程序的性能。并且,Debug 级别的日志,主要是为了能在开发测试阶段更好地 Debug,多是一些不影响现网业务的日志信息。所以,对于 Debug 级别的日志,在服务上线时我们一定要禁止掉。否则,就可能会因为大量的日志导致硬盘空间快速用完,从而造成服务宕机,也可能会影响服务的性能和产品体验。 | ||
Debug 这个级别的日志可以随意输出,任何你觉得有助于开发、测试阶段调试的日志,都可以在这个级别打印。 | 无 | |
Info | Info 级别的日志可以记录一些有用的信息,供以后的运营分析,所以 Info 级别的日志不是越多越好,也不是越少越好,应以满足需求为主要目标。一些关键日志,可以在 Info 级别记录,但如果日志量大、输出频度过高,则要考虑在 Debug 级别记录。 | |
现网的日志级别一般是 Info 级别,为了不使日志文件占满整个磁盘空间,在记录日志时,要注意避免产生过多的 Info 级别的日志。例如,在 for 循环中,就要慎用 Info 级别的日志。 | 无 | |
Warn | 一些警告类的日志可以记录在 Warn 级别,Warn 级别的日志表示遇到了预期之内的错误,并且已经进行了处理,不会影响主要功能。像这些日志,就需要你关注起来。Warn 更多的是业务级别的警告日志。 | Lark |
Error | Error 级别的日志告诉我们程序执行出错,这些错误肯定会影响到程序的执行结果,例如请求失败、创建资源失败等。要记录每一个发生错误的日志,避免日后排障过程中这些错误被忽略掉。大部分的错误可以归在 Error 级别 | Lark 转电话 |
Panic | Panic 级别的日志在实际开发中很少用,通常只在需要错误堆栈,或者不想因为发生严重错误导致程序退出,而采用 defer 处理错误时使用 | Lark + 电话 |
Fatal | Fatal 是最高级别的日志,这个级别的日志说明问题已经相当严重,严重到程序无法继续运行,通常是系统级的错误。在开发中也很少使用,除非我们觉得某个错误发生时,整个程序无法继续运行 | Lark + 电话 |
根据 OneX 项目的日志规范 Panic、Fatal 级别的日志不需要打印,如果需要,可以使用Error 级别的日志 + os.Exit() 进行处理。
通常, 为了能够及时发现问题并排障,在发生错误日志时,要告警通知到相关的研发或运维,上述表格,也针对不同的日志级别,给出了告警级别,供你参考。
提示:Lark 指代飞书办公软件。
日志级别选择图:
这里用一张图来总结下,如何选择 Debug、Info、Warn、Error、Panic、Fatal 这几种日志级别。
日志打印时机
在打印日志时,要选择合适的时机进行打印,不能随便打印。
建议打印时机
日志主要是用来定位问题的,所以整体来说,我们要在有需要的地方打印日志。那么具体是哪些地方呢?我给你几个建议。
- **打印程序的配置参数:**系统在启动过程中通常会首先读启动参数,可以在系统启动后将这些参数输出到日志中,方便确认系统是按照期望的参数启动的;
- **网络通信部分:**发送请求前、收到请求结果均应打印 Info 级别的日志。根据我的研发经验,如果你的程序调用了第三方组件,在排障时,需要提供给第三方组件的研发/运维足够的上下文,帮助复现问题,才会得到他们及时有效的支持,所以这里建议在请求第三方接口时,至少要记录请求包、返回包、URL 等信息。注意,如果请求包和返回包很大,需谨慎打印;
- **在分支语句处打印日志:**在分支语句处打印日志,可以判断出代码走了哪个分支,有助于判断请求的下一跳,继而继续排查问题;
- **写操作必须打印日志:**写操作最可能会引起比较严重的业务故障,写操作打印日志,可以在出问题时找到关键信息;
- **非预期执行时打印日志:**如果程序走到了跟我们预期不一样的分支,需要打印日志。例如:正常情况下,服务的某个状态应该是 Running 的,但真实的状态是 Pending 的,这种异常的状态,很可能会带来问题,后期可能需要定位排障,这时候可以打印相关日志;
- **后台定期执行的任务:**如定期更新缓存的任务,可以记录任务开始时间,任务结束时间,更新了多少条缓存配置等等,这样可以掌握定期执行的任务的状态;
- **业务流程关键节点:**我们经常会面对流程比较复杂的业务流程,在整个流程的关键节点上,可以记录下日志,例如,当进行物品交换时,可以将要交换的物品打印出来;
- **数据状态变化时:**服务端程序的最核心的逻辑就是维护数据状态的变化,因此,在状态有变化的时候,可以记录下日志,例如,订单从创建状态变为已支付状态时,可以记录日志;
- **在错误产生的最原始位置打印日志:**对于嵌套的 Error,可在 Error 产生的最初位置打印 Error 日志,上层如果不需要添加必要的信息,可以直接返回下层的 Error。我给你举个例子:
package main
import (
"flag"
"fmt"
"github.com/golang/glog"
)
func main() {
flag.Parse()
defer glog.Flush()
if err := loadConfig(); err != nil {
glog.Error(err)
}
}
func loadConfig() error {
return decodeConfig() // 直接返回
}
func decodeConfig() error {
if err := readConfig(); err != nil {
return fmt.Errorf("could not decode configuration data for user %s: %v", "colin", err) // 添加必要的信息,用户名称
}
return nil
}
func readConfig() error {
glog.Errorf("read: end of input.")
return fmt.Errorf("read: end of input")
}
通过在最初产生错误的位置打印日志,我们可以很方便地追踪到日志的根源,进而在上层追加一些必要的信息。这可以让我们了解到该错误产生的影响,有助于排障。另外,直接返回下层日志,还可以减少重复的日志打印。
当代码调用第三方包的函数,且第三方包函数出错时,会打印错误信息。比如:
if err := os.Chdir("/root"); err != nil {
log.Errorf("change dir failed: %v", err)
}
不建议打印时机
当然,在记录日志时,也有有一些地方是不需要记录日志的,这些地方建议如下:
- **在循环中打印日志要慎重:**如果循环次数过多,会导致打印大量的日志,严重拖累代码的性能,建议的办法是在循环中记录要点,在循环外面总结打印出来;
- **QPS 特别高的接口谨慎打印日志:**对于 QPS 特别高的接口,要谨慎选择是否打印日志或者少打印日志,否则可能会影响接口的性能和 CPU 的负载。
日志级别设置规范
- 项目刚刚上线可将日志级别设置为 Debug 级别:
- github.com/superproj/onex/pkg/log 设置 level 为 debug;
- k8s.io/klog/v2 设置 -v 为 4。
- 如果项目经过一段时间的运行,达到一种很稳定的状态,为了不影响性能,需要设置日志级别为 Info:
- github.com/superproj/onex/pkg/log 设置 level 为 info;
- k8s.io/klog/v2 设置 -v 为 2。
- 测试/开发环境可以设置日志级别为 Debug 级别:
- github.com/superproj/onex/pkg/log 设置 level 为 debug;
- k8s.io/klog/v2 设置 -v 为 4。
日志格式设置规范
- 线上日志为了便于日志采集工具采集,需要设置为 JSON 格式;
- 开发、测试环境的日志,可以根据需要设置为TEXT 或 JSON 格式。
日志打印检查
Kubernetes 提供了 logcheck 工具,来检查 Kubernetes 中的日志记录是否符合规范。你也可以安装,并检查,命令如下:
$ go install sigs.k8s.io/logtools/logcheck@latest
$ logcheck -check-contextual ${ONEX_ROOT}/...
$ logcheck -check-structured ${ONEX_ROOT}/...
${ONEX_ROOT}为 OneX 项目根目录,你可以根据需要修改检查目录。
提示:logcheck工具建议可以了解下,真正的项目开发中,并不实用,尤其不适合集成在 CI 流程中,作为项目发布的强制规范检查。因为很多代码确实难以,也不需要完全遵循logcheck工具制定的规范。
其他日志规范参考
- Kubernetes 日志记录规范:Logging;
- Structured and Contextual Logging migration instructions