工程

在 Kibana 脚本字段中使用 Painless

Kibana 提供了强大的功能,可用来搜索和可视化 Elasticsearch 中存储的数据。为了实现可视化,Kibana 会查找 Elasticsearch 映射中定义的字段,并将其作为选项呈现给构建图表的用户。但是,如果忘了在架构中将重要的值定义为独立的字段,会出现什么情况?或者,如果要合并两个字段并将其视为一个字段,该怎么办?这时 Kibana 脚本字段就派上用场了。

脚本字段最早出现于 Kibana 4 初期。起初问世时,脚本字段的唯一定义方法是依托 Lucene 表达式,Elasticsearch 专门利用这种脚本语言处理数值。因此,脚本字段只能在一部分用例中发挥作用。在 5.0 中,Elasticsearch 引入了 Painless,这是一种安全、功能强大的脚本语言,可对各种数据类型进行操作。因此,Kibana 5.0 中的脚本字段功能要强大得多。

在本博文的其余部分,我们将向您介绍如何为常见用例创建脚本字段。为此,我们将依托 Kibana 入门教程 的数据集,并使用在 Elastic Cloud 中运行的 Elasticsearch 和 Kibana 实例。您可以免费快速完成相应实例的部署。

以下视频将向您介绍如何在 Elastic Cloud 中快速部署个人的 Elasticsearch 和 Kibana 实例,并将示例 数据集 加载至该实例中。 

脚本字段如何运作

Elasticsearch 允许您为每个请求指定脚本字段。Kibana 在这一方面进行了改进,您只需在“管理”部分中定义一次脚本字段,以后即可在 UI 的多个位置使用该字段。请注意,虽然 Kibana 将脚本字段与其他配置一起存储在 .kibana 索引中,但该配置是 Kibana 特有的,并且 Kibana 脚本字段不会向 Elasticsearch 的 API 用户公开。

在 Kibana 中定义脚本字段时,您可以选择脚本语言,即从 Elasticsearch 节点安装的所有启用了动态脚本的语言中进行选择。默认情况下,5.0 提供“表达式”和 “Painless”两种脚本语言,2.x 仅提供“Painless”这一种脚本语言。您可以安装其他脚本语言并为其启用动态脚本,但不建议这样做,因为其他脚本语言无法充分沙盒化 并且已弃用。

脚本字段一次对一个 Elasticsearch 文档进行操作,但可引用文档中的多个字段。因此,建议使用脚本字段合并或转换单个文档中的多个字段,但不能基于多个文档执行计算(例如时间序列计算)。Painless 和 Lucene 表达式均可用于 doc_values 中存储的字段。因此,对于字符串数据,您需要将字符串存储在数据类型关键字中。基于 Painless 的脚本字段也不能直接对 _source 进行操作。

在“管理”部分中定义脚本字段后,用户可以像处理 Kibana 其余部分的其他字段那样,与脚本字段进行交互。脚本字段会自动显示在 Discover 字段列表中,并且可在 Visualize 中用于创建可视化。Kibana 只有在查询时才会将脚本字段定义传递给 Elasticsearch 进行计算。生成的数据集与从 Elasticsearch 返回的其他结果合并,并以表格或图表的形式呈现给用户。

截至本文撰写时,使用脚本字段存在若干已知限制。您可以将 Kibana 可视化生成器中多数可用的 Elasticsearch 聚合应用于脚本字段,而最需要注意的例外情形是重要词聚合。此外,您还可以在 Discover、Visualize 和仪表板中通过筛选栏对脚本字段进行筛选,但您需要认真编写适当的脚本以返回定义明确的值,如下所示。在使用脚本字段时,还须参阅下方的“最佳实践”部分,确保不会破坏环境的稳定性。

下列视频演示了如何使用 Kibana 创建脚本字段。

脚本字段示例

这一部分将介绍常见场景中 Kibana 的 Lucene 表达式和 Painless 脚本字段的几个示例。如上所述,这些示例是基于 Kibana 入门教程的数据集开发的,并假设您使用的是 Elasticsearch 和 Kibana 5.1.1,因为早期版本存在若干与特定类型脚本字段筛选和排序相关的已知问题。

在大多数情况下,脚本字段应该能够即点即用,因为 Elasticsearch 5.0 默认启用 Lucene 表达式和 Painless。唯一例外是,脚本需要基于正则表达式解析字段,为此,您需要在 elasticsearch.yml 中进行以下设置,为 Painless 启用正则表达式匹配:script.painless.regex.enabled: true

对单个字段执行计算

  • 示例:计算单位从字节变为千字节
  • 语言: 表达式
  • 返回类型: 数字
 doc['bytes'].value / 1024

注意:请记住,Kibana 脚本字段一次只能处理一个文档,因此无法在脚本字段中进行时间序列计算。

产生数字的日期计算

  • 示例: 将日期解析为时刻
  • 语言:表达式
  • 返回类型: 数字

Lucene 表达式提供了一整套即点即用的日期处理函数。但是,由于 Lucene 表达式仅返回数值,因此我们必须使用 Painless 返回基于字符串的星期几(如下所示)。

 doc['@timestamp'].date.hourOfDay

注意:以上脚本将返回 1-24

doc['@timestamp'].date.dayOfWeek

注意:以上脚本将返回 1-7

合并两个字符串值

  • 示例: 合并源和目标或合并名字和姓氏
  • 语言: Painless
  • 返回类型: 字符串
 doc['geo.dest.keyword'].value + ':' + doc['geo.src.keyword'].value

注意:由于脚本字段需要对 doc_values 中的字段进行操作,因此我们使用上述字符串的 .keyword 版本。

引入逻辑

  • 示例:对于字节数超过 10000 的任何文档,返回标签“大量下载”
  • 语言:Painless
  • 返回类型:字符串
 if (doc['bytes'].value > 10000) { 
return "big download";
}
return "";

注意:引入逻辑时,请确保每个执行路径均有明确定义的返回语句和返回值(非 NULL)。例如,上述脚本字段用于 Kibana 筛选器时,如果最后没有返回语句或该语句返回 NULL,则会失败并出现编译错误。另请记住,Kibana 脚本字段不支持将逻辑分解为函数。 

返回子字符串

  • 示例:返回 URL 中最后一个斜线之后的部分
  • 语言:Painless
  • 返回类型:字符串
 def path = doc['url.keyword'].value;
if (path != null) {
int lastSlashIndex = path.lastIndexOf('/');
if (lastSlashIndex > 0) {
return path.substring(lastSlashIndex+1);
}
}
return "";

注意:尽可能避免使用正则表达式提取子字符串,因为 indexOf() 运算消耗的资源更少,也更不易出错。 

使用正则表达式匹配字符串,并对匹配项执行操作

  • 示例:如果在字段“referer”中找到子字符串“error”,则返回字符串“error”,否则返回字符串“no error”。
  • 语言:Painless
  • 返回类型:字符串
if (doc['referer.keyword'].value =~ /cn/error/) { 
return "error"
} else {
return "no error"
}

注意:简化的正则表达式语法可用于基于正则表达式匹配的条件语句。 

匹配字符串并返回匹配项

  • 示例:返回域,即“host”字段中最后一个点之后的字符串。
  • 语言:Painless
  • 返回类型:字符串
def m = /^.*\.([a-z]+)$/.matcher(doc['host.keyword'].value);
if ( m.matches() ) {
return m.group(1)
} else {
return "no match"
}

注意:通过 regex matcher() 函数定义对象,您可以提取并返回多组与该正则表达式匹配的字符。 

匹配数字并返回匹配项

  • 示例:返回 IP 地址的前八位字节(存储为字符串)并将其视为数字。
  • 语言:Painless
  • 返回类型:数字
 def m = /^([0-9]+)\..*$/.matcher(doc['clientip.keyword'].value);
if ( m.matches() ) {
return Integer.parseInt(m.group(1))
} else {
return 0
}

注意:必须在脚本中返回正确的数据类型。即使匹配了数字,正则表达式匹配也会返回字符串,因此应在返回时将其显式转换为整数。 

产生字符串的日期计算

  • 示例:将日期解析为星期几,然后解析为字符串
  • 语言:Painless
  • 返回类型:字符串
LocalDateTime.ofInstant(Instant.ofEpochMilli(doc['@timestamp'].value), ZoneId.of('Z')).getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.getDefault())

注意:Painless 支持 Java 的所有原生类型,因此提供对这些类型的原生函数的访问权限,例如 LocalDateTime(),可用于执行更高级的日期计算。

最佳实践

如您所见,Painless 脚本语言提供了强大的功能,可以通过 Kibana 脚本字段从 Elasticsearch 中存储的任意字段提取有用信息。然而,功能越强大,责任越重大。 

我们在下方概述了一些有关使用 Kibana 脚本字段的最佳实践。

  • 始终使用开发环境试验脚本字段。由于脚本字段一经保存在 Kibana“管理”部分即会变成活动状态(例如,出现在所有用户的索引模式的“Discover”屏幕),因此您不应直接在生产环境中开发脚本字段。我们建议您首先在开发环境中试用语法,评估脚本字段对暂存中的实际数据集和数据量的影响,然后才将其运用到生产环境中。 
  • 若您确信脚本字段可为用户提供价值,则可以考虑修改采集方式,以便在索引时针对新数据提取该字段。这将节省 Elasticsearch 在查询时的处理工作量,为 Kibana 用户加快响应时间。此外,您还可以使用 Elasticsearch 中的 _reindex API 重新索引现有数据。