自定义组件 · 怎么扩展 Collector

几百个社区组件都是同一个模子刻出来的。这一章讲 Factory + Component 生命周期这套扩展契约、写一个 processor 要注意什么(尤其是 MutatesData 这个坑),以及用 OCB 组装自己的发行版。

作者 David更新于 第 42 篇(共 52 篇)

自定义组件 · 怎么扩展 Collector

Collector 有几百个社区组件,而它们全是同一个模子刻出来的。这一章讲那个模子——顺便,读懂它你也就读懂了那几百个组件(它们都是同一模式的复制)。

这也是第 6 部分的收尾。

Component 接口:两个方法

// component/component.go:25(真实注释,节选)
type Component interface {
    // Start tells the component to start. Host parameter can be used for communicating
    // with the host after Start() has already returned. If an error is returned by
    // Start() then the collector startup will be aborted.
    //
    // If the component needs to perform a long-running starting operation, then
    // it is recommended that Start() returns quickly and the long-running
    // operation is performed in the background. Background operations should
    // create their own context using context.WithCancel(context.Background())
    // rather than using the passed context, which is intended only for the
    // startup operation itself.
    Start(ctx context.Context, host Host) error
    Shutdown(ctx context.Context) error
}

注释里藏着两条很重要的工程纪律**:**

  • “Start() 应该快速返回,长任务放后台” —— 因为 Graph.StartAll 要按顺序启动所有组件。一个组件在 Start() 里干半天(比如死等后端连上),会卡住整个 Collector 的启动。
  • “后台任务要自己建 context,别用传进来的那个” —— 因为传进来的 ctx 只代表“启动这个操作”的生命周期,Start() 返回后它可能就被取消了。如果你的后台 goroutine 用了它,会莫名其妙提前退出。

这是个真实的、常见的 Go 陷阱——把“启动用的 context”当成“组件生命周期的 context”。正确做法是 context.WithCancel(context.Background()),然后在 Shutdown() 里 cancel。 注释把这条写得如此详细,说明踩的人不少。

Factory:注册的入口

官方文档点名的模式:每类组件都有 Factory + NewFactory。写一个 processor 大致是:

func NewFactory() processor.Factory {
    return processor.NewFactory(
        component.MustNewType("myprocessor"),   // 配置里用的名字
        createDefaultConfig,                    // 默认配置
        processor.WithMetrics(createMetrics, component.StabilityLevelBeta),
        processor.WithTraces(createTraces, component.StabilityLevelBeta),
        // 没声明的信号类型 = 不支持,配了会报错
    )
}

func createMetrics(ctx context.Context, set processor.Settings,
                   cfg component.Config, next consumer.Metrics) (processor.Metrics, error) {
    // next 就是 nextConsumer —— 下游是谁,由图注入
    return &myProcessor{next: next, cfg: cfg.(*Config)}, nil
}

三个设计点:

  • 按信号类型分别声明 —— WithMetrics/WithTraces/WithLogs。你可以只支持一种。配置里如果把只支持 metrics 的 processor 放进 traces 管道,图构建时就会报错——在启动期发现,而不是运行时。
  • StabilityLevel —— 每个组件要声明自己的稳定级别(Development/Alpha/Beta/Stable)。这是个诚实的设计:用户能知道自己在用什么成熟度的东西,而不是全都装作 1.0。
  • next consumer.Metrics —— 下游由图注入,组件不知道也不该知道下游是谁。

写 processor 时最该小心的:MutatesData

上一章讲过,但值得再说一遍,因为它是自定义组件最容易出的错**:**

func (p *myProcessor) Capabilities() consumer.Capabilities {
    // ⚠️ 这里必须诚实!
    return consumer.Capabilities{MutatesData: true}   // 我会改数据
}

func (p *myProcessor) ConsumeMetrics(ctx context.Context, md pmetric.Metrics) error {
    // 如果你在这里改了 md(哪怕只是加个属性),
    // 上面就必须是 true —— 否则[扇出时](/manual/observability/collector-architecture)
    // 另一条管道的数据会被你悄悄改掉
    md.ResourceMetrics().At(0).Resource().Attributes().PutStr("env", "prod")
    return p.next.ConsumeMetrics(ctx, md)
}

申报错了的后果:另一条管道的数据莫名其妙被改——只在特定配置下出现、极难复现、且没有任何报错。

两条保命规则:

  1. 拿不准就填 true —— 牺牲一次复制,换正确性。这个交易永远划算。
  2. 或者自己先复制再改,然后填 false —— 注释里明确允许这条路:“If the processor creates a copy of the data before modifying then this flag can be safely set to false.”

OCB:组装自己的发行版

上一章说过生产该用 OCB(OpenTelemetry Collector Builder) 只打包你要的组件。它的配置就是一份清单:

# builder-config.yaml
dist:
  name: my-otelcol
  output_path: ./dist

receivers:
  - gomod: go.opentelemetry.io/collector/receiver/otlpreceiver v0.156.0
processors:
  - gomod: go.opentelemetry.io/collector/processor/batchprocessor v0.156.0
  - gomod: github.com/mycompany/myprocessor v0.1.0    # ← 你自己的!
exporters:
  - gomod: github.com/open-telemetry/.../prometheusremotewriteexporter v0.156.0
ocb --config=builder-config.yaml    # 生成 + 编译出一个定制二进制

OCB 干的事其实很朴素:根据清单生成一个 Go main 包**(把各组件的 NewFactory() 注册进去),然后 go build。**

注意“你自己的组件”和官方组件在清单里是平等的**——都只是一个 Go module。** 这就是 Factory + 接口那套设计带来的红利:你的组件和官方组件享有完全相同的地位,不需要 fork Collector、不需要给上游提 PR、不需要等 release。

这个“核心 + 清单式组装”的模式很值得学:它让一个项目能同时做到“核心精简”和“生态繁荣”——和 Prometheus 用 file_sd/http_sd 做“万能逃生舱”是同一类智慧,只是 Collector 把它推到了极致:整个 Collector 就是一堆可插拔组件 + 一张把它们连起来的图。

小结与下一部分

几百个社区组件都是同一个模子:Component 接口只有 Start/Shutdown,但注释藏着两条纪律——Start() 要快速返回(否则卡住整个启动)、后台任务要自己建 context(传进来的那个只代表启动操作,Start() 返回后可能就被取消 → goroutine 莫名退出);这是个常见的 Go 陷阱,注释写这么细说明踩的人不少。Factory 是注册入口:按信号类型分别声明(配错管道在启动期就报错)、要声明 StabilityLevel(诚实的设计)、next consumer 由图注入。写 processor 最该小心 MutatesData——申报错了会让另一条管道的数据被悄悄改,极难复现;拿不准填 true,或自己先复制再改然后填 false。OCB 按清单组装自己的发行版——你的组件和官方组件地位完全平等,不用 fork、不用等 release;这个“核心 + 清单式组装”是Factory + 接口设计带来的红利,和 Prometheus 的 file_sd 逃生舱同类智慧,只是 Collector 推到了极致:整个系统就是一堆可插拔组件 + 一张连接它们的图。

Collector 讲完了。它运送的三大支柱里,我们只深入讲过指标。下一部分补上另外两根——以及把它们串起来的那些桥。👉 第 7 部分 · 追踪与日志。

参考资料

  • [1] OTel Collector 源码 component/component.go:25(Component 接口与 Start 的注释纪律)、processor/processor.go(NewFactory)。
  • [2] OTel Collector 官方文档:Building a custom collector(OCB)、docs/component-stability.md。
  • [3] 关联Collector 架构、MutatesData、发行版。

本页目录