跳至主要內容

日志规范

孔令飞原创约 5277 字大约 18 分钟OneXOneX

在项目开发中,记录日志是一个非常高频的操作。日志包提供了各种接口,可以让我们根据需要记录日志,但是在记录日志时也需要遵循一定的规范。

如果在阅读日志时,你遇到以下问题,说明你的日志打印需要规范起来:

  • **过多或冗余的日志,干扰排障:**有时候,系统可能会记录过多的日志信息,包括一些无关紧要的或冗余的信息。这会导致日志文件过大,不易于查找和分析关键的日志记录;
  • 缺乏一致性和标准化: 在多个模块或组件中,日志格式和结构可能不一致,导致日志的解析和分析困难。缺乏统一的标准化规范,使得日志的可读性和可维护性下降;
  • **缺乏上下文信息:**打印日志时,只把原始的错误打印出来,没有补充打印上下文信息,例如:请求参数、关键变量值等,导致排障困难,需要重新复现;
  • **同一个错误层层打印:**例如在 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/v2open in new window 包。所有非 Operator、Controller、Kube APIServer Style 的组件统一使用 github.com/superproj/onex/pkg/logopen in new window 包;
  • 当时用 k8s.io/klog/v2open in new window 记录日志时,需要遵循以下规范:
    • 要使用结构化的日志记录方式: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>)进行转换;
  • 如果使用 github.com/superproj/onex/pkg/log日志包:open in new window
    • 要使用结构化的日志记录方式:log.C(ctx).Errorw()、log.C(ctx).Infow()等;
    • 如果日志能获取到 context.Context 变量,需要使用 log.C() 函数打印,例如:log.C(ctx).Warnw("please enable redis, otherwise the idempotent is invalid")。使用 log.C(ctx) 可以输出必要的 KV,例如:trace.idopen in new windowuser.idopen in new window 等。
  • 不要使用 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/dbopen in new window 只返回错误,不记录日志。因为共享库可能会用在命令行工具、其他项目中,如果记录日志,势必会造成命令行工具有日志输出,影响使用体验、共享库的日志输出跟其他项目的日志输出格式不一致等问题;
  • 日志包名字统一为 github.com/superproj/onex/pkg/logopen in new windowk8s.io/klog/v2,如果同一个文件中有其他同名的日志包,需要将其他日志包重命名,而且重命名的名字要在open in new window OneX 项目中保持一致,例如:kratoslog "github.com/go-kratos/kratos/v2/logopen in new window";
  • Error 日志应该在最原始的报错位置打印,一是避免上层代码缺失部分入参,二是避免漏打;
  • 服务初始化时,成功信息和失败信息都需要打印,影响启动的错误需要 panic,并打印 FATAL 日志;
  • 打印参数类型已知的情况下,建议按照对应类型格式化方式打印参数;不确定类型可采用 %v;结构体打印可使用 %+v,可将变量名和变量值都打印出来,但需要注意结构体包含指针类型变量,那打印的只是地址信息,因此需要单独打印。

选择合适的日志级别

不同级别的日志,具有不同的意义,能实现不同的功能,在开发中,我们应该根据目的,在合适的级别记录日志,这里我同样给你一些建议。具体如下表所示:

日志级别描述告警级别
Debug为了获取足够的信息进行 Debug,通常会在 Debug 级别打印很多日志。例如,可以打印整个 HTTP 请求的请求 Body 或者响应 Body。
Debug 级别需要打印大量的日志,这会严重拖累程序的性能。并且,Debug 级别的日志,主要是为了能在开发测试阶段更好地 Debug,多是一些不影响现网业务的日志信息。所以,对于 Debug 级别的日志,在服务上线时我们一定要禁止掉。否则,就可能会因为大量的日志导致硬盘空间快速用完,从而造成服务宕机,也可能会影响服务的性能和产品体验。
Debug 这个级别的日志可以随意输出,任何你觉得有助于开发、测试阶段调试的日志,都可以在这个级别打印。
InfoInfo 级别的日志可以记录一些有用的信息,供以后的运营分析,所以 Info 级别的日志不是越多越好,也不是越少越好,应以满足需求为主要目标。一些关键日志,可以在 Info 级别记录,但如果日志量大、输出频度过高,则要考虑在 Debug 级别记录。
现网的日志级别一般是 Info 级别,为了不使日志文件占满整个磁盘空间,在记录日志时,要注意避免产生过多的 Info 级别的日志。例如,在 for 循环中,就要慎用 Info 级别的日志。
Warn一些警告类的日志可以记录在 Warn 级别,Warn 级别的日志表示遇到了预期之内的错误,并且已经进行了处理,不会影响主要功能。像这些日志,就需要你关注起来。Warn 更多的是业务级别的警告日志。Lark
ErrorError 级别的日志告诉我们程序执行出错,这些错误肯定会影响到程序的执行结果,例如请求失败、创建资源失败等。要记录每一个发生错误的日志,避免日后排障过程中这些错误被忽略掉。大部分的错误可以归在 Error 级别Lark 转电话
PanicPanic 级别的日志在实际开发中很少用,通常只在需要错误堆栈,或者不想因为发生严重错误导致程序退出,而采用 defer 处理错误时使用Lark + 电话
FatalFatal 是最高级别的日志,这个级别的日志说明问题已经相当严重,严重到程序无法继续运行,通常是系统级的错误。在开发中也很少使用,除非我们觉得某个错误发生时,整个程序无法继续运行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 的负载。

日志级别设置规范

日志格式设置规范

  • 线上日志为了便于日志采集工具采集,需要设置为 JSON 格式;
  • 开发、测试环境的日志,可以根据需要设置为TEXT 或 JSON 格式。

日志打印检查

Kubernetes 提供了 logcheckopen in new window 工具,来检查 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工具制定的规范。

其他日志规范参考

贡献者: 孔令飞
手机看手机看
微信扫一扫
手机看
可以手机看或分享至朋友圈
星球星球
一个可以助你进阶为Go+云原生高级开发工程师的实战星球
星球
交流群交流群
添加微信,进Go项目开发交流群
交流群
PS:添加时请备注 入群
下资料下资料
扫码公众号,回复 资料 获取海量编程学习资源「无任何套路」
下资料