主页

索引

模块索引

搜索页面

2.1.10. 日志规范

日志包需要实现的功能分为:

1. 基础功能是一个日志包必须要具备的功能
2. 高级功能是在特定场景下可增加的功能
3. 可选功能是在特定场景下可增加的功能

基础功能

  1. 支持基本的日志信息:

    日志包需要支持基本的日志信息,包括:
    时间戳、文件名、行号、日志级别和日志信息
    
  2. 支持不同的日志级别:

    不同的日志级别代表不同的日志类型,例如:
    Error 级别的日志,说明日志是错误类型,在排障时,会首先查看错误级别的日志
    Warn 级别日志说明出现异常,但还不至于影响程序运行,可以用来参考、定位出异常所在
    Info 级别的日志,可以协助我们 Debug,并记录一些有用的信息,供后期进行分析
    
    打印日志时,一个日志调用其实具有两个属性:
    a. 输出级别:打印日志时,我们期望日志的输出级别
    b. 开关级别:启动应用程序时,期望哪些输出级别的日志被打印
      如果开关级别设置为 L ,只有输出级别 >=L 时,日志才会被打印
    
  3. 支持自定义配置:

    不同的运行环境,需要不同的日志输出配置,例如:
      开发测试环境为了能够方便地 Debug,需要设置日志级别为 Debug 级别;
      现网环境为了提高应用程序的性能,则需要设置日志级别为 Info 级别
    
    现网环境为了方便日志采集,通常会输出 JSON 格式的日志
    开发测试环境为了方便查看日志,会输出 TEXT 格式的日志
    
    日志包需要能够被配置,不同环境采用不同的配置
    通过配置,可以在不重新编译代码的情况下,改变记录日志的行为
    
  4. 支持输出到标准输出和文件:

    日志总是要被读的,要么输出到标准输出,供开发者实时读取,要么保存到文件,供开发者日后查看
    输出到标准输出和保存到文件是一个日志包最基本的功能
    

高级功能

  1. 支持多种日志格式:

    一个好的日志格式,不仅方便查看日志,还能方便日志采集组件采集日志,
      并对接类似 Elasticsearch 这样的日志搜索引擎
    
    一个日志包至少需要提供以下两种格式:
    a. TEXT 格式
    b. JSON 格式
    
  2. 能够按级别分类输出:

    为了能够快速定位到需要的日志,一个比较好的做法是:
      将日志按级别分类输出,至少错误级别的日志可以输出到独立的文件中
    
  3. 支持结构化日志:

    结构化日志(Structured Logging),就是使用 JSON 或者其他编码方式使日志结构化
    这样可以方便后续使用 Filebeat、Logstash Shipper 等各种工具,对日志进行采集、过滤、分析和查找
    
  4. 支持日志轮转:

    在一个大型项目中,一天可能会产生几十个 G 的日志
    为了防止把磁盘空间占满,就需要确保日志大小达到一定量级时,对日志进行切割、压缩,并转存
    
  5. 具备 Hook 能力:

    Hook 能力可以使我们对日志内容进行自定义处理。
    例如:
      当某个级别的日志产生时,发送邮件或者调用告警接口进行告警
    
    在一个大型系统中,日志告警是非常重要的功能,但更好的实现方式是将告警能力做成旁路功能:
      通过旁路功能,可以保证日志包功能聚焦、简洁
      例如:可以将日志收集到 Elasticsearch,并通过 ElastAlert 进行日志告警
    

可选功能

  1. 支持颜色输出

  2. 兼容标准库 log 包

  3. 支持输出到不同的位置

需要关注的点

  1. 高性能:

    因为我们要在代码中频繁调用日志包,记录日志,所以日志包的性能是首先要考虑的点
    
  2. 并发安全:

    Go 应用程序会大量使用 Go 语言的并发特性,也就意味着需要并发地记录日志,这就需要日志包是并发安全的
    
  3. 插件化能力:

    日志包应该能提供一些插件化的能力,比如:
      允许开发者自定义输出格式,自定义存储位置,自定义错误发生时的行为(例如 告警、发邮件等)
    插件化的能力不是必需的,因为日志自身的特性就能满足绝大部分的使用需求,
    例如:
      输出格式支持 JSON 和 TEXT
      存储位置支持标准输出和文件
      日志监控可以通过一些旁路系统来实现。
    
  4. 日志参数控制:

    日志包应该能够灵活地进行配置,初始化时配置或者程序运行时配置
    例如:
      初始化配置可以通过 Init 函数完成
      运行时配置可以通过 SetOptions / SetLevel 等函数来完成
    

如何记录日志

  1. 在何处打印日志:

    a. 在分支语句处打印日志:
      在分支语句处打印日志,可以判断出代码走了哪个分支,有助于判断请求的下一跳,继而继续排查问题
    b. 写操作必须打印日志:
      写操作最可能会引起比较严重的业务故障,写操作打印日志,可以在出问题时找到关键信息
    c. 在循环中打印日志要慎重:
      如果循环次数过多,会导致打印大量的日志,严重拖累代码的性能
      建议的办法: 在循环中记录要点,在循环外面总结打印出来
    d. 在错误产生的最原始位置打印日志:
      对于嵌套的 Error,可在 Error 产生的最初位置打印 Error 日志
      上层如果不需要添加必要的信息,可以直接返回下层的 Error
    
  2. 在哪个日志级别打印日志:

    不同级别的日志,具有不同的意义,能实现不同的功能:
    a. Debug 级别
    为了获取足够的信息进行 Debug,通常会在 Debug 级别打印很多日志。
    例如,可以打印整个 HTTP 请求的请求 Body 或者响应 Body。
    Debug 级别需要打印大量的日志,这会严重拖累程序的性能。
    并且,Debug 级别的日志,主要是为了能在开发测试阶段更好地 Debug,多是一些不影响现网业务的日志信息。
    所以,对于 Debug 级别的日志,在服务上线时我们一定要禁止掉。
    Debug 这个级别的日志可以随意输出,任何你觉得有助于开发、测试阶段调试的日志,都可以在这个级别打印。
    
    b. Info 级别
    Info 级别的日志可以记录一些有用的信息,供以后的运营分析,
    所以 Info 级别的日志不是越多越好,也不是越少越好,应以满足需求为主要目标。
    一些关键日志,可以在 Info 级别记录,但如果日志量大、输出频度过高,则要考虑在 Debug 级别记录。
    
    现网的日志级别一般是 Info 级别,在记录日志时,要注意避免产生过多的 Info 级别的日志
    例如,在 for 循环中,就要慎用 Info 级别的日志。
    
    c. Warn 级别
    一些警告类的日志可以记录在 Warn 级别
    Warn 级别的日志往往说明程序运行异常,不符合预期,但又不影响程序的继续运行,或者是暂时影响,但后续会恢复
    
    Warn 更多的是业务级别的警告日志。
    
    d. Error 级别
    Error 级别的日志告诉我们程序执行出错,这些错误肯定会影响到程序的执行结果,
    例如请求失败、创建资源失败等。
    要记录每一个发生错误的日志,避免日后排障过程中这些错误被忽略掉。
    大部分的错误可以归在 Error 级别。
    
    e. Panic 级别
    Panic 级别的日志在实际开发中很少用
    通常只在需要错误堆栈,或者不想因为发生严重错误导致程序退出,而采用 defer 处理错误时使用。
    
    f. Fatal 级别
    Fatal 是最高级别的日志
    这个级别的日志说明问题已经相当严重,严重到程序无法继续运行,通常是系统级的错误
    在开发中也很少使用,除非我们觉得某个错误发生时,整个程序无法继续运行。
    
  3. 如何记录日志内容:

    a. 在记录日志时,不要输出一些敏感信息,例如密码、密钥等
    b. 为了方便调试,通常会在 Debug 基本记录一些临时日志,这些日志内容可以用一些特殊的字符开头
        例如: log.Debugf("XXXXXXXXXXXX-1:Input key was: %s", setKeyName)
        这样,在完成调试后,可以通过查找 XXXXXXXXXXXX 字符串,找到这些临时日志,在 commit 前删除
    c. 日志内容应该小写字母开头,以英文点号 . 结尾
        例如: log.Info("update user function called.")
    d. 为了提高性能,尽可能使用明确的类型
        例如:
          使用 log.Warnf("init datastore: %s", err.Error())
          而非 log.Warnf("init datastore: %v", err) 。
    e. 根据需要,日志最好包含两个信息
        一个是请求 ID(RequestID),是每次请求的唯一 ID
          便于从海量日志中过滤出某次请求的日志,可以将请求 ID 放在请求的通用日志字段中
        另一个是用户和行为,用于标识谁做了什么
    f. 不要将日志记录在错误的日志级别上
        例如:
          不要将正常的日志信息打印在 Error 级别,将错误的日志信息打印在 Info 级别
    

日志的 “最佳” 实践总结

  1. 开发调试、现网故障排障时,不要遗忘一件事情:

    根据排障的过程优化日志打印
    【注】好的日志,可能不是一次就可以写好的
    可以在实际开发测试,还有现网定位问题时,不断优化
    
  2. 打印日志要 “不多不少”:

    避免打印没有作用的日志,也不要遗漏关键的日志信息。
    最好的信息是,仅凭借这些关键的日志就能定位到问题。
    
  3. 支持动态日志输出,方便线上问题定位:

    总是将日志记录在本地文件
    通过将日志记录在本地文件,可以和日志中心化平台进行解耦
    这样当网络不可用,或者日志中心化平台故障时,仍然能够正常的记录日志
    
  4. 集中化日志存储处理

    因为应用可能包含多个服务,一个服务包含多个实例 为了查看日志方便,最好将这些日志统一存储在同一个日志平台上, 例如 Elasticsearch,方便集中管理和查看日志。

  5. 结构化日志记录:

    添加一些默认通用的字段到每行日志,方便日志查询和分析。
    
  6. 支持 RequestID:

    使用 RequestID 串联一次请求的所有日志,这些日志可能分布在不同的组件,不同的机器上
    支持 RequestID 可以大大提高排障的效率,降低排障难度
    在一些大型分布式系统中,没有 RequestID 排障简直就是灾难
    
  7. 支持动态开关 Debug 日志:

    对于定位一些隐藏得比较深的问题,可能需要更多的信息,这时候可能需要打印 Debug 日志
    最好的办法是能够在请求中通过 debug=true 这类参数动态控制某次请求是否开启 Debug 日志
    

主页

索引

模块索引

搜索页面