如何更好的打印日志 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
XdpCs
V2EX    Go 编程语言

如何更好的打印日志

  •  
  •   XdpCs
    XdpCs 2024-05-24 17:07:17 +08:00 2123 次点击
    这是一个创建于 509 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在日常的生产过程中,会出现一些 bug ,导致很难查找问题的所在

    想问问各位前辈 有什么打印日志的标准

    是否能给予一些例子

    谢谢各位大佬

    6 条回复    2024-09-03 19:47:17 +08:00
    vczyh
        1
    vczyh  
       2024-05-24 17:10:34 +08:00   1
    首先分离责任边界,对外提供的接口的入参都打印出来,出现错误的话必须打印出来,可以在出现错误的时候打印,也可以在最高层统一打印,永远不要直接吞掉错误。
    kuanat
        2
    kuanat  
       2024-05-25 14:57:37 +08:00   2
    以打印日志的方式来 debug ,在开发阶段和生产阶段是有区别的。正好前两天我在 https://v2ex.com/t/1038327 这个帖子里有个回复,可以参考一下。

    以我的经验来看,日志 debug 这个话题表面上看是个技术问题,然而我更倾向于把它们定义为工程问题。把问题聚焦在代码怎么写上面很难形成有效的思路,想明白动机和目标之间的联系,才能得出如何正确实现的方法。



    我就针对生产环境日志打印做个补充,这里就拿一般 go 服务器后端的场景来说明。

    - 日志采集的方式

    一般要么写本地文件,然后有 agent 负责汇总到日志服务器。要么直接根据配置向日志数据库来写。也就是说一般生产环境里日志排错主要靠数据库查询。

    - 日志生成方式

    上面帖子里有提到,生产环境的日志是“共享”的,也就是说日志主数据库很可能是全链路的信息都在,而某个 go 模块的信息只是一部分。这个主日志里面,多数时间理论上只包含业务信息,无法用于 debug 。

    所以在我接触的项目里,会构建 release/debug 两个版本,线上部署 release 版本。当出现故障需要排错的时候,切一部分流量进入 debug 版本,debug 版本单独输出详细日志。这个日志包含程序运行信息,stack trace 等等。

    - 日志记录的内容

    生产环境排错主要是回答两个问题:首先是判断是不是该模块的错误,其次才是为什么出错。利用日志作为 debug 手段属于某种意义上的“异步”,感到困难的主要原因是,这个异步行为很难在本地开发工作流中复现。

    如果能方便且顺利地在本地复现,困难可能就解决了一半。为了达到复现的目的,需要 debug 级日志中一定要包含进入本模块的输入,以及离开本模块的输出。



    回到代码的层面,要做的就是把上面的思路具体化。

    - 构建测试用例

    当拿到输入和输出的时候,就可以在本地开发流程构建测试用例了。考虑到 go 实现的功能模块会涉及其他模块的调用,那就需要以 mock 的方式,直接引入其他模块的输入输出结果。这里就能看出来,debug 级别的日志,一定要包含和其他模块交互的输入输出。

    这里顺便一提,mock 实现本地测试应该是开发流程中就要做的。

    - 日志可读性

    如果 go 实现的模块比较复杂,要记录各种输入输出就需要一定的组织规范,这就是你提到的问题。如果直接回答如何做,可能很难帮助你解决问题。

    由于异常分析要在请求层面上,所以要求整个系统必须有统一的 traceID 之类的标志,不然就会可能拿着不匹配的输入与输出做调试。多数时候这个 traceID 是以中间件控制,或者在各个模块之间显式传递,无论哪种实现方式,都一定要有。

    大的层面上,一定要记录 stack trace 即调用栈的回溯信息。它的作用是帮助你快速定位出问题的子模块,我个人比较倾向使用的方式类似于 error wrapping 的方式,可以参考 https://go.dev/blog/go1.13-errors 这篇文章。

    这样对于错误,可以输出类似于 `调用方法 A ,入参 XXX : 调用方法 B ,入参 XXX : ...` 这样形式的字符串,冒号用于分隔。怎么记录不重要,标准一致即可。如果有需要,可以本地额外做一个更加 verbose 的 debug 版本,利用反射机制输出调用代码的行数等等。(这个事情主要是 API 接口相关的,模块内部流程的底层错误建议 wrap ,到达模块边界只返回定义好的 sentinel 错误。)

    对于非错误的情况,一般在每个调用的入口和出口,根据调用的状态来输出不同等级的信息,比如某个方法调用的入口,输出 log.Info("XXX"),如果是走的正常分支,就在出口处再记录 log.Info("XXX"),如果走的是异常分支,可以在出口处记录 log.Warning("XXX")。这样最终会形成类似

    ```
    INFO: 调用 A 开始
    INFO: > 调用 B 开始
    INFO: >> 调用 C 开始
    INFO: << 调用 C 分支 C1
    WARN: < 调用 B 分支 B2
    INFO: 调用 A 结束
    ```

    这个样子的日志记录,可以肉眼观察也可以利用文本工具来筛选。

    这个日志的主要作用是帮助你确定异常发生时,对应的逻辑流程是什么,免去了你动态调试跟踪的麻烦。即便后续需要动态调试,也可以很准确地在目标位置下断点。

    - 其他细节

    我暂时能想起来一部分,如果以后想起来再补充。

    日志层面一定要使用结构化的方式来记录,这样无论是写入数据库,还是查询筛选都会方便很多。

    代码层面不要忽略任何错误,所有的错误都要有相应的处理(忽略也是一种处理,但一定要有对忽略错误这个行为的日志记录)。

    模块内部对于所有外部输入都做 mock ,以提高测试覆盖率,方便后期 debug 。

    这个事情的终极目标就是,出问题的时候,日志数据库筛一下就知道业务层面上是否有异常。有的话,立即切 debug 版本就能复现输入输出。拿到输入输出直接扔给本地 mock 版本,跑一下流程就知道是自己模块的问题,还是涉及其他交互模块的问题。如果确定是自己的问题,看 verbose 日志快速确定是哪个方法哪一行的错误。

    写日志本质上就是以上述思路为目标做准备。
    gvison
        3
    gvison  
       2024-05-26 10:21:52 +08:00
    把请求经过的整条链路日志打印出来,通过 trace_id 或 request_id 把入参信息、返回信息、错误信息串联起来,通过 id 过滤可以查看整条请求链路日志信息,排查 bug 挺方便。可以参考这里封装的 gin 中间件 https://github.com/zhufuyi/sponge/blob/main/pkg/gin/middleware/logging.go
    qloog
        4
    qloog  
       2024-06-21 20:15:13 +08:00
    wusu
        5
    wusu  
       2024-06-28 18:33:53 +08:00
    @qloog 正则可以定义成全局变量,提升一点性能
    Charlie17Li
        6
    Charlie17Li  
       2024-09-03 19:47:17 +08:00 via iPhone
    @vczyh 一直很纠结,是在出错的地方打印,还是上层打印。在出错的地方打印,有时候链不知道调用链路,在上层打印,链路一长,定位起来也不够直观
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5313 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 33ms UTC 07:03 PVG 15:03 LAX 00:03 JFK 03:03
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86