命令行工具的监控告警建设

本人五年的工作经验,历经三份工作,竟然每份都开发维护过前端命令行工具,大家对前端页面和服务端应用有监控告警这件事习以为常,其实这类工具也需要监控告警,本文将从错误处理到上报排查进行分享。


背景

作为前端大家其实都习惯了前端页面有 sentry 类的应用进行错误监控;Node.js 应用打印日志,并在需要的时候使用类 kibana 的应用进行日志查询,且往往配套监控告警。而前端开发几乎每天都会打交道的命令行工具,却在每次报错时,要么联系开发者,要么去用户群咨询。

目的

我们先来看看命令行工具的几个特点:

  1. 在用户的终端中执行,像一个客户端
  2. 运行在 Node.js 环境中,像服务端应用
  3. 用户是开发者,往往不能像一个真实产品一样被运营

基于如上几个特点,命令行工具的监控告警需要有上报处理;错误处理需要借鉴 Node.js 应用;开发者对 bug 的容忍度比较高,善于自行排查,命令行工具的维护者需要主动解决某一类问题,以减少开发者在排查中浪费时间;需要一个反馈机制,使得工具能够越来越 bug less。

作为命令行的维护者,借助监控告警的目的主要有:

  1. 主动发现异常,提前介入处理,而不是积累到一定程度,用户主动上门时才介入
  2. 大部分用户往往不会主动上门,如果有替代品或者不是必须使用的话,用户就流失了
  3. 即使用户没有流失,一个经常出错的命令行工具,会使得用户变得不信任
  4. 接入监控告警重点是发现『重复类』的错误,解决一批错误而不是偶现错误,从而迅速收敛错误

错误处理

在阐述监控告警之前,有必要重点说明下如何科学地处理程序出现的错误。如果程序侧无法很好地处理和上报错误,那么监控告警将起不到有效的作用。

错误分类

正确地区分错误分类,有助于我们分别采取不同的方式处理错误,错误主要分为:

  1. 操作性的错误(预期内)
  2. 程序员的错误(预期外)

这两类错误的区别:

  1. 『操作性的错误』是程序正常操作的一部分
  2. 『程序员的错误』是 Bug,往往由于程序员没有正确处理导致

举几个例子:

  • 操作性的错误

    • 连接不到服务器
    • 无法解析主机名
    • 无效的用户输入
    • 请求超时
    • 服务器返回500
    • 套接字被挂起
  • 程序员的错误

    • 读取 undefined 的一个属性
    • 调用异步函数没有指定回调
    • 该传对象的时候传了一个字符串
    • 该传IP地址的时候传了一个对象

不同类别的错误如何处理?

操作性的错误

  • 直接处理(处理完成后,继续执行)
  • 向上层传错(及时向上层抛错,由上层处理)
  • 重试操作(尝试重试,比如重发请求)
  • 直接崩溃(崩溃退出进程,比如内存不足)
  • 记录错误(仅记录或上报错误)

程序员的错误

  • 无法处理(log & crash)

错误上报

我们往往能妥善地处理大部分『操作性的错误』,但是一旦出现无能为力的『操作性的错误』或者『程序员的错误』,此时我们能做的一般只有打印错误告知用户后退出应用,同时将错误上报。接着在服务端根据错误信息区分监控和告警,那么什么样的错误属于告警,什么样的错误属于监控呢?

告警

  • 当程序出现『程序员的错误』,需要紧急介入时,比如用户初始化模版时,进程异常退出
  • 当程序出现『操作性的错误』,且最终无法被处理时,比如用户创建 gitlab 时,重试次数达到上限,且仍未成功时

监控

  • 当程序出现『操作性的错误』,需要引起足够重视,比如用户输入了不合法的路径、用户创建 gitlab 时经常需要重试 2 次才能成功
  • 当程序进入需要引起足够重视的逻辑(可以不是错误),比如出现了新用户、新部门、新工程

哪些地方需要上报

预期外的错误

1
2
3
4
5
6
7
8
9
10
process.on('unhandledRejection', error => {
reportAlarm({ error });
console.error('Unhandled Rejection Error: ', error);
setTimeout(() => process.exit(1), 1000);
});
process.on('uncaughtException', error => {
reportAlarm({ error });
console.error('Unhandled Exception Error: ', error);
setTimeout(() => process.exit(1), 1000);
});

预期内的错误

1
2
3
4
5
6
7
try {
// 命令行处理逻辑
} catch (error) {
console.error('Handled Error: ', error);
await reportAlarm({ error });
process.exit(1);
}

错误排查

有效的错误上报,对错误的监控告警处理起到了决定性的作用,我们来看看两则上报消息:

Bad case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
错误模块:模版初始化
错误信息:输入路径非法
错误栈:/Users/linleyang/code/temp/case.js:2
throw new Error('输入路径非法');
^
Error: 输入路径非法
at init (/Users/linleyang/code/temp/case.js:2:9)
at Object.<anonymous> (/Users/linleyang/code/temp/case.js:5:1)
at Module._compile (internal/modules/cjs/loader.js:999:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47

Good case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
错误模块:模版初始化
错误信息:输入路径非法
错误栈:/Users/linleyang/code/temp/case.js:2
throw new Error('输入路径非法');
^
Error: 输入路径非法
at init (/Users/linleyang/code/temp/case.js:2:9)
at Object.<anonymous> (/Users/linleyang/code/temp/case.js:5:1)
at Module._compile (internal/modules/cjs/loader.js:999:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47
输入值:$/home/linleyang
触发用户:林宜丙

这两个示例,下面那个除了上报错误消息和错误栈之外,还上报了足够多的错误上下文,我们称之为错误现场,有了错误现场,我们便可能自行复现,或者阅读源码就能解决问题,错误现场既然如此重要,我们来看看一般可以上报哪些现场信息:

  1. 错误发生时的入参
  2. 错误发生时的状态(关键变量)
  3. 触发人
  4. 错误信息/错误栈
  5. 环境信息(SCM,CI,本地)
    ……

我们正确处理了错误,正确上报了现场,这样服务端便可根据这些信息将错误分发到各个模块负责人,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
模版初始化 | 载入模版失败 | 错误报警

模块负责人
@林宜丙

基本信息
Version: 1.0.1 Node: v12.22.5 Env: local
Project: https://github.com/quanru/bagu

用户信息
姓名: @林某某
首次使用的时间: 2021-08-31 05:00:29
最近使用的时间: 2021-11-23 06:00:00
历史执行次数: 111

部门信息
名称: XX部门
首次使用的时间: 2021-05-24 04:03:43
使用的总人数: 21
信息链接: https://quanru.github.io/

错误信息
级别: 未设置
信息:
Cannot read property 'replace' of undefined
近 24 小时内该用户出现该错误信息的次数: 4
近 24 小时内该用户出现所有错误信息的次数: 4
近三个月该用户出现该错误信息的次数: 4
近三个月所有用户出现该错误信息的次数: 26

错误栈:
Error: 输入路径非法
at init (/Users/linleyang/code/temp/case.js:2:9)
at Object.<anonymous> (/Users/linleyang/code/temp/case.js:5:1)
at Module._compile (internal/modules/cjs/loader.js:999:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47

上下文信息
输入值:$/home/linleyang

附加信息
command: cra init hello-world

上报策略

为了让命令行维护者更聚焦的处理错误,请切记『监控告警』:

  1. 只提供一个通道,用户需要酌情上报
  2. 都上报和都不上报区别不大

服务侧需提供默认的上报策略:

这些策略提供参数供上报侧设置

  1. 同一个人,半小时内仅上报一次
  2. 同一个人,首次无需上报
  3. 不存在上下文信息和附加信息的降级为监控

上报侧需要考虑:

  • 调试流量/测试版本不上报
  • 上报侧应主动区分监控和告警(比如:npm start)
    • 用户目录下的代码改动,可以是监控
    • 我们的模块代码出错,应该是告警
  • 尽量主动处理预期内错误,减少抛到兜底处理(比如:某个 npm 包未安装)
  • 按需配置『上报策略』
  • 发生主动异常退出 (process.exit(1)) 时,使用 console.error 而不是 console.log,这样父进程能获取到 stderr 标准错误输出
  • spawnSync 子进程时,将标准错误 pipe 到父进程进行处理

参考

作者

林宜丙

发布于

2021-12-05

更新于

2023-07-13

许可协议