在 Logstash 中有一个叫做 aggregate 的 filter。此过滤器的目的是聚合属于同一任务的多个事件(通常是日志行)中的可用信息,最后将聚合的信息推送到最终任务事件中。你应该非常小心地将 Logstash 过滤器工作程序设置为 1(-w 1 标志),此过滤器才能正常工作,否则事件可能会被按顺序处理,并且会发生意外的结果。在 Elastic 的官方文档中,你可以看到很多相应的例子。
在今天的例子中,我们将以一个关系数据库的例子为例来讲述如何把一个关系数据库中的数据导入到 Elasticsearch 中,并形成 nested 数据类型。这个对于很多的数据库搜索是非常有用的。在我们的例子中,一个关系数据库是这样的:
就如同上面所显示的那样,我们在关系数据库中有两个表格:一个是员工个人的详细情况 employees,而另外一个表格是孩子的表格 children。通过关联 employee_id,我们可以找到一个员工的所有的子女。这个显然是 N:N 的一种关系。一个员工可能有多个子女。在之前的教程 “Logstash:如何使用 Logstash 和 JDBC 确保 Elasticsearch 与关系型数据库保持同步”,我详述了如何从一个数据吧数据导入到 Elasticsearch 中。我们可以在 statement 中,使用 join 把上面的两个表格结合起来,并形成一个 id 的多个事件。
在今天的教程中,我来讲述如何把这多个事件进行聚合,并形成一个单独的事件,并在 Elasticsearch 中形成一个 nested 的数据类型。
在今天的练习中,我们准备了如下的练习数据:
logstash_aggregate.conf
input {
generator {
lines => [
'{ "id": "1", "name": "Adam", "child": "Alex"}',
'{ "id": "1", "name": "Adam", "child": "Galen"}'
]
count => 1
codec => "json"
}
}
filter {}
output {
stdout {
codec => "rubydebug"
}
}
在上面,我们使用 generator 来生成两个模拟的数据。它们相当于从 employees 和 children 两个表格里 join 过后的事件。在这里必须注意的是,它们的 id 为 “1”。这个非常重要,aggregate filter 是针对同一个任务的多个事件。针对我们的情况,就是 id 都是一样的。这个事件只发生一次,并使用 json decoder。我们在 filter 的部分不使用东西。
我们可以使用如下的命令来启动 Logstash:
./bin/logstash -r --java-execution=false -w 1 -f logstash_aggregate.conf
上面的 -r 表示 reload,也就是当我们的 logstash_aggregate.conf 的文件发生改变时,会自动装载这个最新的配置文件。 -w 1 表明是只使用一个 worker,这样才能使得 aggregate filter 能工作正常。请注意上面的 --java-execution=false 设置。由于一些原因,我们强制 ruby 方式来执行 Logstash,而不是 java。
运行上面的命令显示的结果是这样的:
我们看到了两个事件。在上面我们看到了一些我们并不想要的字段。我们通过修改 filter 的部分去掉 host, sequence, @version 及 @timestamp 字段。
filter {
mutate {
remove_field => [
"host", "sequence", "@version", "@timestamp"
]
}
}
这样我们的输出结果是这样的:
从上面,我们看到,那些不想要的字段都被删除了。
接下来,我们来使用 aggregate filter。它被用来把相同的 id 进行组合起来,并聚合他们。所有相同的 id 的事件都会被 aggregate 起来。我们重新修改 filter 部分:
filter {
mutate {
remove_field => [
"host", "sequence", "@version", "@timestamp"
]
}
aggregate {
"task_id" => "%{id}"
"code" => "
map['m_id'] = event.get('id')
"
}
}
经过这样的修改,我们在输出部分发现没有任何的变化,输出的结果和上一个输出是完全一样的。这是可以理解的。原因是我们没有发布任何的 event。我们需要在 aggregate filter 的部分添加 push_previous_map_as_event。这样当 task_id 发生改变或者 pipeline 运行完毕时,会自动触发事件。我们进一步修改 filter:
filter {
aggregate {
task_id => "%{id}"
code => "
map['m_id'] = event.get('id')
"
push_previous_map_as_event => true
}
mutate {
remove_field => [
"host", "sequence", "@version", "@timestamp"
]
}
}
上面的返回的结果是:
从上面,我们可以看出来总共有三个事件。多出来的一个就是我们使用 aggregate filter 产生的。这个是一个聚合的结果。上面显示我们已经得到了一个字段 m_id 为1。我们接着改造 filter 部分,我们删除不需要的 tags 字段:
filter {
aggregate {
task_id => "%{id}"
code => "
map['m_id'] = event.get('id')
"
push_previous_map_as_event => true
}
mutate {
remove_field => [
"tags", "host", "sequence", "@version", "@timestamp"
]
}
}
运行的结果是:
接下来,我们把 name 及 child 字段都加进来:
filter {
aggregate {
task_id => "%{id}"
code => "
map['m_id'] = event.get('id')
map['name'] = event.get('name')
map['child'] = event.get('child')
"
push_previous_map_as_event => true
}
mutate {
remove_field => [
"tags", "host", "sequence", "@version", "@timestamp"
]
}
}
上面运行的结果是:
我们已经接近目的了。我们没有看到 child 是一个数组,这是因为它被最后的一个事件所覆盖了。我们需要再次进行改造:
filter {
aggregate {
task_id => "%{id}"
code => "
map['m_id'] = event.get('id')
map['name'] = event.get('name')
if event.get('child') then
map['child'] ||=[]
if !(map['child'].include? event.get('child')) then
map['child'] << event.get('child')
end
end
"
push_previous_map_as_event => true
}
mutate {
remove_field => [
"tags", "host", "sequence", "@version", "@timestamp"
]
}
}
在上面的 code 部分:
code => "
map['m_id'] = event.get('id')
map['name'] = event.get('name')
if event.get('child') then
map['child'] ||=[]
if !(map['child'].include? event.get('child')) then
map['child'] << event.get('child')
end
end
"
我们首先得到 m_id 字段的值,然后得到 name 字段的值。再接着检查是否有 child 字段,如果没有的话,初始化一个空的数组。再接着检查当前的 child 字段是否含有当前的值,如果没有的话就添加到数组里去,并形成最终的 child 字段。运行上面的配置后:
我们可以看到 child 字段现在变成了一个数值。它同时含有 Alex 及 Galen 两个字符串。现在剩下的问题是,我们多了两个不想要的事件。我们再接着改造 filter 这个部分:
filter {
aggregate {
task_id => "%{id}"
code => "
map['m_id'] = event.get('id')
map['name'] = event.get('name')
if event.get('child') then
map['child'] ||=[]
if !(map['child'].include? event.get('child')) then
map['child'] << event.get('child')
end
end
event.cancel()
"
push_previous_map_as_event => true
}
mutate {
remove_field => [
"tags", "host", "sequence", "@version", "@timestamp"
]
}
}
这次,我们添加了 event.cancel(),这样就可以取消之前的事件,而只保留我们聚合后的事件。运行之后的结果是:
显然这个是我们想要的结果。
然后,我们修改 Logstash 的 output 部分:
logstash_aggregate.conf
input {
generator {
lines => [
'{ "id": "1", "name": "Adam", "child": "Alex"}',
'{ "id": "1", "name": "Adam", "child": "Galen"}'
]
count => 1
codec => "json"
}
}
filter {
aggregate {
task_id => "%{id}"
code => "
map['m_id'] = event.get('id')
map['name'] = event.get('name')
if event.get('child') then
map['child'] ||=[]
if !(map['child'].include? event.get('child')) then
map['child'] << event.get('child')
end
end
event.cancel()
"
push_previous_map_as_event => true
}
mutate {
remove_field => [
"tags", "host", "sequence", "@version", "@timestamp"
]
}
mutate {
rename => ["child", "children" ]
}
}
output {
stdout {
codec => "rubydebug"
}
elasticsearch {
index => "employees"
hosts => "localhost:9200"
}
}
重新运行我们的 Logstash。我们可以在 Kibana 中查看到最新导入的文件:
GET employees/_search