菜单
开源

标签

作者:Ed Welch 日期:2019 年 2 月

这是截至 2019 年 4 月 3 日此文档的官方版本,最初讨论通过一个 Google 文档 进行,该文档将保留以供后世查阅,但今后不再更新。

问题陈述

我们应该能够根据从日志内容中提取的标签来过滤日志。

请记住:Loki 不是一个日志搜索工具,我们需要阻止将日志标签用作尝试重新创建日志搜索功能的手段。为“订单号”加上标签是不好的,但是,为“orderType=plant”加上标签,然后在时间窗口内通过订单号过滤结果是可以的。(可以这样想:grep “plant” | grep “12324134”)将 Loki 用作 grep 替代品、日志跟踪或日志滚动工具是非常有用的,日志标签有助于减少查询结果和提高查询性能,结合 LogQL 可以进一步缩小结果范围。

用例

正如为 Prometheus 定义的那样,“使用标签区分正在测量的事物的特征”。在一些常见情况下,用户希望搜索所有级别为“Error”的日志,或针对特定的 HTTP 路径(基数可能过高),或特定订单或事件类型的日志。示例

  • 日志级别。
  • HTTP 状态码。
  • 事件类型。

挑战

  • 日志通常是非结构化数据,从某些非结构化格式中提取可靠数据可能非常困难,往往需要使用复杂的正则表达式。
  • 容易被滥用。很容易创建高基数的标签,即使是偶然由于一个不良的正则表达式造成的。
  • 我们应该在哪里提取指标和标签?是在客户端(Promtail 或其他)还是在 Loki 端?在服务器端 (Loki) 提取有一些优点/缺点。我们可以两者都做吗?至少对于标签,我们可以定义一组预期的标签,如果 Loki 没有收到它们,就可以进行提取。
    • 服务器端提取将提高互操作性,但代价是增加服务器工作负载和成本。
    • 通过 Loki 或 Agent 暴露的指标是否存在可发现性问题/担忧?也许这样更容易管理?
    • 可能会更难以管理配置,因为服务器端必须将配置与传入的日志流匹配。

现有解决方案

从非结构化日志数据中提取处理并提取指标的解决方案已经存在,但是,如果不做一些工作,它们无法很好地提取标签,而且也都不支持作为库轻松集成。值得注意的是,理解它们的工作原理有助于在我们的解决方案中获得最佳特性。mtail https://github.com/google/mtail 1721 GitHub 星标,大量的提交、发布和贡献者,谷歌项目

完全用 Go 编写,使用 Go RE2 正则表达式,这比下面的 grok_exporter 性能更好,grok_exporter 使用完整的正则表达式实现,允许回溯和前瞻以符合 Grok 标准,但这也会导致速度较慢。

grok_exporter https://github.com/fstab/grok_exporter 278 GitHub 星标,成熟/活跃项目

如果您熟悉 Grok,这会更方便,许多人使用 ELK 技术栈,可能已经熟悉或拥有其日志的 Grok 字符串,这使得使用 grok_exporter 提取指标变得容易。

一个注意事项是依赖于 oniguruma C 库来解析正则表达式。

实现

详情

正如前面在处理非结构化数据的挑战中提到的,没有一种通用的解决方案能够很好地提取结构化数据。

Docker 日志格式就是一个例子,可能需要多级处理,其中 Docker 日志是 json 格式,但它也包含日志消息字段,该字段本身可能包含嵌入的 json,或者需要通过 regex 解析的日志消息。

管道化方法应该能够处理这些更具挑战性的场景。

Promtail 中已经有两个接口支持构建管道。

Go
type EntryMiddleware interface {
    Wrap(next EntryHandler) EntryHandler
}
Go
type EntryHandler interface {
    Handle(labels model.LabelSet, time time.Time, entry string) error
}

本质上,管道中的每个条目都会使用另一个 EntryHandler 来包装日志行,该 EntryHandler 可以在日志行传递到管道的下一阶段之前添加到 LabelSet、设置时间戳以及(可选地)修改日志行。

示例

json
{
  "log": "level=info msg=\”some log message\”\n",
  "stream": "stderr",
  "time": "2012-11-01T22:08:41+00:00"
}

这是一个 Docker 格式的日志文件,它是 JSON 格式,但同时也包含一个日志消息字段,其中包含一些键值对。

我们的管道化配置可能如下所示:

yaml
scrape_configs:
- job_name: system
  pipeline_stages:
    - json:
        timestamp:
          source: time
          format: RFC3339
        labels:
          stream:
            source: json_key_name.json_sub_key_name
        output: log
    - regex:
        expr: '.*level=(?P<level>[a-zA-Z]+).*'
        labels:
          level:
    - regex:
        expr: '.*msg=(?P<message>[a-zA-Z]+).*'
        output: message

仔细看看这个配置

yaml
     - json:
        timestamp:
          source: time
          format: TODO                               ①
        labels:
          stream:
            source: json_key_name.json_sub_key_name  ②
        output: log                                  ③

① format 键很可能是 Go 的 time.Parse 的格式字符串或 strptime 的格式字符串,这仍待确定,但其思想是指定用于提取时间戳数据的格式字符串;对于 regex 解析器,还需要一个用于提取时间戳的 expr 键。② 其中一个 json 元素是“stream”,因此我们将其作为标签提取;如果 json 值与所需的标签名匹配,只需将标签名指定为键即可;如果需要进行映射,您可以选择提供一个“source”键来指定在文档中查找标签的位置。(注意:此处使用 json_key_name.json_sub_key_name 仅为示例,与我们的示例日志不匹配)③ 告诉管道将 json 中的哪个元素发送到下一阶段。

yaml
    - regex:
        expr: '.*level=(?P<level>[a-zA-Z]+).*'  ①
        labels:
          level:                                ②

① 定义 Go RE2 regex,确保使用命名捕获组。② 使用命名捕获组名称提取标签。

注意此处未定义 output 部分,省略 output 键应该会指示解析器将传入的日志消息原样返回到下一阶段。

yaml
    - regex:
        expr: '.*msg=(?P<message>[a-zA-Z]+).*'
        output: message                          ①

① 将日志消息作为 output 发送到管道的最后一个阶段,这将是您希望 Loki 作为日志消息存储的内容。

这里可以使用另一种配置来实现相同的结果

yaml
scrape_configs:
- job_name: system
  pipeline_stages:
    - json:
        timestamp:
          source: time
          format: FIXME
        labels:
          stream:
        output: log
    - regex:
        expr: '.*level=(?P<level>[a-zA-Z]+).*msg=(?P<message>[a-zA-Z]+).*'
        labels:
          level:                                                             ①
          log:
            source: message                                                  ②
        output: message

① 与 json 解析器类似,如果您的日志标签与 regex 命名组匹配,您只需将标签名指定为 yaml 键即可。② 如果您有使用与 regex 组名称不同的标签名的用例,您可以选择提供 source 键,其值与命名捕获组匹配。

您可以使用包含多个捕获组的更复杂的正则表达式,在一个条目解析器中提取多个标签和/或输出日志消息。这样做的好处是性能更好,但是正则表达式也会变得复杂得多。

注意,message 的 regex 是不完整的,并且在匹配任何可能包含空格或非字母字符的标准日志消息方面表现会很差。

注意事项

  • 调试,特别是当管道阶段正在修改日志条目时。
  • 标签冲突以及如何处理(两个阶段尝试设置同一个标签)。
  • 性能与编写/使用的便捷性之间的权衡:如果每个标签都是逐个提取的,并且标签很多,日志行也很长,这将导致日志行被读取多次;但这与只需要读取一次但难以编写、更改和维护的非常长且复杂的 regex 形成对比。

进一步改进

我们的管道有一些基本的构建块,它们将使用 EntryMiddleware 接口,最常用的两个可能是

  • Regex 解析器
  • JSON 解析器

然而,我们不想让人们一遍又一遍地复制粘贴非常常见用例的基本配置,因此添加一些额外的解析器会更有意义,这些解析器实际上是上面基础解析器的超集。

例如,上面的配置可以简化为

yaml
scrape_configs:
- job_name: system
  pipeline_stages:
    - docker:

yaml
scrape_configs:
- job_name: system
  pipeline_stages:
    - cri:

并且仍然可以轻松扩展以提取更多标签。

yaml
scrape_configs:
- job_name: system
  pipeline_stages:
    - docker:
    - regex:
        expr: '.*level=(?P<level>[a-zA-Z]+).*'
        labels:
          level:

自动检测?

更进一步的简化是尝试自动检测日志格式,这项工作的 PR 已经提交,然后配置可以像这样简单:

yaml
scrape_configs:
- job_name: system
  pipeline_stages:
    - auto:

这对于首次采用和测试 Loki 的用户肯定有一些优势,允许他们将其指向自己的日志,并且至少能正确地提取 Docker 和 CRI 等常见格式的时间戳和日志消息。

自动检测也存在一些挑战和边缘情况,尽管大多数人会希望通过添加额外的标签来增强基本配置,所以也许默认开启自动检测,但在人们开始编写配置时建议他们选择正确的解析器是有道理的?

其他想法和考虑

  • 我们应该以某种方式提供一个独立的客户端,允许在命令行测试日志解析,以便用户可以验证正则表达式或配置,查看提取了哪些信息。
  • 不从日志文件中读取的管道的其他输入格式,例如 containerd grpc api,或从 stdin 或 unix 管道等。
  • 如果某个时候我们能支持将代码加载到管道阶段,以获得更高级/更强大的解析能力,那就更好了。