Skip to content

异常监控管理工具调研

一、需求背景:

当我们完成一个业务系统的上线时,总是要观察线上的运行情况,查看日志发现问题并进行优化迭代。因为测试永远无法做到 100% 覆盖,用户也不会总是按照我们所预期的进行操作,因此我们需要在系统异常时主动对其进行收集上报,以制定解决方案。

当生产环境中产生了一个 Bug 时,如何做到迅速报警,找到问题原因,修复后又如何在线上验证?此时我们需要一个高效的错误监控系统。

前端监控包括行为监控异常监控性能监控等,目前本文主要讨论异常监控实现方式,后续在进行扩展。

二、异常分析:

前端异常是指在用户使用 Web 应用时无法快速得到符合预期结果的情况,不同的异常带来的后果程度不同,轻则引起用户使用不悦,重则导致产品无法使用,使用户丧失对产品的认可。

一个监控系统,大致可以分为四个阶段:日志采集、日志存储、统计与分析、报告和警告。

分类说明
采集阶段收集异常日志,先在本地做一定的处理,采取一定的方案上报到服务器。
存储阶段后端接收前端上报的异常日志,经过一定处理,按照一定的存储方案存储。
分析阶段分为机器自动分析和人工分析。机器自动分析,通过预设的条件和算法,对存储的日志信息进行统计和筛选,发现问题,触发报警。人工分析,通过提供一个可视化的数据面板,让系统用户可以看到具体的日志数据,根据信息,发现异常问题根源。
报警阶段分为告警和预警。告警按照一定的级别自动报警,通过设定的渠道,按照一定的触发规则进行。预警则在异常发生前,提前预判,给出警告。

根据异常代码的后果的程度,对前端异常的表现分为如下几类:出错、呆滞、损坏、假死、崩溃。

分类说明
出错界面呈现的内容与用户预期的内容不符,例如点击进入非目标界面,数据不准确,出现的错误提示不可理解,界面错位,提交后跳转到错误界面等情况。这类异常出现时,虽然产品本身功能还能正常使用,但用户无法达成自己目标。
呆滞界面出现操作后没有反应的现象,例如点击按钮无法提交,提示成功后无法继续操作。这类异常出现时,产品已经存在界面级局部不可用现象。
损坏界面出现无法实现操作目的的现象,例如点击无法进入目标界面,点击无法查看详情内容等。这类异常出现时,应用部分功能无法被正常使用。
假死界面出现卡顿,无法对任何功能进行使用的现象。例如用户无法登陆导致无法使用应用内功能,由于某个遮罩层阻挡且不可关闭导致无法进行任何后续操作。这类异常出现时,用户很可能杀死应用。
崩溃应用出现经常性自动退出或无法操作的现象。例如间歇性crash,网页无法正常加载或加载后无法进行任何操作。这类异常持续出现,将直接导致用户流失,影响产品生命力。

前端产生异常的原因主要分5类:

原因案例
逻辑错误1)    业务逻辑判断条件错误;
2)    事件绑定顺序错误;
3)    调用栈时序错误;
4)    错误的操作 js 对象。
数据类型错误1)    将 Null 视作对象读取 Property;
2)    将 Undefined 视作数组进行遍历;
3)    将字符串形式的数字直接用于加运算;
4)    函数参数未传。
语法句法错误
网络错误1)    慢;
2)    服务端未返回数据但仍 200,前端按正常进行数据遍历;
3)    提交数据时网络中断;
4)    服务端 500 错误时前端未做任何错误处理。
系统错误1)    内存不够用;
2)    磁盘塞满;
3)    壳不支持 API;
4)    不兼容。

三、异常采集:

当异常出现的时候,我们需要知道异常的具体信息,根据异常的具体信息来决定采用什么样的解决方案。在采集异常信息时,可以遵循 4W 原则:WHO did WHAT and get WHICH exception in WHICH environment?

分类说明
用户信息出现异常时该用户的信息,例如该用户在当前时刻的状态、权限等,以及需要区分用户可多终端登录时,异常对应的是哪一个终端。
行为信息用户进行什么操作时产生了异常:所在的界面路径;执行了什么操作;操作时使用了哪些数据;当时的 API 吐了什么数据给客户端;如果是提交操作,提交了什么数据;上一个路径;上一个行为日志记录 ID 等。
异常信息产生异常的代码信息:用户操作的 DOM 元素节点;异常级别;异常类型;异常描述;代码 Stack 信息等。
环境信息网络环境;设备型号和标识码;操作系统版本;客户端版本;API 接口版本等。
例如:

四、异常捕获:

前端捕获异常分为全局捕获和单点捕获。全局捕获代码集中,易于管理;单点捕获作为补充,对某些特殊情况进行捕获,但分散,不利于管理。

a、全局捕获 通过全局的接口,将捕获代码集中写在一个地方,可以利用的接口有:

  • window.addEventListener(‘error’) / window.addEventListener(“unhandledrejection”) / document.addEventListener(‘click’) 等;
  • 框架级别的全局监听,例如 Aixos 中使用 Interceptor 进行拦截,Vue、React 都有自己的错误采集接口;
  • 通过对全局函数进行封装包裹,实现在在调用该函数时自动捕获异常;
  • 对实例方法重写(Patch),在原有功能基础上包裹一层,例如对 console.error 进行重写,在使用方法不变的情况下也可以异常捕获。

b、单点捕获 在业务代码中对单个代码块进行包裹,或在逻辑流程中打点,实现有针对性的异常捕获:

  • try…catch;
  • 专门写一个函数来收集异常信息,在异常发生时,调用该函数;
  • 专门写一个函数来包裹其他函数,得到一个新函数,该新函数运行结果和原函数一模一样,只是在发生异常时可以捕获异常。

c、跨域脚本异常: 由于浏览器安全策略限制,跨域脚本报错时,无法直接获取错误的详细信息,只能得到一个 Script Error。例如,我们会引入第三方依赖,或者将自己的脚本放在 CDN 时。解决 Script Error 的方法:

方案一:

  • 将 js 内联到 HTML 中;
  • 将 js 文件与 HTML 放在同域下;

方案二:

  • 为页面上 script 标签添加 crossorigin 属性;
  • 被引入脚本所在服务端响应头中,增加 Access-Control-Allow-Origin 来支持跨域资源共享。

五、异常整理与上报方案:

5.1. 异常级别:

一般而言,我们会将收集信息的级别分为 info,warn,error 等,并在此基础上进行扩展。 当我们监控到异常发生时,可以将该异常划分到“重要——紧急”模型中分为 A、B、C、D 四个等级。有些异常,虽然发生了,但是并不影响用户的正常使用,用户其实并没有感知到,虽然理论上应该修复,但是实际上相对于其他异常而言,可以放在后面进行处理。

在收集异常阶段,可根据第二节划分的异常后果来判断异常的严重程度,在发生异常时选择对应的上报方案进行上报。

5.2. 异常整理:

(1)前端日志存储: 单纯一条异常日志并不能帮助我们快速定位问题根源,找到解决方案。但如果要收集用户的行为日志,又要采取一定的技巧,而不能用户每一个操作后,就立即将该行为日志传到服务器,对于具有大量用户同时在线的应用,如果用户一操作就立即上传日志,无异于对日志服务器进行 DDOS 攻击。因此,我们先将这些日志存储在用户客户端本地,达到一定条件之后,再同时打包上传一组日志。

我们不可能直接将这些日志用一个变量保存起来,这样会挤爆内存,而且一旦用户进行刷新操作,这些日志就丢失了,因此,我们自然而然想到前端数据持久化方案。

目前,可用的持久化方案可选项也比较多了,主要有:Cookie、localStorage、sessionStorage、IndexedDB、webSQL 、FileSystem 等等。那么该如何选择呢?我们通过一个表来进行对比:

综合之后,IndexedDB 是最好的选择,它具有容量大、异步的优势,异步的特性保证它不会对界面的渲染产生阻塞。而且 IndexedDB 是分库的,每个库又分 store,还能按照索引进行查询,具有完整的数据库管理思维,比localStorage 更适合做结构化数据管理。但是它有一个缺点,就是 API 非常复杂,不像 localStorage 那么简单直接。针对这一点,我们可以使用 hello-indexeddb 这个工具,它用 Promise 对复杂 API 进行来封装,简化操作,使 IndexedDB 的使用也能做到 localStorage 一样便捷。另外,IndexedDB 是被广泛支持的 HTML5 标准,兼容大部分浏览器,因此不用担心它的发展前景。

上图展示了前端存储日志的流程和数据库布局。当一个事件、变动、异常被捕获之后,形成一条初始日志,被立即放入暂存区(indexedDB 的一个 store),之后主程序就结束了收集过程,后续的事只在 webworker 中发生。在一个 webworker 中,一个循环任务不断从暂存区中取出日志,对日志进行分类,将分类结果存储到索引区中,并对日志记录的信息进行丰富,将最终将会上报到服务端的日志记录转存到归档区。而当一条日志在归档区中存在的时间超过一定天数之后,它就已经没有价值了,但是为了防止特殊情况,它被转存到回收区,再经历一段时间后,就会被从回收区中清除。

(2)整理日志: 主要就是根据日志特征,整理出不同的索引。我们在收集日志时,会给每一条日志打上一个 type,以此进行分类,并创建索引,同时通过 object-hashcode 计算每个 log 对象的 hash 值,作为这个 log 的唯一标志。

  • 将所有日志记录按时序存放在归档区,并将新入库的日志加入索引;
  • BatchIndexes:批量上报索引(包含性能等其他日志),可一次批量上报 100 条;
  • MomentIndexes:即时上报索引,一次全部上报;
  • FeedbackIndexes:用户反馈索引,一次上报一条;
  • BlockIndexes:区块上报索引,按异常/错误(traceId,requestId)分块,一次上报一块;
  • 上报完成后,被上报过的日志对应的索引删除;
  • 3 天以上日志进入回收区;
  • 7 天以上的日志从回收区清除;

rquestId:同时追踪前后端日志。由于后端也会记录自己的日志,因此,在前端请求 API 的时候,默认带上requestId,后端记录的日志就可以和前端日志对应起来。

traceId:追踪一个异常发生前后的相关日志。当应用启动时,创建一个 traceId,直到一个异常发生时,刷新traceId。把一个 traceId 相关的 requestId 收集起来,把这些 requestId 相关的日志组合起来,就是最终这个异常相关的所有日志,用来对异常进行复盘。

上图举例展示了如何利用 traceId 和 requestId 找出和一个异常相关的所有日志。在上图中,hash4 是一条异常日志,我们找到 hash4 对应的 traceId 为 traceId2,在日志列表中,有两条记录具有该 traceId,但是 hash3 这条记录并不是一个动作的开始,因为 hash3 对应的 requestId 为 reqId2,而 reqId2 开始于 hash2,因此,我们实际上要把 hash2 也加入到该异常发生的整个复盘备选记录中。总结起来就是,我们要找出同一个 traceId 对应的所有 requestId 对应的日志记录。 我们把这些和一个异常相关的所有日志集合起来,称为一个 block,再利用日志的 hash 集合,得出这个 block 的hash,并在索引区中建立索引,等待上报。

(3)上报日志: 上报日志也在 webworker 中进行,为了和整理区分,可以分两个 worker。上报的流程大致为:在每一个循环中,从索引区取出对应条数的索引,通过索引中的 hash,到归档区取出完整的日志记录,再上传到服务器。 按照上报的频率(重要紧急度)可将上报分为四种:及时上报、批量上报、区块上报、用户主动提交;

类型说明
及时上报收集到日志后,立即触发上报函数。仅用于A类异常(异常级别)。而且由于受到网络不确定因素影响,A 类日志上报需要有一个确认机制,只有确认服务端已经成功接收到该上报信息之后,才算完成。否则需要有一个循环机制,确保上报成功。
批量上报将收集到的日志存储在本地,当收集到一定数量之后再打包一次性上报,或者按照一定的频率(时间间隔)打包上传。这相当于把多次合并为一次上报,以降低对服务器的压力。
区块上报将一次异常的场景打包为一个区块后进行上报。它和批量上报不同,批量上报保证了日志的完整性,全面性,但会有无用信息。而区块上报则是针对异常本身的,确保单个异常相关的日志被全部上报。
用户主动提交在界面上提供一个按钮,用户主动反馈 bug。这有利于加强与用户的互动。或者当异常发生时,虽然对用户没有任何影响,但是应用监控到了,弹出一个提示框,让用户选择是否愿意上传日志。这种方案适合涉及用户隐私数据时。

上图展示了上报的一个大致流程。对于合并上报这种情况,一次的数据量可能要十几k,对于日 pv 大的站点来说,产生的流量还是很可观的。所以有必要对数据进行压缩上报。lz-string 是一个非常优秀的字符串压缩类库,兼容性好,代码量少,压缩比高,压缩时间短,压缩率达到惊人的 60%。但它基于LZ78压缩,如果后端不支持解压,可选择 gzip 压缩,一般而言后端会默认预装 gzip,因此,选择 gzip 压缩数据也可以,工具包 pako 中自带了 gzip 压缩,可以尝试使用。

六、异常日志接收、存储与分析:

6.1. 接入层与消息队列:

一般通过提供独立的日志服务器接收客户端日志,接收过程中,要对客户端日志内容的合法性、安全性等进行甄别,防止被人攻击。而且由于日志提交一般都比较频繁,多客户端同时并发的情况也常见。通过消息队列将日志信息逐一处理后写入到数据库进行保存也是比较常用的方案。

上图为腾讯 BetterJS 的架构图,其中“接入层”和“推送中心”就是这里提到的接入层和消息队列。BetterJS 将整个前端监控的各个模块进行拆分,推送中心承担了将日志推送到存储中心进行存储和推送给其他系统(例如告警系统)的角色,但我们可以把接收日志阶段的队列独立出来看,在接入层和存储层之间做一个过渡。

6.2. 日志存储系统:

对于小应用,单库单表加优化就可以应付。一个成规模的应用,如果要提供更标准高效的日志监控服务,常常需要在日志存储架构上下一些功夫。目前业界已经有比较完备的日志存储方案,主要有:Hbase 系,Dremel 系,Lucene 系等。总体而言,日志存储系统主要面对的问题是数据量大,数据结构不规律,写入并发高,查询需求大等。一般一套日志存储系统,要解决上面这些问题,就要解决写入的缓冲,存储介质按日志时间选择,为方便快速读取而设计合理的索引系统等等。

6.3. 搜索:

日志的最终目的是要使用,由于一般日志的体量都非常大,因此,要在庞大的数据中找到需要的日志记录,需要依赖比较好的搜索引擎。Splunk 是一套成熟的日志存储系统,但它是付费使用的。按照 Splunk 的框架,Elk 是 Splunk 的开源实现,Elk 是 ElasticSearch、Logstash、Kibana 的结合,ES 基于 Lucene 的存储、索引的搜索引擎;logstash 是提供输入输出及转化处理插件的日志标准化管道;Kibana 提供可视化和查询统计的用户界面。

七、异常监控告警及部署方案:

对异常进行统计和分析只是基础,而在发现异常时可以推送和告警,甚至做到自动处理,才是一个异常监控系统应该具备的能力。

7.1. 自定义触发条件的告警:

a. 监控实现 当日志信息进入接入层时,就可以触发监控逻辑。当日志信息中存在较为高级别的异常时,也可以立即出发告警。告警消息队列和日志入库队列可以分开来管理,实现并行。

对入库日志信息进行统计,对异常信息进行告警。对监控异常进行响应。所谓监控异常,是指:有规律的异常一般而言都比较让人放心,比较麻烦的是突然之间的异常。例如在某一时段突然频繁接收到 D 级异常,虽然 D 级异常是不紧急一般重要,但是当监控本身发生异常时,就要提高警惕。

b. 自定义触发条件 除了系统开发时配置的默认告警条件,还应该提供给日志管理员可配置的自定义触发条件。

  • 日志内含有什么内容时;
  • 日志统计达到什么度、量时;
  • 向符合什么条件的用户告警;

7.2. 推送渠道:

可选择的途径有很多,例如邮件、短信、微信、电话、钉钉。

7.3. 推送频率:

针对不同级别的告警,推送的频率也可以进行设定。低风险告警可以以报告的形式一天推送一次,高风险告警 10 分钟循环推送,直到处理人手动关闭告警开关。

7.4. 自动报表:

对于日志统计信息的推送,可以做到自动生成日报、周报、月报、年报并邮件发送给相关群组。

7.5. 自动产生 Bug 工单:

当异常发生时,系统可以调用工单系统API实现自动生成 Bug 单,工单关闭后反馈给监控系统,形成对异常处理的追踪信息进行记录,在报告中予以展示。

7.6. 异常修复:

SourceMap 使用,前端代码大部分情况都是经过压缩后发布的,上报的 stack 信息需要还原为源码信息,才能快速定位源码进行修改。

发布时,只部署 js 脚本到服务器上,将 SourceMap 文件上传到监控系统,在监控系统中展示 stack 信息时,利用 SourceMap 文件对 stack 信息进行解码,得到源码中的具体信息。

但是这里有一个问题,就是 SourceMap 必须和正式环境的版本对应,还必须和 Git 中的某个 commit 节点对应,这样才能保证在查异常的时候可以正确利用 stack 信息,找到出问题所在版本的代码。这些可以通过建立 CI任务,在集成化部署中增加一个部署流程,以实现这一环节。

7.7. 从告警到预警:

预警的本质是,预设可能出现异常的条件,当触发该条件时异常并没有真实发生,因此,可以赶在异常发生之前对用户行为进行检查,及时修复,避免异常或异常扩大。

其实就是一个统计聚类的过程。将历史中发生异常的情况进行统计,从时间、地域、用户等不同维度加以统计,找出规律,并将这些规律通过算法自动加入到预警条件中,当下次触发时,及时预警。

7.8. 智能修复:

自动修复错误。例如,前端要求接口返回数值,但接口返回了数值型的字符串,那么可以有一种机制,监控系统发送正确数据类型模型给后端,后端在返回数据时,根据该模型控制每个字段的类型。

7.9. 部署:

  • 多客户端:一个用户在不同终端上登录,或者一个用户在登录前和登录后的状态。通过特定算法生成 requestId,通过该 requestId 可以确定某个用户在独立客户端上的一系列操作,根据日志时序,可以梳理出用户产生异常的具体路径。
  • 集成便捷性:前端写成包,全局引用即可完成大部分日志记录、存储和上报。在特殊逻辑里面,可以调用特定方法记录日志。后端与应用本身的业务代码解耦,可以做成独立的服务,通过接口和第三方应用交互。利用集成部署,可以将系统随时进行扩容、移植等操作。
  • 管理系统的可扩展:整套系统可扩展,不仅服务单应用,可支持多个应用同时运行。同一个团队下的所有应用都可以利用同一个平台进行管理。
  • 日志系统权限:不同的人在访问日志系统时权限不同,一个访问者只能查看自己相关的应用,有些统计数据如果比较敏感,可以单独设置权限,敏感数据可脱敏。

八、其他说明:

8.1. 异常录制:

对于一个异常,仅仅拥有该异常的信息还不足以完全抓住问题的本质,因为异常发生的位置,并不一定是异常根源所在的位置。我们需要对异常现场进行还原,才能复原问题全貌,甚至避免类似问题在其他界面中发生。这里需要引进一个概念,就是“异常录制”。录制通过“时间”“空间”两个维度记录异常发生前到发生的整个过程,对于找到异常根源更有帮助,可根据需要再去扩展。

8.2. 日志统计与分析:

一个完善的日志统计分析工具需要提供各方面方便的面板,以可视化的方式给日志管理员和开发者反馈信息。根据需要可具体分析实现。

8.3. 异常测试:

主动异常测试和随机异常测试两种,不做详细讨论。

8.4. 性能监控:

异常监控主要针对代码级别的报错,但也应该关注性能异常。性能监控主要包括:

  • 运行时性能:文件级、模块级、函数级、算法级
  • 网络请求速率
  • 系统性能

8.5. 数据脱敏:

敏感数据不被日志系统采集。由于日志系统的保存是比较开放的,虽然里面的数据很重要,但是在存储上大部分日志系统都不是保密级,因此,如果应用涉及了敏感数据,最好做到:

  • 独立部署,不和其他应用共享监控系统;
  • 不采集具体数据,只采集用户操作数据,在重现时,通过日志信息可以取出数据 API 结果来展示;
  • 日志加密,做到软硬件层面的加密防护;
  • 必要时,可采集具体数据的 ID 用于调试,场景重现时,用 Mock 数据替代,Mock 数据可由后端采用假的数据源生成;
  • 对敏感数据进行混淆;

九、主流监控系统对比:

系统简介功能优点缺点文档地址(教程、开源库)其他说明
Sentry是一个开源的实时错误监控的项目,它支持很多端的配置,包括 Web 前端、服务器端、移动端及其游戏端。支持各种语言,例如 Python、OC、Java、Node、JavaScript 等。也可以应用到各种不同的框架上面,如前端框架中的 Vue 、Angular 、React 等最流行的前端框架。提供了 Github、Slack、Trello 的常见的开发工具的集成。可以自己安装并且搭建 Sentry 应用。跨平台支持;围绕错误的丰富上下文;实时,可自定义的错误通知;优先解决问题;清理敏感数据。开源(需通过docker安装)对各种前端框架的友好支持 (Vue、React、Angular);支持 SourceMap;性能强大,可监控全栈异常。易于未来异常监控扩展。还能和 Gitlab ,钉钉等各个平台进行结合,将异常监控上报到 Gitlab 或与钉钉机器人结合,这对于一般公司来说,够用了,这对于线上前端出现的异常能够实时的发现,修复。官方文档使用者多,有官方文档;
JSTracker通过对 Error 的监听,获取异常的相关信息并添加这些信息到异常信息数组,在达到上限或者一定时间之后做处理(返回服务端保存数据之类)。捕获页面 JavaScript 异常报错,捕获异常类型包含:1. JavaScript runtime 异常捕捉 ;2. 静态资源 load faided 异常捕捉 ;3. console.error 的异常捕获。使用安装简单方便,满足基本异常监控需求监控功能过于基本,没有其他高级操作;没有自带后台管理页面;cnpm 使用人数较少,版本更新也很慢;cnpm 使用地址没有官方文档
BadjsWeb 前端脚本错误监控及跟踪解决方案。页面脚本错误监控、上报、统计、查看等系统化的跟踪解决方案;支持告警和定时发送邮件报表功能。一站式体系化解决方案:业务只需要简单的配置,引入上报文件,即可实现脚本错误上报,每日统计邮件跟踪方便。可视化查询系统,快速定位错误信息:Web 应用程序脚本数量庞大,开发人员在如此之多的脚本中定位某个问题变得困难。BadJS 能够巧妙定位错误脚本代码,进行反馈。通过各种查询条件,快速找到详细错误日志。跨域、Script Error 等棘手问题不再是难题:Try.js 帮你发现一切。真实用户体验监控与分析:通过浏览器端真实用户行为与体验数据监控,为您提供JavaScript、AJAX 请求错误诊断和页面加载深度分析帮助开发人员深入定位每一个问题细节。即使没有用户投诉,依然能发现隐蔽 Bug,主动提升用户体验。用户行为分析:细粒度追踪真实的用户行为操作及流程,前端崩溃、加载缓慢及错误问题,可关联到后端进行深度诊断。产品质量的保障:浏览器百花齐放,用户环境复杂,巨大的差异导致开发人员难以重现用户遇到的问题。无法像后台一样上报所有用户操作日志。通过 BadJS,上报用户端脚本错误,为产品质量保驾护航。安装使用及部署需要折腾(有环境要求,安装模块有点多);异常上报操作用法比较多,需要自定义操作;GitHub 地址有官方文档,还有一个转发给第三方系统的客户端(类似可以转发到钉钉群告警通知)
FdSafe
Saijs通用前端监控采集脚本JavaScript 异常监控;敏感信息监控;DOM 合法性检查;静态资源监控;网站监控等;没有管理后台GitHub 地址使用人数一般,文档也很简单
Fundebug付费暂不考虑
FrontJS付费暂不考虑
应用实时监控服务 ARMS付费暂不考虑
TrackJS付费暂不考虑
Instabug付费暂不考虑

十、最终选择方案:

Sentry 监控系统 原因如下:

  • 支持目前主流语言及框架的项目,比较成熟和稳定;
  • 开源免费和付费版本,免费暂时够用;
  • Docker 集成好的 Sentry 安装环境。

十一、有价值的资料: