谈谈对系统交互中日志重要性的理解

点融黑帮 / 2017年10月30日 05:52

互联网+

如果我问你,当你的系统在生产环境出现问题时,你需要花费多长时间来定位问题,找到问题的根本原因?

假如你的系统与其他系统有着强依赖,你需要花费多长时间来确定是你的系统问题,还是第三方系统的问题?

几分钟?几个小时?甚至几天?我相信,这个时间很大一部分会与你在生产环境下的日志记录习惯有关系。 接下来我就谈谈对于多个系统交互时日志记录的一些理解和体会。

为什么需要记录日志

此处就不拷贝各种对于日志记录重要性的鸡汤,仅谈谈从实践中得来的一点感悟。作者所在的系统对多个第三方系统有着强依赖,粗略梳理与本系统核心依赖的系统就有五个以上,涉及到业务的核心流程以及报表、销售管理、用户管理等模块。

当在生产环境的某个功能出现异常时,你无法像开发环境一样进行调试找出问题,能留给你的就只有日志系统。及时通过日志定位出错的代码,确认是己方问题还是对方问题;若为对方系统出错,向对方系统提出请求的时间、接口、参数、返回帮助对方系统及时定位就显得尤为重要

如何去记录交互日志

☞记录你的接口调

查看BCRM老的代码,会发现存在如下的代码块

String finalUrl = String.format(url,productCode);Get get = new Get(finalUrl);get.setAccept("application/json");get.addHeader("Accept-Language",Locale.CHINA.getLanguage()+"-"+ Locale.CHINA.getCountry());HTTPResponse httpResponse = httpClient.execute(get);return httpResponse.getResponseText();

在该代码中,通过HTTPClient调用接口后,直接将接口的返回值向上返回,可是假如出现http请求错误,或者是对方系统逻辑错误的返回,导致本系统出现错误,没有日志你该如何排查?即使定位到是对方系统问题,如何给人家提供你的请求的参数,时间等信息呢?因此,首先你应该想到要去解析接口的返回并作以合理的日志记录。

☞日志应该包含上下文

虽然开始记录日志了,但在kibana中查看错误日志,你会发现有如下日志信息:

ERROR BUSINESSLOGGER - 远程调用报错了result==null

或者error:{"code":50000,"message":"Internal Error"}

或者ERROR BUSINESSLOGGER - 下载文件失败

当你看到这条日志的时候你肯定会很郁闷:“远程调用报错”,光本系统调用的外部接口就有几十个,到底是哪个接口?即使通过上报功能错误定位到使用的接口,可是请求的参数呢?“下载文件失败”,下载什么文件,报什么错?这种没有上下文的日志是没有任何意义的。

☞日志应该记录请求相关的一切信息

对于接口调用,需要在对于http请求完成后,通过对于响应头,响应状态码进行解析判断http请求的状态,以及在请求成功后对于接口返回的格式,按照双方对接时的约定进行解析,在约定规范下调用的逻辑错误情况下,通过日志记录此次请求的接口路径、请求方法、请求参数、接口返回内容、调用时间。如此以来,若在发生调用失败的情况,我们就能够通过日志快速定位到对应的接口调用的返回情况,判断是对方系统问题还是己方问题。

☞日志记录应该提供统一入口与出口,不要copy代码

在翻阅BCRM老的代码时会发现,对于大多数接口调用的方法下面,均有一个validateResponse的方法:

public LoanUpdateResult validateSubmitResponse(HTTPResponse httpResponse) { LoanUpdateResult responseResult = new LoanUpdateResult(); if (httpResponse == null) { responseResult.setStatus(false); responseResult.setMessage("远程调用出错了"); return responseResult; } JsonNode jsonNode; try { jsonNode = JSONBinder.binder(JsonNode.class).fromJSON(httpResponse.getResponseText()); if (jsonNode.get("code").toString().contains("20000")) { responseResult.setStatus(true); responseResult.setMessage("操作成功"); } else { responseResult.setStatus(false); responseResult.setMessage(jsonNode.get("message").asText()); } } catch (Exception e) { LOG.error("validateSubmitResponse: ", e); responseResult.setMessage("返回信息出错"); responseResult.setStatus(false); } return responseResult;}

虽然相比于不解析响应并记录日志的情况已经算一个很大的改善了;但是逐渐你会发现,每次与三方系统对接一个接口,相应的调用方法下面就会跟随这样的一个validate方法;一个方法为了不同的接口去拷贝多份,根据不同接口名称和返回做一些定制修改,各种validateSubmitResponse, validateReviewResponse等名称不同却逻辑相同的方法充斥于代码块中,代码冗余;一旦对方系统修改接口返回格式,你就需要将每一个validate方法的解析逻辑进行修改,人为增加工作量;日志按照自己想要的格式输出,没有上下文,让一堆无任何价值的日志占用着磁盘空间,实际上是没有意义的。

一个规范的系统向外部提供的接口的返回格式都是统一的,因此我们可以为该系统封装一个HTTPClient的调用入口,在该入口内部完成HTTP的调用、解析、日志记录、结果封装、返回等操作,这样避免了上面提到的接口返回解析的冗余代码,若对接系统接口返回格式发生改变,我们只需要修改一处即可。

我是怎么做的

以对系统老代码中使用HTTPClient调用的重构为例,首先对框架内接口调用类HTTPClient进行了封装,保持HTTPclient中方法不变向外提供了execute和download两个方法;在方法内部完成HTTP接口调用获取接口返回,并且使用统一处理方法对接口返回进行解析

public class ThirdPartyApiClient { public <T> ThirdPartyApiResponseDto<T> execute(HTTPRequest httpRequest, Class<?>...

elementClass) { HTTPResponse httpResponse = httpClient.execute(httpRequest); return this.validateResponse(httpRequest, httpResponse, elementClass); }}

在解析方法里分别对接口无返回、调用失败、调用成功情况进行了判断,将其封装到一个统一格式的DTO中,并且记录日志

private void logResponse(HTTPRequest request, ThirdPartyApiResponseDtoresponse) { // 只有在接口无返回、接口返回格式错误、接口返回code不为成功的情况下才会记录错误日志 if (response == null || response.getCode() == null || !ThirdPartyApiConstant.RESPONSE_SUCCESS_CODE.equals(response.getCode())) { try { String requestLine = buildRequest(request); String responseLine = buildResponse(response); logger.error("ThirdPartyApiLog | [execute failed] | request : [{}] | response : [{}]", requestLine, responseLine); } catch (Exception e) { logger.error("ThirdPartyApiLog | [execute exception] | exception : {}", e); } }}

在日志记录中,分别去获取HTTP请求的方法、接口地址、请求参数、接口响应内容,将其作为一条日志记录,保证该条日志的上下文信息完整。

最后在调用接口时,只需要将接口构建成对应的请求通过封装好的client调用即可,无需再关心接口的解析和日志的记录的细节啦。

public List<TestConditionsResponse> getSomeConditions(Integer param1, String param2) { String url = getMaps(ApiConstants.BORROWER_TEST_GET_CONDITIONS.toString()).get("url"); TextPost post = new TextPost(url); post.setBody(buildPostBody(param1, param2)); ThirdPartyApiResponseDto<List<TestConditionsResponse>> responseDto = thirdPartyApiClient .execute(post, List.class, TestConditionsResponse.class); return responseDto.getData();}

调用时当接口发生任何错误,你都会得到如下一条日志,可以很好的帮助你定位到问题

ERROR c.d.c.l.client.ThirdPartyApiClient - ThirdPartyApiLog | [execute failed] | request : [{"method":"POST","body":"{"param1":"1024","param2":"HelloWorld"}","url":"http://127.0.0.1:8080/v1/conditions/test"}] | response : [{"code":50000,"message":"Internal Error"}]

即使对应系统修改了接口返回的格式或逻辑,我只需要在我唯一的validateResponse方法内修改相应的解析逻辑即可,对于方法调用方来说是完全透明,大大减少了工作量。

写在最后

开发时我们习惯使用调试模式去逐行运行代码发现问题,这确实很快,不过,请尝试尽可能少的使用调试模式;当出现错误时,能否根据现有的日志帮你定位到问题所在,如果不行,那么它将在生产环境带来灾难性的影响,你可能就因为一次懒惰而浪费几个小时宝贵的时间去排查与猜测。

系统之间交互难免会出现各种各样的问题,很多问题在dev与demo环境也很难暴露出来,记录系统交互之间的日志详情,不是为了在出了问题之后快速甩锅给对方,而是定位问题所在来帮助自己或对方及时做出改进,为了以后更好的合作。

以上是我在平时对于系统交互中日志重要性的一些心得体会,代码能力有限可能一些设计有违开发规范,虚心接受各位赐教,那将是对我最大的帮助,我将不胜感激!也希望各位能够分享一下对于这个主题的理解和做法

so,来回答我刚开始提出的问题吧,你需要多长时间来排查问题呢?

返回搜狐,查看更多

责任编辑:

1.环球科技网遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.环球科技网的原创文章,请转载时务必注明文章作者和"来源:环球科技网",不尊重原创的行为环球科技网或将追究责任;3.作者投稿可能会经环球科技网编辑修改或补充。