多行日志(比如异常信息)为调试应用问题提供了很多非常有价值的信息。在分布式微服务流行的今天,日志基本都是统一采集的。比如常见的ELK、EFK等方案,但是如果这些方案配置不当,它们不会将多行日志作为一个整体来处理,而是将每一行都当作独立的一行日志来处理。说是不能接受。在本文中,我们将介绍一些常见的日志收集工具处理多行日志的策略。1JSON确保多行日志作为单个事件处理。最简单的方式是记录JSON格式的日志。例如,以下是常规Java日常日志的示例:#javaApp.log2019-08-1414:51:22,299ERROR[http-nio-8080-exec-8]classOne:Indexoutofrangejava.lang.StringIndexOutOfBoundsException:Stringindexoutofrange:18atjava.lang.String.charAt(String.java:658)atcom.example.app.loggingApp.classOne.getResult(classOne.java:15)atcom.example.app.loggingApp.AppController.tester(AppController.java:27)atsun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethod)atsun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)atsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.invoke):43)atjava.lang.reflect.Method.invoke(Method.java:498)atorg.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)atorg.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)[…]如果上面的日志是直接采集,会被识别为多行日志。如果我们把这些日志记录成JSON格式,那么引入JSON数据就会简单很多,比如使用Log4J2来记录,改变下面的格式:{"@timestamp":"2019-08-14T18:46:04.449+00:00","@version":"1","message":"Indexoutofrange","logger_name":"com.example.app.loggingApp.classOne","thread_name":"http-nio-5000-exec-6","level":"错误","level_value":40000,"stack_trace":"java.lang.StringIndexOutOfBoundsException:Stringindexoutofrange:18\n\tatjava.lang.String.charAt(String.java:658)\n\tatcom.example.app.loggingApp.classOne.getResult(classOne.java:15)\n\tatcom.example.app.loggingApp.AppController.tester(AppController.java:27)\n\tatsun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethod)\n\tatsun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n\tatsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tatjava.lang.reflect.Method.invoke(Method.java:498)\n\tatorg.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)\n\tatorg.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)\n\tat[...]}这样,整个日志消息包含在一个JSON对象中,其中包含完整的异常堆栈信息。大多数工具都支持直接解析JSON日志数据。这是最简单的方式,对于运维同学来说也是最省心的,但是大部分开发者是反对使用JSON格式来记录日志的~~~2Logstash对于使用Logstash的用户来说,支持多端并不难行日志。Logstash可以使用插件来解析多行日志。插件配置在日志管道的输入部分。比如下面的配置,意思是让Logstash匹配你日志文件中ISO8601格式的时间戳。当匹配到这个时间戳时,它会替换之前所有不以某个时间戳开头的时间戳的内容,并折叠到之前的日志条目中。input{file{path=>"/var/app/current/logs/javaApp.log"mode=>"tail"codec=>multiline{pattern=>"^%{TIMESTAMP_ISO8601}"negate=>truewhat=>"previous"}}}3Fluentd类似于Logstash。Fluentd还允许我们使用插件来处理多行日志。我们可以配置插件来接收一个或多个正则表达式。以下面的Python多行日志为例:2019-08-0118:58:05,898ERROR:ExceptiononmainhandlerTraceback(mostrecentcalllast):File"python-logger.py",line9,inmake_logreturnword[13]IndexError:stringindexoutofrange如果没有多行multilineparser,Fluentd会将每一行都当做一个完整的日志,我们可以在模块中添加一个multiline解析规则,其中必须包含一个format_firstline参数来指定一个新的日志条目以什么开头。另外,可以使用常规的分组抓取来解析日志中的属性,如下配置示例:@typetailpath/path/to/pythonApp.logtagsample.tag@typemultilineformat_firstline/\d{4}-\d{1,2}-\d{1,2}/format1/(?<时间戳>[^]*[^]*)(?<级别>[^\s]+:)(?[\s\S]*)/在解析部分我们使用@typemultiline指定多行解析器,然后在我们的开头使用format_firstline指定规则多行日志。这里我们使用简单的正则匹配日期,然后指定其他部分的匹配模式,并为其分配标签。这里我们将日志拆分为时间戳、级别和消息字段。在解析上述规则后,Fluentd现在将记录每个回溯日志,将其视为单个日志:{"timestamp":"2019-08-0119:22:14,196","level":"ERROR:","message":"Exceptiononmainhandler\nTraceback(mostrecentcalllast):\nFile\"python-logger.py\",line9,inmake_log\nreturnword[13]\nIndexError:stringindexoutofrange"}日志已经格式化为JSON,我们匹配的tag也设置好了作为关键。Fluentd官方文档中也有几个例子:Rails日志比如输入Rails日志如下:StartedGET"/users/123/"for127.0.0.1at2013-06-1412:00:11+0900ProcessingbyUsersController#showasHTMLParameters:{"user_id"=>"123"}Renderedusers/show.html.erbwithinlayouts/application(0.3ms)Completed200OKin4ms(Views:3.2ms|ActiveRecord:0.0ms)我们可以使用如下解析配置进行多行匹配:@typemultilineformat_firstline/^Started/format1/Started(?[^]+)"(?[^"]+)"for(?[^]+)at(?解析后得到的log如下如下图:{"method":"GET","path":"/users/123/","host":"127.0.0.1","controller":"UsersController","controller_method":"show""format":"HTML","parameters":"{\"user_id\":\"123\"}",...}Java栈日志比如我们现在要解析的日志如下:2013-3-0314:27:33[main]INFOMain-Start2013-3-0314:27:33[main]ERRORMain-Exceptionjavax.management.RuntimeErrorException:nullatMain.main(Main.java:16)~[bin/:na]2013-3-0314:27:33[main]INFOMain-End然后我们可以使用如下解析规则进行多行匹配:@typemultilineformat_firstline/\d{4}-\d{1,2}-\d{1,2}/format1/^(?解析出来的日志是:{"thread":"main","level":"INFO","message":"Main-Start"}{"thread":"main","level":"ERROR","message":"Main-Exception\njavax.management.RuntimeErrorException:null\natMain.main(Main.java:16)~[bin/:na]"}{"thread":"main","level":"INFO","message":"Main-End"}在上面的多行解析配置中,除了format_firstline指定多行日志的起始行匹配外,format1和format2也使用...formatN配置,其中N取值范围为1...20,是一个Regexp格式的多行日志列表。为了方便匹配,可以将Regexp模式分成多个regexpN个参数,将这些匹配模式连接起来,构造多个线模式的正则匹配4FluentBitFluentBit的尾部输入插件也提供了处理多行日志的配置选项。比如我们还是处理之前的Python多行日志:2019-08-0118:58:05,898ERROR:ExceptiononmainhandlerTraceback(mostrecentcalllast):File"python-logger.py",line9,inmake_logreturnword[13]IndexError:stringindexoutofrangeIf如果不使用多行解析器,FluentBit也会将每一行当成一条日志,我们可以配置使用FluentBit内置的正则表达式解析器插件来构造多行日志:[PARSER]Namelog_dateFormatregexRegex/\d{4}-\d{1,2}-\d{1,2}/[PARSER]Namelog_attributesFormatregexRegex/(?[^]*[^]*)(?[^\s]+:)(?[\s\S]*)/[INPUT]Nametailtagsample.tagpath/path/to/pythonApp.logMultilineOnParser_Firstlinelog_dateParser_1log_attributes与Fluentd类似,Parser_Firstline参数指定匹配开头的解析器的名称日志行,当然我们还可以包含额外的解析器来进一步构建您的日志。这里我们配置为先使用Parser_Firstline参数来匹配以ISO8601日期开始的日志行,然后使用Parser_1参数指定匹配模式来匹配日志消息的其余部分,并分配timestamp、level和message标签给他们。最终转换后,我们的日志变成如下格式:{"timestamp":"2019-08-0119:22:14,196","level":"ERROR:","message":"Exceptiononmainhandler\nTraceback(mostrecentcallast):\n文件\"python-logger.py\",line9,inmake_log\nreturnword[13]\nIndexError:stringindexoutofrange"}