Collector Workload
The OpenTelemetry Collector is a telemetry router. It receives data from application workloads, optionally processes or samples it, and exports it to one or more backends.
The two files that must agree are:
- The Control Plane workload template, which exposes ports.
- The collector
config.yaml, which binds receivers and exporters to those same ports.
If these files disagree, telemetry can fail silently.
Control Plane Workload Template
Add a template such as
.controlplane/templates/open-telemetry-collector.yml.
kind: workload
name: open-telemetry-collector
spec:
type: standard
containers:
- name: open-telemetry-collector
image: "registry.example.com/example/open-telemetry-collector:0.155.0"
args:
- "--config=/etc/otelcol-contrib/config.yaml"
# Uncomment when config.yaml references ${env:TELEMETRY_BACKEND_TOKEN}.
# env:
# - name: TELEMETRY_BACKEND_TOKEN
# value: cpln://secret/{{APP_NAME}}-telemetry-backend.TELEMETRY_BACKEND_TOKEN
cpu: 100m
memory: 256Mi
ports:
# OTLP over HTTP from application SDKs.
- number: 4318
protocol: http
# StatsD over TCP, not UDP. Delete this port unless your collector
# config enables statsd/tcp and app clients set TCP transport.
# 9127 is project-specific; StatsD defaults to 8125/UDP.
# When deleting it, also remove receivers.statsd/tcp and statsd/tcp
# from service.pipelines.metrics.receivers in config.yaml.
- number: 9127
protocol: tcp
# Prometheus-formatted metrics exposed by the collector.
- number: 9292
protocol: http
defaultOptions:
autoscaling:
metric: disabled
minScale: 1
maxScale: 1
capacityAI: false
firewallConfig:
internal:
inboundAllowType: same-gvc
external:
# Prefer a narrow allowlist for production. Use the hostnames or CIDRs
# required by your telemetry backend.
outboundAllowHostname:
- telemetry-backend.example.com
# Use a collector-specific identity when the collector reads telemetry
# backend secrets that app workloads should not reveal.
identityLink: "//identity/{{APP_NAME}}-otel-collector-identity"
Use outboundAllowCIDR instead of outboundAllowHostname when your backend
requires IP ranges. Avoid leaving collector egress open to 0.0.0.0/0 in
production unless your security model explicitly accepts that risk.
Create the collector identity and a secret policy in a separate
open-telemetry-collector-secrets template before applying the workload
template if the collector reads backend tokens from Control Plane secrets:
kind: identity
name: "{{APP_NAME}}-otel-collector-identity"
description: "{{APP_NAME}}-otel-collector-identity"
---
kind: policy
name: "{{APP_NAME}}-otel-collector-secrets"
description: "{{APP_NAME}}-otel-collector-secrets"
bindings:
- permissions:
- reveal
principalLinks:
- "//gvc/{{APP_NAME}}/identity/{{APP_NAME}}-otel-collector-identity"
targetKind: secret
targetLinks:
- "//secret/{{APP_NAME}}-telemetry-backend"
Create the {{APP_NAME}}-telemetry-backend dictionary secret with a
TELEMETRY_BACKEND_TOKEN key, or replace that name consistently before
applying the templates. Keep this policy scoped to only the backend secret the
collector needs.
cpflow apply-template replaces {{APP_NAME}} with the actual app name. When
applying the YAML directly with cpln, replace {{APP_NAME}} manually before
running cpln apply.
Keep the collector image pinned to a tested release and update it as part of
normal dependency maintenance. Do not rely on a floating latest tag for
production workloads.
Delivering Collector Config
The official collector image does not include your application-specific
config.yaml. A simple Control Plane pattern is to build a small collector image
that starts from the pinned upstream image and copies the config into the path
used by the workload command.
FROM otel/opentelemetry-collector-contrib:0.155.0
COPY config.yaml /etc/otelcol-contrib/config.yaml
Build this image, publish it to your registry, and set the workload template
image: field to that published image. The upstream image is only the base image
for this Dockerfile because it does not contain your config.yaml.
Baking the config into the image keeps collector startup deterministic. If your
team already manages runtime files through Control Plane, you can instead mount a
secret-backed file with a cpln://secret/... URI and path: entry, following
the same pattern used by other file-delivery templates.
Matching Collector Config
Mount or bake the collector config at the path passed to --config. The contrib
image convention is /etc/otelcol-contrib/config.yaml; use a different path only
when your image or command is built for it.
The collector config must bind every exposed port. This minimal config receives OTLP over HTTP, optionally receives StatsD over TCP, and exposes metrics for Prometheus scraping.
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
statsd/tcp:
endpoint: 0.0.0.0:9127
transport: tcp
aggregation_interval: 60s # Use 5s during initial validation.
processors:
memory_limiter:
check_interval: 1s
limit_percentage: 80
spike_limit_percentage: 20
batch: {}
exporters:
prometheus:
endpoint: 0.0.0.0:9292
# Replace this placeholder with your real trace/log backend.
otlphttp/backend:
endpoint: "https://telemetry-backend.example.com"
# This is a base URL. The exporter appends /v1/traces, /v1/metrics,
# and /v1/logs automatically. Do not include the signal path here.
# headers:
# Authorization: "Bearer ${env:TELEMETRY_BACKEND_TOKEN}"
# Optional validation-only exporter. Do not leave debug in production
# pipelines because it writes telemetry payloads to collector logs.
# debug:
# verbosity: basic
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlphttp/backend]
metrics:
receivers: [otlp, statsd/tcp]
processors: [memory_limiter, batch]
exporters: [prometheus, otlphttp/backend]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlphttp/backend]
Before applying the config, replace telemetry-backend.example.com with your
real backend endpoint and headers. Keep memory_limiter before batch, and
keep the Prometheus exporter only when your platform or backend scrapes collector
metrics from port 9292. For push-only metrics backends, remove prometheus
from the metrics pipeline and keep otlphttp/backend. If your application only
sends OTLP metrics and does not use StatsD, also remove the statsd/tcp
receiver block and statsd/tcp from the metrics.receivers list. See
StatsD and UDP for the full three-piece removal.
If you have no OTLP push backend at all, remove otlphttp/backend from the
traces, metrics, and logs pipelines and delete the otlphttp/backend exporter
block entirely.
Store backend tokens such as TELEMETRY_BACKEND_TOKEN in Control Plane secrets
and bind them only to the collector workload identity shown in the template
above. Create the collector identity and a policy that grants reveal on only
the telemetry backend secret before applying the collector workload. When the
collector config references ${env:TELEMETRY_BACKEND_TOKEN}, also add the
matching env entry to the collector workload so Control Plane injects the
secret value at startup. Use the app identity placeholder only when the collector
does not need secrets that are isolated from app workloads. See
Secrets and ENV Values for the recommended
pattern.
The Prometheus exporter exposes an unauthenticated scrape endpoint. With
same-gvc firewall isolation, any workload in the same GVC that can reach port
9292 can read exported metrics. Widen collector inbound access only when that
is acceptable.
Port 9292 is an example scrape port. Configure your scraper to match the port
you expose in the workload template and bind in the collector config.
StatsD and UDP
The upstream OpenTelemetry StatsD receiver defaults to UDP on port 8125.
Control Plane workload templates in this repository currently use http and
tcp ports, not udp. Do not document or deploy UDP StatsD on Control Plane
unless you have verified that the platform supports the required UDP forwarding
path for your workload.
Safer defaults:
- Prefer OTLP metrics over HTTP on
4318. - Use StatsD over TCP only when your app client and collector build support it.
- Treat UDP StatsD as an environment-specific advanced option.
If you remove StatsD, remove all three pieces together: the workload port, the
statsd/tcp receiver block, and statsd/tcp from the metrics pipeline
receivers.
controlplane.yml
Include the collector in setup and informational commands:
aliases:
common: &common
setup_app_templates:
- app
- worker
- open-telemetry-collector-secrets
- open-telemetry-collector
app_workloads:
- app
- worker
additional_workloads:
- open-telemetry-collector
apps:
example:
<<: *common
Use open-telemetry-collector-secrets for the identity and policy YAML shown
above. List it before open-telemetry-collector so cpflow setup-app creates
the collector identity and secret reveal policy before applying the workload
that references identityLink.
Keep app_workloads limited to real application workloads such as app and
worker. Put the collector under additional_workloads so informational
commands show it without treating it as an app process.
For an existing app, apply the identity and policy template before applying the collector workload:
cpflow apply-template open-telemetry-collector-secrets -a $APP_NAME
cpflow apply-template open-telemetry-collector -a $APP_NAME
For a brand-new app, cpflow setup-app applies both collector templates with
the other entries listed in setup_app_templates, in the order shown above.
Port Agreement Checklist
The Control Plane workload template and collector config must agree on every exposed port. See Verify Port Agreement for the canonical port checklist. Remove any workload port that is not enabled in the collector config.