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

Java项目中打印错误日志的正确姿势

时间:2023-03-13 05:33:43 科技观察

在程序中打印错误日志的主要目的是为更好的排查问题和解决问题提供重要的线索和指导。但在实际应用中,错误日志的内容和格式千差万别,错误提示可能不完整、没有相关背景、不清楚,使得排查和解决问题成为一项非常不便或耗时的操作。事实上,如果你在编程时稍加注意,就会减少很多排错时的无用功。在解释如何编写有效的错误日志之前,了解错误是如何发生的很重要。错误是如何产生的对于目前的系统,错误主要来自三个地方:1.上层系统引入的非法参数。对于非法参数引入的错误,可以通过参数校验和前置条件校验来拦截错误;2.与底层系统交互产生的错误。与下层交互导致的错误有两种:a.下层系统处理成功,但通讯错误,会导致子系统之间数据不一致;在这种情况下,可以使用超时补偿机制将任务提前记录下来,稍后通过定时任务来修正数据。如果大家有更好的设计方案,也可以留言。b.通讯成功,但下层处理错误。在这种情况下,需要与下层开发人员进行沟通,协调子系统之间的交互;需要根据下层返回的错误码和错误描述进行适当的处??理或给出合理的提示信息。在任何一种情况下,都需要假设底层系统的可靠性是平均水平,并针对错误进行设计考虑。3.本层系统处理错误。本层系统出错的原因:原因一:疏忽。疏忽是指程序员有能力避免此类错误但实际上没有做到。例如,将&&键入&,将==键入=;边界错误、复合逻辑判断错误等。疏忽要么是程序员不够专注,比如累了,通宵加班,边开会边写程序;或者急于实现功能而没有考虑程序的健壮性。改善措施:使用代码静态分析工具,通过单元测试行覆盖,有效避免此类问题。原因二:错误和异常处理不充分造成的。例如,输入问题。计算两个数的加法,不仅要考虑计算溢出的问题,还要考虑非法输入的情况。对于前者,可以通过理解、犯错或经验来避免,而对于后者,则必须加以限制,使其在我们智商的控制范围内,例如使用正则表达式来过滤非法输入。对于正则表达式必须进行测试。对于非法输入,尽可能给出详细、易懂、友好的提示信息、原因和建议。改进:尽可能全面地考虑错误条件和异常处理。实现主流程后,增加一个步骤:仔细考虑各种可能的错误和异常,并返回合理的错误码和错误描述。每个接口或模块有效处理自己的错误和异常,可以有效避免复杂场景交互带来的bug。例如,一个业务用例是由场景A.B.C交互完成的。A.B的实际执行成功,但C失败。此时B需要根据C返回的合理代码和消息进行回滚,并将合理的代码和消息返回给A。A根据B的返回进行回滚,并将合理的代码和消息返回给客户端。代码和消息。这是一种分段回滚机制,要求每个场景都考虑异常情况下的回滚。原因三:逻辑紧耦合导致。由于业务逻辑的紧耦合,随着软件产品的逐步开发,各种逻辑关系错综复杂,难以看到全局,导致局部修改影响全局,导致在不可预知的问题中。改进措施:编写短函数和短方法,每个函数或方法最好不要超过50行。编写无状态的函数和方法,只读的全局状态,相同的前提条件总是输出相同的结果,不会根据外部状态改变其行为;定义合理的结构、接口和逻辑段,使接口之间的交互尽可能正交和低耦合;对于服务层,尽可能提供简单正交的接口;继续重构,保持应用模块化和松耦合,理清逻辑依赖。对于大量业务接口相互影响的情况,需要梳理各个业务接口的逻辑流程和相互依赖关系,并进行整体优化;对于状态数量较多的实体,还需要梳理相关的业务接口,梳理状态间的转换关系。原因四:算法不正确。改进措施:首先将算法与应用分离。如果算法有多种实现,可以通过交叉验证单元测试找出,比如排序操作;如果算法是可逆的,可以通过加解密操作等可逆验证单元测试发现。原因五:相同类型的参数传入顺序错误。比如modifyFlow(intrx,inttx),实际调用的是modifyFlow(tx,rx)改进措施:类型越具体越好。该使用浮点数时使用浮点数,该使用字符串时使用字符串,该使用特定对象类型时使用特定对象类型;同一类型的参数尽量错开;如果不能满足以上,则必须通过接口测试验证,接口参数值必须不同。原因六:空指针异常。空指针异常通常是由于对象没有正确初始化,或者在使用对象之前没有检查对象是否为非空引起的。改进措施:对于配置对象,检查是否初始化成功;对于普通对象,在获取使用的实体对象前先检查是否非空。原因七:网络通讯错误。网络通信错误通常是由网络延迟、拥塞或故障引起的。网络通信错误通常是小概率事件,但小概率事件很可能导致大规模的故障和难以重现的BUG。改进措施:分别在前一个子系统的终点和后一个子系统的入口点打开INFO日志。通过两者之间的时差提供一点线索。原因八:事务和并发错误。事务和并发的结合很容易产生错误,而且很难定位。改进措施:对于程序中涉及共享变量和重要状态修改的并发操作,增加INFO日志。如果有更有效的方法,欢迎留言指出。原因九:配置错误。改善措施:启动应用或启动相应配置时,检测所有配置项,打印相应INFO日志,确保所有配置加载成功。原因十:对业务不熟悉导致的失误。在中大型系统中,部分业务逻辑和业务交互比较复杂,整个业务逻辑可能存在于多个开发人员的大脑中,每个人的理解都不完整。这很容易导致业务编码错误。改进措施:通过多人讨论交流,设计正确的业务用例,根据业务用例编写和实现业务逻辑;最终的业务逻辑和业务用例必须完整归档;在业务接口中注明业务的前置条件、处理逻辑、事后检查和注意事项;当业务发生变化时,需要同步更新业务备注;代码审查。业务注解是业务接口的重要文档,对业务理解起着重要的缓存作用。原因十一:设计问题导致的错误。比如同步串行方式存在性能和响应慢的问题,而并发异步方式可以解决性能和响应慢的问题,但是会带来安全性和正确性的隐患。异步方式会导致编程模型的改变,增加异步消息推送和接收等新问题。使用缓存可以提高性能,但是会存在缓存更新的问题。改进:编写并仔细审查设计文档。设计文档必须描述背景、需求、要实现的业务目标、要实现的业务绩效指标、可能产生的影响、总体设计思路、详细方案、方案可预见的优缺点以及可能产生的影响;通过测试和验收,确保改进设计的解决方案确实满足业务目标和业务绩效指标。原因十二:细节不明造成的错误。比如缓冲区溢出,SQL注入攻击。从功能上看,没有问题,但从恶意利用的角度上看,就存在漏洞。再举个例子,JSON字符串解析选择jackson库。默认情况下,当对象添加字段时,会导致解析错误。必须使用@JsonIgnoreProperties(ignoreUnknown=true)注释对象才能正确处理更改。如果你选择其他的JSON库,就不一定会出现这个问题。改进措施:一方面要积累经验;另一方面考虑安全问题和异常情况,选择成熟且经过严格测试的库。原因十三:随时间变化的错误。过去看起来不错的解决方案在当前或未来的情况下变得笨拙甚至无用的情况并不少见。比如加解密算法,在过去可能被认为是完美的,但破解后还是要慎用。改进措施:关注变化和bug修复,及时纠正过时的代码、库和行为。原因十四:硬件相关的错误。如内存泄漏、存储空间不足、OutOfMemoryError等。改善措施:增加对应用系统的CPU/内存/网络等重要指标的性能监控。系统常见错误:该实体在数据库中的记录不存在,必须指定是哪个实体或实体ID;实体配置不正确,哪个配置有问题,正确的配置应该是什么;实体资源不满足条件,必须说明当前资源是什么,资源需求是什么;如果不满足实体操作的前提条件,则必须指定需要满足的前提条件以及当前状态;如果实体操作的事后检查不满足,则必须指定需要满足的当前状态是什么?性能问题导致超时。有必要具体说明是什么导致了性能问题,以及以后如何优化。多个子系统交互通信出错导致状态或数据不一致?一般难以定位的错误都会出现在比较低级的地方。因为底层无法预测具体的业务场景,所以给出的错误信息比较笼统。这就需要在业务的上层提供尽可能多的线索。错误一定是多系统或多层交互时,某层栈上的前置条件不满足导致的。编程时尽量保证栈的每一层都满足所有必要的前置条件,尽量避免向底层传递错误的参数,尽可能拦截业务层的错误。大多数错误是由多种原因共同引起的。但每个错误都必须有其原因。解决错误后,需要深入分析错误是如何发生的,如何防止错误再次发生。努力导致成功,但是:反思导致进步!推荐:JavaElegantLogging:Log4jPracticalHowtoWriteErrorLogsThatEasiertoTroubleshootProblems错误日志记录的基本原则:尽可能完整。每个错误日志都有完整的描述:什么场景下发生什么错误,什么原因(或可能原因),如何解决(或解决提示);尽可能具体。比如NC资源不足,具体指什么资源不足可以直接通过程序指定;对于一般的错误,比如VMNOTEXIST,需要具体说明发生的场景,这样可以方便后续的统计工作。尽可能直接。理想的错误日志应该让人第一眼就知道是什么原因以及如何解决,而不是经过几个步骤才能找到真正的原因。将现有经验直接集成到系统中。所有已经解决的问题和经验,都尽可能以友好的方式融入到系统中,给新人更好的提示,而不是埋没在其他地方。排版要工整有序,版式要统一规范。密密麻麻的散文式日志,看着让人揪心,相当不友好,也不容易排查问题。使用多个关键字来唯一标识请求,关键字高亮显示:时间、实体ID(如vmname)、操作名称。排查问题的基本步骤:登录应用服务器->打开日志文件->定位到错误日志位置->按照错误日志的引导排查、确认并解决问题。其中:从登录到打开日志文件。由于有多台应用服务器,不方便登录一一查看。需要写一个工具直接查看AG上的所有服务器日志,甚至可以直接过滤掉需要的错误日志。找到错误日志位置。目前日志排版比较密集,不容易定位错误日志。一般可以使用“时间”定位到错误日志靠前的地方,然后使用实体关键字/操作名的组合来锁定错误日志位置。根据requestId定位错误日志比较传统,但是必须先找到requestId,不具描述性。最好直接根据时间/内容关键字定位错误日志位置。3.分析错误日志。错误日志的内容应该更直接、更清晰,能够清楚地表明它符合当前要检查的问题的特点,能够给出重要的线索。通常,程序错误日志的问题在于日志的内容只能根据当前的代码情况来理解。看起来很简洁,但总是用半英文格式写得不完整;让人想一想或者看代码就能明白日志说的是什么。这不是对自己的犯罪吗?拓展:详解Java主流日志工具库如:if((storageType==StorageType.dfs1||storageType==StorageType.dfs2)&&(zone.hasStorageType(StorageType.io3)||zone.hasStorageType(StorageType.io4))){//输入io3io4中存储的dfs1和dfs2。}else{log.info("zonestoragetypenotsupport,zone:"+zone.getZoneId()+",storageType:"+storageType.name());thrownewBizException(DeviceErrorCode.ZONE_STORAGE_TYPE_NOT_SUPPORT);}支持?不要让我想!错误日志应该这样做:即使在代码上下文之外也能清楚地描述发生了什么。另外,如果能直接在errorlog中说明原因,在做inspectionlog的时候也能省点力气。从某种意义上说,错误日志也可以是一个非常有用的文件,记录了各种非法操作的案例。当前程序错误日志的内容可能存在以下问题:1.错误日志没有指定错误参数和内容:catch(Exceptionex){log.error("controlipinsertfailed",ex);returnnewResultSet(ControlIpErrorCode.ERROR_CONTROL_IP_INSERT_FAILURE);}None表示插入失败的控制ip。如果加上controlip关键字,更容易查找和锁定错误。类似地:log.error("GetsomeerrorswheninsertsubnetanditsIPsintodatabase.AddsubnetorIPfailure.",e);没有指定哪个子网的IP属于它。值得注意的是,有必要指定这些额外的东西,这可能会稍微影响性能。这时候就需要在性能和可调试性之间进行权衡。解决方法:使用String.format("SomemsgtoErrorObj:%s",errobj)方法指明错误参数和内容。这通常需要在DO对象上编写一个可读的toString方法。2.错误场景不明确:log.error("nchasexist,ncip"+request.getIp());createNc中检测到NC已经存在,报错。但是log中并没有具体说明错误场景,让人猜测为什么会报NC错误。可以改为log.error("nchasexistwhenwanttocreatenc,pleasecheckncparameters.Givencip:"+request.getIp());log.error("[createnc]nchaseexist,pleasecheckncparameters.Givencip:"+request.getIp());类似:log.error("notallvmdestroyed,ncid"+request.getNcId());更改为log.error("[deletenc]somevms[%s]inthencarenotdestroyed.ncid:%s",vmNames,request.getNcId());解决方法:在报错信息中添加when,或者在报错信息前添加[接口名称],表示报错场景,直接从报错日志中就可以了解。一般如果能知道executor的话,可以在service中加上[interfacename],加上when。3、内容不清楚,或者含义不明确:if(aliMonitorReporter==null){log.error("aliMonitorReporterisnull!");}else{aliMonitorReporter.attach(newThreadPoolMonitor(namePrefix,asynTaskThreadPool.getThreadPoolExecutor()));}更改为:log.error("aliMonitorReporterisnull,probablynotinitializedproperly,pleasecheckconfigurationinfilexxx.");类似:if(diskWbps==null&&diskRbps==null&&diskWiops==null&&diskRiops==null){log.error("noneofattributeisspecifiedExformodrrifying");EprownewBiztion(.NO_ATTRIBUTE_FOR_MODIFY);}更改为log.error("[modifydiskattribute]Noneof[diskWbps,diskRbps,diskWiops,diskRiops]isspecifiedfordiskid:"+diskId);解决方法:将错误内容描述得更清楚、更恰当。4.排错指南内容不清楚:log.error("getgwgroupipsegmentfailed.zkPath:"+LockResource.getGwGroupIpSegmnetLockPath(request.getGwGroupId()));zkPath?如何解决这个问题?我应该去找谁?我在哪里可以找到更具体的线索?解决办法:增加相应的背景知识和引导调查措施。5.错误内容不够具体:if(!ncResourceService.isNcResourceEnough(ncResourceDO,vmResourceCondition)){log.error("diskspaceisnotenoughatvm'snc,ncid:"+vmDO.getNcId());thrownewBizException(ResourceErrorCode.ERROR_RESOURCE_NOT_ENOUGH);}什么资源不够?还剩多少?你现在需要多少钱?值得注意的是,指定这些需要一些额外的工作,这可能会稍微影响性能。这时候就需要在性能和可调试性之间进行权衡。解决方案:通过提高程序或程序技巧,尽可能揭示具体差异,减少人工比对操作。6.半英文句型看不懂,需要自己想办法拼出一个完整的意思:log.warn("cachestatusconflict,deviceid"+deviceDO.getId()+"dbstatus"+deviceDO.getStatus()+",ncstatus"+状态);更改为:log.warn(String.format([querycachestatus]devicecachestatusconflictsbetweenregiondbandnc,statusofdevice'%s'inregiondbis%s,butis%sinnc.",deviceDO.getId(),deviceDO.getStatus(),status));解决办法:换成自然易读的英文句型。总结一下,错误日志格式可以是:log.error("[接口名称或操作名称][SomeErrorMsg]happens.[params][ProbablyBecause].[Probablyneedtodo].");log.error(String.format(“[接口名称或操作名称][SomeErrorMsg]happens.[%s].[ProbablyBecause].[Probablyneedtodo].”,params));orlog.error([SomeErrorMsg]在[insomecondition]时发生参数或内容错误。[ProbablyBecause].[Probablyneedtodo]。”);log.error(String.format([SomeErrorMsg]在[insomecondition]时发生%s.[ProbablyBecause].[Probablyneedtodo].",parameters));[ProbablyReason].[Probablyneededtodo].在某些情况下可以省略;最好对一些重要的接口和场景进行说明。每条错误日志都是独立的,尽可能完整,具体,直接说明在什么场景下发生了什么错误,是什么原因导致的,采取什么措施或步骤。问题:1.String.format的性能会影响日志记录吗?一般来说错误日志应该比较少,使用String.format的频率也不会太高,不会对应用和日志造成影响。2.开发时间很紧的时候,有没有时间考虑的话?建立标准化的内容格式并将内容与格式对齐可以节省审议单词和句子的时间。3.什么时候用info,warn,error?info用于打印程序中应该出现的正常状态信息,方便跟踪定位;warn表示系统略有不合理,但不影响操作使用;error表示系统出现错误和异常,目标操作无法正常完成。错误日志是排查问题的重要手段之一。我们在编程实现一个功能的时候,通常会考虑可能出现的各种错误以及相应的原因:要找出相应的原因,我们需要一些关键的描述来定位原因。这样就会形成一个三元组:错误现象->错误关键描述->最终错误原因。对于每一个错误,都需要尽可能提供相应的错误键描述,以便定位相应的错误原因。也就是说,在编程的时候,仔细想想哪些描述对定位错误原因很有帮助,尽可能把这些描述添加到错误日志中。