日志

日志规范

文件命名:appName_logType_logName.log

日志文件的保存时间衡量因素:

  1. 重要程度
  2. 文件大小
  3. 磁盘空间

日志级别

日志类别

日志实现

使用类似 Canal 的中间件监听数据变化 写日志

日志文件

LoginUtil.setCurrentUser("cxk")

配置不同的logger

private final Logger businessLog = LoggerFactory.getLogger("businessLog");
template = "用户 %s 做了 %s";
log.error(String.format(template, "cxk", "打篮球"))

AOP方法注解

// bizNo 业务唯一ID 方便进行日志搜索
// 通过使用自定义函数配合SpEL来分离业务逻辑与日志记录
@LogRecord(content = "修改了订单的配送员:从“{queryOldUser{#request.deliveryOrderNo()}}”, 修改到“{deveryUser{#request.userId}}”",
        bizNo="#request.deliveryOrderNo")
public void modifyAddress(updateDeliveryRequest request){
    // 更新派送信息 电话,收件人、地址
    doUpdate(request);
}

AOP日志设计

日志使用

预先判断日志级别

避免使用字符串的形式连接打印日志

log.debug("user " + id + "create new order " + orderId) // bad

if (log.enableDebug){ // good
  log.debug(...)
}
log.debug('user {} create new order {}', id, orderId) // good

主要是防止无谓的字符串连接消耗系统资源

避免无效日志打印

对于debug log 等低级别的日志 一定要控制好输出量 避免磁盘空间被快速吞掉,

因为 String 字符串拼接会使用 StringBuilder 的 append () 方式,有一定的性能损耗。使用占位符可以有效提高性能

打印大文本日志非常影响性能,很多大文本对排查问题帮助不大,打印该信息的意义不大,因此尽量避免打印该内容或只截取一部分关键信息

区别对待错误日志

不能将所有错误一股脑归类为ERROR级别, ERROR 日志专门输出到一个 error.log 文件。调试时通过 tail -f error.log 随时监控出现的错误日志

ERROR级别就代表是需要人工介入处理的级别

日志记录的内容

OpenTelemetry 推荐记录的日志内容

字段 描述
Timestamp 事件发生的时间
ObservedTimestamp 事件被记录的时间
Traceld 请求链路ID
SpanID 请求span ID
TraceFlags W3C链路标记
SeverityText 日志级别
SeverityNumber 日志级别对应的数值
Body 日志内容
Resource 描述日志的来源
InstrumentationScope 描述发出日志的作用域
Attributes 有关该事件的其他信息
  1. 一定要输出异常堆栈
  2. 输出对象实例时 要确保对象重写了 toString 方法
  3. 避免敏感信息
  4. 避免引用到慢操作信息
  5. 避免输出的信息有误导性

日志的追踪

对于没有链路追踪基础设施的单体应用,为了在大量的日志中找到自己所需的日志。就需要能以某种情况准确唯一标识日志,如

log.info("业务上下文-事件xxx xxxx")

同时为了追踪执行流或者数据流,通常都需要在某条日志之前或之后追踪日志,此时可以使用日志纪录里的线程名称来明确整条执行链路,如下面的bussiness-task1/bussiness-task2...

2022年1月21日20:30:21 INFO --- [-] [main] wang.ismy.xxx 日志内容1
2022年1月21日20:30:21 INFO --- [-] [bussiness-task1] wang.ismy.xxx 日志内容2
2022年1月21日20:30:21 INFO --- [-] [bussiness-task2] wang.ismy.xxx 日志内容3
...

日志的性能

单体在流量比较高的情况下,没有做好日志级别的控制,可能会狂打日志,这些大量的日志若没有使用专门的日志收集工具收集,大量打印不仅会消耗性能,同时大量的日志也会快速消耗磁盘空间,所以在做好日志级别控制的同时,也可以采取随机打印日志的方式

if (random(0, 1000) == 3) {
  log.info("xx")
}

使用这种方式的目的在于观察代码路径是否覆盖,并且前提一定得是流量特别高的场景,否则会出现问题就没有日志可进行排查的境地

错误的日志形式

日志实现

Java 日志框架

分布式日志体系

stateDiagram-v2
  direction LR
  应用1 --> 日志收集器1
  应用2 --> 日志收集器2
  应用3 --> 日志收集器2
  日志收集器1 --> 缓冲消息队列
  日志收集器2 --> 缓冲消息队列
  缓冲消息队列 --> 日志处理器: 聚合加工
  日志处理器 --> 日志存储器
  日志存储器 --> 日志分析查询器

收集&缓冲

为了缓解收集大量日志的压力 可以在收集器之前假设Kafka或者Redis作为缓冲层 面对突发流量

加工&聚合

存储&查询

ES是这方面唯一的选择

日志有如下性质:

  1. 写入后基本无需修改
  2. 分为冷热数据 更早的日志价值更低
  3. 日志可离线查询与实时查询