用运行时字段探索你的数据
考虑你有一个非常大的日志数据的数据集你想要从中提取字段。索引这些数据是耗时的并会使用大量的磁盘空间,你只是想探索数据的结构而不想预先提交一个模式。
你知道你的日志数据里包含你想要提取的特定字段。在这个案例中,我们想要关注 @timestamp 和 message 字段。通过使用运行时字段,你可以定义一个脚本在搜索时计算这些字段的值。
1. 定义索引的字段作为起点
你可以以一个简单的示例开始,将 @timestamp 和 message 字段作为索引字段添加到 my-index-000001 的映射里。为保持灵活性,使用 wildcard 作为 message 的字段类型:
PUT /my-index-000001/
{
"mappings": {
"properties": {
"@timestamp": {
"format": "strict_date_optional_time||epoch_second",
"type": "date"
},
"message": {
"type": "wildcard"
}
}
}
}
2. 摄取一些数据
映射完你想要检索的字段后,从你的日志数据中索引一些记录到 ES 。下面的请求使用了 bulk API 索引未加工的日志数据到 my-index-000001。你能使用一个小的示例来试验运行时字段,而不用索引全部的日志数据。
最后的那个文档不是一个有效的 Apache 日志格式,但是我们可以在脚本中解释这种情况。
POST /my-index-000001/_bulk?refresh
{"index":{}}
{"timestamp":"2020-04-30T14:30:17-05:00","message":"40.135.0.0 - - [30/Apr/2020:14:30:17 -0500] "GET /images/hm_bg.jpg HTTP/1.0" 200 24736"}
{"index":{}}
{"timestamp":"2020-04-30T14:30:53-05:00","message":"232.0.0.0 - - [30/Apr/2020:14:30:53 -0500] "GET /images/hm_bg.jpg HTTP/1.0" 200 24736"}
{"index":{}}
{"timestamp":"2020-04-30T14:31:12-05:00","message":"26.1.0.0 - - [30/Apr/2020:14:31:12 -0500] "GET /images/hm_bg.jpg HTTP/1.0" 200 24736"}
{"index":{}}
{"timestamp":"2020-04-30T14:31:19-05:00","message":"247.37.0.0 - - [30/Apr/2020:14:31:19 -0500] "GET /french/splash_inet.html HTTP/1.0" 200 3781"}
{"index":{}}
{"timestamp":"2020-04-30T14:31:22-05:00","message":"247.37.0.0 - - [30/Apr/2020:14:31:22 -0500] "GET /images/hm_nbg.jpg HTTP/1.0" 304 0"}
{"index":{}}
{"timestamp":"2020-04-30T14:31:27-05:00","message":"252.0.0.0 - - [30/Apr/2020:14:31:27 -0500] "GET /images/hm_bg.jpg HTTP/1.0" 200 24736"}
{"index":{}}
{"timestamp":"2020-04-30T14:31:28-05:00","message":"not a valid apache log"}
此时,你可以查看 ES 是如何存储你的原始数据的。
GET /my-index-000001
mapping 那包含了两个字段:@timestamp 和 message。
{
"my-index-000001" : {
"aliases" : { },
"mappings" : {
"properties" : {
"@timestamp" : {
"type" : "date",
"format" : "strict_date_optional_time||epoch_second"
},
"message" : {
"type" : "wildcard"
},
"timestamp" : {
"type" : "date"
}
}
},
...
}
}
3. 使用 grok 模式定义运行时字段
如果你想检索包含了 clientip 的结果,你可以将那个字段添加为映射里的一个运行时字段。下面的运行时脚本定义了一个能从一个文档里的一个单独的文本字段里提取结构化字段的 grok 模式。一个 grok 模式像正则表达式那样支持别名的表达式-你可以重复使用它。
这脚本匹配 %{COMMONAPACHELOG} 日志模式,这个模式能明白 Apache 日志的结构。如果这个模式匹配(clientip != null),那么脚本将发射匹配到的 IP 地址的值。如果模式没有匹配到,那么脚本将返回字段的值而不会崩溃。
PUT my-index-000001/_mappings
{
"runtime": {
"http.client_ip": {
"type": "ip",
"script": """
String clientip=grok('%{COMMONAPACHELOG}').extract(doc["message"].value)?.clientip;
if (clientip != null) emit(clientip); // @1
"""
}
}
}
@1:这个条件确保当 message 的模式不匹配的时候脚本也不会崩溃。
或者,你可以在一个搜索请求的上下文里定义同样的运行时字段。这个运行时字段的定义还有脚本跟你之前在索引映射里定义的完全的一样。只需要复制这个定义到搜索请求里的 runtime_mappings 部分下面,并且包含一个在运行时字段上做匹配的查询就可以了。这个查询和你在你的索引映射里的 http.clientip 运行时字段上定义一个搜索查询返回的结果是一样的,不同的是只在这个特定搜索的上下文有效。
GET my-index-000001/_search
{
"runtime_mappings": {
"http.clientip": {
"type": "ip",
"script": """
String clientip=grok('%{COMMONAPACHELOG}').extract(doc["message"].value)?.clientip;
if (clientip != null) emit(clientip);
"""
}
},
"query": {
"match": {
"http.clientip": "40.135.0.0"
}
},
"fields" : ["http.clientip"]
}
4. 定义一个复合(composite)运行时字段
你也可以定义一个从一个单个的脚本里发射多个字段的复合运行时字段。你可以定义一组类型化的子字段并发射值的映射。在搜索时,每一个子字段检索在映射里和他们的名字相关联的值。这意味着你只需要指定你的 grok 模式一次就可以返回多个值:
PUT my-index-000001/_mappings
{
"runtime": {
"http": {
"type": "composite",
"script": "emit(grok("%{COMMONAPACHELOG}").extract(doc["message"].value))",
"fields": {
"clientip": {
"type": "ip"
},
"verb": {
"type": "keyword"
},
"response": {
"type": "long"
}
}
}
}
}
4.1 搜索一个特定 IP 地址
使用 http.clientip 运行时字段,你可以定义一个简单的查询来运行一个查找指定 IP 地址的搜索并返回所有相关的字段。
GET my-index-000001/_search
{
"query": {
"match": {
"http.clientip": "40.135.0.0"
}
},
"fields" : ["*"]
}
这个 API 返回下面的结果。因为 http 是一个 复合(composite) 运行时字段,所以响应中在 fields 下面包含了每一个子字段,包含了任何匹配查询的相关联的值。不需要预先建立你的数据结构,你可以用有意义的方式去搜索和探索你的数据做试验,然后决定哪个字段需要做索引。
{
...
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "my-index-000001",
"_id" : "sRVHBnwBB-qjgFni7h_O",
"_score" : 1.0,
"_source" : {
"timestamp" : "2020-04-30T14:30:17-05:00",
"message" : "40.135.0.0 - - [30/Apr/2020:14:30:17 -0500] "GET /images/hm_bg.jpg HTTP/1.0" 200 24736"
},
"fields" : {
"http.verb" : [
"GET"
],
"http.clientip" : [
"40.135.0.0"
],
"http.response" : [
200
],
"message" : [
"40.135.0.0 - - [30/Apr/2020:14:30:17 -0500] "GET /images/hm_bg.jpg HTTP/1.0" 200 24736"
],
"http.client_ip" : [
"40.135.0.0"
],
"timestamp" : [
"2020-04-30T19:30:17.000Z"
]
}
}
]
}
}
还记得脚本里的 if 语句吗?
if (clientip != null) emit(clientip);
如果脚本里没有包含这个条件,那么在任何不匹配模式的切片上的查询将会失败。通过包含这个条件,查询到不匹配 grok 模式时会跳过数据。
4.2 按指定的范围搜索文档
你也可以运行一个 范围查询 在 timestamp 字段上操作。下面的查询返回任何 timestamp 大于或等于 2020-04-30T14:31:27-05:00 的文档:
GET my-index-000001/_search
{
"query": {
"range": {
"timestamp": {
"gte": "2020-04-30T14:31:27-05:00"
}
}
}
}
响应中包含了虽然日志格式不匹配,但是 timestamp 落在了定义的范围内的文档。
{
...
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "my-index-000001",
"_id" : "hdEhyncBRSB6iD-PoBqe",
"_score" : 1.0,
"_source" : {
"timestamp" : "2020-04-30T14:31:27-05:00",
"message" : "252.0.0.0 - - [30/Apr/2020:14:31:27 -0500] "GET /images/hm_bg.jpg HTTP/1.0" 200 24736"
}
},
{
"_index" : "my-index-000001",
"_id" : "htEhyncBRSB6iD-PoBqe",
"_score" : 1.0,
"_source" : {
"timestamp" : "2020-04-30T14:31:28-05:00",
"message" : "not a valid apache log"
}
}
]
}
}
5. 使用 dissect 模式定义一个运行时字段
如果你不需要正则表达式的能力,你可以使用dissect 模式取代 grok 模式。dissect 模式在固定的边界上做匹配然而通常比 grok 要快。
你可以使用 dissect 得到和使用 grok 模式解析 Apache 日志同样的结果。你包含了字符串中你想要丢弃的部分,而不是在日志模式上做匹配。特别注意到字符串中你想要丢弃的部分将帮助你建立成功的 dissect 模式。
PUT my-index-000001/_mappings
{
"runtime": {
"http.client.ip": {
"type": "ip",
"script": """
String clientip=dissect('%{clientip} %{ident} %{auth} [%{@timestamp}] "%{verb} %{request} HTTP/%{httpversion}" %{status} %{size}').extract(doc["message"].value)?.clientip;
if (clientip != null) emit(clientip);
"""
}
}
}
相似地,你可以定义一个 dissect 模式来提取 HTTP 返回码:
PUT my-index-000001/_mappings
{
"runtime": {
"http.responses": {
"type": "long",
"script": """
String response=dissect('%{clientip} %{ident} %{auth} [%{@timestamp}] "%{verb} %{request} HTTP/%{httpversion}" %{response} %{size}').extract(doc["message"].value)?.response;
if (response != null) emit(Integer.parseInt(response));
"""
}
}
}
然后你可以使用 http.responses 运行时字段运行一个检索特定 HTTP 响应的查询。使用 _search 请求的 fields 参数指定你想要检索的字段:
GET my-index-000001/_search
{
"query": {
"match": {
"http.responses": "304"
}
},
"fields" : ["http.client_ip","timestamp","http.verb"]
}
这个响应包含了一个 HTTP 响应是 304 的单个的文档:
{
...
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "my-index-000001",
"_id" : "A2qDy3cBWRMvVAuI7F8M",
"_score" : 1.0,
"_source" : {
"timestamp" : "2020-04-30T14:31:22-05:00",
"message" : "247.37.0.0 - - [30/Apr/2020:14:31:22 -0500] "GET /images/hm_nbg.jpg HTTP/1.0" 304 0"
},
"fields" : {
"http.verb" : [
"GET"
],
"http.client_ip" : [
"247.37.0.0"
],
"timestamp" : [
"2020-04-30T19:31:22.000Z"
]
}
}
]
}
}