编码插件(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 对输入数据验证报异常。

有两个办法解决这个问题:

  1. 用​​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
  1. 日志格式中统一记录为字符串格式(即都带上双引号​​"​​​),然后再在 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端口接收的数据。

推荐阅读

​github.com/elasticsear…​

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 表达式。​