Elasticsearch:解析和丰富日志数据以在 Elastic 平台上进行故障排除

1,057 阅读11分钟

作者:Luca Wintergerst

在较早的博客文章 “日志监控和非结构化日志数据中,超越 tail -f”,我们讨论了收集和使用非结构化日志数据。 我们了解到,将数据添加到 Elastic Stack 非常容易。 到目前为止,我们所做的唯一解析是从该数据中提取时间戳,以便正确回填旧数据。

我们还谈到了在博客末尾搜索这种非结构化数据。 虽然非结构化数据在与全文搜索功能结合使用时非常有用,但在某些情况下,我们需要更多的结构来使用数据来回答我们的问题。

写时模式或读时模式 —— 两者兼而有之,何乐而不为呢?

Schema on write 仍然是 Elasticsearch 用来处理传入数据的默认选项。 文档中的所有字段在被摄取时都被索引,也称为写时模式。 这就是在 Elastic 中运行搜索速度如此之快的原因,无论返回的数据量或执行的查询数量如何。 这也是我们的用户喜爱 Elastic 的重要原因。比如我们很容易轻松地创建一个索引:



1.  PUT twitter/_doc/1
2.  {
3.    "content": "I like Elastic Stack very much!"
4.  }


在默认的情况下,在我们执行完上面的命令后,它会自动生成一个叫做 twitter 的索引,并生成相应的 mapping:

GET twitter/_mapping

上面的命令将返回:

`

1.  {
2.    "twitter": {
3.      "mappings": {
4.        "properties": {
5.          "content": {
6.            "type": "text",
7.            "fields": {
8.              "keyword": {
9.                "type": "keyword",
10.                "ignore_above": 256
11.              }
12.            }
13.          }
14.        }
15.      }
16.    }
17.  }

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

也就是说在写入文档的同时,如果该字段从来没有被创建过,Elasticsearch 会自动帮我们生产相应的字段 content。这也就是 schema on write。

如果你了解自己的数据及其在摄取前的结构方式,写模式就非常有效。 这样,模式(也即 schema, 数据结构的逻辑视图)可以在索引映射中完全定义。 当针对索引运行查询时,它还需要坚持定义的模式。 然而,在现实世界中,监控和遥测数据经常会发生变化。 例如,新数据源可能会出现在你的环境中。 在数据被索引后动态提取或查询新字段的灵活性增加了巨大的价值,即使它会以轻微的性能成本为代价。

这就是 schema on read 的用武之地。数据可以以原始形式快速摄取,无需任何索引,除了某些必要的字段,如时间戳或响应代码。 当对数据运行查询时,可以即时创建其他字段。 你不需要提前对数据有深入的了解,也不必预测数据最终可能被查询的所有可能方式。 你可以随时更改数据结构,即使在文档已被索引之后 —— 读时模式的巨大好处。

以下是 Elastic 在读取时实施模式的独特之处。 我们在同一个 Elastic 平台上构建了运行时字段 —— 相同的架构、相同的工具和你已经在使用的相同界面。 没有新的数据存储、语言或组件,也没有额外的程序开销。 Schema on read 和 schema on write 可以很好地协同工作并无缝互补,因此你可以决定在查询需要它们时计算哪些字段,以及在将数据提取到 Elasticsearch 中时对哪些字段进行索引。

通过在单个堆栈上为你提供两全其美的功能,我们可以让你轻松决定哪种写入模式和读取模式的组合最适合你的特定用例。

在 Elastic Stack 上使用运行时字段

让我们从一个简单的例子开始。

使用非结构化数据,我们可以轻松回答诸如 “过去 15 分钟内我们犯了多少错误?” 之类的问题。 或者 “我们上次遇到错误 X 是什么时候?” 但是如果我们想问这样的问题 “我们日志中出现的数字 X 的总和是多少?” 或者 “我们的前 5 个错误是什么?”,那么我们需要先提取相关信息以便进行聚合。

如果你关注了我们上一篇博客,我们集群中的数据现在如下所示:



1.  {
2.    “@timestamp”: "2022-06-01T14:05:30.000Z",
3.    “message”: "INFO Returning 1 ads"
4.  }


为了能在自己的 Elasticsearch 里生产相应的文档,我们可以采取如下的方法来生成一些测试数据。我们先运行如下的 ingest pipeline:



1.  PUT _ingest/pipeline/add-timestamp
2.  {
3.    "processors": [
4.      {
5.        "set": {
6.          "field": "@timestamp",
7.          "value": "{{_ingest.timestamp}}"
8.        }
9.      }
10.    ]
11.  }


然后我们使用如下的命令来生成一些文档:



1.  POST my_index/_doc?pipeline=add-timestamp
2.  {
3.    "message": "INFO Returning 1 ads"
4.  }


我们运行上面的命令 5 次,这样它就会帮我们生产 5 个文档。我们可以使用如下的命令来进行查询:

GET my_index/_search?filter_path=**.hits
`

1.  {
2.    "hits": {
3.      "hits": [
4.        {
5.          "_index": "my_index",
6.          "_id": "1GHxqIUBJX3yAzzY7v7L",
7.          "_score": 1,
8.          "_source": {
9.            "message": "INFO Returning 1 ads",
10.            "@timestamp": "2023-01-13T02:25:05.656703463Z"
11.          }
12.        },
13.        {
14.          "_index": "my_index",
15.          "_id": "1WHyqIUBJX3yAzzYP_6o",
16.          "_score": 1,
17.          "_source": {
18.            "message": "INFO Returning 1 ads",
19.            "@timestamp": "2023-01-13T02:25:26.440656042Z"
20.          }
21.        },
22.        {
23.          "_index": "my_index",
24.          "_id": "1mHyqIUBJX3yAzzYQf7s",
25.          "_score": 1,
26.          "_source": {
27.            "message": "INFO Returning 1 ads",
28.            "@timestamp": "2023-01-13T02:25:27.020167501Z"
29.          }
30.        },
31.        {
32.          "_index": "my_index",
33.          "_id": "12HyqIUBJX3yAzzYRv4O",
34.          "_score": 1,
35.          "_source": {
36.            "message": "INFO Returning 1 ads",
37.            "@timestamp": "2023-01-13T02:25:28.078360418Z"
38.          }
39.        },
40.        {
41.          "_index": "my_index",
42.          "_id": "2GHyqIUBJX3yAzzYSP5J",
43.          "_score": 1,
44.          "_source": {
45.            "message": "INFO Returning 1 ads",
46.            "@timestamp": "2023-01-13T02:25:28.649390085Z"
47.          }
48.        }
49.      ]
50.    }
51.  }

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

从上面的输出结果中,我们可以看出来文档含有一个 message 及一个 @timestamp 的字段。为了使得我们的 message 不那么统一,我们可以使用如下的命令来生成其它类型和上面并不一样格式的 message。



1.  POST my_index/_doc?pipeline=add-timestamp
2.  {
3.    "message": "INFO received ad request (context_words=[Vintage])"
4.  }

6.  POST my_index/_doc?pipeline=add-timestamp
7.  {
8.    "message": "INFO received ad request (context_words=[Cookware])"
9.  }


很显然,我们上面的这两个文档的格式和之前的并不一样。这样我们有一个混合结构的 message 文档。

如果我们想总结我们的服务投放了多少广告,我们需要先提取这些日志消息的数值。

最简单的方法是使用运行时字段(runtime fields)。 此功能允许你在文档中定义其他字段,即使它们不存在于你发送到 Elasticsearch 的原始值中。

要执行此查询,让我们从 Discover UI 开始,我们将在其中选择一个包含我们要解析的日志消息的文档。 打开文档详细信息后,我们将复制文档 _id。 这是使脚本编写更容易一点的重要步骤。在进行下面的操作之前,我们需要为 my_index 创建一个 dataview:

 

 

 

我们回到 Discover UI:

 

我们可以看到共有 7 个文档。我们可以看到文档的内容:

接下来,我们将单击索引模式或数据视图的名称(在我们的示例中为 my_index),然后单击 Add a field to this data view。 

这将打开一个弹出窗口,让我们创建运行时字段。 在右上角,我们将粘贴我们之前复制的文档 id。 然后加载该文档,脚本将针对该文档运行并提供即时结果,因此我们可以查看我们的尝试是否成功。

接下来,我们将为新字段命名。 然后我们将从可用选项中选择 “Set val”。 这个选项不仅可以设置静态值,还可以让我们编写脚本。

在上面,我们把如下的 script 拷贝到上面的 define script 框里:



1.  String ads=grok('INFO Returning %{NUMBER:ads} ads').extract(params._source.message)?.ads;
2.  if (ads != null) emit(Integer.parseInt(ads));


这是一个相对简单的 Painless 脚本。 最重要的组件是 “grok”,它允许我们对我们的数据运行任何 GROK 表达式,类似于我们在之前的博客中处理时间戳提取的方式。关于 painless script 的编程,你可以进一步阅读我的文章 “Elastic:开发者上手指南” 中的 “Painless 编程” 章节。

由于这是一个 grok 表达式,我们暂时将提取的数据保存在我们称为 “ads” 的字段中。

grok('INFO Returning %{NUMBER:ads}

该表达式正在 message 字段上运行。 

.extract(params._source.message)

一旦执行,我们就可以像这样访问临时的 “ads” 字段:

?.ads;

特别值得指出的是上面的 ?. 操作符。我们可以参阅链接来进一步阅读。它的意思是对一个 null 对象使用 ?. 操作符会返回 null,而不会使得脚本崩溃。上面的 if 检查,此条件可确保脚本不会崩溃,即使 message 的模式不匹配也是如此。比如我们有两个 message 不是按照这样的格式的信息,那么就不能正确提取 ads 这个运行时字段。

在脚本末尾,我们检查是否能够成功提取值。 如果是,我们发出它。

if (ads != null) emit(Integer.parseInt(ads));

要记住的最重要的一点是,在针对 “text” 字段使用运行时字段和 .extract(doc[“message”]) 时,你必须使用 .extract(params._source.message)。

将字段添加到 Kibana 后,我们现在可以再次开始在 Discover 中查看我们的数据。 如果一切正常,我们就可以像使用任何其他字段一样使用这个字段 —— 在 Lens 可视化中使用它只是其中的一个例子。我们到 Discover 的界面进行查看:

 

 

我们可以使用 Lens 来对这个数值进行统计:

 

这些运行时字段也可用于过滤,因此我们可以在示例中搜索“ ads:1” 以将文档限制为仅返回单个广告的文档。 这些字段确实表现得就像它们作为现有模式的一部分存在于我们的文档中一样。 

运行时字段的好处

因为运行时字段没有索引,所以添加运行时字段不会增加索引大小。 你直接在索引映射中定义运行时字段,从而节省存储成本并提高摄取速度。 当你定义一个运行时字段时,你可以立即在搜索请求、聚合、过滤和排序中使用它,而无需额外重新索引你的数据。

运行时字段的缺点

每次你对运行时字段运行搜索时,Elasticsearch 都必须再次评估该字段的值,因为它不是你文档中被索引的真实字段。 如果此字段是你打算在将来经常查询的字段,那么你应该考虑将其提取为摄取管道的一部分。

如果你从文档中提取的数据只是偶尔使用一次,那么你可以安全地继续使用运行时字段功能。 但是,如果你计划经常使用该字段或者如果你注意到性能问题,那么将它添加到我们在之前的博客日志监控和非结构化 “日志数据中定义的摄取管道可能是个好主意,超越 tail-f"。

非 Kibana 用户的运行时字段

如果你不使用 Kibana 来探索你的数据,或者想在你的自定义应用程序中使用相同的功能,那么你还可以在你的索引映射中映射这些运行时字段。

它们的计算方式将保持不变。 这意味着即使你在映射中定义它们,提取它们的潜在繁重工作也必须在搜索时完成。 但是,由于它们是在你的索引映射中定义的,你可以通过直接访问 Elasticsearch 将它们用于自定义查询和聚合。

这方面的一个例子如下所示:

`

1.  PUT my-index-000001/
2.  {
3.    "mappings": {
4.      "runtime": {
5.        "day_of_week": {
6.          "type": "keyword",
7.          "script": {
8.            "source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))"
9.          }
10.        }
11.      },
12.      "properties": {
13.        "@timestamp": {"type": "date"}
14.      }
15.    }
16.  }

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

你可以在我们的文档中找到有关此方法的更多信息:映射运行时字段

在此博客中,我们了解了在已索引数据上使用运行时字段是多么容易。 我们不必重新索引单个文档,但仍然能够查询和分析我们的数据,就好像我们一直拥有该字段一样。 Elastic Stack 中的这一功能可以节省时间,并允许进行更强大的即席分析和数据探索。

请继续关注我们接下来处理数据可视化和仪表板以利用你丰富的日志记录数据! 同时,查看我们的日志记录快速入门培训资源以了解更多信息。

跟多阅读:Elasticsearch:使用 runtime fields 探索你的数据