背景
前面项目基本功能已经实现完了,这一篇集成一下监控平台,监控一下前后端异常。
监控平台选型
阿里云的应用实时监控服务ARMS和字节的应用性能监控全链路版这两个我都在公司里使用过,使用起来差不多,支持的功能也差不多,不过是收费的。
这里我推荐一个开源的监控平台sentry,功能比上面两个强大很多,支持多种语言。有saas版,也可以自己部署一套。

私有化部署sentry
环境准备
- Docker 19.03.6+
- Docker-Compose 1.28.0+
- 4 CPU Cores
- 8 GB RAM
- 20 GB Free Disk Space
部署
执行下面命令,拉取项目
sh
git clone https://github.com/getsentry/onpremise
代码拉下来之后,项目根目录下,会有一个install.sh
文件,执行这个文件。
sh
cd onpremise
sh
./install.sh

这里选n,继续安装,中间需要设置管理员帐号和密码,这里设置的帐号和密码要记住,后面登录需要用到。安装过程比较慢,要多等一会。

执行结束后,执行下面命令启动服务,启动也比较慢。
sh
docker-compose up -d
启动项目
启动成功后,访问9000端口,输入刚才设置的帐号和密码登录。

设置语言
登录之后,点开用户设置,可以设置语言为中文。

小结
如果不想自己部署,可以到sentry saas平台注册一个帐号,不过只能免费试用30天。
新建前端项目

前端项目集成sentry
安装依赖
sh
pnpm i @sentry/react --save
测试验证
安装上面的教程把代码复制到项目里,改造main.tsx
文件。

在登录方法里故意整个错,测试一下

进入项目后,可以发现报错

这个最牛的是,可以回放用户的操作,有利于定位问题,这个功能其它几个监控平台都不支持。

对接react-router
为了更精准的获取页面加载性能信息和页面报错信息,需要把sentry和react-router结合。


设置用户上下文
线上报错了,为了更好的调试,我们需要知道当前报错是哪个用户触发的,sentry支持在上传报错信息时注入当前用户信息。
登录成功后,调用setUser方法全局设置用户信息。

查看报错信息时,可以看到当前用户id了。

对接React Error Boundary
现在组件渲染的时候,如果有报错,会出现不友好的报错界面。
模拟组件渲染出现异常


这个报错页面,其实是react-router默认报错页面,react-router支持自定义报错页面,使用errorElement属性就行了。


如果这样写,因为异常被拦截了,没办法上报。还好sentry支持了异常组件,只需要用Sentry.ErrorBoundary
组件包裹最外层组件就行了。

改造完测试了一下,发现不生效,因为上面我们拦截了异常,所以Sentry.ErrorBoundary
监听不到。把errorElement去掉也不行,因为react-router内部会捕获异常,不设置errorElement会用内置的。这里我被卡了一段时间,后来在react-router官网中找到了useRouteError
这个api,可以获取报错信息。
后来的解决方案是,写一个组件,组件里使用useRouteError
获取到报错信息后,再抛出异常,这时候外面的Sentry.ErrorBoundary
就能捕获到异常了。
tsx
import { useRouteError } from 'react-router-dom';
const RouterErrorElement = () => {
const error = useRouteError();
throw error;
}
export default RouterErrorElement;


异常也能正常上报了

美化一下报错页面
tsx
import React from 'react';
import { Button, Result } from 'antd';
import { router } from './router';
const ErrorPage: React.FC = () => (
<Result
status="error"
title="出错了"
subTitle="我们正在努力修复中,请稍后再试。"
extra={[
<Button onClick={() => { router.navigate('/') }} type="primary" key="console" >
回到首页
</Button>
]}
/>
);
export default ErrorPage;
使用刚才封装的报错页面


上传sourcemap
打包发布到线上后,因为代码压缩混淆,没办法定位报错的代码。
sentry提供了命令,可以快速生成配置。
sh
npx @sentry/wizard@latest -i sourcemaps
执行命令后,可以选择使用它的saas平台,还是自己搭建的,如果是自己搭建的选第二个,然后输入自己的平台地址。

我们已经创建了用户,这里选yes

选择后,会打开网页,让你授权,授权成功后,选择项目
这里选择Vite

选择后,会使用pnpm帮你安装@sentry/vite-plugin
依赖

这里选择yes,我们使用CICD发布

上面选择完yes后,会给你生成一个authToken,这个token要配置github环境变量里。下面继续选yes。
然后就完成了,有几个地方需要改造一下。
-
删除env.sentry-build-plugin文件,因为这个文件里存了token。生成的token不能放在项目里,项目是公开的,token会泄漏。
-
这里因为我们使用的是github workflow,可以把token配在github中,然后代码里从环境变量里去token。

-
因为需要上传sourcemap,所以脚本把sourcemap打开了,打包出来的代码有sourcemap文件,这玩意不能上传到线上,不然源代码就泄漏了,所以在sourcemap上传到sentry平台后,需要给移除掉。
打包发布后,这里可以查看到sourcemap。

报错也能定位源码了

到此前端异常监控搞定了,前端性能监控和埋点我还在研究中,等研究好了,再出一篇文章。
新建后端项目
sentry没有midway插件,但是支持koa项目,midway底层用的就是koa,所以这里选koa项目。


后端项目集成sentry
安装依赖
sh
pnpm i --save @sentry/node @sentry/profiling-node @sentry/utils
初始化Sentry
在src/configuration.ts
文件中加入下面代码,初始化Sentry
使用sentry捕获异常
普通的业务报错,我们不用上报,只上报500的异常。
改造src/filter/default.filter.ts
文件
ts
import { Catch } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import * as Sentry from '@sentry/node';
@Catch()
export class DefaultErrorFilter {
async catch(err: Error, ctx: Context) {
// 捕获异常,并把异常和接口绑定一起上报
Sentry.withScope(scope => {
scope.addEventProcessor(event => {
return Sentry.addRequestDataToEvent(event, ctx.request);
});
Sentry.captureException(err, { user: { id: ctx?.userInfo?.userId } });
});
ctx.status = 500;
return {
code: 500,
message: '系统错误',
};
}
}
改造获取当前用户信息接口,故意写一段报错代码

前端访问一下接口,然后线上就能看到报错信息。

统计接口执行时间
编写中间件,这里的代码参考了官网koa项目的示例代码改造而来的,理解起来比较费劲。
ts
// src/middleware/sentry.ts
import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
import * as Sentry from '@sentry/node';
import { stripUrlQueryAndFragment } from '@sentry/utils';
@Middleware()
export class SentryMiddleware implements IMiddleware<Context, NextFunction> {
resolve() {
return async (ctx: Context, next: NextFunction) => {
return new Promise<void>(resolve => {
Sentry.runWithAsyncContext(() => {
const hub = Sentry.getCurrentHub();
hub.configureScope(async scope => {
scope.addEventProcessor(event => {
return Sentry.addRequestDataToEvent(event, ctx.request);
});
const reqMethod = (ctx.method || '').toUpperCase();
const reqUrl = ctx.url && stripUrlQueryAndFragment(ctx.url);
const transaction = Sentry.startTransaction({
name: `${reqMethod} ${reqUrl}`,
op: 'api',
});
Sentry.getCurrentHub().configureScope(scope => {
scope.setSpan(transaction);
});
ctx.__sentry_transaction = transaction;
await next();
if (ctx._matchedRoute) {
const mountPath = ctx.mountPath || '';
transaction.setName(
`${reqMethod} ${mountPath}${ctx._matchedRoute}`
);
}
transaction.setHttpStatus(ctx.status);
transaction.finish();
resolve();
});
});
});
};
}
static getName(): string {
return 'sentry';
}
}
查看效果

上面有几个参数要说一下
p50: 所有用户请求这个接口请求时间的中位数,从小到大排序。
p95: 所有用户请求这个接口请求时间的95%的值,从小到大排序。假设有有100个人请求,从小到大排序后,第95个位置上的值。
这里为啥不使用请求时间平均值,因为如果某次接口出现异常调用时间特别长,会导致平均值和实际值差距会很大。
最后
到此最基本的前后端异常监控搞定了,性能分析和埋点我还在研究中,后面研究完再给大家分享吧。
项目体验地址:fluxyadmin.cn/user/login
前端仓库地址:github.com/dbfu/fluxy-...
后端仓库地址:github.com/dbfu/fluxy-...