科技改變生活 · 科技引領(lǐng)未來(lái)
大部分時(shí)候,發(fā)現(xiàn)問(wèn)題通常比解決問(wèn)題更加重要。異常是我們?cè)陂_(kāi)發(fā)和運(yùn)維過(guò)程中頻繁會(huì)遇到的東西,通過(guò)異常我們可以知道程序中發(fā)生了一些非正常的事件,進(jìn)而排查原因、解決問(wèn)題。能夠有效的對(duì)異常進(jìn)行收集分析,及時(shí)發(fā)現(xiàn)異常報(bào)告異常,對(duì)于我們提高系統(tǒng)的穩(wěn)定性
大部分時(shí)候,發(fā)現(xiàn)問(wèn)題通常比解決問(wèn)題更加重要。
異常是我們?cè)陂_(kāi)發(fā)和運(yùn)維過(guò)程中頻繁會(huì)遇到的東西,通過(guò)異常我們可以知道程序中發(fā)生了一些非正常的事件,進(jìn)而排查原因、解決問(wèn)題。能夠有效的對(duì)異常進(jìn)行收集分析,及時(shí)發(fā)現(xiàn)異常報(bào)告異常,對(duì)于我們提高系統(tǒng)的穩(wěn)定性和可用性都具有很大的現(xiàn)實(shí)意義。
本文介紹了我們?cè)诜植际较到y(tǒng)異常日志收集與分析方面的實(shí)踐過(guò)程,文中提出了一種對(duì)異常日志分類的算法,可以相對(duì)有效的分析海量異常日志。
一、背景
首先,我們先來(lái)討論下為什么要做這件事情。
一般在開(kāi)發(fā)過(guò)程中出現(xiàn)的異常大部分都能被大家及時(shí)發(fā)現(xiàn)并解決,但是通常在系統(tǒng)上線之后,由于大家不會(huì)實(shí)時(shí)查看日志,我們的異常信息可能會(huì)被淹沒(méi)在大量應(yīng)用系統(tǒng)日志中。
如果沒(méi)有將異常信息及時(shí)收集并暴露出來(lái),可能某些異常便會(huì)被忽略或造成很大影響之后才被發(fā)現(xiàn)。大家也可能經(jīng)歷過(guò)如下類似的場(chǎng)景:
場(chǎng)景一:某些leader和運(yùn)維同學(xué)在看線上日志的時(shí)候發(fā)現(xiàn)了異常日志,于是便截圖或?qū)惓P畔⒖截惓鰜?lái)貼到溝通群里,@某個(gè)同學(xué)排查,這種方式通常是隨機(jī)的,很容易漏掉了某些重要異常,或者是不能及時(shí)發(fā)現(xiàn)異常。
場(chǎng)景二:某一天突然接到運(yùn)營(yíng)或客服同學(xué)反饋某個(gè)問(wèn)題,經(jīng)過(guò)排查發(fā)現(xiàn)系統(tǒng)bug,但是其實(shí)異常早就打印出來(lái)了,只不過(guò)一直沒(méi)有發(fā)現(xiàn),不得不緊急修復(fù)bug,上線,更麻煩的是可能還需要修復(fù)異常數(shù)據(jù)。
所以,我們一直想找一種方法可以對(duì)異常日志進(jìn)行分析處理,但是僅僅是將異常日志收集上來(lái)報(bào)告給各業(yè)務(wù)系統(tǒng)研發(fā)同學(xué)效果并不會(huì)很好。
其實(shí)在之前的工作中我們已經(jīng)把項(xiàng)目的異常日志統(tǒng)一收集起來(lái)了,大家可以看到自己項(xiàng)目的異常,也可以看到每天異常數(shù)量的趨勢(shì),如下圖所示:
上面的圖中明顯的有兩個(gè)點(diǎn)開(kāi)始異常數(shù)變多了,為什么變多了呢?是哪些項(xiàng)目變多了,哪些異常變多了,我們目前很難給出準(zhǔn)確的答案,所以需要我們對(duì)現(xiàn)有的異常做數(shù)據(jù)分析,以便可以更好的分析問(wèn)題。
那需要做哪些分析呢?我們希望通過(guò)分析可以知道如下信息:
如下是我們最終實(shí)現(xiàn)的一個(gè)效果圖:
在上圖中可以看到,針對(duì)于每一個(gè)項(xiàng)目,在異常日志分析tab中,可以看到這個(gè)項(xiàng)目今天、昨天以及所有異常日志的分類信息。針對(duì)于每一個(gè)異常,點(diǎn)擊畫(huà)像分析,可以看到這個(gè)異常的具體詳細(xì)信息,標(biāo)識(shí)了一個(gè)異常發(fā)生的位置,出現(xiàn)的頻率,發(fā)生的具體時(shí)間等,如下圖所示。
下面帶著這些目標(biāo)我們來(lái)一步一步分析該如何實(shí)現(xiàn)這些需求。
二、技術(shù)實(shí)現(xiàn)
1、已知信息
假如我們把這個(gè)問(wèn)題看成一個(gè)數(shù)學(xué)題,第一步當(dāng)然是先要搞清楚我們當(dāng)前已知的信息。
通過(guò)我們的集中日志收集系統(tǒng),我們已經(jīng)把所有的異常信息統(tǒng)一收集到了 kafka 的 p_error_log 這個(gè)topic中,其中某一行的日志格式如下:
{ "env": "docker", "hostAddress": "x.x.72.x-192.x.x.x", "hostName": "yxy-ody-bridge-ody-bridge-api-dev-bbbb99cb5-8q825", "level": "ERROR", "log": "[2019-12-17 11:49:18.803 ERROR] [http-nio-8080-exec-75] (WebExceptionHandle:56) 服務(wù)內(nèi)部錯(cuò)誤ncom.yxy.ody.api.exception.ServiceException: 訂單號(hào):1908xxxxxx052沒(méi)有查到交易信息,請(qǐng)檢查參數(shù)!ntat com.yxy.ody.api.handler.CustomsPayHandler.getTradeFlowByOrderCode(CustomsPayHandler.java:146) ~[classes/:?]ntat com.yxy.ody.api.handler.CustomsPayHandler.GetCreateDeliveryOrderArgs(CustomsPayHandler.java:93) ~[classes/:?]ntat sun.reflect.GeneratedMethodAccessor118.invoke(Unknown Source) ~[?:?]ntat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_161]ntat java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_161]ntat org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:215) ~[spring-web-4.1.0.RELEASE.jar:4.1.0.RELEASE]ntat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:132) ~[spring-web-4.1.0.RELEASE.jar:4.1.0.RELEASE]ntat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104) ~[spring-webmvc-4.1.0.RELEASE.jar:4.1.0.RELEASE]ntat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:781) ~[spring-webmvc-4.1.0.RELEASE.jar:4.1.0.RELEASE]ntat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:721) ~[spring-webmvc-4.1.0.RELEASE.jar:4.1.0.RELEASE]ntat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:83) ~[spring-webmvc-4.1.0.RELEASE.jar:4.1.0.RELEASE]ntat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:943) ~[spring-webmvc-4.1.0.RELEASE.jar:4.1.0.RELEASE]ntat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:877) ~[spring-webmvc-4.1.0.RELEASE.jar:4.1.0.RELEASE]ntat org.springframework.web.servlet.frameworkServlet.processRequest(frameworkServlet.java:961) ~[spring-webmvc-4.1.0.RELEASE.jar:4.1.0.RELEASE]ntat org.springframework.web.servlet.frameworkServlet.doPost(frameworkServlet.java:863) ~[spring-webmvc-4.1.0.RELEASE.jar:4.1.0.RELEASE]ntat javax.servlet.http.HttpServlet.service(HttpServlet.java:648) ~[servlet-api.jar:?]ntat org.springframework.web.servlet.frameworkServlet.service(frameworkServlet.java:837) ~[spring-webmvc-4.1.0.RELEASE.jar:4.1.0.RELEASE]ntat javax.servlet.http.HttpServlet.service(HttpServlet.java:729) ~[servlet-api.jar:?]ntat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:292) ~[catalina.jar:8.0.53]ntat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[catalina.jar:8.0.53]ntat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) ~[tomcat-websocket.jar:8.0.53]ntat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[catalina.jar:8.0.53]ntat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[catalina.jar:8.0.53]ntat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:88) ~[spring-web-4.1.0.RELEASE.jar:4.1.0.RELEASE]ntat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.1.0.RELEASE.jar:4.1.0.RELEASE]ntat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[catalina.jar:8.0.53]ntat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[catalina.jar:8.0.53]ntat org.apache.logging.log4j.web.Log4jServletFilter.doFilter(Log4jServletFilter.java:71) ~[log4j-web-2.11.1.jar:2.11.1]ntat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[catalina.jar:8.0.53]ntat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[catalina.jar:8.0.53]ntat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:212) ~[catalina.jar:8.0.53]ntat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:94) ~[catalina.jar:8.0.53]ntat org.apache.catalina.authenticator.Authenticatorbase.invoke(Authenticatorbase.java:492) ~[catalina.jar:8.0.53]ntat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:141) ~[catalina.jar:8.0.53]ntat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:80) ~[catalina.jar:8.0.53]ntat org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:620) ~[catalina.jar:8.0.53]ntat org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:684) ~[catalina.jar:8.0.53]ntat org.apache.catalina.core.StandardEnginevalve.invoke(StandardEnginevalve.java:88) ~[catalina.jar:8.0.53]ntat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:502) ~[catalina.jar:8.0.53]ntat org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1152) ~[tomcat-coyote.jar:8.0.53]ntat org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:684) ~[tomcat-coyote.jar:8.0.53]ntat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1539) ~[tomcat-coyote.jar:8.0.53]ntat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1495) ~[tomcat-coyote.jar:8.0.53]ntat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[?:1.8.0_161]ntat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[?:1.8.0_161]ntat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-util.jar:8.0.53]ntat java.lang.Thread.run(Thread.java:748) [?:1.8.0_161]n", "serviceName": "ody-bridge-api", "threadName": "http-nio-8080-exec-75", "timeMillis": 1576554558803 }
該日志格式描述了以下基本事實(shí):什么時(shí)候哪臺(tái)機(jī)器哪個(gè)應(yīng)用打印了什么日志。其中`log`字段標(biāo)識(shí)了原始打印的日志內(nèi)容,對(duì)應(yīng)的log4j2的PatternLayout定義如下:
[%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p] [%t] (%C{1}:%L) %m%n
一個(gè)比較直觀的異常如下:
所以,其實(shí)我們能拿到所有的異常文本信息,只要我們能夠把異常數(shù)據(jù)結(jié)構(gòu)化,然后抽取出特征信息,將相同的異常歸為同一類,那么我們就可以完成最基本也是最重要的分類任務(wù)。
在這之前我們深入剖析下Java異常日志。
2、java異常日志剖析
如果在代碼中出現(xiàn)異常,我們通常會(huì)使用如下方式打印日志,
logger.error("同步寶寶數(shù)據(jù)異常", e);
打印到控制臺(tái)如下所示:
接下來(lái)我們通過(guò)源碼具體分析這上面這行代碼背后發(fā)生了什么事情:
1、調(diào)用logger.error() 最終會(huì)執(zhí)行如下方法:
@Override public void logMessage(final String fqcn, final Level mgsLevel, final Marker marker, final Message msg, final Throwable throwable) { final StringBuilder sb = new StringBuilder(); // Append date-time if so configured if (showDateTime) { final Date now = new Date(); String dateText; synchronized (dateFormatter) { dateText = dateFormatter.format(now); } sb.append(dateText); sb.append(SPACE); } sb.append(mgsLevel.toString()); sb.append(SPACE); if (Strings.isNotEmpty(logName)) { sb.append(logName); sb.append(SPACE); } sb.append(msg.getFormattedMessage()); if (showContextMap) { final Map mdc = ThreadContext.getImmutableContext(); if (mdc.size() > 0) { sb.append(SPACE); sb.append(mdc.toString()); sb.append(SPACE); } } final Object[] params = msg.getParameters(); Throwable t; if (throwable == null && params != null && params.length > 0 && params[params.length - 1] instanceof Throwable) { t = (Throwable) params[params.length - 1]; } else { t = throwable; } stream.println(sb.toString()); if (t != null) { stream.print(SPACE); t.printStackTrace(stream); } }
可以看到logMessage方法最終執(zhí)行了 t.printStackTrace 來(lái)打印異常信息,如下是printStackTrace方法體:
private void printStackTrace(PrintStreamOrWriter s) { // Guard against malicious overrides of Throwable.equals by // using a Set with identity equality semantics. Set dejaVu = Collections.newSetFromMap(new IdentityHashMap()); dejaVu.add(this); synchronized (s.lock()) { // Print our stack trace s.println(this); StackTraceElement[] trace = getOurStackTrace(); for (StackTraceElement traceElement : trace) s.println("tat " + traceElement); // Print suppressed exceptions, if any for (Throwable se : getSuppressed()) se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "t", dejaVu); // Print cause, if any Throwable ourCause = getCause(); if (ourCause != null) ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu); } }
在synchronized同步代碼塊中,我們看如下一行是將 Throwable 對(duì)象本身做了輸出,所以我們繼續(xù)看toString方法的實(shí)現(xiàn)。
s.println(this); public String toString() { String s = getClass().getName(); String message = getLocalizedMessage(); return (message != null) ? (s + ": " + message) : s; }
toString方法依次輸出了className 和 message 信息。對(duì)應(yīng)于異常日志的如下部分:
然后開(kāi)始輸出堆棧信息
StackTraceElement[] trace = getOurStackTrace(); for (StackTraceElement traceElement : trace) s.println("tat " + traceElement); // Print suppressed exceptions, if any for (Throwable se : getSuppressed()) se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "t", dejaVu); // Print cause, if any Throwable ourCause = getCause(); if (ourCause != null) ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu);
我們知道棧是先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu),所以我們?cè)谌罩局锌吹降漠惓P畔ⅲ娇壳暗恼f(shuō)明調(diào)用得越深,也最接近案發(fā)現(xiàn)場(chǎng),所以看異常信息的時(shí)候尤其注意。
3、分析當(dāng)前項(xiàng)目的異常日志
在實(shí)際的開(kāi)發(fā)過(guò)程中,大家打印日志的方式是有一些差異的,所以會(huì)導(dǎo)致我們收集的異常日志也會(huì)存在多種格式,經(jīng)過(guò)對(duì)當(dāng)前項(xiàng)目異常日志的分析我們可以分為如下幾類:
1)異常帶堆棧
如果代碼中使用如下形式打印error日志:
logger.error("[TestCaseController] error={}",e);
異常日志輸出類似于如下形式:
2)異常不帶堆棧信息
如果代碼中使用如下形式打印error日志:
logger.error("[viewErrorLog] request:{}", JSON.toJSonString(request));
異常日志輸出類似于如下形式:
3)api-gateway 和 Rpcbase 統(tǒng)一打印的日志
還有一類異常,日志是在框架層統(tǒng)一打印的
public class RpcbaseService { Logger logger = LoggerFactory.getLogger(RpcbaseService.class); protected RpcbaseResponse handleException(Throwable e, RpcbaseResponse response) { if(e instanceof ServiceException) { ServiceException se = (ServiceException)e; response.setCode(se.getCode()); response.setMessage(se.getMessage()); } else if(e instanceof IllegalArgumentException) { response.setCode(ResponseStatus.FAILD.getCode()); response.setMessage(e.getMessage()); }else { logger.error("rpc handleException :",e); response.setCode(ResponseStatus.FAILD.getCode()); response.setMessage(ResponseStatus.FAILD.getMessage()); } return response; } }
@Controller @RequestMapping("/api") public class APIController extends MultiActionController { private String handleException(Throwable e, AbstractProtocolPrint print){ if (e instanceof ServiceException) { ServiceException se = (ServiceException) e; String returnValue = null; if(se.getCode() == AuthTokenConstants.TOKEN_CODE_EMPTY || se.getCode() == AuthTokenConstants.TOKEN_CODE_BREAK || se.getCode() == AuthTokenConstants.TOKEN_CODE_RELOGIN) { // Token 異常 TokenRouters router = TokenSkipModel.loginTokenSkipModel(e.getMessage()); returnValue =print.error(ResponseStatus.OK.getCode(), se.getMsg(),router.getSkipModel()); }else { returnValue = print.error(se.getCode(), se.getMsg()); } return returnValue; } else if (e instanceof CallableException) { log.error("[api-callable CallableException] exception={}",e); CallableException ce = (CallableException) e; return print.error(ResponseStatus.FAILD.getCode(), ce.getMessage()); } else if (e instanceof SmartValidateException) { log.error("[api-callable SmartValidateException] exception={}",e); SmartValidateException sve = (SmartValidateException) e; return print.error(ResponseStatus.BIZ_FAILD.getCode(), sve.getMessage()); } else if (e instanceof InvocationTargetException) { InvocationTargetException ite = (InvocationTargetException) e; return handleException(ite.getTargetException(), print); } else { log.error("[api-callable] exception={}",e); e.printStackTrace(); return print.error(ResponseStatus.FAILD.getCode(), "服務(wù)器開(kāi)小差兒了,請(qǐng)稍候重試"); } } }
此種情況打印的異常,位置信息都會(huì)相同,所以可能會(huì)產(chǎn)生大量的相似的日志。
4、定義異常分析算法
知道了異常日志的基本格式,接下來(lái)我們就需要找到一個(gè)方法對(duì)異常日志進(jìn)行分析了。
日志分析最重要的一個(gè)問(wèn)題就是如何對(duì)海量的異常日志做分類,我們知道如果僅僅是把異常上報(bào)上來(lái),可能每天會(huì)出現(xiàn)幾千幾萬(wàn)條異常我們是沒(méi)辦法逐條進(jìn)行處理的,所以我們需要定義一個(gè)算法對(duì)異常進(jìn)行分類。
算法定義如下:
所以現(xiàn)在的問(wèn)題是如何找到異常的簽名,我們繼續(xù)分析。
一個(gè)異常出現(xiàn)一次,那么就有可能出現(xiàn)多次,這些異常應(yīng)該歸屬為同一類,我們可以簡(jiǎn)單地認(rèn)為在同一個(gè)位置出現(xiàn)的異常是同一類。對(duì)其中某個(gè)異常結(jié)構(gòu)分析如下所示:
同一個(gè)位置這里包含兩個(gè)部分:
所以只要我們成功提取出這兩個(gè)信息,使用他們就可以計(jì)算簽名了。
4.1 提取異常位置信息
提取位置信息可以使用正則提取,如下所示:
Pattern logPattern = Pattern.compile("^[(d{4}-d{2}-d{2} d{2}:d{2}:d{2}.d{3})sERROR].*?((S+))s+(.*)"); String[] arr = logMessageVO.getString("log").split("n"); if(arr.length <=0) { return; } Matcher matcher = logPattern.matcher(arr[0]); matcher.find(); String logTime = matcher.group(1); String logLocation = matcher.group(2);
4.2 提取執(zhí)行方法信息
提取執(zhí)行方法則需要一些技巧。從上面的分析我們可以知道我們系統(tǒng)中的異常大體分為了三類:
前兩種其實(shí)只要提取出異常打印的具體類就可以定位位置了,但是第三種也只使用位置信息的話則無(wú)法區(qū)分具體的異常,如下所示:
所以針對(duì)于這種框架打印的異常,我們最好要分析出異常堆棧中最接近案發(fā)現(xiàn)場(chǎng)的信息作為簽名,我們實(shí)現(xiàn)了如下算法:
private Tuple2 getSignature(String logLocation, String[] arr) { //簽名等于異常的位置 String signature = logLocation; //如果是公共類打印的異常,則繼續(xù)尋找具體打印異常信息的方法位置 if(logLocation.startsWith("RpcbaseService") || logLocation.startsWith("APIController") || logLocation.startsWith("LogAspect") || logLocation.startsWith("WebExceptionHandle") || logLocation.startsWith("baseController") || logLocation.startsWith("ExceptionUtil")){ if(arr.length>1){ Tuple2 temp = null; for(int i=1;im()); temp = processChange(a,temp); if(temp.getSecond()==3){ break; } } if(temp!=null){ signature = signature + "-" + temp.getFirst(); } } } String alias = signature.length()>=256? signature.substring(0,255) : signature; return new Tuple2(alias,MD5Util.md5(signature)); }
計(jì)算分?jǐn)?shù)的代碼如下:
private Integer getScore(String newline) { if(newline.startsWith("at com.yxy") && !newline.startsWith("at com.yxy.framework")){ return 3; } if(newline.startsWith("at com.yxy") || newline.startsWith("at com.callable") ||newline.startsWith("at com.smart.validate") ){ return 2; } if(newline.startsWith("at ") ){ return 1; } return 0; }
原則是,如果是堆棧是自己框架層的代碼則分?jǐn)?shù)為2,如果是業(yè)務(wù)代碼則為3,如果是其它類庫(kù)或jdk代碼則為1,默認(rèn)為0。這樣通過(guò)計(jì)算之后我們能拿到最接近業(yè)務(wù)代碼的堆棧信息作為簽名的一部分。
5、異常分析
一個(gè)異常經(jīng)過(guò)分析之后,我們獲得了異常的基本信息:
然后我們將這些數(shù)據(jù)存儲(chǔ)到mysql或clickhouse類似的數(shù)據(jù)庫(kù),執(zhí)行具體的分析即可得到異常的統(tǒng)計(jì)數(shù)據(jù)和畫(huà)像信息。
三、結(jié)束語(yǔ)
本文只是簡(jiǎn)述了Java異常的處理流程,其實(shí)將這種分類算法擴(kuò)展,我們可以分析其它類型的異常日志,比如我們后續(xù)做了客戶端崩潰日志分析,針對(duì)于Android上報(bào)的ANR、Native和Java崩潰日志,使用類似分類方式,將異常信息分類統(tǒng)計(jì)展示,供客戶端同學(xué)排查問(wèn)題。
本文沒(méi)有介紹異常日志收集的部分,這個(gè)地方可以有多種實(shí)現(xiàn)方式,我們是內(nèi)部實(shí)現(xiàn)了一套集中式日志管理系統(tǒng),所有的日志是通過(guò)KafkaAppender實(shí)時(shí)異步輸出到kafka的,大家也可以使用其他日志收集方式,比如filebeat、logstash等。
王同明
版權(quán)所有 未經(jīng)許可不得轉(zhuǎn)載
增值電信業(yè)務(wù)經(jīng)營(yíng)許可證備案號(hào):遼ICP備14006349號(hào)
網(wǎng)站介紹 商務(wù)合作 免責(zé)聲明 - html - txt - xml