14. 09. 2026 Francesco Penasa APM

Building a Custom OpenTelemetry Collector

OpenTelemetry’s graduation within the Cloud Native Computing Foundation was an important milestone. It confirmed that the project’s APIs, governance, and release processes had reached a level of maturity suitable for long-term adoption.

However, graduation doesn’t solve one practical problem: The distribution containing the Collector you need may not include all the components your environment requires.

One distribution might provide the receiver you need, while another includes the processor or exporter you want. If neither distribution contains the complete combination, you’re usually left with two choices:

  • Use the distribution that covers most of your requirements and work around the missing components
  • Run multiple collectors and connect them with an additional pipeline

But there’s a third option: Build your own collector distribution with the OpenTelemetry Collector Builder, or OCB. OCB allows you to assemble a collector containing only the components required by your use case.

This article explains how to build a custom collector with OCB, using an OpenShift and Elastic deployment as an example.

Why Build a Custom Collector?

Suppose you’re running an OpenShift cluster and want to collect and monitor telemetry from it. The Red Hat build of OpenTelemetry is a sensible starting point. As discussed in my previous article, it provides a curated and supported set of OpenTelemetry components.

However, it may not include all the processors and exporters you need for a particular observability platform.

In this example, we want to use Elastic for monitoring and would like the collector to handle Elastic-specific enrichment and processing natively. In particular, we want to:

  • Enrich traces in a way that works well with Elastic APM
  • Export telemetry directly to Elasticsearch
  • Use Elastic Common Schema (ECS) where appropriate
  • Avoid adding another transformation or forwarding step outside the Collector

Consider this common situation: A platform-specific distribution provides most of what you need, but it’s missing components from a particular vendor or ecosystem. OCB provides a way to combine those pieces into a single binary.

There is no universally correct distribution to use as a starting point. The OpenTelemetry project provides both the core collector and the contrib distribution. There are also distributions maintained by vendors such as Red Hat, Elastic, Grafana, and AWS.

You can use any of these as a reference, or start from an empty manifest and define the required components yourself. In this article, we use the OpenShift and Elastic combination because it represents a realistic deployment scenario. The same process applies to other platforms and vendors.

Prerequisites

You’ll need two things before building a collector:

  • A working Go installation
  • The OpenTelemetry Collector Builder binary

OCB is written in Go, so the Go toolchain must be installed before you build or install it. Use the Go version recommended by the Collector release you plan to use.

For this example, we’ll use OCB version 0.156.0:

# curl --proto '=https' --tlsv1.2 -fL -o ocb \
  https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/cmd/builder/v0.156.0/ocb_0.156.0_linux_amd64

# chmod +x ocb

Pin-compatible Versions

Version alignment is important when building a custom collector. The OCB version, the OpenTelemetry components in the manifest, and third-party components must be compatible with one another. Using arbitrary versions can cause Go dependency conflicts or compilation errors when APIs have changed or been removed.

The version numbers do not all need to be identical. Each component is identified by its Go module path and its own version, and different projects may follow different release cycles. For example, the core OpenTelemetry Collector components and the OCB builder may use v0.156.0, Elastic’s Collector components may use v0.68.0, and the OpenTelemetry configuration-provider modules may use v1.62.0. These numbers cannot be compared directly: v1.62.0 is not necessarily newer than v0.156.0, because they belong to different modules. What matters is that the selected versions expose compatible APIs and are tested together. For example, the following combination can be valid if the Elastic v0.68.0 components support the Collector APIs used by v0.156.0:

dist:
  otelcol_version: "0.156.0"

processors:
  - gomod: go.opentelemetry.io/collector/processor/batchprocessor v0.156.0
  - gomod: github.com/elastic/opentelemetry-collector-components/processor/elasticapmprocessor v0.68.0

providers:
  - gomod: go.opentelemetry.io/collector/confmap/provider/envprovider v1.62.0

While an incompatible dependency set may result in errors such as: undefined: xconfmap.Validator

Using the latest version of every component is tempting, but it is not always the safest approach. Pin the versions deliberately and update them together after testing the resulting Collector.

Define the Collector Components

A useful way to approach the manifest is to start with the smallest set of components that meet your requirements.

Include only the receivers, processors, connectors, exporters, extensions, and configuration providers that you actually need. This keeps the resulting binary smaller and makes its behavior easier to understand and maintain. It also avoids shipping unused components that may increase the operational and security footprint of the image.

In our example we want at least:

  • Elastic’s APM processor and connector
  • The Elasticsearch exporter
  • Elastic’s APM configuration extension

A Minimal Manifest

Create a file named manifest.yaml with the following content:

dist:
  name: otelcol-neteye
  description: Custom OpenTelemetry Collector for NetEye (Elastic-ready)
  output_path: ./otelcol-neteye
  version: 1.0.0
  otelcol_version: "0.156.0"


receivers:
  - gomod: go.opentelemetry.io/collector/receiver/otlpreceiver v0.156.0
  - gomod: github.com/elastic/opentelemetry-collector-components/receiver/elasticapmintakereceiver v0.68.0

processors:
  - gomod: go.opentelemetry.io/collector/processor/batchprocessor v0.156.0
  - gomod: github.com/elastic/opentelemetry-collector-components/processor/elasticapmprocessor v0.68.0

connectors:
  - gomod: github.com/elastic/opentelemetry-collector-components/connector/elasticapmconnector v0.68.0

exporters:
  - gomod: go.opentelemetry.io/collector/exporter/debugexporter v0.156.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/exporter/elasticsearchexporter v0.156.0
  - gomod: go.opentelemetry.io/collector/exporter/otlpexporter v0.156.0

extensions:
  - gomod: github.com/elastic/opentelemetry-collector-components/extension/apmconfigextension v0.68.0

providers:
  - gomod: go.opentelemetry.io/collector/confmap/provider/envprovider v1.62.0
  - gomod: go.opentelemetry.io/collector/confmap/provider/fileprovider v1.62.0
  - gomod: go.opentelemetry.io/collector/confmap/provider/httpprovider v1.62.0
  - gomod: go.opentelemetry.io/collector/confmap/provider/httpsprovider v1.62.0
  - gomod: go.opentelemetry.io/collector/confmap/provider/yamlprovider v1.62.0

replaces:
  - github.com/elastic/opentelemetry-collector-components/internal/elasticattr => github.com/elastic/opentelemetry-collector-components/internal/elasticattr v0.68.0

The manifest is declarative, describing the components that should be included in the final distribution. OCB then generates the source code and builds our collector from that definition.

Build the Collector

./ocb --config manifest.yaml

The output will appear in the output_path directory defined in your manifest (in this case, ./otelcol-neteye). You can verify that the build succeeded by checking for the binary executable (otelcol-neteye).

A Collector Configuration

Once built, the binary is configured exactly like any other OTel Collector, it needs a YAML configuration file and nothing else. Here’s an example.

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

connectors:
  elasticapm: {}

processors:
  batch: {}
  elasticapm: {}

exporters:
  debug:
    verbosity: detailed
  elasticsearch/otel:
    endpoints: ["https://your-elasticsearch:9200"]
    api_key: ${env:ELASTIC_API_KEY}
    mapping:
      mode: otel

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [elasticapm]
      exporters: [elasticapm, elasticsearch/otel, debug]
    metrics:
      receivers: [otlp]
      exporters: [elasticsearch/otel, debug]
    metrics/aggregated-otel-metrics:
      receivers: [elasticapm]
      exporters: [elasticsearch/otel]
    logs:
      receivers: [otlp]
      exporters: [elasticsearch/otel, debug]

And now we’re ready to test it.

Validate the configuration: ./build/otelcol-neteye validate --config config.yaml

Run it and watch for errors: Using the debug exporter you’ll be able to see if there are any issues with the configuration ./build/otelcol-neteye --config config.yaml

Send some telemetry: The Collector-contrib repository ships telemetrygen, a small load generator, which you can use to generate sample telemetry:

git clone https://github.com/open-telemetry/opentelemetry-collector-contrib.git
cd opentelemetry-collector-contrib/cmd/telemetrygen
go build -o telemetrygen .
./telemetrygen traces --otlp-endpoint localhost:4317 --otlp-insecure --traces 5

If everything is wired up correctly, you should see the spans printed by the debug exporter in your collector’s logs, confirming that we have our own working OpenTelemetry Collector!

Containerizing the Custom Collector

Running a local binary is fine for testing and for a handful of use cases. Most of the time though you’ll want a collector packaged into a neat container image; let’s look at how to do it.

The cleanest approach is a multi-stage Dockerfile: One stage compiles the binary with the full Go toolchain, and a second, minimal stage just delivers the finished binary.

# ---- Stage 1: build the custom collector with OCB ----
FROM golang:1.23-bookworm AS builder

ARG OCB_VERSION=0.156.0
WORKDIR /build

# Install the builder pinned to the same release train as the manifest
RUN go install go.opentelemetry.io/collector/cmd/builder@v${OCB_VERSION}

# Bring in the manifest and build
COPY manifest.yaml .
RUN builder --config manifest.yaml

# ---- Stage 2: minimal runtime image ----
FROM alpine:3.20

# TLS roots so the collector can reach Elasticsearch over HTTPS
RUN apk add --no-cache ca-certificates

# Copy only the compiled binary from the builder stage
COPY --from=builder /build/otelcol-neteye/otelcol-neteye /otelcol-neteye

# OTLP gRPC and HTTP receiver ports
EXPOSE 4317 4318

ENTRYPOINT ["/otelcol-neteye"]
CMD ["--config", "/etc/otelcol/config.yaml"]

Build it and run it!

Wrapping Up

The OpenTelemetry Collector Builder is useful whenever an existing distribution is close to what you need, but is missing one or two important components.

Instead of choosing between several incomplete distributions or operating multiple collectors, you can create a focused binary that contains exactly the receivers, processors, connectors, exporters, extensions, and providers required by your deployment.

The same approach works beyond OpenShift and Elastic. Change the receivers, processors, or exporters to match your environment, keep the manifest limited to the components you need, and update the dependency versions as a tested set.

These Solutions are Engineered by Humans

Did you find this article interesting? Does it match your skill set? Our customers often present us with problems that need customized solutions. In fact, we’re currently hiring for roles just like this and others here at Würth IT Italy.

Francesco Penasa

Francesco Penasa

Hi! I'm Francesco, Observability Engineer at Wuerth IT Italy focused on making sure complex systems stay transparent, predictable and do exactly what they're supposed to. That drive traces back to my Master's in Computer Science, where formal methods and the theoretical foundations of the field taught me to care deeply about correctness and understanding the root cause of things. That same curiosity naturally pulled me into software development, and more recently into AI and machine learning (including LLMs of course), which are, for better or worse, the opposite of deterministic. It's a strange place to end up for someone who likes things to behave. My biggest daily challenge? Myself. I firmly believe that comfort zones are overrated, so I constantly push myself to take on new challenges and keep laziness at bay. Spoiler: it's an ongoing battle.

Author

Francesco Penasa

Hi! I'm Francesco, Observability Engineer at Wuerth IT Italy focused on making sure complex systems stay transparent, predictable and do exactly what they're supposed to. That drive traces back to my Master's in Computer Science, where formal methods and the theoretical foundations of the field taught me to care deeply about correctness and understanding the root cause of things. That same curiosity naturally pulled me into software development, and more recently into AI and machine learning (including LLMs of course), which are, for better or worse, the opposite of deterministic. It's a strange place to end up for someone who likes things to behave. My biggest daily challenge? Myself. I firmly believe that comfort zones are overrated, so I constantly push myself to take on new challenges and keep laziness at bay. Spoiler: it's an ongoing battle.

Leave a Reply

Your email address will not be published. Required fields are marked *

Archive