编码插件(Codec)
Codec 是 logstash 从 1.3.0 版开始新引入的概念(Codec 来自 Coder/decoder 两个单词的首字母缩写)。
在此之前,logstash 只支持纯文本形式输入,然后以过滤器处理它。但现在,我们可以在输入 期处理不同类型的数据,这全是因为有了 codec 设置。
所以,这里需要纠正之前的一个概念。Logstash 不只是一个input | filter | output
的数据流,而是一个 input | decode | filter | encode | output
的数据流!codec 就是用来 decode、encode 事件的。
codec 的引入,使得 logstash 可以更好更方便的与其他有自定义数据格式的运维产品共存,比如 graphite、fluent、netflow、collectd,以及使用 msgpack、json、edn 等通用数据格式的其他产品等。
事实上,我们在第一个 "hello world" 用例中就已经用过 codec 了 —— rubydebug 就是一种 codec!虽然它一般只会用在 stdout 插件中,作为配置测试或者调试的工具。
小贴士:这个五段式的流程说明源自 Perl 版的 Logstash (后来改名叫 Message::Passing 模块)的设计。本书最后会对该模块稍作介绍。
采用 JSON 编码
在早期的版本中,有一种降低 logstash 过滤器的 CPU 负载消耗的做法盛行于社区(在当时的 cookbook 上有专门的一节介绍):直接输入预定义好的 JSON 数据,这样就可以省略掉 filter/grok 配置!
这个建议依然有效,不过在当前版本中需要稍微做一点配置变动 —— 因为现在有专门的 codec 设置。
配置示例
社区常见的示例都是用的 Apache 的 customlog。不过我觉得 Nginx 是一个比 Apache 更常用的新型 web 服务器,所以我这里会用 nginx.conf 做示例:
logformat json '{"@timestamp":"$time_iso8601",' '"@version":"1",' '"host":"$server_addr",' '"client":"$remote_addr",' '"size":$body_bytes_sent,' '"responsetime":$request_time,' '"domain":"$host",' '"url":"$uri",' '"status":"$status"}'; access_log /var/log/nginx/access.log_json json;
注意:在 requesttime和request_time 和 requesttime和body_bytes_sent 变量两头没有双引号 ",这两个数据在 JSON 里应该是数值类型!
重启 nginx 应用,然后修改你的 input/file 区段配置成下面这样:
input { file { path => "/var/log/nginx/access.log_json"" codec => "json" } }
运行结果
下面访问一下你 nginx 发布的 web 页面,然后你会看到 logstash 进程输出类似下面这样的内容:
{ "@timestamp" => "2014-03-21T18:52:25.000+08:00", "@version" => "1", "host" => "raochenlindeMacBook-Air.local", "client" => "123.125.74.53", "size" => 8096, "responsetime" => 0.04, "domain" => "www.domain.com", "url" => "/path/to/file.suffix", "status" => "200" }
小贴士
对于一个 web 服务器的访问日志,看起来已经可以很好的工作了。不过如果 Nginx 是作为一个代理服务器运行的话,访问日志里有些变量,比如说 $upstream_response_time
,可能不会一直是数字,它也可能是一个 "-"
字符串!这会直接导致 logstash 对输入数据验证报异常。
有两个办法解决这个问题:
- 用
sed
在输入之前先替换-
成0
。
运行 logstash 进程时不再读取文件而是标准输入,这样命令就成了下面这个样子:
tail -F /var/log/nginx/proxy_access.log_json \ | sed 's/upstreamtime":-/upstreamtime":0/' \ | /usr/local/logstash/bin/logstash -f /usr/local/logstash/etc/proxylog.conf
- 日志格式中统一记录为字符串格式(即都带上双引号
"
),然后再在 logstash 中用filter/mutate
插件来变更应该是数值类型的字符字段的值类型。
有关 LogStash::Filters::Mutate
的内容,本书稍后会有介绍。
合并多行数据(多)
有些时候,应用程序调试日志会包含非常丰富的内容,为一个事件打印出很多行内容。这种日志通常都很难通过命令行解析的方式做分析。
而logstash正为此准备好了codec / multiline插件!
小贴士:multiline插件也可以用于其他类似的堆栈式信息,比如linux的内核日志。
配置示例
input { stdin { codec => multiline { pattern => "^\[" negate => true what => "previous" } } }
运行结果
运行logstash进程,然后在等待输入的终端中输入如下几行数据:
[Aug/08/08 14:54:03] hello world [Aug/08/09 14:54:04] hello logstash hello best practice hello raochenlin [Aug/08/10 14:54:05] the end
你会发现logstash输出下面这样的返回:
{ "@timestamp" => "2014-08-09T13:32:03.368Z", "message" => "[Aug/08/08 14:54:03] hello world\n", "@version" => "1", "host" => "raochenlindeMacBook-Air.local" } { "@timestamp" => "2014-08-09T13:32:24.359Z", "message" => "[Aug/08/09 14:54:04] hello logstash\n\n hello best practice\n\n hello raochenlin\n", "@version" => "1", "tags" => [ [0] "multiline" ], "host" => "raochenlindeMacBook-Air.local" }
你看,后面这个事件,在“message”字段里存储了三行数据!
小贴士:你可能注意到输出的事件中都没有最后的“the end”字符串。这是因为你最后输入的回车符\n并不匹配设定的^[正则表达式,logstash还得等下一行数据直到匹配成功后才会输出这个事件。
解释
其实这个插件的原理很简单,就是把当前行的数据添加到前面一行后面,,新直到进的当前行匹配^\[
正则为止。
这个正则还可以用grok表达式,稍后你就会学习这方面的内容。
Log4J的另一种方案
说到应用程序日志,log4j肯定是第一个被大家想到的。使用codec/multiline
也确实是一个办法。
不过,如果你本事就是开发人员,或者可以推动程序修改变更的话,logstash还提供了另一种处理log4j的方式:input / log4j。与codec/multiline
不同,这个插件是直接调用了org.apache.log4j.spi.LoggingEvent
处理TCP端口接收的数据。
推荐阅读
Grok 正则捕获
Grok 是 Logstash 最重要的插件。你可以在 grok 里预定义好命名正则表达式,在稍后(grok参数或者其他正则表达式里)引用它。
正则表达式语法
运维工程师多多少少都会一点正则。你可以在 grok 里写标准的正则,像下面这样:
\s+(?<request_time>\d+(?:\.\d+)?)\s+
小贴士:这个正则表达式写法对于 Perl 或者 Ruby 程序员应该很熟悉了,Python 程序员可能更习惯写 (?Ppattern),没办法,适应一下吧。
现在给我们的配置文件添加第一个过滤器区段配置。配置要添加在输入和输出区段之间(logstash 执行区段的时候并不依赖于次序,不过为了自己看得方便,还是按次序书写吧):
input {stdin{}} filter { grok { match => { "message" => "\s+(?<request_time>\d+(?:\.\d+)?)\s+" } } } output {stdout{}}
运行 logstash 进程然后输入 "begin 123.456 end",你会看到类似下面这样的输出:
{ "message" => "begin 123.456 end", "@version" => "1", "@timestamp" => "2014-08-09T11:55:38.186Z", "host" => "raochenlindeMacBook-Air.local", "request_time" => "123.456" }
漂亮!不过数据类型好像不太满意……request_time 应该是数值而不是字符串。
我们已经提过稍后会学习用 LogStash::Filters::Mutate
来转换字段值类型,不过在 grok 里,其实有自己的魔法来实现这个功能!
Grok 表达式语法
Grok 支持把预定义的 grok 表达式 写入到文件中,官方提供的预定义 grok 表达式见:github.com/logstash/lo…。
注意:在新版本的logstash里面,pattern目录已经为空,最后一个commit提示core patterns将会由logstash-patterns-core gem来提供,该目录可供用户存放自定义patterns
下面是从官方文件中摘抄的最简单但是足够说明用法的示例:
USERNAME [a-zA-Z0-9._-]+ USER %{USERNAME}
第一行,用普通的正则表达式来定义一个 grok 表达式;第二行,通过打印赋值格式,用前面定义好的 grok 表达式来定义另一个 grok 表达式。
grok 表达式的打印复制格式的完整语法是下面这样的:
%{PATTERN_NAME:capture_name:data_type}
小贴士:data_type 目前只支持两个值:int 和 float。
所以我们可以改进我们的配置成下面这样:
filter { grok { match => { "message" => "%{WORD} %{NUMBER:request_time:float} %{WORD}" } } }
重新运行进程然后可以得到如下结果:
{ "message" => "begin 123.456 end", "@version" => "1", "@timestamp" => "2014-08-09T12:23:36.634Z", "host" => "raochenlindeMacBook-Air.local", "request_time" => 123.456 }
这次 request_time 变成数值类型了。
最佳实践
实际运用中,我们需要处理各种各样的日志文件,如果你都是在配置文件里各自写一行自己的表达式,就完全不可管理了。所以,我们建议是把所有的 grok 表达式统一写入到一个地方。然后用 filter/grok 的 patterns_dir
选项来指明。
如果你把 "message" 里所有的信息都 grok 到不同的字段了,数据实质上就相当于是重复存储了两份。所以你可以用 remove_field
参数来删除掉 message 字段,或者用 overwrite
参数来重写默认的 message 字段,只保留最重要的部分。
重写参数的示例如下:
filter { grok { patterns_dir => "/path/to/your/own/patterns" match => { "message" => "%{SYSLOGBASE} %{DATA:message}" } overwrite => ["message"] } }
小贴士
多行匹配
在和 codec/multiline 搭配使用的时候,需要注意一个问题,grok 正则和普通正则一样,默认是不支持匹配回车换行的。就像你需要 =~ //m
一样也需要单独指定,具体写法是在表达式开始位置加 (?m)
标记。如下所示:
match => { "message" => "(?m)\s+(?<request_time>\d+(?:\.\d+)?)\s+" }
多项选择
有时候我们会碰上一个日志有多种可能格式的情况。这时候要写成单一正则就比较困难,或者全用 |
隔开又比较丑陋。这时候,logstash 的语法提供给我们一个有趣的解决方式。
文档中,都说明 logstash/filters/grok 插件的 match
参数应该接受的是一个 Hash 值。但是因为早期的 logstash 语法中 Hash 值也是用 []
这种方式书写的,所以其实现在传递 Array 值给 match
参数也完全没问题。所以,我们这里其实可以传递多个正则来匹配同一个字段:
match => [ "message", "(?<request_time>\d+(?:\.\d+)?)", "message", "%{SYSLOGBASE} %{DATA:message}", "message", "(?m)%{WORD}" ]
logstash 会按照这个定义次序依次尝试匹配,到匹配成功为止。虽说效果跟用 |
分割写个大大的正则是一样的,但是可阅读性好了很多。
最后也是最关键的,我强烈建议每个人都要使用 Grok Debugger 来调试自己的 grok 表达式。