为 OpenTelemetry Collector Contrib 贡献 Unroll Processor

在 OpenTelemetry Collector 中解压(unrolling)捆绑日志的想法并非始于一个 processor。

“解压”的意思是,将单个日志记录中包含的多个逻辑事件(例如,包含十个日志条目的 JSON 数组)展开成十个单独的日志记录,每个记录对应一个事件。这样,您就可以处理单个日志条目,而不是捆绑的有效负载。

当 Collector SIG 首次讨论如何处理包含多个逻辑事件的日志(如 JSON 数组)的主体时,最初的想法是使用 `transform` processor 中的 OTTL(OpenTelemetry Transform Language)函数 来解决。

乍一看,这似乎是合理的。OTTL 功能强大、灵活,并且可以处理记录级别的转换。但我们发现了更深层次的挑战。`transform` processor 在迭代过程中添加新日志记录时遇到了困难。它可以修改和过滤现有数据,但将一条记录扩展成多条记录并非它作为单个 processor 的职责范围内可以有效完成的任务。

这时,我们想要介入并提供帮助。今年一月份,我帮助开发了 OpenTelemetry Collector 发行版中的一个专用解压 processor,主要是因为我们的客户遇到了这些问题。

Unroll processor 以一种清晰、确定的方式展开捆绑的记录。在生产环境中运行了数月后,我想通过 upstream 贡献 unroll processor,以便 OpenTelemetry 社区能够从共享解决方案中受益。

让我解释一下 unroll processor 是什么,它是如何工作的,它如何帮助您,以及我们如何 upstream 贡献到 Contrib 发行版中。

为何要解压?

核心问题很简单:一些源以一个日志记录的形式传递多个事件。您希望处理干净、独立的日志条目。

在 unroll 之前,您有两种选择:

  1. 在 Collector 之外预处理日志——如果您能够插入逻辑。
  2. 尝试让 OTTL/transform 去做它从未被设计用来做的事情。

这两种方法都未能正确解决问题。

Unroll Processor 的作用

Unroll processor 会接收一个列表状的日志主体(例如 JSON 数组),并将其展开成每个元素一条日志记录,同时保留时间戳、资源和日志属性。

如果您的输入是 JSON 数组中的十个对象,您将得到十条独立的日志记录。每条日志记录都将保留其元数据,并为转换、过滤、归约等操作做好准备。

它简单、可预测且生产安全。

为何选择放弃 OTTL?

我们对此进行了深入探讨。

理论上,使用 transform + OTTL 组合解决此问题似乎更简单。但当我们深入研究时,遇到了一个核心限制:OTTL 无法在迭代过程中安全地添加新记录。尝试在循环中生成新条目会导致记录跳过、语句执行不可靠以及行为脆弱。

Transform 和 filter processor 在变异和抑制数据方面表现出色。但展开是另一项职责。它需要自己的语义、生命周期和保证。

Unroll processor 将添加记录的关注点与转换逻辑清晰地分开,并以一种可组合且可预测的方式运行。

我帮助开发了 Bindplane Distro for OpenTelemetry Collector 中 unroll processor 的第一个版本。它于 2025 年 1 月首次发布并被客户使用,此后一直在生产环境中运行。

我看到客户在以下方面使用了它:

  • VPC 日志
  • CloudWatch 摄入管道
  • Windows + 端点日志
  • 捆绑的 collector 遥测数据

即使在真实的生产负载下,我们也观察到极低的故障率,尤其是在初始接收器或日志信号源对格式不太敏感的情况下。这让我们有信心将该组件 upstream。

如何配置 Unroll Processor

将 unroll processor 放入您的管道中,以便在需要展开捆绑的日志有效负载的地方使用。这是一个基本的配置示例,供您开始使用:

processors:
  unroll:
service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [..., unroll, ...]
      exporters: [logging]

常见的解压模式

Unroll processor 仅在 `log.body` 是可迭代列表(例如,一个有效的 JSON 数组)时才执行操作。但在实际的管道中,日志记录并非总是如此结构化。有时需要额外的预处理,将原始日志有效负载转换为 unroll processor 可以操作的格式。

示例:单个日志记录中的多个 JSON 对象

考虑一个将多个 JSON 对象连接成单个日志记录的场景,如下所示:

{"@timestamp":"2025-09-19T02:20:17.920Z", "log.level": "INFO", "message":"initialized", "ecs.version": "1.2.0","service.name":"ES_ECS","event.dataset":"elasticsearch.server","process.thread.name":"main","log.logger":"org.elasticsearch.node.Node","elasticsearch.node.name":"es-test-3","elasticsearch.cluster.name":"elasticsearch"},{"type": "server", "timestamp": "2025-09-18T20:44:01,838-04:00", "level": "INFO", "component": "o.e.n.Node", "cluster.name": "elasticsearch", "node.name": "es-test", "message": "initialized" }

以下是如何使用 `transform` processor 然后是 `unroll` 进行预处理:

receivers: ...

processors:
  transform:
    error_mode: ignore
    log_statements:
      - context: log
        statements:
          - set(body, Split(body, "\"},"))
  unroll: {}
exporters: ...

services:
  pipelines:
    logs:
      receivers: [...]
      processors: [transform, unroll]
      exporters: [...]

此转换语句使用 `Split` 函数,以 `"},"` 作为分隔符来分割 `body`,生成一个 unroll processor 可以展开的列表状 `body`。

摘要

此功能源于一个简单的需求:使 Collector 更通用,能够展开日志记录。

我们尝试了 OTTL 路线,发现它不容易实现,然后 upstream 了专用的、经过生产测试的、易于使用的 unroll processor。结果是一个小的配置更改,可以解决大量现实世界中的遥测数据摄入问题。

我们目前不建议将记录展开添加到 OTTL。展开会改变数据流的基数,可能需要不同的生命周期和正确性保证,因此专用 processor 目前是合适的选择。将职责分开,让 OTTL 专注于转换现有日志记录,而 unroll processor 则负责展开。

Unroll processor 现在已在官方的 OpenTelemetry Collector Contrib 中可用。请随时创建 issues 并测试它,以用于您的日志管道。