前端错误监控系统的搭建

前端错误监控是什么

  • 把错误日志打点到系统,系统提供日志数据可视化便于分析与定位问题。也可以称为日志分析平台。
  • ELK是常用的日志分析平台,ELK是ElasticSearch、Logstash和Kiabana三个开源工具的简称,分别提供搜索、存储、可视化功能。具体可参考一篇文章:戳这里
  • 针对前端的监控有多种,“应用错误”、“代码级别错误”、“性能”、“用户行为”,这次主要从“代码级别错误”来展开讨论。

最小化的需求

image
image

监控系统如何实现

  • 从需求上考虑,无必要搭建如ELK这样庞大的系统,有点杀鸡用牛刀的节奏。在一定应用群规模以内,可以把日志系统简单理解为一个能读写数据库的服务,加上其他辅助手段(定时任务、发消息套件)就能整合成一套可用、灵活、方便扩展的系统。
  • 系统用前后分离的方式实现,罗列一下主要的技术。前端:vue、element-ui、echarts,服务端:nodejs7.6+koa2、db:mysql、orm: Sequelize、nodejs守护进程:pm2
  • 系统架构图:
    image
    image
  • 选择从零开始搭建一套系统,另一方面也是为了自己实践一下服务端开发领域上面的技术。有了上述方案以后,掌握一定服务端技术的人其实已经能知道具体采用什么技术手段、如何去实现。我自己在实现功能过程中,也有过很多考虑,也踩过一些坑。

具体实现

  • 业务端的埋点捕获不多说了,在这篇文章有较为详细的说明,延伸到其他技术栈也是同样的思路。

管理端

  • 后台管理界面天然适合做成SPA,利用vue实现组件化开发,工作流上配合webpack进行一系列的工程化处理,可以十分快速地构建起一个前端应用。
  • UI框架上面,element-ui在之前的一些后台系统上面有过实践,这次为了图方便也选用了同样的。element-ui由国内饿了么团队开源,有vue、react版本,vue版本相对支持力度大,目前已经更新到2.0版本,而项目中用到的是1.4.X版本,这个版本官方维护到这年12月份。element-ui的文档友好,在github的issue回应也及时,UI组件能满足大多数业务常用需求。属于国内基于vue的UI组件库中较领先的。
  • 另外值得一提的是在基于react的UI库中,由阿里开源的ant-design是个十分大而全的技术,UI设计上面也更简洁紧凑(提供了一套管理系统的设计规范),组件、功能更加丰富,提供一站式应用架构,可以短时间内就能把最小应用搭建起来。
  • 在实现一些小规模的数据可视化需求中,国内echarts的出现频率很高,较大规模的需求可能就要选择highcharts,毕竟后者起步早,背后团队力量大,文档还详细易懂。两者都是以全配置的方式驱动,每种常用的图表官方或社区都有对应的例子参考。但如果有特殊需求就要花点时间翻阅长溜溜的配置文档。在配置驱动这个点上延伸想象,可以编写数据过滤器来实现图表的复用,快速消化基本的数据可视化需求。

服务端

  • 为了能让系统能在短时间内运行起来,加上自己在服务端开发上涉足还浅,服务端选择的技术相对保守。市面上有整套的服务端解决方案,如eggjsthinkjs,这类框架已经把service、日志、进程管理、定时任务、部署等都囊括了,最后并没有选用这些技术。npm庞大的生态,让我不需要担心功能的实现,每块领域都有优秀的npm包来解决。
  • Koa的使用需要理解中间件与context(上下文)的概念,但其实express也一样,只是koa对express而言是承接的关系,koa是面向未来的web框架。

    Koa是一个轻量级的、极富表现力的http开发框架。
    一个web request会通过Koa的中间件栈,来动态完成response的处理。
    同时,Koa2采用了async和await的语法来增强中间件的表现力。
    Koa本身做的工作仅仅是定制了中间件的编写规范,而不内置任何中间件。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 伪代码,捕获路由间抛出的错误日志。
async function logger(ctx, next) {
try {
await next();
} catch (error) {
//记录异常日志
console.error(moment().format('YYYY-MM-DD HH:mm:ss'), ', errorMsg:', JSON.stringify(formatLog({ctx, msg: error.toString()})));
}
}
router.all(
'*',
logger
);
  • pm2会把应用内的console输出作为日志,以上代码简单利用了这点做了简单的日志服务,另外配合定时任务按天分割日志、清除N天前的日志文件。pm2作为node的进程管理包是主流选择,它支持负载均衡(node cluster),0秒重载等,还能支持配置启动。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apps : [
{
name : 'linghit_fe',
script : 'bin/www',
log_file : "./logs/pm2-log.log",
out_file : "./logs/pm2-out.log",
error_file : "./logs/pm2-error.log",
pid_file: "./logs/pm2-linghit_fe.pid",
merge_logs : true,
log_date_format : "YYYY-MM-DD HH:mm:SS",
max_memory_restart: "150M",
instances : 2,
exec_mode : "cluster",
env: {
COMMON_VARIABLE: 'true'
},
env_production : {
NODE_ENV: 'production'
},
env_development : {
NODE_ENV: 'development'
}
}
]
  • pm2能以cluster的方式启动应用,充分发挥多核cpu的性能,本身是利用了node的cluster集群模块。在这个监控应用上,我开启了两个web进程(linghit_fe),与其他的辅助进行,linghit_fe_logmaster用于管理日志,linghit_fe_task用于管理定时任务。再往深一点的主子进程控制,数据共享等还要继续学习深化。
    image
    image
  • Sequelize作为一个mysql的orm包,能简化大量的数据库操作,通过定义model与数据表建立映射关系,在这之后的数据库操作都是方便、可靠的。Sequelize基于Promise实现异步流程控制,也能与async await很好配合使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const modelA = sequelize.define('modelName', {
columnA: {
type: Sequelize.BOOLEAN,
validate: {
is: ["[a-z]",'i'], // will only allow letters
max: 23, // only allow values <= 23
isIn: {
args: [['en', 'zh']],
msg: "Must be English or Chinese"
}
},
field: 'column_a'
// Other attributes here
},
columnB: Sequelize.STRING
})
sequelize.sync();
await modelA.findAndCountAll({
where: whereQuery,
offset: per_page * (current - 1),
limit: per_page,
order: [['create_time', 'DESC']]
});
  • 报警机制,定时任务+方糖。
    image
  • 路由管理。引用了koa-router包,使用方式。在这套代码里我设计成集中管理的模式,集中在一处能看到整个应用的路由关系,避免随着业务扩展而逻辑凌乱路由分散。具体看码。
  • 使用loadtest进行简单的压力测试,目前能稳定在每秒100请求下。到了120以上开始内存上涨,延时增加,但服务还没崩。150以上开始出现极大延时,大量请求超时,吞吐量下降。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var loadtest = require('loadtest');
var options = {
url: url,
method: 'GET',
// maxRequests: 1000,
requestsPerSecond: 100,
concurrency: 20,
statusCallback: function (error, result, latency) {
console.log('总请求量: %j,错误: %j,rps: %j,延时:%j ms ', latency.totalRequests, latency.totalErrors, latency.rps, latency.meanLatencyMs);
}
};
loadtest.loadTest(options, function(error, result)
{
if (error)
{
return console.error('Got an error: %s', error);
}
console.log('Tests run successfully');
});

待扩展

  • 统计数据的查询。根据传参判断,大于72小时的以小时为单位,其余以分钟为单位。查询语句上先以create_time(时间戳)用FROM_UNIXTIME格式化成对应的sqlFormat格式,在此基础上(date_format)分组查询做出统计。有没有更好的查询办法让统计粒度更自由?比如按10分钟,30分钟,天。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义时段分割,小于1小时的以分钟为分割单位
let timeSplit = (countQuery.create_time.$lt - countQuery.create_time.$gt) >= (72 * 60 * 60) ? {
unit: 60 * 60,
sqlFormat: '%Y-%m-%e %H:00',
momentFormat: 'YYYY-MM-D HH:00'
} : {
unit: 60,
sqlFormat: '%Y-%m-%e %H:%i',
momentFormat: 'YYYY-MM-D HH:mm'
};
// 查询
let queryCount = await db[modelName].count({
where: countQuery,
order: ['create_time', 'ASC'],
attributes: [
[db.sequelize.fn('FROM_UNIXTIME', db.sequelize.col('create_time'), timeSplit.sqlFormat), 'date_format']
],
group: 'date_format'
});
  • 用户模块、权限模块。在系统内部就可以管理不同项目接收报警信息的人,而不需要通过方糖扫码。
    image
    image
  • APM模块。页面性能分析,浏览器分布分析。
  • 页面巡航模块。利用UI自动化测试定时跑测试脚本,监控各个页面的实际情况。
  • 大数据量的解决方案?目前定时(1个月)清理db。
  • 防刷、防攻击手段???
  • nginx实现的多机集群均衡负载???(单机都还要省着用,还多机?)
1
2
3
4
upstream XXX.XXX.com {
server 123.456.789.111:7082;
server 123.456.789.222:7082;
}
  • 还有什么使用上面的优化??