Elasticsearch:使用 runtime fields 探索你的数据

2023年 7月 15日 44.6k 0

Elasticsearch

考虑要提取字段的大量日志数据。 为数据建立索引非常耗时,并且会占用大量磁盘空间,而你只想探索数据结构而无需预先提交 schema。

你知道你的日志数据包含你要提取的特定字段。 在这种情况下,我们要关注 @timestamp 和消息字段。 通过使用运行时字段(runtime fields),你可以定义脚本来计算这些字段在搜索时的值。

定义索引字段作为起点

你可以从一个简单的示例开始,将 @timestamp 和 message 字段作为索引字段添加到 my-index-000001 映射中。 为了保持灵活性,使用 wildcard 作为消息的字段类型:

1.  PUT /my-index-000001/
2.  {
3.    "mappings": {
4.      "properties": {
5.        "@timestamp": {
6.          "format": "strict_date_optional_time||epoch_second",
7.          "type": "date"
8.        },
9.        "message": {
10.          "type": "wildcard"
11.        }
12.      }
13.    }
14.  }

在上面,我们有意使用 wildcard 字段来定义 message。这样它非常节省存储空间,并且会提高写入文档的速度。

摄取一些数据

映射完要检索的字段后,将日志数据中的几条记录索引到 Elasticsearch 中。 以下请求使用 _bulk API 将原始日志数据索引到 my-index-000001。 你可以使用一个小样本来试验运行时字段,而不是索引所有日志数据。

最终文档不是有效的 Apache 日志格式,但我们可以在脚本中考虑到这种情况。

`1.  POST /my-index-000001/_bulk?refresh
2.  {"index":{}}
3.  {"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"}
4.  {"index":{}}
5.  {"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"}
6.  {"index":{}}
7.  {"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"}
8.  {"index":{}}
9.  {"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"}
10.  {"index":{}}
11.  {"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"}
12.  {"index":{}}
13.  {"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"}
14.  {"index":{}}
15.  {"timestamp":"2020-04-30T14:31:28-05:00","message":"not a valid apache log"}

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

此时,你可以查看 Elasticsearch 如何存储你的原始数据。

GET my-index-000001

该映射包含两个字段:@timestamp 和 message。

`1.  {
2.    "my-index-000001": {
3.      "aliases": {},
4.      "mappings": {
5.        "properties": {
6.          "@timestamp": {
7.            "type": "date",
8.            "format": "strict_date_optional_time||epoch_second"
9.          },
10.          "message": {
11.            "type": "wildcard"
12.          },
13.          "timestamp": {
14.            "type": "date"
15.          }
16.        }
17.      },
18.      "settings": {
19.        "index": {
20.          "routing": {
21.            "allocation": {
22.              "include": {
23.                "_tier_preference": "data_content"
24.              }
25.            }
26.          },
27.          "number_of_shards": "1",
28.          "provided_name": "my-index-000001",
29.          "creation_date": "1672032735783",
30.          "number_of_replicas": "1",
31.          "uuid": "X1cBJOl3TFKd6v0oeTRlng",
32.          "version": {
33.            "created": "8050399"
34.          }
35.        }
36.      }
37.    }
38.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

使用 grok 模式定义运行时字段

如果要检索包含 clientip 的结果,可以将该字段添加为映射中的运行时字段。 以下运行时脚本定义了一个 grok 模式,该模式从文档中的单个文本字段中提取结构化字段。 grok 模式就像支持可以重用的别名表达式的正则表达式。

该脚本匹配 %{COMMONAPACHELOG} 日志模式,该模式了解 Apache 日志的结构。 如果模式匹配 (clientip != null),脚本将发出匹配 IP 地址的值。 如果模式不匹配,脚本只会返回字段值而不会崩溃。

1.  PUT my-index-000001/_mappings
2.  {
3.    "runtime": {
4.      "http.client_ip": {
5.        "type": "ip",
6.        "script": """
7.          String clientip=grok('%{COMMONAPACHELOG}').extract(doc["message"].value)?.clientip;
8.          if (clientip != null) emit(clientip); 
9.        """
10.      }
11.    }
12.  }

我们可以为已经存在的索引动态地添加一个新的字段。特别值得指出的是上面的 ?. 操作符。我们可以参阅链接来进一步阅读。它的意思是对一个 null 对象使用 ?. 操作符会返回 null,而不会使得脚本崩溃。上面的 if 检查,此条件可确保脚本不会崩溃,即使 message 的模式不匹配也是如此。

这样,我们可以针对索引进行搜索,比如:

1.  GET my-index-000001/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "match": {
5.        "http.client_ip": "40.135.0.0"
6.      }
7.    }
8.  }

上面的 runtime 字段 http.client_ip 在查询时动态生成,并使得我们可以对它进行搜索:

`1.  {
2.    "hits": {
3.      "hits": [
4.        {
5.          "_index": "my-index-000001",
6.          "_id": "Zn7rTIUBIjh__4nuBm2T",
7.          "_score": 1,
8.          "_source": {
9.            "timestamp": "2020-04-30T14:30:17-05:00",
10.            "message": """40.135.0.0 - - [30/Apr/2020:14:30:17 -0500] "GET /images/hm_bg.jpg HTTP/1.0" 200 24736"""
11.          }
12.        }
13.      ]
14.    }
15.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

在上面,我们在 mapping 中定义 runtime fields。在实际的使用中,我们也可以在搜索的时候定义。你可以在搜索请求的上下文中定义相同的运行时字段。 运行时定义和脚本与之前在索引映射中定义的完全相同。 只需将该定义复制到 runtime_mappings 部分下的搜索请求中,并包含与运行时字段匹配的查询。 此查询返回的结果与你在索引映射中为 http.clientip 运行时字段定义搜索查询时返回的结果相同,但仅在此特定搜索的上下文中:

`1.  GET my-index-000001/_search?filter_path=**.hits
2.  {
3.    "runtime_mappings": {
4.      "http.clientip": {
5.        "type": "ip",
6.        "script": """
7.          String clientip=grok('%{COMMONAPACHELOG}').extract(doc["message"].value)?.clientip;
8.          if (clientip != null) emit(clientip);
9.        """
10.      }
11.    },
12.    "query": {
13.      "match": {
14.        "http.clientip": "40.135.0.0"
15.      }
16.    },
17.    "fields" : ["http.clientip"]
18.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

上面的搜索返回结果:

`1.  {
2.    "hits": {
3.      "hits": [
4.        {
5.          "_index": "my-index-000001",
6.          "_id": "Zn7rTIUBIjh__4nuBm2T",
7.          "_score": 1,
8.          "_source": {
9.            "timestamp": "2020-04-30T14:30:17-05:00",
10.            "message": """40.135.0.0 - - [30/Apr/2020:14:30:17 -0500] "GET /images/hm_bg.jpg HTTP/1.0" 200 24736"""
11.          },
12.          "fields": {
13.            "http.clientip": [
14.              "40.135.0.0"
15.            ]
16.          }
17.        }
18.      ]
19.    }
20.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

定义复合运行时字段

你还可以定义复合(composite)运行时字段以从单个脚本发出多个字段。 你可以定义一组类型化的子字段并发出值映射。 在搜索时,每个子字段在地图中检索与其名称关联的值。 这意味着你只需指定一次 grok 模式并可以返回多个值:

`1.  PUT my-index-000001/_mappings
2.  {
3.    "runtime": {
4.      "http": {
5.        "type": "composite",
6.        "script": "emit(grok(\"%{COMMONAPACHELOG}\").extract(doc[\"message\"].value))",
7.        "fields": {
8.          "clientip": {
9.            "type": "ip"
10.          },
11.          "verb": {
12.            "type": "keyword"
13.          },
14.          "response": {
15.            "type": "long"
16.          }
17.        }
18.      }
19.    }
20.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

搜索一个特定的 IP 地址

使用 http.clientip 运行时字段,你可以定义一个简单的查询来运行对特定 IP 地址的搜索并返回所有相关字段。

1.  GET my-index-000001/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "match": {
5.        "http.clientip": "40.135.0.0"
6.      }
7.    },
8.    "fields" : ["*"]
9.  }

上面的 API 返回以下结果。 因为 http 是复合运行时字段,所以响应包括字段下的每个子字段,包括任何与查询匹配的关联值。 无需提前构建数据结构,你就可以以有意义的方式搜索和探索数据,以试验并确定要索引的字段。

`1.  {
2.    "hits": {
3.      "hits": [
4.        {
5.          "_index": "my-index-000001",
6.          "_id": "Zn7rTIUBIjh__4nuBm2T",
7.          "_score": 1,
8.          "_source": {
9.            "timestamp": "2020-04-30T14:30:17-05:00",
10.            "message": """40.135.0.0 - - [30/Apr/2020:14:30:17 -0500] "GET /images/hm_bg.jpg HTTP/1.0" 200 24736"""
11.          },
12.          "fields": {
13.            "http.verb": [
14.              "GET"
15.            ],
16.            "http.clientip": [
17.              "40.135.0.0"
18.            ],
19.            "http.response": [
20.              200
21.            ],
22.            "message": [
23.              """40.135.0.0 - - [30/Apr/2020:14:30:17 -0500] "GET /images/hm_bg.jpg HTTP/1.0" 200 24736"""
24.            ],
25.            "http.client_ip": [
26.              "40.135.0.0"
27.            ],
28.            "timestamp": [
29.              "2020-04-30T19:30:17.000Z"
30.            ]
31.          }
32.        }
33.      ]
34.    }
35.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

另外,还记得脚本中的 if 语句吗?

if (clientip != null) emit(clientip);

如果脚本不包含此条件,则查询将在任何与模式不匹配的分片上失败。 通过包含此条件,查询会跳过与 grok 模式不匹配的数据。

搜索特定范围内的文档

你还可以运行对时间戳字段进行操作的范围查询。 以下查询返回时间戳大于或等于 2020-04-30T14:31:27-05:00 的任何文档:

1.  GET my-index-000001/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "range": {
5.        "timestamp": {
6.          "gte": "2020-04-30T14:31:27-05:00"
7.        }
8.      }
9.    }
10.  }

响应包括日志格式不匹配但时间戳在定义范围内的文档。

使用 dissect 模式定义运行时字段

如果你不需要正则表达式的强大功能,你可以使用解剖模式而不是 grok 模式。 解剖模式匹配固定的分隔符,但通常比 grok 更快。

你可以使用 dissect 来获得与使用 grok 模式解析 Apache 日志相同的结果。 你不匹配日志模式,而是包括要丢弃的字符串部分。 特别注意要丢弃的字符串部分将有助于构建成功的解析模式。

1.  PUT my-index-000001/_mappings
2.  {
3.    "runtime": {
4.      "http.client.ip": {
5.        "type": "ip",
6.        "script": """
7.          String clientip=dissect('%{clientip} %{ident} %{auth} [%{@timestamp}] "%{verb} %{request} HTTP/%{httpversion}" %{status} %{size}').extract(doc["message"].value)?.clientip;
8.          if (clientip != null) emit(clientip);
9.        """
10.      }
11.    }
12.  }

同样,你可以定义一个解析模式来提取 HTTP 响应代码:

1.  PUT my-index-000001/_mappings
2.  {
3.    "runtime": {
4.      "http.responses": {
5.        "type": "long",
6.        "script": """
7.          String response=dissect('%{clientip} %{ident} %{auth} [%{@timestamp}] "%{verb} %{request} HTTP/%{httpversion}" %{response} %{size}').extract(doc["message"].value)?.response;
8.          if (response != null) emit(Integer.parseInt(response));
9.        """
10.      }
11.    }
12.  }

然后,你可以运行查询以使用 http.responses 运行时字段检索特定的 HTTP 响应。 使用 _search 请求的 fields 参数来指示你要检索的字段:

1.  GET my-index-000001/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "match": {
5.        "http.responses": "304"
6.      }
7.    },
8.    "fields" : ["http.client_ip","timestamp","http.verb"]
9.  }

响应包括单个文档,其中 HTTP 响应为 304:

`1.  {
2.    "hits": {
3.      "hits": [
4.        {
5.          "_index": "my-index-000001",
6.          "_id": "an7rTIUBIjh__4nuBm2T",
7.          "_score": 1,
8.          "_source": {
9.            "timestamp": "2020-04-30T14:31:22-05:00",
10.            "message": """247.37.0.0 - - [30/Apr/2020:14:31:22 -0500] "GET /images/hm_nbg.jpg HTTP/1.0" 304 0"""
11.          },
12.          "fields": {
13.            "http.verb": [
14.              "GET"
15.            ],
16.            "http.client_ip": [
17.              "247.37.0.0"
18.            ],
19.            "timestamp": [
20.              "2020-04-30T19:31:22.000Z"
21.            ]
22.          }
23.        }
24.      ]
25.    }
26.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论