当前位置: 首页 > 科技观察

前端代码异常日志收集和监控

时间:2023-03-14 13:27:51 科技观察

在复杂的网络环境和浏览器环境中,自测、QA测试、CodeReview是不够的。如果你对页面的稳定性和准确性有很高的要求,那么你必须要有一套完善的代码异常监控系统。本文从前端代码异常监控的方法和问题入手,力图全面阐述错误日志采集各个阶段可能遇到的障碍和解决方案。收集日志的方法通常收集日志的方法可以分为两个方面,一是逻辑上的错误判断,是主动判断;另一种是利用语言提供的快捷方式暴力获取错误信息,如try..catch、window.onerror。1.主动判断经过一些操作我们得到了预期的结果,但是结果不是我们想要的//test.jsfunctioncalc(){//code...returnval;}if(calc()!=="someVal"){Reporter.send({position:"test.js::calc"msg:"calcerror"});}这种逻辑错误/状态错误反馈常用于界面状态判断。2.try..catch在一段代码中捕获并判断错误:try{init();//code...}catch(e){Reporter.send(format(e));}以init作为程序入口,代码同步执行的所有错误都会被捕获,这种方法也可以防止程序刚运行就挂了。3、window.onerror捕获全局错误:window.onerror=function(){varerrInfo=format(arguments);Reporter.send(errInfo);returntrue;};在上面的函数中返回true,错误就不会暴露在控制台中。下面是它的参数信息:/***@param{String}errorMessage错误信息*@param{String}scriptURI错误文件*@param{Long}lineNumber错误码行号*@param{Long}columnNumber错误码columnnumber*@param{Object}errorObjerrordetails,Anything*/window.onerror=function(errorMessage,scriptURI,lineNumber,columnNumber,errorObj){//code..}window.onerror是一个特别暴力的容错手段,试试..catch是一样的,它们的底层实现都是使用C/C++中的goto语句,一旦发现错误,不管当前栈有多深,无论代码运行到哪里,都会直接运行到toplevel或者try在..catch捕获的层,这种kickaway的错误处理不是很好。收集日志的问题收集日志的目的是为了及时发现问题。***日志可以告诉我们错误在哪里。更好的方法是不仅告诉我们错误在哪里,而且告诉我们如何处理错误。***目标是发现错误并自动容忍它们。这一步是最难的。1.没有具体的错误信息,脚本错误。先看下面这个例子,test.htmltest.js//http://barret/test.jsfunctiontest(){vera=1;return+1;}test();我们期望收集的日志有以下具体信息:为了更好的配置和管理资源,我们通常将静态资源放在外域但是结果是:打开Chromium的WebCore源码,你可以看到:在跨域的情况下,返回的结果是Scripterror。//http://trac.webkit.org/browser/branches/chromium/1453/Source/WebCore/dom/ScriptExecutionContext.cpp#L333Stringmessage=errorMessage;intline=lineNumber;StringsourceName=sourceURL;//alreadygotAll错误信息,但是如果发现是非同源的,则在`sanitizeScriptError`中覆盖错误信息sanitizeScriptError(message,line,sourceName,cachedScript);在老版本的WebCore中,只判断了securityOrigin()->canRequest(targetURL),而在新版本中还有一个cachedScript的判断,可见浏览器对这方面的限制越来越严格.本地测试:可以看出在file://协议下,securityOrigin()->canRequest(targetURL)也是false。为什么脚本错误。?简单报错:脚本错误,目的是为了避免数据泄露到不安全的域,一个简单的例子:上面我们没有引入js文件,但是一个html,这个html是银行的登录页面,如果你已经登录bank.com,那么登录页面会自动跳转到Welcomexxx...,如果你没有登录,会跳转到PleaseLogin...,那么JS错误也会是Welcomexxx...isnotdefined,PleaseLogin...isnotdefined。通过这些信息可以判断用户是否登录了自己的银行账户,这就为黑客提供了非常方便的途径。从频道来看,这是非常不安全的。?crossOrigin参数跳过跨域限制。image和script标签都有crossorigin参数。它的作用是告诉浏览器我要从外域加载一个资源,我信任这个资源。但是报错:这是预料中的错误,跨域资源共享策略要求服务器也设置Access-Control-Allow-Origin响应头:header('Access-Control-Allow-Origin:*');回头看我们的CDN资源,Javascript/CSS/Image/Font/SWF等静态资源其实已经加入了早期的CORS响应头。2、压缩后的代码无法定位错误的具体位置。线上几乎所有的代码都打包压缩,几十个上百个文件压缩打包成一个,只有一行。当我们收到aisnotdefined时,如果只是在特定场景下报错,我们将无法定位到压缩后的a是什么,那么此时的错误日志将无效。第一个想到的方法是使用sourceMap,可以在未压缩代码中定位压缩代码中某个点的具体位置。下面是sourceMap引入的格式,在代码的最后一行添加://#sourceMappingURL=index.js.map以前用'//@'开头,现在用'//#',但是对于错误报告,这东西没用。JS无法获取到它的真实行数,只能借助ChromeDevTools之类的工具辅助,而且并不是每个网上的资源都会添加sourceMap文件。sourceMap的使用目前只能体现在开发阶段。当然,如果了解sourceMap的VLQ编码和位置对应关系,也可以对获取的日志进行二次分析,映射到真实的路径位置。这个比较贵,好像暂时没有人试过。那么,有什么方法可以定位错误的具体位置,或者有什么方法可以降低我们定位问题的难度吗?你可以这样想:打包的时候,每两个合并后的文件之间加1000个空行,最后上线的文件就变成了(function(){varlongCode.....})();//file1//1000个空行(function(){varlongCode.....})();//file2//1000个空行(function(){varlongCode.....})();//file3//1000个空行(function(){varlongCode.....})();//file4var_fileConfig=['file1','file2','file3','file4']如果报错在3001行,window.onerror=function(msg,url,line,col,error){//line=3001varlineNum=line;console.log("Errorlocation:"+_fileConfig[lineNum%1000-1]);//->"错误位置:file3"};可以算出错误出现在第三个文件,范围缩小了很多。3、错误事件的注册多次注册错误事件不会重复执行多次回调:varfn=window.onerror=function(){console.log(arguments);};window.addEventListener("error",fn);window.addEventListener("错误",fn);触发错误后,上述代码的结果是:window.onerror和addEventListener都执行了,而且只执行了一次。4.收集的日志量没有必要把所有的错误信息都发送到Log,这个量太大了。如果网页PV有1kw,那么就会有1kw的某个错误发送的日志信息,大概是一个G日志。我们可以给Reporter函数添加一个采样率:functionneedReport(sampling){//sampling:0-1returnMath.random()<=sampling;}Reporter.send=function(errInfo,sampling){if(needReport(sampling||1)){Reporter._send(errInfo);}};这个采样率可以根据需要进行处理。可以和上面一样,使用随机数,或者使用cookie中某个字段(比如昵称)的最后一个字母/可以通过数字来判断,也可以对用户的昵称进行hash计算,以及然后根据最后一个字母/数字判断。简而言之,有很多方法。收集日志分布位置为了更准确的获取错误信息,更有效的统计错误日志,我们应该使用更活跃的埋点,比如在一个接口请求中://ModuleAGetShopsData$.ajax({url:URL,dataType:"jsonp",success:function(ret){if(ret.status==="failed"){//埋点1returnReporter.send({category:"WARN",msg:"Module_A_GET_SHOPS_DATA_FAILED"});}if(!ret.data||!ret.data.length){//埋点2returnReporter.send({category:"WARN",msg:"Module_A_GET_SHOPS_DATA_EMPTY"});}},error:function(){//埋点3Reporter.发送({类别:“错误”,消息:“Module_A_GET_SHOPS_DATA_ERROR”});}});上面我们已经准确的列出了三点,描述的非常清楚,这三点将会为我们后续的调查在线问题提供非常有用的信息。关于try..catch的使用对于try..catch的使用,我的建议是:能用就尽量不要用。JS代码是我自己写的。哪里会出现什么问题,心里应该有个谱。通常只有两个地方使用try..catch://JSON格式错误try{JSON.parse(JSONString);}catch(e){}//有无法解码的字符}catch(e){}这样的错误是不可控的。可以考虑在用try..catch的地方能不能用其他的方法做兼容。关于window.onerror的使用,可以试试下面的代码://test.jsthrownewError("SHOWME");window.onerror=function(){console.log(arguments);//防止在控制台打印错误信息returntrue;};上面代码直接报错,没有继续执行。页面上可能有好几个script标签,但是错误监视器window.onerror一定要放在最前面!错误警报与提示何时警报?不要报告错误。如上所述,由于网络环境和浏览器环境因素,我们允许复杂页面的错误率为千分之一。日志处理后的数据图:图中有两条线,橙色线是今天的数据,浅蓝色线是过去的平均数据,每10分钟产生一条记录,横坐标是时间轴0-24个点,纵坐标为误差量。可以清楚的看到凌晨一两点左右,服务出现了异常,报错信息是平均值的十几倍,所以这个时候就会报警。警报条件可以设置得更严格,因为误报很烦人。短信、邮件、软件等信息的狂轰滥炸,有时是在半夜。那么,一般满足以下条件就可以报警:错误超过阈值。例如10分钟内最多允许100次错误,结果超过100次,错误超过平均值的10倍。如果超过平均值,则发出警报。这个逻辑显然是不正确的,但它超过了平均值。10倍的数值,基本可以确定服务有问题。在被纳入对比之前,需要过滤掉同一个IP出现的错误,比如for循环或者while循环中出现的错误,或者用户在某个地方抢购不断刷新友情对比下面两条日志,catch的错误日志:UncaughtReferenceError:vdisnotdefined自定义错误日志:"生日模块中获取后端接口信息时,出现eval解析错误,错误内容是:vd未定义。”这个错误在最近10分钟内发生了1000次,过去这个错误的平均错误量是50次/10分钟WorkingDraftofNetworkErrorLogW3CWebPerformance工作组发布了一个网络错误日志的工作草案,这个文档定义了一种允许网站声明网络错误报告策略的机制,浏览器等用户代理可以使用这种机制来报告影响资源正确加载的网络错误,该文档还定义了错误报告的标准格式及其传输机制浏览器和web服务器之间,详细草稿:http://www.w3.org/TR/2015/WD-network-error-logging-20150305/总结功能,测试和监控是程序开发的三招,很多工程师都可以完善功能,也知道一些测试方面的知识,但是在监控方面我的大脑基本是一片空白,错误日志的收集整理是监控的一小部分,但是我了解网站的稳定性对我们来说非常重要。希望读者能对文中疏漏的部分进行补充,不当之处还望指正。

猜你喜欢