你真的会打印日志?

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一看这个标题我想大家一定进来想“怼”我,你这不是小题大作吗?在java中打印日志不是一件非常简单的事情吗?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在java中常用的日志级别为DEBUG、INFO、WARN、ERROR四个日志级别。通常开发环境开启DEBUG,生产环境开启INFO级别,采用主流的日志采集工具包诸如log4j、logback。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但日志输出真的有这么简单吗?其实里面蕴含着很多的规范,或者是最佳实践,并且还有一些非常有用设计技巧方便查询关联日志的技巧,容我慢慢道来。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"输出日志的终极目标","attrs":{}},{"type":"text","text":":助力于快速定位问题、解决问题。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下来将围绕该目标,阐述一下日志相关的一些最佳实践。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#48b378","name":"user"}}],"text":"1、日志的基本规范","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,我们还是简单介绍一下常用的4个日志级别,并说明各个级别在使用时应该注意的问题。","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"DEBUG输出程序的调试信息,优雅的DEBUG日志可以让我们在排查问题的时候,压根就不需要使用开发工具的DEBUG断点调试功能,而是直接看Debug的输出日志即可定位问题。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"打印请求、响应数据包,特别是入口处将所有请求参数打印","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"对核心方法,特别核心计算逻辑前后打印当时的输入与输出,并日志中显示包含方法名称。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"对核心流程(循环、分支)等条件判断时输出必要的入参于与返回结果,清晰的展示程序的运行轨迹。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"INFOINFO日志我们通常用于记录系统/组件的基本运行情况和运行状态,特别适合打印一次性日志,例如核心类的启动过程、状态变更等信息,输出的内容一定要非常详细,不要担心影响性能。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#6a737d","name":"user"}},{"type":"strong","attrs":{}}],"text":"目前的主流日志输出框架例如logback,其日志的打印基本都是基于异步的,性能已经非常高,无需担心性能损耗。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"WARN警告级别,通常用于可预知但又不希望发生的情况,典型的使用场景是打印业务类异常日志,例如参数校验不通过、权限不足,余额不足等用户可处理的;再例如中间件开发时,有一些分支是我们不希望进入的,因为进入就代表性能差等场景,但这类异常不需要相关系统负责人干预就能得到处理的。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"ERROR通常用来打印系统级别的日志,需要人为来干预,通常较大业务规模的公司都会将系统级别的异常(ERROR)接入监控告警中心,一旦持续发生多少条,错误率占比多少,将会触发告警,相关负责人跟进处理。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"既然需要人为来干预,ERROR日志不仅要打印出错误堆栈,同时一定要主动打印出上下文环境,至少可以打印出该异常所在方法的入参,尽量让人能够根据错误日志与上下文,就能快速定位到具体的代码行。","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#48b378","name":"user"}}],"text":"2、日志进阶","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述是一些基本的日志使用规范,分布式已成为企业架构的标配,一个应用至少会部署2台机器,当用户反馈业务异常时尝试去跟踪日志时会面临一个问题:去哪台业务机器上去查询日志,如果只有2台还好办,大不了一台台去尝试,但如果有10台,20台甚至上百台,在这样轮询几乎不可能实现,那该如何处理呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"经典的ELK架构如下图所示:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/3d/3d9a4ffffe86e6d47cf8610bd71b64e1.jpeg","alt":"在这里插入图片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常,为了避免在每台业务机器上部署一个logstash去抽日志,我们通常建议自定义一个log append,直接将日志写入到kafka中,然后再挂logstash从kafka中抽取日志,写入到es集群,然后通过kibana对日志进行可视化搜索。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然后我们查询日志就变得类似这样了:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/28/28baacc28fc0a68fe70cd5fbae56693f.png","alt":"在这里插入图片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文并没有打算探讨ELK架构,这个后续应该会单独展开详细介绍,而是就算我们接入了ELK,从ELK可以统一查看根据关键字查询日志了,该日志会包含所有服务器上的日志,比单独一台一台去找依然方便了很多。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但这些日志其实是杂乱无章的,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"查询出来的日志与日志之间没有任何关联性","attrs":{}},{"type":"text","text":",而我们在解决特定问题时通常希望日志的**“隔离性”**,希望我们可以根据一个统一的关键字,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"例如请求号筛选出所有相关的日志,这样对我们分析排查问题能起到极大的促进作用","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.1 每条日志包含一个请求序","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那我们如何将“请求号”统一写入到日志文件中,肯定不能要求在项目中去修改所有日志输出到地方,手动去增加请求编号。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们可以通过自定义一个append,在append中对用户的日志统一进行二次加工。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"logback、log4j都可以自定append,接下来以当前使用最广logback举例,和大家介绍一下自定义append。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、继承 AppenderBase 并初始化","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先需要继承logback的append的基础类:AppenderBase,入下图所示:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e8/e8bf9ef258f28a3f0832b5617b2e649f.png","alt":"在这里插入图片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中有一个初始化方法start,通常的做法是先调用super.start()标记append启动,然后可以在该方法中初始化kafka的消息发送者对象。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、重写append方法","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/68/6868cd7cbcd1ca0505e7fb80b441f494.png","alt":"在这里插入图片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主要是从ILoggingEvent对象中获取原始日志,然后我们对原始日志加以加工,加工代码如下图所示:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/8a/8aed600b4345e020dabbbeaae94ed798.png","alt":"在这里插入图片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"关键是reqeustId的获取,这个通常会配置一个http filter,进入请求链中放入到线程本地变量中(ThreadLocal),然后在日志输出时从线程上下文环境中获取,为了能在线程池等复杂环境下使用,通常可以使用(TransmittableThreadLocal),关于在线程池中传递数据,需要使用ttl框架,关于这块的想象介绍可以查看笔者的另一篇博文:","attrs":{}},{"type":"link","attrs":{"href":"https://blog.csdn.net/prestigeding/article/details/96642492","title":null,"type":null},"content":[{"type":"text","text":"全链路压测必备基础组件之线程上下文管理之“三剑客”","attrs":{}}],"marks":[{"type":"color","attrs":{"color":"#48b378","name":"user"}},{"type":"strong"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文章首发于公众号「中间件兴趣圈」","attrs":{}},{"type":"link","attrs":{"href":"https://mp.weixin.qq.com/s/-sww4Yn2Vsn84SdIzrv6XQ","title":"","type":null},"content":[{"type":"text","text":"https://mp.weixin.qq.com/s/-sww4Yn2Vsn84SdIzrv6XQ","attrs":{}}]}]},{"type":"horizontalrule","attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者简介:丁威,《RocketMQ 技术内幕》一书作者、RocketMQ 开源社区优秀布道师,公众号「中间件兴趣圈」维护者,主打成体系剖析 Java 主流中间件,已发布 Kafka、RocketMQ、Dubbo、Sentinel、Canal、ElasticJob 等中间件 15 个专栏。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章