系统是否稳定,性能是否需要优化等都依赖于统计,为了能及时反应出系统状态,并方便添加告警指标,我将相关的统计数据写入influxdb,主要指标如下:
tags:
method,请求类型
type,根据响应状态码分组,1xx -> 1, 2xx -> 2
spdy,根据自定义的响应时间划分区间,方便将接口响应时间分组
route,接口路由
fields:
connecting,处理请求数
use,处理时长
bytes,响应数字长度
code,响应状态码
url,请求地址
ip,用户IP
在influxdb中,tags可用于对数据分组,根据 type 将接口请求分组,将 4 与 5 的单独监控,可以简单快速的把当前接口出错汇总。统计中间件代码如下:
function stats() { let connecting = 0; const spdyList = [ 100, 300, 1000, 3000, ]; return async (ctx, next) => { const start = Date.now(); const tags = { method: ctx.method, }; connecting++; const fields = { connecting, url: ctx.url, } let status = 0; try { await next(); } catch (err) { // 出错时状态码从error中获取 status = err.status; throw err; } finally { // 如果非出错,则从ctx中取状态码 if (!status) { status = ctx.status; } const use = Date.now() - start; connecting--; tags.route = ctx._matchedRoute; tags.type = `${status / 100 | 0}` let spdy = 0; // 确认处理时长所在区间 spdyList.forEach((v, i) => { if (use > v) { spdy = i + 1; } }); tags.spdy = `${spdy}`; fields.use = use; fields.bytes = ctx.length || 0; fields.code = status; fields.ip = ctx.ip; // 统计数据写入统计系统(如influxdb) console.info(tags); console.info(fields); } }; } app.use(stats()); router.post('/users/v1/:type', async (ctx) => { await new Promise(resolve => setTimeout(resolve, 100)) ctx.body = { account: 'vicanso', }; });
接口全日志记录
为了方便排查问题,需要将接口的相关信息输出至日志中,中间件的实现如下:
function tracker() { const stringify = (data) => JSON.stringify(data, (key, value) => { // 对于隐私数据做***处理 if (/password/.test(key)) { return '***'; } return value; }); return async (ctx, next) => { const trackerInfo = { url: ctx.url, form: ctx.request.body, }; try { await next(); } catch (err) { trackerInfo.error = err.message; throw err; } finally { trackerInfo.params = ctx.params; if (!trackerInfo.error) { trackerInfo.body = ctx.body; } console.info(stringify(trackerInfo)) } }; } app.use(bodyParser()); app.use(tracker()); router.post('/users/v1/:type', async (ctx) => { // ctx.throw(400, '密码出错'); await new Promise(resolve => setTimeout(resolve, 100)) ctx.body = { account: 'vicanso', }; });
使用此中间件之后,可以将所有接口的参数、正常响应数据或出错信息都全部输出至日志中,可根据需要调整 stringify 的实现,将一些隐私数据做***处理。需要注意的是,由于部分接口的body响应体部分较大,是否需要将所有数据都输出至日志最好根据实际情况衡量。如可根据HTTP Method过滤,或者根据url规则等。
参数校验
由于javascript的弱类型,接口参数校验一直是要求最严格的一点,而在了解过 joi 之后,我就一直使用它来做参数校验,如注册功能,账号、密码为必选参数,而邮箱为可选,接口校验的代码如下:
function validate(data, schema) { const result = Joi.validate(data, schema); if (result.error) { // 出错可创建自定义的校验出错类型 throw result.error; } return result.value; } router.post('/users/v1/register', async (ctx) => { const data = validate(ctx.request.body, Joi.object({ // 账号限制长度为3-20个字符串 account: Joi.string().min(3).max(20).required(), // 密码限制长度为6-30,而且只允许字母与数字 password: Joi.string().regex(/^[a-zA-Z0-9]{6,30}$/).required(), email: Joi.string().email().optional(), })); ctx.body = { account: data.account, }; });