# 自定义组件 · 怎么扩展 Collector

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

- 作者：David（道雾轩）
- 专栏：指标采集与观测（https://daiw.org/manual/observability.md）
- 最后更新：2026-10-01
- 原文：https://daiw.org/manual/observability/custom-components
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 自定义组件 · 怎么扩展 Collector

[Collector 有几百个社区组件](https://daiw.org/manual/observability/collector-config)，而它们**全是同一个模子刻出来的**。这一章讲那个模子——**顺便，读懂它你也就读懂了那几百个组件**（它们都是同一模式的复制）。

这也是第 6 部分的收尾。

## Component 接口：两个方法

```go
// 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
}
```

<Callout type="info">
  **注释里藏着两条**很重要的工程纪律**：**

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

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

## Factory：注册的入口

[官方文档点名的模式](https://daiw.org/manual/observability/collector-architecture)：每类组件都有 `Factory` + `NewFactory`。写一个 processor 大致是：

```go
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
}
```

<Callout type="info">
  **三个设计点：**

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

## 写 processor 时最该小心的：MutatesData

<Callout type="warn">
  **[上一章讲过](https://daiw.org/manual/observability/consumer-backpressure)，但值得再说一遍，因为它是自定义组件**最容易出的错**：**

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

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

  **申报错了的后果**：[另一条管道的数据莫名其妙被改](https://daiw.org/manual/observability/pdata)——**只在特定配置下出现、极难复现、且没有任何报错**。

  **两条保命规则：**
  1. **拿不准就填 `true`** —— 牺牲一次复制，换正确性。**这个交易永远划算。**
  2. **或者自己先复制再改，然后填 `false`** —— [注释里明确允许这条路](https://daiw.org/manual/observability/consumer-backpressure)：“If the processor creates a copy of the data before modifying then this flag can be safely set to false.”
</Callout>

## OCB：组装自己的发行版

<Callout type="info">
  **[上一章说过](https://daiw.org/manual/observability/collector-config)生产该用 **OCB（OpenTelemetry Collector Builder）** 只打包你要的组件。它的配置就是一份清单：**

  ```yaml
  # 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
  ```

  ```bash
  ocb --config=builder-config.yaml    # 生成 + 编译出一个定制二进制
  ```

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

  **注意“你自己的组件”和官方组件在清单里是**平等的**——都只是一个 Go module。** 这就是 [Factory + 接口那套设计带来的红利](https://daiw.org/manual/observability/collector-architecture)：**你的组件和官方组件享有完全相同的地位**，不需要 fork Collector、不需要给上游提 PR、不需要等 release。

  **这个“核心 + 清单式组装”的模式很值得学**：它让一个项目能同时做到“核心精简”和“生态繁荣”——[和 Prometheus 用 `file_sd`/`http_sd` 做“万能逃生舱”](https://daiw.org/manual/observability/service-discovery)是同一类智慧，**只是 Collector 把它推到了极致：整个 Collector 就是一堆可插拔组件 + 一张把它们连起来的图**。
</Callout>

## 小结与下一部分

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

Collector 讲完了。它运送的[三大支柱](https://daiw.org/manual/observability/three-pillars)里，我们只深入讲过指标。下一部分补上另外两根——以及把它们**串起来**的那些桥。👉 第 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 架构](https://daiw.org/manual/observability/collector-architecture)、[MutatesData](https://daiw.org/manual/observability/consumer-backpressure)、[发行版](https://daiw.org/manual/observability/collector-config)。
