日志分析常规操作|精选技术文章
前言
日志结构
/log
/mysql
/rocketmq
/app
/app_name1
/app_name2
application.log
, application-error.log
和application-warn.log
。正如文件的名称所示,它们分别对应不同级别的日志,application.log
中会包含应用生命周期中的全部日志,而application-error.log
和application-warn.log
日志则分别只记录error级别日志和warn级别日志,从而方便快速定位系统异常。但是,如果你直接查看目录下的所有日志文件,会发现它不止有这三个文件.application.log
appilcation-error.log
application-warn.log
rpc-client.log
rpc-server.log
mq.log
日志分析小操作
查看日志
cat是我们最常用的阅读文件的指令,通过
cat ${filename}
即可展示文件的内容。以application.log为例cat application.log
上文提到,因为Logrotate机制的存在,日志文件往往会被截断成多个带有不同时间戳后缀的文件,而我们又不确定想要查询的日志具体在哪个文件中,这时候可以将多个文件都传给cat指令,
cat ${filename} ${filename2} ${filename...}
,cat会逐个读取文件并展示。但是如果文件数量非常大呢?幸好cat指令支持类似正则的匹配,*
关键字允许我们匹配任意多个字段。cat application1.log application2.log application3.log
cat application.log*
cat指令会将整个日志文件从头到尾读取并展示在控制台,但是有时我们往往只需要看最近一段时间的日志即可。而且在日志文件特别大的时候,用cat指令不仅比较慢,而且会导致大量无关的日志充斥屏幕影响阅读。这时用tail指令就可以很好的解决这个问题。tail指令可以只读取日志最后几行内容并展示在屏幕上。
tail application.log # 读取application.log文件最后一部分日志
tail application1.log application2.log application3.log
tail -n
来配合查询:tail -n 100 application.log
有时,我们希望实时查看日志文件的内容,从而更快速的捕捉到系统的行为,
tail -f
指令则支持动态的展示文件新增的内容。如果想要退出自动刷新,可以通过ctrl+c
指令来实现:tail -f application.log
有时,因为日志内容实在太多,导致控制台疯狂输出,直接淹没了关键信息。因此,需要一个指令能够分页查看日志内容,降低控制台刷新的频率。
more
指令为这个场景提供了非常好的支持。more application.log
关键字检索
在分布式系统中,往往会有数十甚至数百个系统参与到流程中,这时流量的入口会生成一个唯一的logId用来串联和标记全链路请求。当我们需要上下游排查问题时,往往会将logId提供给对方来排查。同样,当我们拿到logId时也需要从日志中跟该logId有关联的日志内容查询出来。这就是一个典型的根据关键字检索日志的场景。
grep指令很好的解决了这个问题,它能够将日志中和关键字匹配的行打印出来。
grep "logId" application.log
grep指令同样支持多文件查询
grep "logId" application1.log application2.log application3.log
grep "logId" application*.log
grep "logId" application.log -i # 大小写不敏感
grep -E "[\w\d]*" application.log # 正则表达式
不知道大家在程序中是否写过这样的日志逻辑:
log.error("something went wrong keyId:{}", keyId, ex)
。通过这一行日志,我们期望将异常的关键id和堆栈在日志中打出来。但是,日志系统会将这一行日志和堆栈分为两行进行打印。如果直接使用grep ${keyId} application.log
,则无法查询到下一行的堆栈信息。因此,我们需要查看这个关键字行附近的N行的日志帮助我们快速定位异常原因。grep也提供了这个能力:grep 'something went wrong' -A 100 application.log
something went wrong
这一行日志后100行的日志,包括命中的这一行。这里-A
是--after-context
的缩写,方便记忆。同理,也可以查看匹配行前N行的日志:
grep -B 100 application.log
-B
是--before-context
的缩写。当然,这两个指令也可以一起使用~有时我们并不一定能够拿到请求的logId,可能只有调用方法以及方法中的关键传参,如果只根据方法名称进行匹配,则可能查出许多无关的调用,反之只用关键传参查询也是,这个参数可能调用了许多别的方法,这些日志都将影响我们找到正确的关键性请求。grep同样提供了多关键字匹配的模式,假设我们现在有一个方法叫做
helloWorld(String name)
,并传入了值ralegrep -e "helloWorld" -e "rale" application.log
|
可以将两个shell指令串联起来,其中前一个指令的Output将会是下一个指令的Input。grep -e "helloWorld" application.log | grep "rale"
有时,我们可能希望反向查询日志,即查出不包含某些关键字的日志,这通常适用于问题更模糊的场景。举个例子,现在有用户提出客诉,说某一个功能出现了异常,但是作为中台服务我们往往很难立刻定位具体是哪个方法或是哪个服务出现问题,因此就会先将这个用户(假设有uid)的所有请求打印出来并过滤掉结果正常的方法。这里可以结合上面的管道指令一起使用。
grep "$uid" application.log | grep -v "SUCCESS"
-v
是--revert-match
的缩写,这里会先找出uid的所有日志,并过滤掉结果为成功SUCCESS的调用。问题排查时能够获得的信息只有更少没有最少,导致日志的查询只有更秀没有最秀。用户在反馈问题时,有时只能给到我们时间发生的大概时间,其它任何信息都没有。因此我们需要查询这一时间区间内的所有入口调用并结合上面的反向查询逐渐过滤出真正需要关心的内容。这里假设每一行日志中都有时间戳信息。
grep -E '2021-10-02 19:\d\d:\d\d:' application.log
sed
。sed是一个流编辑器,相对于grep来说能够支持更加复杂的指令,和对文件的编辑。这里仅提供针对时间范围日志查询的语句,其它功能大家可以做一些扩展阅读:
sed -n '/2021-10-02 19:58:00/,/2021-10-02 20:02:00/p' application.log
-n
会过滤出匹配的行,p
代表打印到控制台。再看一下-n
后紧跟的内容,这里,
代表范围查询,这个范围可以是行号,也可以是正则表达式,或者是两个的结合。示例中给出的是精准匹配。范围查询的规则是会找到所有满足的区间都打印出,比如假如范围是/a/,/z/
,则sed会顺序找出所有满足开始行包含a结束行包含z的区间。每找到一个a~z的区间,sed就开始找下一个包含a的行,找到后再找包含z的行直到文件结束。这里针对grep指令给出一个提高可读性的option,即高亮关键字,从而更快的定位日志行。
grep "logId" application.log --color=auto
这个场景其实我在开发过程中真实遇到过。当时出现了一些线上问题,需要统计究竟有多少比例的流量受到影响。统计的指标就是根据方法关键字+执行结果为失败代表受到系统异常影响的请求。因为数据量也已经超过了单次查询的最大行数上限,因此最终是将机器上的日志文件下载下来自己统计。
这时,就要提到一个新的指令
wc
:grep "methodName" application.log* | grep "FAIL" | wc -l
-l
指令表示只展示行数。uniq
,它可以对整行或者是根据行内的部分信息进行去重,并统计每一个关键字关联的行数。这里要注意,uniq仅对排序后的行生效,因此还需要将关键字截取、排序后再使用uniq才能达到预期效果grep "methodName" application.log* | grep "FAIL" | cut -d ' ' -f 1 | sort | uniq -c
-d
声明了切割日志行的分隔符,这里按照空格对行进行切割。-f
则指定取哪一个位置上的值,cut指令后生成的字符串列表是以1作为起始下标的,因此这里取了分割后的第一个字符串(假设日志格式为userId timestamp threadId ....
,当然大多数情况下userId并不会是行首的内容)。接着对所有userId进行排序,这里使用了sort指令。最后用uniq指令去重,-c
选项是指在每列旁边显示该行重复出现的次数。假设现在我希望将上面的统计结果导出到文件中并传给别人,linux管道同样支持将输出流重定向到文件中
...a lot of command ... > result.txt
逆向思维-打印日志最佳实践
%level %d{yyyy-MM-dd HH:mm:ss.SSS} %F:%L %X{HOST_IP:-_} %X{LOG_ID:-_} %X{CLUSTER:-_} %X{STAGE:-_} [%t] %m%n%wEx -->
private static final String START_LOG_TEMPLATE = "[rpc_server_start] [method]:{} [request]:{}";
private static final String FINISH_LOG_TEMPLATE = "[rpc_server_finish] [method]:{} [success]:{} [duration]:{}s [request]:{} [result]:{}";
private static final String DIGEST_LOG_TEMPLATE = "[rpc_server_digest] [method]:{} [success]:{} [duration]:{}s";
小结
评论