写在前面:本文是学习总结内容,侵犯到别的文章,在这里说声抱歉~~
在终端中,像下面这样运行命令来启动 Logstash 进程:
bin/logstash -e 'input{stdin{}}output{stdout{codec=>rubydebug}}'
然后你会发现终端在等待你的输入。没问题,敲入 Hello World,回车,然后看看会返回什么结果!
{
"message" => "Hello World",
"@version" => "1",
"@timestamp" => "2014-08-07T10:30:59.937Z",
"host" => "raochenlindeMacBook-Air.local",
}
数据在线程之间以 事件 的形式流传。不要叫行,因为 logstash 可以处理多行事件。
Logstash 会给事件添加一些额外信息。最重要的就是 @timestamp,用来标记事件的发生时间。因为这个字段涉及到 Logstash 的内部流转,所以必须是一个 joda 对象,如果你尝试自己给一个字符串字段重命名为 @timestamp 的话,Logstash 会直接报错。
此外,大多数时候,还可以见到另外几个:
- 1.host 标记事件发生在哪里。
- 2.type 标记事件的唯一类型。
- 3.tags 标记事件的某方面属性。这是一个数组,一个事件可以有多个标签。
你可以随意给事件添加字段或者从事件里删除字段。事实上事件就是一个 Ruby 对象,或者更简单的理解为就是一个哈希也行。
小贴士:每个 logstash 过滤插件,都会有四个方法叫 add_tag, remove_tag, add_field 和 remove_field。它们在插件过滤匹配成功时生效。
单独使用命令启动配置文件之后,你肯定会发现一点:一旦你按下 Ctrl+C,logstash 进程也就随之停止了。作为一个肯定要长期运行的程序,应该怎么处理呢?
下面介绍四种最常用的办法:
采用 RPM、DEB 发行包安装的读者,推荐采用这种方式。发行包内,都自带有 sysV 或者 systemd 风格的启动程序/配置,你只需要直接使用即可。
以 RPM 为例,/etc/init.d/logstash
脚本中,会加载 /etc/init.d/functions
库文件,利用其中的 daemon 函数,将 logstash 进程作为后台程序运行。
所以,你只需把自己写好的配置文件,统一放在 /etc/logstash/ 目录下(注意目录下所有配置文件都应该是 .conf 结尾,且不能有其他文本文件存在。因为 logstash agent 启动的时候是读取全文件夹的),然后运行service logstash start
命令即可。
这是最简单的方式,想要维持一个长期后台运行的 logstash,你需要同时在命令前面加 nohup,后面加 &。
screen 算是 linux 运维一个中高级技巧。通过 screen 命令创建的环境下运行的终端命令,其父进程不是 sshd 登录会话,而是 screen 。这样就可以即避免用户退出进程消失的问题,又随时能重新接管回终端继续操作。
创建独立的 screen 命令如下:
screen -dmS elkscreen_1
接管连入创建的 elkscreen_1 命令如下:
screen -r elkscreen_1
然后你可以看到一个一模一样的终端,运行 logstash 之后,不要按Ctrl+C
,而是按Ctrl+A+D
键,断开环境。想重新接管,依然screen -r elkscreen_1
即可。
如果创建了多个 screen,查看列表命令如下:
screen -list
不管是 nohup 还是 screen,都不是可以很方便管理的方式,在运维管理一个 ELK 集群的时候,必须寻找一种尽可能简洁的办法。所以,对于需要长期后台运行的大量程序(注意大量,如果就一个进程,还是学习一下怎么写 init 脚本吧),推荐大家使用一款 daemontools 工具。
daemontools 是一个软件名称,不过配置略复杂。所以这里我其实是用其名称来指代整个同类产品,包括但不限于 python 实现的 supervisord,perl 实现的 ubic,ruby 实现的 god 等。
以 supervisord 为例,因为这个出来的比较早,可以直接通过 EPEL 仓库安装。
yum -y install supervisord --enablerepo=epel
在/etc/supervisord.conf
配置文件里添加内容,定义你要启动的程序:
[program:elkpro_1]
environment=LS_HEAP_SIZE=5000m
directory=/opt/logstash
command=/opt/logstash/bin/logstash -f /etc/logstash/pro1.conf --pluginpath /opt/logstash/plugins/ -w 10 -l /var/log/logstash/pro1.log
[program:elkpro_2]
environment=LS_HEAP_SIZE=5000m
directory=/opt/logstash
command=/opt/logstash/bin/logstash -f /etc/logstash/pro2.conf --pluginpath /opt/logstash/plugins/ -w 10 -l /var/log/logstash/pro2.log
然后启动 service supervisord start
即可。
logstash 会以 supervisord 子进程的身份运行,你还可以使用 supervisorctl 命令,单独控制一系列 logstash 子进程中某一个进程的启停操作:supervisorctl stop elkpro_2
Logstash 设计了自己的 DSL —— 有点像 Puppet 的 DSL,或许因为都是用 Ruby 语言写的吧 —— 包括有区域,注释,数据类型(布尔值,字符串,数值,数组,哈希),条件判断,字段引用等。
Logstash 用 {} 来定义区域。区域内可以包括插件区域定义,你可以在一个区域内定义多个插件。插件区域内则可以定义键值对设置。示例如下:
input {
stdin {}
syslog {}
}
Logstash 支持少量的数据值类型:
- bool
debug => true
host => "hostname"
port => 514
match => ["datetime", "UNIX", "ISO8601"]
options => {
key1 => "value1",
key2 => "value2"
}
字段是 Logstash::Event 对象的属性。我们之前提过事件就像一个哈希一样,所以你可以想象字段就像一个键值对。
小贴士:我们叫它字段,因为 Elasticsearch 里是这么叫的。
如果你想在 Logstash 配置中使用字段的值,只需要把字段的名字写在中括号 [] 里就行了,这就叫字段引用。
对于 嵌套字段(也就是多维哈希表,或者叫哈希的哈希),每层的字段名都写在 [] 里就可以了。比如,你可以从 geoip 里这样获取 longitude 值(是的,这是个笨办法,实际上有单独的字段专门存这个数据的):
[geoip][location][0]
小贴士:logstash 的数组也支持倒序下标,即 [geoip][location][-1] 可以获取数组最后一个元素的值。
Logstash 还支持变量内插,在字符串里使用字段引用的方法是这样:
"the longitude is %{[geoip][location][0]}"
表达式支持下面这些操作符:
- equality, etc: ==, !=, <, >, <=, >=
- regexp: =~, !~
- inclusion: in, not in
- boolean: and, or, nand, xor
- unary: !()
通常来说,你都会在表达式里用到字段引用。比如:
if "_grokparsefailure" not in [tags] {
} else if [status] !~ /^2\d\d/ and [url] == "/noc.gif" {
} else {
}
Logstash 提供了一个 shell 脚本叫 logstash 方便快速运行。它支持一下参数:
- -e
意即执行。你可以不写任何具体配置,直接运行 bin/logstash -e ''
,这个参数的默认值是下面这样:
input {
stdin { }
}
output {
stdout { }
}
bin/logstash -f agent.conf
这样的形式来运行。 bin/logstash -f /etc/logstash.d/
来运行。logstash 会自动读取/etc/logstash.d/
目录下所有的文本文件,然后在自己内存里拼接成一个完整的大配置文件,再去执行。grammar.treetop
定义的。尤其是使用了上一条提到的读取目录方式的读者,尤其要提前测试。bin/logstash -l logs/logstash.log
命令来统一存储日志。bin/logstash -w 5
这样的方式强制 Logstash 为过滤插件运行 5 个线程。 提示:Logstash 目前不支持对过滤器线程的监测管理。如果 filterworker 挂掉,Logstash 会处于一个无 filter 的僵死状态。这种情况在使用 filter/ruby 自己写代码时非常需要注意,很容易碰上 NoMethodError: undefined method ‘*’ for nil:NilClass 错误。需要妥善处理,提前判断。
bin/logstash --pluginpath /path/to/own/plugins
加载它们。**“`以上请读者自明。“`**限于篇幅,接下来内容中,配置示例不一定能贴完整。请记住一个原则:Logstash 配置一定要有一个 input 和一个 output。在演示过程中,如果没有写明 input,默认就会使用 “hello world” 里我们已经演示过的 input/stdin ,同理,没有写明的 output 就是 output/stdout。
input {
stdin {
add_field => {"key" => "value"}
codec => "plain"
tags => ["add"]
type => "std"
}
}
{
"message" => "hello world",
"@version" => "1",
"@timestamp" => "2014-08-08T06:48:47.789Z",
"type" => "std",
"tags" => [
[0] "add"
],
"key" => "value",
"host" => "raochenlindeMacBook-Air.local"
}
input {
stdin {
type => "web"
}
}
filter {
if [type] == "web" {
grok {
match => ["message", %{COMBINEDAPACHELOG}]
}
}
}
output {
if "_grokparsefailure" in [tags] {
nagios_nsca {
nagios_status => "1"
}
} else {
elasticsearch {
}
}
}
input
file {
path => ["/var/log/*.log", "/var/log/message"]
type => "system"
start_position => "beginning"
}
}
exclude
不想被监听的文件可以排除出去,这里跟 path 一样支持 glob 展开。
sincedb_path
如果你不想用默认的 $HOME/.sincedb(Windows 平台上在 C:\Windows\System32\config\systemprofile.sincedb),可以通过这个配置定义 sincedb 文件到其他位置。
sincedb_write_interval
logstash 每隔多久写一次 sincedb 文件,默认是 15 秒。
stat_interval
logstash 每隔多久检查一次被监听文件状态(是否有更新),默认是 1 秒。
start_position
logstash 从什么位置开始读取文件数据,默认是结束位置,也就是说 logstash 进程会以类似 tail -F
的形式运行。如果你是要导入原有数据,把这个设定改成 "beginning"
,logstash 进程就从头开始读取,有点类似 cat,但是读到最后一行不会终止,而是继续变成 tail -F
。
filter/date
插件来修改默认的”@timestamp” 字段值。稍后会学习这方面的知识。path => "/path/to/%{+yyyy/MM/dd/hh}.log"
写法。达到相同目的,你只能写成 path => "/path/to/*/*/*/*.log"
。start_position
仅在该文件从未被监听过的时候起作用。如果 sincedb 文件中已经有这个文件的 inode 记录了,那么 logstash 依然会从记录过的 pos 开始读取数据。所以重复测试的时候每回需要删除 sincedb 文件。Logstash 其实也有自己的 TCP/UDP 插件,在临时任务的时候,也算能用,尤其是测试环境。
小贴士:虽然LogStash::Inputs::TCP 用 Ruby 的 Socket 和 OpenSSL 库实现了高级的 SSL 功能,但 Logstash 本身只能在 SizedQueue 中缓存 20 个事件。这就是我们建议在生产环境中换用其他消息队列的原因。
input {
tcp {
port => 8888
mode => "server"
ssl_enable => false
}
}
目前来看,LogStash::Inputs::TCP 最常见的用法就是配合 nc 命令导入旧数据。在启动 logstash 进程后,在另一个终端运行如下命令即可导入数据:
# nc 127.0.0.1 8888 < olddata
这种做法比用 LogStash::Inputs::File 好,因为当 nc 命令结束,我们就知道数据导入完毕了。而用 input/file 方式,logstash 进程还会一直等待新数据输入被监听的文件,不能直接看出是否任务完成了。
实际运行的时候这个插件是派不上用途的,但这个插件依然是非常重要的插件之一。因为每一个使用 ELK stack 的运维人员都应该清楚一个道理:数据是支持操作的唯一真理(否则你也用不着 ELK)。所以在上线之前,你一定会需要在自己的实际环境中,测试 Logstash 和 Elasticsearch 的性能状况。这时候,这个用来生成测试数据的插件就有用了!
input {
generator {
count => 10000000
message => '{"key1":"value1","key2":[1,2],"key3":{"subkey1":"subvalue1"}}'
codec => json
}
}
插件的默认生成数据,message 内容是 “hello world”。你可以根据自己的实际需要这里来写其他内容
做测试有两种主要方式:
- 配合 LogStash::Outputs::Null
inputs/generator 是无中生有,output/null 则是锯嘴葫芦。事件流转到这里直接就略过,什么操作都不做。相当于只测试 Logstash 的 pipe 和 filter 效率。测试过程非常简单:
$ time ./bin/logstash -f generator_null.conf
real 3m0.864s
user 3m39.031s
sys 0m51.621s
output {
stdout {
codec => dots
}
}
LogStash::Codecs::Dots 也是一个另类的 codec 插件,他的作用是:把每个 event 都变成一个点(.)。这样,在输出的时候,就变成了一个一个的 . 在屏幕上。显然这也是一个为了测试而存在的插件。
下面就要介绍 pv 命令了。这个命令的作用,就是作实时的标准输入、标准输出监控。我们这里就用它来监控标准输出:
$ ./bin/logstash -f generator_dots.conf | pv -abt > /dev/null
2.2MiB 0:03:00 [12.5kiB/s]
可以很明显的看到在前几秒中,速度是 0 B/s,因为 JVM 还没启动起来呢。开始运行的时候,速度依然不快。慢慢增长到比较稳定的状态,这时候的才是你需要的数据。
这里单位是 B/s,但是因为一个 event 就输出一个 .,也就是 1B。所以 12.5kiB/s 就相当于是 12.5k event/s。
注:如果你在 CentOS 上通过 yum 安装的 pv 命令,版本较低,可能还不支持 -a 参数。单纯靠 -bt 参数看起来还是有点累的
既然单独花这么一节来说测试,这里想额外谈谈一个很常见的话题: ELK 的性能怎么样?
ELK 并不是一个软件而是一个并不耦合的套件。所以,我们需要分拆开讨论这三个软件的性能如何?怎么优化?
- LogStash 的性能,是最让新人迷惑的地方。因为 LogStash 本身并不维护队列,所以整个日志流转中任意环节的问题,都可能看起来像是 LogStash 的问题。这里,需要熟练使用本节说的测试方法,针对自己的每一段配置,都确定其性能。另一方面,就是本书之前提到过的,LogStash 给自己的线程都设置了单独的线程名称,你可以在top -H
结果中查看具体线程的负载情况。
Elasticsearch 的性能。这里最需要强调的是:Elasticsearch 是一个分布式系统。从来没有分布式系统要跟人比较单机处理能力的说法。所以,更需要关注的是:在确定的单机处理能力的前提下,性能是否能做到线性扩展。当然,这不意味着说提高处理能力只能靠加机器了——有效利用 mapping API 是非常重要的。不过暂时就在这里讲述了。
Kibana 的性能。通常来说,Kibana 只是一个单页 Web 应用,只需要 nginx
发布静态文件即可,没什么性能问题。页面加载缓慢,基本上是因为 Elasticsearch 的请求响应时间本身不够快导致的。不过一定要细究的话,也能找出点 Kibana 本身性能相关的话题:因为 Kibana3 默认是连接固定的一个 ES 节点的 IP 端口的,所以这里会涉及一个浏览器的同一 IP 并发连接数的限制。其次,就是 Kibana 用的 AngularJS 使用了 Promise.then 的方式来处理 HTTP 请求响应。这是异步的。
syslog 可能是运维领域最流行的数据传输协议了。当你想从设备上收集系统日志的时候,syslog 应该会是你的第一选择。
我们这里不解释如何配置你的 syslog.conf
, rsyslog.conf
或者 syslog-ng.conf
来发送数据,而只讲如何把 logstash 配置成一个 syslog 服务器来接收数据。
有关 rsyslog
的用法,稍后的类型项目一节中,会有更详细的介绍。
input {
syslog {
port => "514"
}
}
作为最简单的测试,我们先暂停一下本机的 syslogd (或 rsyslogd )进程,然后启动 logstash 进程(这样就不会有端口冲突问题)。现在,本机的 syslog 就会默认发送到 logstash 里了。我们可以用自带的 logger 命令行工具发送一条 “Hello World”信息到 syslog 里(即 logstash 里)。看到的 logstash 输出像下面这样:
{
"message" => "Hello World",
"@version" => "1",
"@timestamp" => "2014-08-08T09:01:15.911Z",
"host" => "127.0.0.1",
"priority" => 31,
"timestamp" => "Aug 8 17:01:15",
"logsource" => "raochenlindeMacBook-Air.local",
"program" => "com.apple.metadata.mdflagwriter",
"pid" => "381",
"severity" => 7,
"facility" => 3,
"facility_label" => "system",
"severity_label" => "Debug"
}
Logstash 是用 UDPSocket, TCPServer
和 LogStash::Filters::Grok
来实现 LogStash::Inputs::Syslog
的。所以你其实可以直接用 logstash 配置实现一样的效果:
input {
tcp {
port => "8514"
}
}
filter {
grok {
match => ["message", %{SYSLOGLINE} ]
}
syslog_pri { }
}
建议在使用 LogStash::Inputs::Syslog 的时候走 TCP 协议来传输数据。
因为具体实现中,UDP 监听器只用了一个线程,而 TCP 监听器会在接收每个连接的时候都启动新的线程来处理后续步骤。
如果你已经在使用 UDP 监听器收集日志,用下行命令检查你的 UDP 接收队列大小:
# netstat -plnu | awk 'NR==1 || $4~/:514$/{print $2}'
Recv-Q
228096
228096 是 UDP 接收队列的默认最大大小,这时候 linux 内核开始丢弃数据包了!
强烈建议使用LogStash::Inputs::TCP和 LogStash::Filters::Grok 配合实现同样的 syslog 功能!
虽然 LogStash::Inputs::Syslog 在使用 TCPServer 的时候可以采用多线程处理数据的接收,但是在同一个客户端数据的处理中,其 grok 和 date 是一直在该线程中完成的,这会导致总体上的处理性能几何级的下降 —— 经过测试,TCPServer 每秒可以接收 50000 条数据,而在同一线程中启用 grok 后每秒只能处理 5000 条,再加上 date 只能达到 500 条!
才将这两步拆分到 filters 阶段后,logstash 支持对该阶段插件单独设置多线程运行,大大提高了总体处理性能。在相同环境下, logstash -f tcp.conf -w 20
的测试中,总体处理性能可以达到每秒 30000 条数据!
注:测试采用 logstash 作者提供的 yes "<44>May 19 18:30:17 snack jls: foo bar 32" | nc localhost 3000
命令。
Redis 服务器是 logstash 官方推荐的 broker 选择。Broker 角色也就意味着会同时存在输入和输出俩个插件。这里我们先学习输入插件。
LogStash::Inputs::Redis 支持三种 data_type(实际上是redis_type),不同的数据类型会导致实际采用不同的 Redis 命令操作:
- list => BLPOP
- channel => SUBSCRIBE
- pattern_channel => PSUBSCRIBE
注意到了么?这里面没有 GET 命令!
input {
redis {
data_type => "pattern_channel"
key => "logstash-*"
host => "192.168.0.2"
port => 6379
threads => 5
}
}
首先确认你设置的 host 服务器上已经运行了 redis-server 服务,然后打开终端运行 logstash 进程等待输入数据,然后打开另一个终端,输入redis-cli
命令(先安装好 redis 软件包),在交互式提示符后面输入PUBLISH logstash-demochan "hello world"
:
# redis-cli
127.0.0.1:6379> PUBLISH logstash-demochan "hello world"
你会在第一个终端里看到 logstash 进程输出类似下面这样的内容:
{
"message" => "hello world",
"@version" => "1",
"@timestamp" => "2014-08-08T16:26:29.399Z"
}
注意:这个事件里没有 host 字段!(或许这算是 bug……)
如果你想通过 redis 的频道给 logstash 事件添加更多字段,直接向频道发布 JSON 字符串就可以了。 LogStash::Inputs::Redis
会直接把 JSON 转换成事件。
继续在第二个终端的交互式提示符下输入如下内容:
127.0.0.1:6379> PUBLISH logstash-chan '{"message":"hello world","@version":"1","@timestamp":"2014-08-08T16:34:21.865Z","host":"raochenlindeMacBook-Air.local","key1":"value1"}'
你会看到第一个终端里的 logstash 进程随即也返回新的内容,如下所示:
{
"message" => "hello world",
"@version" => "1",
"@timestamp" => "2014-08-09T00:34:21.865+08:00",
"host" => "raochenlindeMacBook-Air.local",
"key1" => "value1"
}
看,新的字段出现了!现在,你可以要求开发工程师直接向你的 redis 频道发送信息好了,一切自动搞定。
小贴士:这里我们建议的是使用 pattern_channel 作为输入插件的 data_type 设置值。因为实际使用中,你的 redis 频道可能有很多不同的 keys,一般命名成 logstash-chan-%{type} 这样的形式。这时候 pattern_channel 类型就可以帮助你一次订阅全部 logstash 相关频道!
如上段”小贴士”提到的,之前两个使用场景采用了同样的配置,即数据类型为频道发布订阅方式。这种方式在需要扩展 logstash 成多节点集群的时候,会出现一个问题:通过频道发布的一条信息,会被所有订阅了该频道的 logstash 进程同时接收到,然后输出重复内容!
你可以尝试再做一次上面的实验,这次在两个终端同时启动 logstash -f redis-input.conf
进程,结果会是两个终端都输出消息。
这种时候,就需要用 list 类型。在这种类型下,数据输入到 redis 服务器上暂存,logstash 则连上 redis 服务器取走 (BLPOP 命令,所以只要 logstash 不堵塞,redis 服务器上也不会有数据堆积占用空间)数据。
- 配置示例
input {
redis {
batch_count => 1
data_type => "list"
key => "logstash-list"
host => "192.168.0.2"
port => 6379
threads => 5
}
}
logstash -f redis-input-list.conf
进程。然后在第三个终端里启动 redis-cli 命令交互:$ redis-cli
127.0.0.1:6379> RPUSH logstash-list "hello world"
(integer) 1
这时候你可以看到,只有一个终端输出了结果。
连续 RPUSH 几次,可以看到两个终端近乎各自输出一半条目。
重启 logstash 进程后,redis-cli 命令中改成如下发送:
127.0.0.1:6379> RPUSH logstash-list "hello world" "hello world" "hello world" "hello world" "hello world" "hello world"
(integer) 3
可以看到,两个终端也各自输出一部分结果。而你只用了一次 RPUSH 命令。
collectd 是一个守护(daemon)进程,用来收集系统性能和提供各种存储方式来存储不同值的机制。它会在系统运行和存储信息时周期性的统计系统的相关统计信息。利用这些信息有助于查找当前系统性能瓶颈(如作为性能分析 performance analysis)和预测系统未来的 load(如能力部署capacity planning)等
下面简单介绍一下: collectd的部署以及与logstash对接的相关配置实例
rpm -ivh "http://dl.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm"
yum -y install libcurl libcurl-devel rrdtool rrdtool-devel perl-rrdtool rrdtool-prel libgcrypt-devel gcc make gcc-c++ liboping liboping-devel perl-CPAN net-snmp net-snmp-devel
wget http://collectd.org/files/collectd-5.4.1.tar.gz
tar zxvf collectd-5.4.1.tar.gz
cd collectd-5.4.1
./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --libdir=/usr/lib --mandir=/usr/share/man --enable-all-plugins
make && make install
cp contrib/redhat/init.d-collectd /etc/init.d/collectd
chmod +x /etc/init.d/collectd
service collectd start
以下配置可以实现对服务器基本的CPU、内存、网卡流量、磁盘 IO 以及磁盘空间占用情况的监控:
Hostname "host.example.com"
LoadPlugin interface
LoadPlugin cpu
LoadPlugin memory
LoadPlugin network
LoadPlugin df
LoadPlugin disk
interface>
Interface "eth0"
IgnoreSelected false
"10.0.0.1" "25826"> ## logstash 的 IP 地址和 collectd 的数据接收端口号
以下配置实现通过 logstash 监听 25826 端口,接收从 collectd 发送过来的各项检测数据:
- 示例一:
input {
collectd {
port => 25826 ## 端口号与发送端对应
type => collectd
}
udp {
port => 25826
buffer_size => 1452
workers => 3 # Default is 2
queue_size => 30000 # Default is 2000
codec => collectd { }
type => "collectd"
}
下面是简单的一个输出结果:
{
"_index": "logstash-2014.12.11",
"_type": "collectd",
"_id": "dS6vVz4aRtK5xS86kwjZnw",
"_score": null,
"_source": {
"host": "host.example.com",
"@timestamp": "2014-12-11T06:28:52.118Z",
"plugin": "interface",
"plugin_instance": "eth0",
"collectd_type": "if_packets",
"rx": 19147144,
"tx": 3608629,
"@version": "1",
"type": "collectd",
"tags": [
"_grokparsefailure"
]
},
"sort": [
1418279332118
]
}
这里需要纠正之前的一个概念。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 模块)的设计。本书最后会对该模块稍作介绍。
在早期的版本中,有一种降低 logstash 过滤器的 CPU 负载消耗的做法:直接输入预定义好的 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;
注意:在
$request_time
和$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::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 肯定是第一个被大家想到的。使用 codec/multiline 也确实是一个办法。
不过,如果你本事就是开发人员,或者可以推动程序修改变更的话,logstash 还提供了另一种处理 log4j 的方式:input/log4j。
与 codec/multiline
不同,这个插件是直接调用了 org.apache.log4j.spi.LoggingEvent
处理 TCP 端口接收的数据。
丰富的过滤器插件的存在是 logstash 威力如此强大的重要因素。名为过滤器,其实提供的不单单是过滤的功能。在本章我们就会重点介绍几个插件,它们扩展了进入过滤器的原始数据,进行复杂的逻辑处理,甚至可以无中生有的添加新的 logstash 事件到后续的流程中去!
Grok 是 Logstash 最重要的插件。你可以在 grok 里预定义好命名正则表达式,在稍后(grok参数或者其他正则表达式里)引用它。
运维工程师多多少少都会一点正则。你可以在 grok 里写标准的正则,像下面这样:\s+(?
小贴士:这个正则表达式写法对于 Perl 或者 Ruby 程序员应该很熟悉了,Python 程序员可能更习惯写 (?Ppattern),没办法,适应一下吧。
现在给我们的配置文件添加第一个过滤器区段配置。配置要添加在输入和输出区段之间(logstash 执行区段的时候并不依赖于次序,不过为了自己看得方便,还是按次序书写吧):
input {stdin{}}
filter {
grok {
match => {
"message" => "\s+(?\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 表达式见
注意:在新版本的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"]
}
}
match => {
"message" => "(?m)\s+(?\d+(?:\.\d+)?)\s+"
}
logstash/filters/grok
插件的 match 参数应该接受的是一个 Hash 值。但是因为早期的 logstash 语法中 Hash 值也是用 [] 这种方式书写的,所以其实现在传递 Array 值给 match 参数也完全没问题。所以,我们这里其实可以传递多个正则来匹配同一个字段:match => [
"message", "(?\d+(?:\.\d+)?)" ,
"message", "%{SYSLOGBASE} %{DATA:message}",
"message", "(?m)%{WORD}"
]
logstash 会按照这个定义次序依次尝试匹配,到匹配成功为止。虽说效果跟用 | 分割写个大大的正则是一样的,但是可阅读性好了很多。
最后也是最关键的,我强烈建议每个人都要使用 Grok Debugger 来调试自己的 grok 表达式。
之前章节已经提过,filters/date
插件可以用来转换你的日志记录中的时间字符串,变成 LogStash::Timestamp
对象,然后转存到 @timestamp 字段里。
注意:因为在稍后的
outputs/elasticsearch
中常用的%{+YYYY.MM.dd}
这种写法必须读取 @timestamp 数据,所以一定不要直接删掉这个字段保留自己的字段,而是应该用 filters/date 转换后删除自己的字段!
这在导入旧数据的时候固然非常有用,而在实时数据处理的时候同样有效,因为一般情况下数据流程中我们都会有缓冲区,导致最终的实际处理时间跟事件产生时间略有偏差。
小贴士:个人强烈建议打开 Nginx 的 access_log 配置项的 buffer 参数,对极限响应性能有极大提升!
filters/date 插件支持五种时间格式:
- ISO8601
类似 “2011-04-19T03:44:01.103Z” 这样的格式。具体Z后面可以有 “08:00”也可以没有,”.103”这个也可以没有。常用场景里来说,Nginx 的 log_format 配置里就可以使用 $time_iso8601 变量来记录请求时间成这种格式。
- UNIX
UNIX 时间戳格式,记录的是从 1970 年起始至今的总秒数。Squid 的默认日志格式中就使用了这种格式。
UNIX_MS
这个时间戳则是从 1970 年起始至今的总毫秒数。据我所知,JavaScript 里经常使用这个时间格式。
TAI64N
TAI64N 格式比较少见,是这个样子的:@4000000052f88ea32489532c。我目前只知道常见应用中, qmail 会用这个格式。
Joda-Time 库
Logstash 内部使用了 Java 的 Joda 时间库来作时间处理。所以我们可以使用 Joda 库所支持的时间格式来作具体定义。Joda 时间格式定义见下表:
时间格式
Symbol | Meaning | Presentation | Examples |
---|---|---|---|
G | era | text | AD |
C | century of era (>=0) | number | 20 |
Y | year of era (>=0) | year | 1996 |
x | weekyear | year | 1996 |
w | week of weekyear | number | 27 |
e | day of week | number | 2 |
E | day of week | text | Tuesday; Tue |
y | year | year | 1996 |
D | day of year | number | 189 |
M | month of year | month | July; Jul; 07 |
d | day of month | number | 10 |
a | halfday of day | text | PM |
K | hour of halfday (0~11) | number | 0 |
h | clockhour of halfday (1~12) | number | 12 |
H | hour of day (0~23) | number | 0 |
k | clockhour of day (1~24) | number | 24 |
m | minute of hour | number | 30 |
s | second of minute | number | 55 |
S | fraction of second | number | 978 |
z | time zone | text | Pacific Standard Time; PST |
Z | time zone offset/id | zone | -0800; -08:00; America/Los_Angeles |
‘ | escape for text | delimiter | |
” | single quote | literal | ‘ |
下面我们写一个 Joda 时间格式的配置作为示例:
filter {
grok {
match => ["message", "%{HTTPDATE:logdate}"]
}
date {
match => ["logdate", "dd/MMM/yyyy:HH:mm:ss Z"]
}
}
注意:时区偏移量只需要用一个字母 Z 即可
。
很多中国用户经常提一个问题:为什么 @timestamp 比我们早了 8 个小时?怎么修改成北京时间?
其实,Elasticsearch 内部,对时间类型字段,是统一采用 UTC 时间,存成 long 长整形数据的!对日志统一采用 UTC 时间存储,是国际安全/运维界的一个通识——欧美公司的服务器普遍广泛分布在多个时区里——不像中国,地域横跨五个时区却只用北京时间。
对于页面查看,ELK 的解决方案是在 Kibana 上,读取浏览器的当前时区,然后在页面上转换时间内容的显示。
所以,建议大家接受这种设定。否则,即便你用.getLocalTime
修改,也还要面临在 Kibana 上反过去修改,以及 Elasticsearch 原有的["now-1h" TO "now"]
这种方便的搜索语句无法正常使用的尴尬。
以上,请读者自行斟酌。
filters/mutate 插件是 Logstash 另一个重要插件。它提供了丰富的基础类型数据处理能力。包括类型转换,字符串处理和字段处理等。
类型转换是 filters/mutate 插件最初诞生时的唯一功能。其应用场景在之前 Codec/JSON小节已经提到。
可以设置的转换类型包括:”integer”,”float” 和 “string”。示例如下:
filter {
mutate {
convert => ["request_time", "float"]
}
}
注意:mutate 除了转换简单的字符值,还支持对数组类型的字段进行转换,即将 [“1”,”2”] 转换成 [1,2]。但不支持对哈希类型的字段做类似处理。有这方面需求的可以采用稍后讲述的 filters/ruby 插件完成。
gsub => ["urlparams", "[\\?#]", "_"]
filter {
mutate {
split => ["message", "|"]
}
}
随意输入一串以|分割的字符,比如 “123|321|adfd|dfjld*=123”,可以看到如下输出:
{
"message" => [
[0] "123",
[1] "321",
[2] "adfd",
[3] "dfjld*=123"
],
"@version" => "1",
"@timestamp" => "2014-08-20T15:58:23.120Z",
"host" => "raochenlindeMacBook-Air.local"
}
filter {
mutate {
split => ["message", "|"]
}
mutate {
join => ["message", ","]
}
}
filter 区段之内,是顺序执行
的。所以我们最后看到的输出结果是:
{
"message" => "123,321,adfd,dfjld*=123",
"@version" => "1",
"@timestamp" => "2014-08-20T16:01:33.972Z",
"host" => "raochenlindeMacBook-Air.local"
}
filter {
mutate {
split => ["message", "|"]
}
mutate {
merge => ["message", "message"]
}
}
我们会看到输出:
{
"message" => [
[0] "123",
[1] "321",
[2] "adfd",
[3] "dfjld*=123",
[4] "123",
[5] "321",
[6] "adfd",
[7] "dfjld*=123"
],
"@version" => "1",
"@timestamp" => "2014-08-20T16:05:53.711Z",
"host" => "raochenlindeMacBook-Air.local"
}
如果 src 字段是字符串,会自动先转换成一个单元素的数组再合并。把上一示例中的来源字段改成 “host”:
filter {
mutate {
split => ["message", "|"]
}
mutate {
merge => ["message", "host"]
}
}
{
"message" => [
[0] "123",
[1] "321",
[2] "adfd",
[3] "dfjld*=123",
[4] "raochenlindeMacBook-Air.local"
],
"@version" => "1",
"@timestamp" => "2014-08-20T16:07:53.533Z",
"host" => [
[0] "raochenlindeMacBook-Air.local"
]
}
看,目的字段 “message” 确实多了一个元素,但是来源字段 “host” 本身也由字符串类型变成数组类型了!
下面你猜,如果来源位置写的不是字段名而是直接一个字符串,会产生什么奇特的效果呢?
- strip
- lowercase
- uppercase
filter {
mutate {
rename => ["syslog_host", "host"]
}
}
需要注意的是,filter/mutate 内部是有执行次序的。其次序如下:
rename(event) if @rename
update(event) if @update
replace(event) if @replace
convert(event) if @convert
gsub(event) if @gsub
uppercase(event) if @uppercase
lowercase(event) if @lowercase
strip(event) if @strip
remove(event) if @remove
split(event) if @split
join(event) if @join
merge(event) if @merge
filter_matched(event)
而 filter_matched
这个 filters/base.rb 里继承的方法也是有次序的。
@add_field.each do |field, value|
end
@remove_field.each do |field|
end
@add_tag.each do |tag|
end
@remove_tag.each do |tag|
end
GeoIP 是最常见的免费 IP 地址归类查询库,同时也有收费版可以采购。GeoIP 库可以根据 IP 地址提供对应的地域信息,包括国别,省市,经纬度等,对于可视化地图和区域统计非常有用。
filter {
geoip {
source => "message"
}
}
{
"message" => "183.60.92.253",
"@version" => "1",
"@timestamp" => "2014-08-07T10:32:55.610Z",
"host" => "raochenlindeMacBook-Air.local",
"geoip" => {
"ip" => "183.60.92.253",
"country_code2" => "CN",
"country_code3" => "CHN",
"country_name" => "China",
"continent_code" => "AS",
"region_name" => "30",
"city_name" => "Guangzhou",
"latitude" => 23.11670000000001,
"longitude" => 113.25,
"timezone" => "Asia/Chongqing",
"real_region_name" => "Guangdong",
"location" => [
[0] 113.25,
[1] 23.11670000000001
]
}
}
GeoIP 库数据较多,如果你不需要这么多内容,可以通过 fields 选项指定自己所需要的。下例为全部可选内容:
filter {
geoip {
fields => ["city_name", "continent_code", "country_code2", "country_code3", "country_name", "dma_code", "ip", "latitude", "longitude", "postal_code", "region_name", "timezone"]
}
}
需要注意的是:geoip.location
是 logstash 通过 latitude
和 longitude
额外生成的数据。所以,如果你是想要经纬度又不想重复数据的话,应该像下面这样做:
filter {
geoip {
fields => ["city_name", "country_code2", "country_name", "latitude", "longitude", "region_name"] remove_field => ["[geoip][latitude]", "[geoip][longitude]"]
}
}
geoip 插件的 “source” 字段可以是任一处理后的字段,比如 “client_ip”,但是字段内容却需要小心!geoip 库内只存有公共网络上的 IP 信息,查询不到结果的,会直接返回 null,而 logstash 的 geoip 插件对 null 结果的处理是:不生成对应的 geoip.字段。
所以读者在测试时,如果使用了诸如 127.0.0.1, 172.16.0.1, 182.168.0.1, 10.0.0.1 等内网地址,会发现没有对应输出!
在上一章,已经讲过在 codec 中使用 JSON 编码。但是,有些日志可能是一种复合的数据结构,其中只是一部分记录是 JSON 格式的。这时候,我们依然需要在 filter 阶段,单独启用 JSON 解码插件。
filter {
json {
source => "message"
target => "jsoncontent"
}
}
{
"@version": "1",
"@timestamp": "2014-11-18T08:11:33.000Z",
"host": "web121.mweibo.tc.sinanode.com",
"message": "{\"uid\":3081609001,\"type\":\"signal\"}",
"jsoncontent": {
"uid": 3081609001,
"type": "signal"
}
}
如果不打算使用多层结构的话,删掉 target 配置即可。新的结果如下:
{
"@version": "1",
"@timestamp": "2014-11-18T08:11:33.000Z",
"host": "web121.mweibo.tc.sinanode.com",
"message": "{\"uid\":3081609001,\"type\":\"signal\"}",
"uid": 3081609001,
"type": "signal"
}
上一章我们通过 multiline 插件将多行数据合并进一个事件里,那么反过来,也可以把一行数据,拆分成多个事件。这就是 split 插件。
filter {
split {
field => "message"
terminator => "#"
}
}
这个测试中,我们在 intputs/stdin 的终端中输入一行数据:”test1#test2”,结果看到输出两个事件:
{
"@version": "1",
"@timestamp": "2014-11-18T08:11:33.000Z",
"host": "web121.mweibo.tc.sinanode.com",
"message": "test1"
}
{
"@version": "1",
"@timestamp": "2014-11-18T08:11:33.000Z",
"host": "web121.mweibo.tc.sinanode.com",
"message": "test2"
}
split 插件中使用的是 yield 功能,其结果是 split 出来的新事件,会直接结束其在 filter 阶段的历程,也就是说写在 split 后面的其他 filter 插件都不起作用,进入到 output 阶段。所以,一定要保证 split 配置写在全部 filter 配置的最后。
使用了类似功能的还有 clone 插件。
注:从 logstash-1.5.0beta1 版本以后修复该问题。
filter {
useragent {
target => "ua"
source => "useragent"
}
}
在很多情况下,日志内容本身都是一个类似于 key-value 的格式,但是格式具体的样式却是多种多样的。logstash 提供 filters/kv 插件,帮助处理不同样式的 key-value 日志,变成实际的 LogStash::Event
数据。
filter {
ruby {
init => "@kname = ['method','uri','verb']"
code => "event.append(Hash[@kname.zip(event['request'].split(' '))])"
}
if [uri] {
ruby {
init => "@kname = ['url_path','url_args']"
code => "event.append(Hash[@kname.zip(event['uri'].split('?'))])"
}
kv {
prefix => "url_"
source => "url_args"
field_split => "&"
remove_field => [ "url_args", "uri", "request" ]
}
}
}
Nginx 访问日志中的 $request
,通过这段配置,可以详细切分成 method, url_path, verb, url_a, url_b …
如果你稍微懂那么一点点 Ruby 语法的话,filters/ruby 插件将会是一个非常有用的工具。
比如你需要稍微修改一下 LogStash::Event
对象,但是又不打算为此写一个完整的插件,用 filters/ruby 插件绝对感觉良好。
filter {
ruby {
init => "@kname = ['client','servername','url','status','time','size','upstream','upstreamstatus','upstreamtime','referer','xff','useragent']"
code => "event.append(Hash[@kname.zip(event['message'].split('|'))])"
}
}
官网示例是一个比较有趣但是没啥大用的做法 —— 随机取消 90% 的事件。
所以上面我们给出了一个有用而且强大的实例。
通常我们都是用 filters/grok 插件来捕获字段的,但是正则耗费大量的 CPU 资源,很容易成为 Logstash 进程的瓶颈。
而实际上,很多流经 Logstash 的数据都是有自己预定义的特殊分隔符的,我们可以很简单的直接切割成多个字段。
filters/mutate插件里的 “split” 选项只能切成数组,后续很不方便使用和识别。而在 filters/ruby 里,我们可以通过 “init” 参数预定义好由每个新字段的名字组成的数组,然后在 “code” 参数指定的 Ruby 语句里通过两个数组的 zip 操作生成一个哈希并添加进数组里。短短一行 Ruby 代码,可以减少 50% 以上的 CPU 使用率。
filters/ruby 插件用途远不止这一点,下一节你还会继续见到它的身影。
filter{
date {
match => ["datetime" , "UNIX"]
}
ruby {
code => "event.cancel if 5 * 24 * 3600 < (event['@timestamp']-::Time.now).abs"
}
}
在实际运用中,我们几乎肯定会碰到出乎意料的输入数据。这都有可能导致 Elasticsearch 集群出现问题。
当数据格式发生变化,比如 UNIX 时间格式变成 UNIX_MS 时间格式,会导致 logstash 疯狂创建新索引,集群崩溃。
或者误输入过老的数据时,因为一般我们会 close 几天之前的索引以节省内存,必要时再打开。而直接尝试把数据写入被关闭的索引会导致内存问题。
这时候我们就需要提前校验数据的合法性。上面配置,就是用于过滤掉时间范围与当前时间差距太大的非法数据的。
filters/metrics 插件是使用 Ruby 的 Metriks 模块来实现在内存里实时的计数和采样分析。该模块支持两个类型的数值分析:meter 和 timer。下面分别举例说明:
web 访问日志的异常状态码频率是运维人员会非常关心的一个数据。通常我们的做法,是通过 logstash 或者其他日志分析脚本,把计数发送到 rrdtool 或者 graphite 里面。然后再通过 check_graphite 脚本之类的东西来检查异常并报警。
事实上这个事情可以直接在 logstash 内部就完成。比如如果最近一分钟 504 请求的个数超过 100 个就报警:
filter {
metrics {
meter => "error.%{status}"
add_tag => "metric"
ignore_older_than => 10
}
if "metric" in [tags] {
ruby {
code => "event.cancel if event['error.504.rate_1m'] * 60 < 100"
}
}
}
output {
if "metric" in [tags] {
exec {
command => "echo \"Out of threshold: %{error.504.rate_1m}\""
}
}
}
这里需要注意 *60
的含义。
metriks 模块生成的 rate_1m/5m/15m 意思是:最近 1,5,15 分钟的每秒速率!
官版的 filters/metrics 插件只适用于 metric 事件的检查。由插件生成的新事件内部不存有来自 input 区段的实际数据信息。所以,要完成我们的百分比分布箱体检测,需要首先对代码稍微做几行变动,即在 metric 的 timer 事件里加一个属性,存储最近一个实际事件的数值:
https://github.com/chenryn/logstash/commit/bc7bf34caf551d8a149605cf28e7c5d33fae7458
然后我们就可以用如下配置来探测异常数据了:
filter {
metrics {
timer => {"rt" => "%{request_time}"}
percentiles => [25, 75]
add_tag => "percentile"
}
if "percentile" in [tags] {
ruby {
code => "l=event['rt.p75']-event['rt.p25'];event['rt.low']=event['rt.p25']-l;event['rt.high']=event['rt.p75']+l"
}
}
}
output {
if "percentile" in [tags] and ([rt.last] > [rt.high] or [rt.last] < [rt.low]) {
exec {
command => "echo \"Anomaly: %{rt.last}\""
}
}
}
小贴士:有关 box and shisker plot 内容和重要性,参见《数据之魅》一书。
和之前 inputs/stdin 插件一样,outputs/stdout 插件也是最基础和简单的输出插件。同样在这里简单介绍一下,作为输出插件的一个共性了解。
output {
stdout {
codec => rubydebug
workers => 2
}
}
输出插件统一具有一个参数是 workers
。Logstash 为输出做了多线程的准备。
其次是 codec 设置。codec 的作用在之前已经讲过。可能除了 codecs/multiline ,其他 codec 插件本身并没有太多的设置项。所以一般省略掉后面的配置区段。换句话说。上面配置示例的完全写法应该是:
output {
stdout {
codec => rubydebug {
}
workers => 2
}
}
单就 outputs/stdout 插件来说,其最重要和常见的用途就是调试。所以在不太有效的时候,加上命令行参数 -vv 运行,查看更多详细调试信息。
通过日志收集系统将分散在数百台服务器上的数据集中存储在某中心服务器上,这是运维最原始的需求。早年的 scribed ,甚至直接就把输出的语法命名为 。Logstash 当然也能做到这点。
和 LogStash::Inputs::File 不同, LogStash::Outputs::File 里可以使用 sprintf format 格式来自动定义输出到带日期命名的路径。
output {
file {
path => "/path/to/%{+yyyy/MM/dd/HH}/%{host}.log.gz"
message_format => "%{message}"
gzip => true
}
}
使用 output/file 插件首先需要注意的就是 message_format
参数。插件默认是输出整个 event 的 JSON 形式数据的。这可能跟大多数情况下使用者的期望不符。大家可能只是希望按照日志的原始格式保存就好了。所以需要定义为%{message}
,当然,前提是在之前的 filter 插件中,你没有使用 remove_field
或者 update
等参数删除或修改 %{message}
字段的内容。
另一个非常有用的参数是 gzip。gzip 格式是一个非常奇特而友好的格式。其格式包括有:
- 10字节的头,包含幻数、版本号以及时间戳
- 可选的扩展头,如原文件名
- 文件体,包括DEFLATE压缩的数据
- 8字节的尾注,包括CRC-32校验和以及未压缩的原始数据长度
这样 gzip 就可以一段一段的识别出来数据 —— 反过来说,也就是可以一段一段压缩了添加在后面!
这对于我们流式添加数据简直太棒了!
小贴士:你或许见过网络流传的 parallel 命令行工具并发处理数据的神奇文档,但在自己用的时候总见不到效果。实际上就是因为:文档中处理的 gzip 文件,可以分开处理然后再合并的。
Logstash 早期有三个不同的 elasticsearch 插件。到 1.4.0 版本的时候,开发者彻底重写了 LogStash::Outputs::Elasticsearch 插件。从此,我们只需要用这一个插件,就能任意切换使用 Elasticsearch 集群支持的各种不同协议了。
output {
elasticsearch {
host => "192.168.0.2"
protocol => "http"
index => "logstash-%{type}-%{+YYYY.MM.dd}"
index_type => "%{type}"
workers => 5
template_overwrite => true
}
}
一个小集群里,使用 node 协议最方便了。Logstash 以 elasticsearch 的 client 节点身份(即不存数据不参加选举)运行。如果你运行下面这行命令,你就可以看到自己的 logstash 进程名,对应的 node.role 值是 c:
# curl 127.0.0.1:9200/_cat/nodes?v
host ip heap.percent ram.percent load node.role master name
local 192.168.0.102 7 c - logstash-local-1036-2012
local 192.168.0.2 7 d * Sunstreak
特别的,作为一个快速运行示例的需要,你还可以在 logstash 进程内部运行一个内嵌的 elasticsearch 服务器。内嵌服务器默认会在 $PWD/data
目录里存储索引。如果你想变更这些配置,在 $PWD/elasticsearch.yml
文件里写自定义配置即可,logstash 会尝试自动加载这个文件。
对于拥有很多索引的大集群,你可以用 transport 协议。logstash 进程会转发所有数据到你指定的某台主机上。这种协议跟上面的 node 协议是不同的。node 协议下的进程是可以接收到整个 Elasticsearch 集群状态信息的,当进程收到一个事件时,它就知道这个事件应该存在集群内哪个机器的分片里,所以它就会直接连接该机器发送这条数据。而 transport 协议下的进程不会保存这个信息,在集群状态更新(节点变化,索引变化都会发送全量更新)时,就不会对所有的 logstash 进程也发送这种信息。更多 Elasticsearch 集群状态的细节,参阅http://www.elasticsearch.org/guide。
如果你已经有现成的 Elasticsearch 集群,但是版本跟 logstash 自带的又不太一样,建议你使用 http 协议。Logstash 会使用 POST 方式发送数据。
- 小贴士
Logstash 1.4.2 在 transport 和 http 协议的情况下是固定连接指定 host 发送数据。从 1.5.0 开始,host 可以设置数组,它会从节点列表中选取不同的节点发送数据,达到 Round-Robin 负载均衡的效果。
Kibana4 强制要求 ES 全集群所有 node 版本在 1.4 以上,所以采用 node 方式发送数据的 logstash-1.4(携带的 Elasticsearch.jar 库是 1.1.1 版本) 会导致 Kibana4 无法运行,采用 Kibana4 的读者务必改用 http 方式。
开发者在 IRC freenode#logstash 频道里表示:”高于 1.0 版本的 Elasticsearch 应该都能跟最新版 logstash 的 node 协议一起正常工作”。此信息仅供参考,请认真测试后再上线。
- 性能问题
Logstash 1.4.2 在 http 协议下默认使用作者自己的 ftw 库,随同分发的是 0.0.39 版。该版本有内存泄露问题,长期运行下输出性能越来越差!
解决办法:
对性能要求不高的,可以在启动 logstash 进程时,配置环境变量
ENV["BULK"]
,强制采用 elasticsearch 官方 Ruby 库。命令如下:export BULK="esruby"
对性能要求高的,可以尝试采用 logstash-1.5.0RC2 。新版的 outputs/elasticsearch 放弃了 ftw 库,改用了一个 JRuby 平台专有的 Manticore 库。根据测试,性能跟 ftw 比相当接近。
对性能要求极高的,可以手动更新 ftw 库版本,目前最新版是 0.0.42 版,据称内存问题在 0.0.40 版即解决。
Elasticsearch 支持给索引预定义设置和 mapping(前提是你用的 elasticsearch 版本支持这个 API,不过估计应该都支持)。Logstash 自带有一个优化好的模板,内容如下:
{
"template" : "logstash-*",
"settings" : {
"index.refresh_interval" : "5s"
},
"mappings" : {
"_default_" : {
"_all" : {"enabled" : true},
"dynamic_templates" : [ {
"string_fields" : {
"match" : "*",
"match_mapping_type" : "string",
"mapping" : { "type" : "string", "index" : "analyzed", "omit_norms" : true, "fields" : { "raw" : {"type": "string", "index" : "not_analyzed", "ignore_above" : 256} } } }
} ],
"properties" : {
"@version": { "type": "string", "index": "not_analyzed" },
"geoip" : {
"type" : "object",
"dynamic": true,
"path": "full",
"properties" : { "location" : { "type" : "geo_point" } } }
}
}
}
}
这其中的关键设置包括:
template for index-pattern
只有匹配
logstash-*
的索引才会应用这个模板。有时候我们会变更 Logstash 的默认索引名称,记住你也得通过 PUT 方法上传可以匹配你自定义索引名的模板。当然,我更建议的做法是,把你自定义的名字放在"logstash-"
后面,变成index => "logstash-custom-%{+yyyy.MM.dd}"
这样。
refresh_interval for indexing
Elasticsearch 是一个近实时搜索引擎。它实际上是每 1 秒钟刷新一次数据。对于日志分析应用,我们用不着这么实时,所以 logstash 自带的模板修改成了 5 秒钟。你还可以根据需要继续放大这个刷新间隔以提高数据写入性能。
multi-field with not_analyzed
Elasticsearch 会自动使用自己的默认分词器(空格,点,斜线等分割)来分析字段。分词器对于搜索和评分是非常重要的,但是大大降低了索引写入和聚合请求的性能。所以 logstash 模板定义了一种叫”多字段”(multi-field)类型的字段。这种类型会自动添加一个 “.raw” 结尾的字段,并给这个字段设置为不启用分词器。简单说,你想获取 url 字段的聚合结果的时候,不要直接用 “url” ,而是用 “url.raw” 作为字段名。
geo_point
Elasticsearch 支持 geo_point 类型, geo distance 聚合等等。比如说,你可以请求某个 geo_point 点方圆 10 千米内数据点的总数。在 Kibana 的 bettermap 类型面板里,就会用到这个类型的数据。
doc_values
doc_values 是 Elasticsearch 1.3 版本引入的新特性。启用该特性的字段,索引写入的时候会在磁盘上构建 fielddata。而过去,fielddata 是固定只能使用内存的。在请求范围加大的时候,很容易触发 OOM 报错:
ElasticsearchException[org.elasticsearch.common.breaker.CircuitBreakingException: Data too large, data for field [@timestamp] would be larger than limit of [639015321/609.4mb]]
doc_values 只能给不分词(对于字符串字段就是设置了 "index":"not_analyzed"
,数值和时间字段默认就没有分词) 的字段配置生效。
doc_values 虽然用的是磁盘,但是系统本身也有自带 VFS 的 cache 效果并不会太差。据官方测试,经过 1.4 的优化后,只比使用内存的 fielddata 慢 15% 。所以,在数据量较大的情况下,强烈建议开启该配置:
{
"template" : "logstash-*",
"settings" : {
"index.refresh_interval" : "5s"
},
"mappings" : {
"_default_" : {
"_all" : {"enabled" : true},
"dynamic_templates" : [ {
"string_fields" : {
"match" : "*",
"match_mapping_type" : "string",
"mapping" : { "type" : "string", "index" : "analyzed", "omit_norms" : true, "fields" : { "raw" : { "type": "string", "index" : "not_analyzed", "ignore_above" : 256, "doc_values": true } } } }
} ],
"properties" : {
"@version": { "type": "string", "index": "not_analyzed" },
"@timestamp": { "type": "date", "index": "not_analyzed", "doc_values": true, "format": "dateOptionalTime" },
"geoip" : {
"type" : "object",
"dynamic": true,
"path": "full",
"properties" : { "location" : { "type" : "geo_point" } } }
}
}
}
}
order
如果你有自己单独定制 template 的想法,很好。这时候有几种选择:
在 logstash/outputs/elasticsearch 配置中开启
manage_template => false
选项,然后一切自己动手;在 logstash/outputs/elasticsearch 配置中开启
template => "/path/to/your/tmpl.json"
选项,让 logstash 来发送你自己写的 template 文件;避免变更 logstash 里的配置,而是另外发送一个 template ,利用 elasticsearch 的
templates order
功能。
这个 order 功能,就是 elasticsearch 在创建一个索引的时候,如果发现这个索引同时匹配上了多个 template ,那么就会先应用 order 数值小的 template 设置,然后再应用一遍 order 数值高的作为覆盖,最终达到一个 merge 的效果。
比如,对上面这个模板已经很满意,只想修改一下 refresh_interval
,那么只需要新写一个:
{
"order" : 1,
"template" : "logstash-*",
"settings" : {
"index.refresh_interval" : "20s"
}
}
然后运行 curl -XPUT http://localhost:9200/_template/template_newid -d '@/path/to/your/tmpl.json'
即可。
logstash 默认的模板, order 是 0,id 是 logstash,通过 logstash/outputs/elasticsearch
的配置选项 template_name 修改。你的新模板就不要跟这个名字冲突了。
推荐阅读: http://www.elasticsearch.org/guide
input { stdin {} }
output {
redis {
data_type => "channel"
key => "logstash-chan-%{+yyyy.MM.dd}"
}
}
我们还是继续先用 redis-cli 命令行来演示 outputs/redis 插件的实质。
- basical use case
运行 logstash 进程,然后另一个终端启动 redis-cli 命令。输入订阅指定频道的 Redis 命令 ("SUBSCRIBE logstash-chan-2014.08.08"
) 后,首先会看到一个订阅成功的返回信息。如下所示:
# redis-cli
127.0.0.1:6379> SUBSCRIBE logstash-chan-2014.08.08
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "logstash-chan-2014.08.08"
3) (integer) 1
好,在运行 logstash 的终端里输入 “hello world” 字符串。切换回 redis-cli 的终端,你发现已经自动输出了一条信息:
1) "message"
2) "logstash-chan-2014.08.08"
3) "{\"message\":\"hello world\",\"@version\":\"1\",\"@timestamp\":\"2014-08-08T16:34:21.865Z\",\"host\":\"raochenlindeMacBook-Air.local\"}"
看起来是不是非常眼熟?这一串字符其实就是我们在 inputs/redis 一节中使用的那段数据。
看,这样就把 outputs/redis 和 inputs/redis 串联起来了吧!
事实上,这就是我们使用 redis 服务器作为 logstassh 架构中 broker 角色的原理。
让我们把这两节中不同配置的 logstash 进程分别在两个终端运行起来,这次不再要运行 redis-cli 命令了。在配有 outputs/redis 这端输入 “hello world”,配有 “inputs/redis” 的终端上,就自动输出数据了!
- notification use case
我们还可以用其他程序来订阅 redis 频道,程序里就可以随意写其他逻辑了。你可以看看 output/juggernaut插件的原理。这个 Juggernaut 就是基于 redis 服务器和 socket.io 框架构建的。利用它,logstash 可以直接向 webkit 等支持 socket.io 的浏览器推送告警信息。
和 LogStash::Inputs::Redis
一样,这里也有设置成 list 的方式。使用 RPUSH
命令发送给 redis 服务器,效果和之前展示的完全一致。包括可以调整的参数 batch_event
,也在之前章节中讲过。这里不再重复举例。
虽然之前我们已经提到过不建议直接使用LogStash::Inputs::TCP
和 LogStash::Outputs::TCP
做转发工作,不过在实际交流中,发现确实有不少朋友觉得这种简单配置足够使用,因而不愿意多加一层消息队列的。所以,还是把 Logstash 如何直接发送 TCP 数据也稍微提点一下。
output {
tcp {
host => "192.168.0.2"
port => 8888
codec => json_lines
}
}
在收集端采用 tcp 方式发送给远端的 tcp 端口。这里需要注意的是,默认的 codec 选项是 json。而远端的LogStash::Inputs::TCP
的默认 codec 选项却是 plain !所以不指定各自的 codec ,对接肯定是失败的。
另外,由于IO BUFFER
的原因,即使是两端共同约定为 json 依然无法正常运行,接收端会认为一行数据没结束,一直等待直至自己 OutOfMemory
!
所以,正确的做法是,发送端指定 codec 为 json_lines
,这样每条数据后面会加上一个回车,接收端指定 codec 为 json_lines
或者 json 均可,这样才能正常处理。包括在收集端已经切割好的字段,也可以直接带入收集端使用了。
Statsd 最早是 2008 年 Flickr 公司用 Perl 写的针对 graphite、datadog 等监控数据后端存储开发的前端网络应用,2011 年 Etsy 公司用 nodejs 重构。用于接收、写入、读取和聚合时间序列数据,包括即时值和累积值等。
output {
statsd {
host => "statsdserver.domain.com"
namespace => "logstash"
sender => "%{host}"
increment => ["httpd.response.%{status}"]
}
}
Graphite 以树状结构存储监控数据,所以 statsd 也是如此。所以发送给 statsd 的数据的 key 也一定得是 "first.second.tree.four"
这样的形式。而在 outputs/statsd 插件中,就会以三个配置参数来拼接成这种形式:
namespace.sender.metric
其中 namespace 和 sender 都是直接设置的,而 metric 又分为好几个不同的参数可以分别设置。statsd 支持的 metric 类型如下:
示例语法:increment => ["nginx.status.%{status}"]
语法同 increment。
示例语法:count => {"nginx.bytes" => "%{bytes}"}
语法同 count。
语法同 count。
语法同 count。
关于这些 metric 类型的详细说明,请阅读 statsd 文档:https://github.com/etsy/statsd/blob/master/docs/metric_types.md。
Logstash 中有两个 output 插件是 nagios 有关的。outputs/nagios
插件发送数据给本机的 nagios.cmd
管道命令文件,outputs/nagios_nsca
插件则是 调用 send_nsca
命令以 NSCA 协议格式把数据发送给 nagios 服务器(远端或者本地皆可)。
nagios.cmd 是 nagios 服务器的核心组件。nagios 事件处理和内外交互都是通过这个管道文件来完成的。
使用 CMD 方式,需要自己保证发送的 Logstash 事件符合 nagios 事件的格式。即必须在 filter 阶段预先准备好 nagios_host
和 nagios_service
字段;此外,如果在 filter 阶段也准备好 nagios_annotation
和 nagios_level
字段,这里也会自动转换成 nagios 事件信息。
filter {
if [message] =~ /err/ {
mutate {
add_tag => "nagios"
rename => ["host", "nagios_host"]
replace => ["nagios_service", "logstash_check_%{type}"]
}
}
}
output {
if "nagios" in [tags] {
nagios { }
}
}
如果不打算在 filter 阶段提供 nagios_level
,那么也可以在该插件中通过参数配置。
所谓 nagios_level
,即我们通过 nagios plugin 检查数据时的返回值。其取值范围和含义如下:
- “0”,代表 “OK”,服务正常;
- “1”,代表 “WARNNING”,服务警告,一般 nagios plugin 命令中使用 -w 参数设置该阈值;
- “2”,代表 “CRITICAL”,服务危急,一般 nagios plugin 命令中使用 -c 参数设置该阈值;
- “3”,代表 “UNKNOWN”,未知状态,一般会在 timeout 等情况下出现。
默认情况下,该插件会以 “CRITICAL” 等级发送报警给 Nagios 服务器。
nagios.cmd 文件的具体位置,可以使用command_file
参数设置。默认位置是 "/var/lib/nagios3/rw/nagios.cmd"
。
关于和 nagios.cmd 交互的具体协议说明,有兴趣的读者请阅读 Using external commands in Nagios 一文,这是《Learning Nagios 3.0》书中内容节选。
NSCA 是一种标准的 nagios 分布式扩展协议。分布在各机器上的 send_nsca
进程主动将监控数据推送给远端 nagios 服务器的 NSCA 进程。
当 Logstash 跟 nagios 服务器没有在同一个主机上运行的时候,就只能通过 NSCA 方式来发送报警了 —— 当然也必须在 Logstash 服务器上安装send_nsca
命令。
nagios 事件所需要的几个属性在上一段中已经有过描述。不过在使用这个插件的时候,不要求提前准备好,而是可以在该插件内部定义参数:
output {
nagios_nsca {
nagios_host => "%{host}"
nagios_service => "logstash_check_%{type}"
nagios_status => "2"
message_format => "%{@timestamp}: %{message}"
host => "nagiosserver.domain.com"
}
}
这里请注意,host
和 nagios_host
两个参数,分别是用来设置 nagios 服务器的地址,和报警信息中有问题的服务器地址。
关于 NSCA 原理,架构和配置说明,还不了解的读者请阅读官方网站 Using NSClient++ from nagios with NSCA 一节。
除了 nagios 以外,logstash 同样可以发送信息给其他常见监控系统。方式和 nagios 大同小异:
outputs/ganglia 插件通过 UDP 协议,发送 gmetric 型数据给本机/远端的 gmond
或者 gmetad
outputs/zabbix 插件调用本机的 zabbix_sender
命令发送
output {
email {
to => "[email protected],[email protected]"
cc => "[email protected]"
via => "smtp"
subject => "Warning: %{title}"
options => {
smtpIporHost => "localhost",
port => 25,
domain => 'localhost.localdomain',
userName => nil,
password => nil,
authenticationType => nil, # (plain, login and cram_md5)
starttls => true
}
htmlbody => ""
body => ""
attachments => ["/path/to/filename"]
}
}
outputs/email 插件支持 SMTP 协议和 sendmail 两种方式,通过 via 参数设置。SMTP 方式有较多的 options 参数可配置。sendmail 只能利用本机上的 sendmail 服务来完成 —— 文档上描述了 Mail 库支持的 sendmail 配置参数,但实际代码中没有相关处理,不要被迷惑了。。。
outputs/exec 插件的运用也非常简单,如下所示,将 logstash 切割成的内容作为参数传递给命令。这样,在每个事件到达该插件的时候,都会触发这个命令的执行。
output {
exec {
command => "sendsms.pl \"%{message}\" -t %{user}"
}
}
需要注意的是。这种方式是每次都重新开始执行一次命令并退出。本身是比较慢速的处理方式(程序加载,网络建联等都有一定的时间消耗)。最好只用于少量的信息处理场景,比如不适用 nagios 的其他报警方式。示例就是通过短信发送消息。