日志规范

基本要素

一行日志的基本要素必须包含时间戳日志等级traceId线程名称日志所在类(logger名称)日志内容 等要素。

特别的,对于接口层日志,还应该包含 请求URL

对于 日志内容, 必须包含:

  • “中文问题或操作描述”,(强烈建议中文,一是一眼看明白,二是中文很容易在一堆框架日志中凸显出来)。
  • 中文描述前使用 ’[]’用一两个词表示这一组日志是在做什么功能的(或本次调用是做什么功能的)。
  • 所有需要登录的请求,必须打印用户id(RequestInfoUtils)。
  • 文档id(课程id),空间id,以及其他所有正在操作的实体id。 若有,则必须打印(不用特别只为日志调接口获取)。
  • 远程调用时的日志,必须打印参数。

对于对象打印:

建议直接使用log格式, 直接把对象传给logger即可,不需要手动toJSON。

普通日志格式和json格式,都能被常见的日志框架直接解析。

常见日志场景

  1. controller层参数和返回值(强制): “请求进入”和“请求返回”必须打印日志(common实现)。
  2. service层参数和返回值(建议):如无必要,不需要打印“请求进入”和“请求返回”。
  3. rpc调用前后的日志(优先):rpc调用前后打印调用参数和返回值,但对于下方有异常校验,返回值校验的请求可以不打印。
    well case:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    InnerRequest<DocParam> request = new InnerRequest<>();
    log.info("[点赞]请求doc-server获取文档内容-request:{}", request.getData());
    DocResult result = InnerResponseUtils.get(docClient.queryDoc(request));

    // 因为下方校验了返回值,且在出现错误时会打印错误日志,所以此处不需要打印返回结果。
    if (result == null || StringUtils.isBlank(result.getContentJson()) {
    log.error("[点赞]请求doc-server获取文档内容失败,返回值为空或者内容为空-result:{}", result)
    throw new GlobalException("xxx");
    }
  4. 参数校验和结果校验(强制):所有校验不通过提前结束流程的分支,必须打印日志,且需要包含问题描述和具体参数或返回值。
    badcase:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    private class DocParam {
    // 验证注解不写明错误消息
    @NotNull
    private Long docId;
    }

    ......
    if (param.getCreateTime() < someDate) {
    // 提前中断没有日志
    return null;
    }
    Doc doc = doSomething(param.getCreateTime());
    if (doc == null) {
    // 提前中断没有日志
    return new Doc();
    }
    if (StringUtils.isBlank(doc.getDocContentJson()) {
    // 没有打印方法入参
    log.error("[发布评论]查询文档内容为空");
    // 没有说清楚具体错误
    log.error("[发布评论]查询文档报错了-param:{}", param);
    throw new GlobalException("xxx");
    }
  5. mq消息的发送与接收(强制):mq的发送必须打印完整的消息体。 mq的consume必须打印完整的消息体。

  6. 异常捕获(强制):异常捕获必须打印日志,必须有错误描述,必须打印当前方法的入参。除非是checked的异常,否则都应该打印异常栈。

badcase:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
try {
doSomething();
} catchException e) {
} // 啥也不干

try {
doSomething();
} catch (Exception e) {
doSomethingOthers();
} // 不打日志

try {
doSomething();
} catch (Exception e) {
doSomethingOthers();
// 完全没用
log.error("[some]出错了");
// 没有详细信息
log.error("[some]doSomething出错了");
// 没有打印异常, 排错困难
log.error("[some]doSomething出错了-param:{}", param);
// 异常被toString, 没有堆栈
log.error("[some]doSomething出错了-param:{}, exception:{}", param, e);
}

  1. 循环体(强制):循环体不要打印info级别的日志,其他规则遵循1-6。

badcase:

1
2
3
4
5
6
for (Doc doc : list) {
log.info("[文档列表]开始处理-id:{}", doc.getId()); // debug或者脚本进度条有用,其他情况就别了。
boolean result = doSomething();
log.info("[文档列表]处理结束-id:{}, result:{}", doc.getId(), result);
......
}

well case:

1
2
3
4
5
6
7
8
log.info("[文档列表]开始处理文档列表");  // 批量参数可以没有, 通过trace能看到前面的调用。
for (Doc doc : list) {
boolean result = doSomething();
if (!result) {
log.info("[文档列表]处理文档失败-文档id:{}", doc.getId()); // 仅打印错误的id
}
......
}