基于node+express+log4js的前端异常信息监控
近期在熟悉怎样处理前端异常,在客户端跑的h5代码,如果遇到体量大的客户群(几百w,几千w),对前端js进行异常监控就变得很重要了,因为测试并不能完整的捕捉到某些情况和场景下的异常,其中包括了接口返回信息缺失、固定操作下的js报错等,我的思路大概是这样:
1、前端对异常进行收集
2、上报到node server
3、通过 log4js进行日志记录
当然,如果想体验更好,也可以通过node server 将异常入库(结合mongodb),在后台通过数据结合类
chart 组件通过图形化展示异常信息更为直观。
一、前端异常收集。
前端的异常收集常用的两种方式:
1、try catch
try,catch能够知道出错的信息,并且也有堆栈信息可以知道在哪个文件第几行第几列发生错误。
但是try,catch的方案有2个缺点:
没法捕捉try,catch块,当前代码块有语法错误,JS解释器压根都不会执行当前这个代码块,所以也就没办法被catch住;
没法捕捉到全局的错误事件,也即是只有try,catch的块里边运行出错才会被你捕捉到,这里的块你要理解成一个函数块。
2、window.onerror
可以捕捉语法错误,也可以捕捉运行时错误;
可以拿到出错的信息,堆栈,出错的文件、行号、列号;
只要在当前页面执行的js脚本出错都会捕捉到,例如:浏览器插件的javascript、或者flash抛出的异常等。
跨域的资源需要特殊头部支持。
需要注意的是:
1、window.onerror能捕捉到语法错误,但是语法出错的代码块不能跟window.onerror在同一个块
2、对于跨域的JS资源,window.onerror拿不到详细的信息,需要往资源的请求添加额外的头部。
window.onerror = (msg, url, line, col, error) => { //没有URL不上报!上报也不知道错误 if (msg != "Script error." && !url) { return true; } setTimeout(() => { var data = {}; //不一定所有浏览器都支持col参数 col = col || (window.event && window.event.errorCharacter) || 0; data.url = url; data.line = line; data.col = col; if (!!error && !!error.stack) { //如果浏览器有堆栈信息 //直接使用 data.msg = error.stack.toString(); } else if (!!arguments.callee) { //尝试通过callee拿堆栈信息 var ext = []; var f = arguments.callee.caller, c = 3; //这里只拿三层堆栈信息 while (f && (--c > 0)) { ext.push(f.toString()); if (f === f.caller) { break;//如果有环 } f = f.caller; } ext = ext.join(","); data.msg = ext; } //把data上报到后台! //这里可以做日志上报 $.ajax({}) }, 0); return false; };
这里我们在后面返回false,让控制台也能把错误打印出来,至此,前端异常收集完成!
二、上报到node server
服务端要做的,就是提供上传数据的接口,让错误数据能保存下来,很简单,我们加到express路由中就可以了:
/* 写入前端日志 */
router.post('/restapi/reportErrInfo', (req, res, next) => { let logParams = req.query; let logUrl = logParams.url; let sendState = false; if (autoUrl(logUrl)) { logUtil.logh5Error(req, logParams); sendState = true; } return res.json({ data: { responseCode: '0', responseMsg: sendState ? 'success!' : 'failed!' } }) });
这里主要是通过接口写入日志的操作, 这是我通过log4js封装的方法,也就是下面这段:
logUtil.logh5Error(req, logParams);
另外需要重点说明两点:
1、日志上报使用post方式,更安全
2、为了防止恶意请求(csrc等),需要在接口处对信息进行鉴权处理,方法很多,常用的比如通过对比cookie,或者前端传token的方式
三、通过 log4js进行日志记录
nodeJS自带的console.log已经可以打印出日志了,为了让日志看起来没那么糟,我打算对日子进行改造(将常规日志response和错误日志error分开),具体实现如下:
1、新建log配置文件 logConfig.js
let path = require('path'); //日志根目录 let isDevEnv = (process.env.NODE_ENV == 'development' || process.env.NODE_ENV == 'FAT') ? true : false; let baseLogPath = isDevEnv ? path.resolve(__dirname, '../logs') : '/home/reslogs’; //错误日志目录 let errorPath = isDevEnv ? "/error" : ‘/home/errlogs'; //错误日志文件名 let errorFileName = "error"; //错误日志输出完整路径 let errorLogPath = baseLogPath + errorPath + "/" + errorFileName; //响应日志目录 let responsePath = isDevEnv ? "/response" : ''; //响应日志文件名 let responseFileName = "response"; //响应日志输出完整路径 let responseLogPath = baseLogPath + responsePath + "/" + responseFileName; module.exports = { "appenders": [ //错误日志 默认按小时数记录 { "category": "errorLogger", //logger名称 "type": "dateFile", //日志类型 "filename": errorLogPath, //日志输出位置 "alwaysIncludePattern": true, //是否总是有后缀名 "pattern": "-yyyy-MM-dd.log", //后缀,每天创建一个新的日志文件 "path": errorPath //自定义属性,错误日志的根目录 }, //响应日志 响应日志默认按天记录 { "category": "resLogger", "type": "dateFile", "filename": responseLogPath, "alwaysIncludePattern": true, "pattern": "-yyyy-MM-dd.log", //后缀,每天创建一个新的日志文件 "path": responsePath } ], "levels": //设置logger名称对应的的日志等级 { "errorLogger": "ERROR", "resLogger": "ALL" }, "baseLogPath": baseLogPath //logs根目录 }
这里是log4js的配置文件,记录日志类型,保存文件格式以及路径等信息
2、增加 logUtil.js ,代码如下:
let log4js = require('log4js'); let fs = require('fs'); import logConfig from 'config/logConfig'; import _ from 'lodash'; //加载配置文件 log4js.configure(logConfig); let errorLogger = log4js.getLogger('errorLogger'); let resLogger = log4js.getLogger('resLogger'); let logUtil = { initPath() { if (logConfig.baseLogPath) { confirmPath(logConfig.baseLogPath) //根据不同的logType创建不同的文件目录 for (let i = 0; i < logConfig.appenders.length; i++) { if (logConfig.appenders[i].path) { confirmPath(logConfig.baseLogPath + logConfig.appenders[i].path); } } } }, logh5Error(req, error, resTime) { if (req) { errorLogger.error(formatError(req, error, 'h5', resTime)); } }, logError(error, req, resTime) { if (error) { if (typeof (error) == "string") { errorLogger.error('***** node server error *****', error); } else { errorLogger.error(formatError(req, error, 'node', resTime)); } } }, logResponse(ctx, resTime) { if (ctx) { resLogger.info(formatRes(ctx, resTime)); } }, info(key, info) { if (key) { resLogger.info(key, info); } } }; let confirmPath = function (pathStr) { if (!fs.existsSync(pathStr)) { fs.mkdirSync(pathStr); // console.log('createPath: ' + pathStr); } } //格式化响应日志 let formatRes = function (req, resTime) { let logText = new String(); //响应日志开始 logText += "\n" + "*************** response log start ***************" + "\n"; //添加请求日志 logText += formatReqLog(req, resTime); //响应状态码 logText += "response status: " + req.status + "\n"; //响应内容 logText += "response body: " + "\n" + JSON.stringify(req.body) + "\n"; //响应日志结束 logText += "*************** response log end ***************" + "\n"; return logText; } //格式化错误日志 let formatError = function (req = {}, error = {}, type = 'node server', resTime = 0) { let logText = new String(); let err = type === 'h5' ? req.query : error; //错误信息开始 logText += "\n" + "*************** " + type + " error log start ***************" + "\n"; //添加请求日志 if (!_.isEmpty(req)) { logText += formatReqLog(req); } if (type === 'h5') { //用户信息 if (err.userInfo) { logText += "request user info: " + err.userInfo + "\n"; } // 客户端渠道信息 if (err.pageParams) { logText += "request client channel info: " + err.pageParams + "\n"; } // 客户端设备信息 if (err.clientInfo) { logText += "request mobile info: " + err.clientInfo + "\n"; } //报错位置 logText += "err line: " + err.line + ", col: " + err.col + "\n"; //错误信息 logText += "err message: " + err.msg + "\n"; //错误页面 logText += "err url: " + err.url + "\n"; } else { // node server //错误名称 logText += "err name: " + error.name + "\n"; //错误信息 logText += "err message: " + error.message + "\n"; //错误详情 logText += "err stack: " + error.stack + "\n"; } //错误信息结束 logText += "*************** " + type + " error log end ***************" + "\n"; return logText; }; //格式化请求日志 let formatReqLog = function (req) { let logText = new String(); let method = req.method; // 访问路径 logText += "request url: " + req.url + "\n"; //访问方法 logText += "request method: " + method + "\n"; //客户端ip logText += "request client ip: " + req.ip + "\n"; return logText; } module.exports = logUtil;
此处主要是创建日志输出目录,对日志格式进行重新编辑,并约定了前端和node端错误的格式
前端错误调用:logh5Error node端错误调用:logError
这里回到node接口调用时执行的方法,就能看明白了,
logUtil.logh5Error(req, logParams);
在app.js中对log4js进行初始化,主要代码:
//加载中间件 app.use(log4js.connectLogger(logger, { level: 'auto', format: ':method :url HTTP/:http-version :status [:res[content-length]]bytes :remote-addr :referrer :user-agent' }));
// 初始化日志目录 logUtil.initPath();
会在服务启动时在项目生成文件夹
来看看报错的效果吧:
参考文档:
1、前端代码异常监控
- Prev one
基于react-native qq登录窗
react-native是facebook在 react基础上的开发出的可以用js方式开发native应用的项目,基本上沿用了reactjs的组件开发模式,使用flex布局,最近也熟悉了一下flex的布局方式,之前有接触过adoble flex,所以多flex box并不陌生
- Next one
领域驱动设计(DDD:Domain-Driven Design)
Eric Evans的“Domain-Driven Design领域驱动设计”简称DDD,Evans DDD是一套综合软件系统分析和设计的面向对象建模方法,Jdon.com是国内公开最早讨论DDD网站之一