name: otel-collector-config description: >- Use when authoring, debugging, or reviewing an OpenTelemetry Collector (otelcol) configuration — defining receivers/processors/exporters/connectors, wiring service pipelines, choosing between the core and contrib distributions, or exporting to OTLP/Prometheus/Loki/Tempo/Mimir. Also use when a Collector starts but no data arrives, a component is rejected at startup, or metrics lack expected labels. Triggers on "otelcol", "otel collector config", "collector pipeline", "receivers/processors/exporters", "memory_limiter", "otlp exporter", "prometheusremotewrite", "spanmetrics", "tail_sampling".
OpenTelemetry Collector config — doctor
Generate correct configs from a plain-English description, and diagnose broken ones. The Collector is a small set of concepts with a lot of sharp edges; this skill encodes the edges so the output validates on the first try.
Mental model (get this right and everything follows)
A Collector config has four component blocks plus a service block that wires
them:
receivers: # where data comes IN (otlp, prometheus, hostmetrics, filelog, kafka…)
processors: # what happens in the MIDDLE, in order (memory_limiter, batch, transform…)
exporters: # where data goes OUT (otlp, otlphttp, debug, prometheus, prometheusremotewrite…)
connectors: # bridge one pipeline's OUTPUT into another's INPUT (spanmetrics, routing…)
extensions: # collector-level features, not in the data path (health_check, pprof, zpages…)
service:
extensions: [health_check]
pipelines:
traces: { receivers: [...], processors: [...], exporters: [...] }
metrics: { receivers: [...], processors: [...], exporters: [...] }
logs: { receivers: [...], processors: [...], exporters: [...] }
telemetry: { ... } # the Collector's OWN logs/metrics
The #1 rule everyone violates: defining a component under receivers: /
processors: / exporters: does nothing on its own. A component only runs
if it is referenced inside a service.pipelines.<signal> list. A config can be
"valid" yet do nothing because a pipeline was never wired. Always check the
service block last, and confirm every component you defined is actually used
(and every name used is actually defined — that one fails validation).
Pipelines are per-signal. traces, metrics, logs are separate
pipelines. A receiver/exporter must support the signal of the pipeline it sits
in (e.g. the prometheus receiver is metrics-only; putting it in a traces
pipeline fails). You may run multiple named pipelines of the same signal
(metrics/internal, metrics/app).
Workflow
1. Pin the three questions
- Signals? traces / metrics / logs (any subset).
- Sources (receivers)? app via OTLP? scrape Prometheus targets? host metrics? tail log files? Kafka?
- Destinations (exporters)? an OTLP backend (Tempo/Jaeger/vendor)? Prometheus (pull) or remote-write (push)? Loki? Mimir?
2. Choose the distribution — core vs contrib (this trips people constantly)
Many common components ship only in otelcol-contrib, not core otelcol:
prometheus receiver, hostmetrics, filelog, kafka, resourcedetection,
transform/filter (OTTL) processors, tail_sampling, the spanmetrics
connector, prometheusremotewrite/loki exporters. If the config uses any of
these, it needs the contrib image (otel/opentelemetry-collector-contrib)
or a custom build via the OpenTelemetry Collector Builder (ocb). Calling for a
contrib component on the core image is a startup crash, not a YAML error — flag it.
3. Order processors correctly (order is significant and load-bearing)
Processors run in the listed order. The canonical safe order:
processors: [memory_limiter, <resource/detection>, <sampling/filter/transform>, batch]
memory_limitergoes FIRST. It must see data before anything buffers it, otherwise it can't shed load. Puttingbatchbefore it defeats the purpose.batchgoes LAST (just before export) so it batches the final shape.tail_sampling(traces) must come beforebatch, and needs whole traces — don't shard the same trace across replicas without a load-balancing exporter in front.
4. Pick exporters deliberately
otlp= OTLP over gRPC (default 4317).otlphttp= OTLP over HTTP (4318). Match the backend's port/protocol.debugis the console exporter (useverbosity: detailedwhile debugging). The oldloggingexporter is removed/deprecated → usedebug.- Metrics to Prometheus:
prometheus= Collector exposes a/metricsendpoint for Prometheus to scrape (pull).prometheusremotewrite= Collector pushes to a remote-write endpoint (Mimir/Thanos/Cortex/Prometheus--web.enable-remote-write-receiver). Don't confuse pull vs push. - Loki: the dedicated
lokiexporter was removed from contrib — send OTLP to Loki's native OTLP endpoint viaotlphttp(endpoint: http://loki:3100/otlp). - Tempo/Jaeger: send via
otlp/otlphttp— thejaegerexporter is gone (Jaeger ingests OTLP natively). Thejaegerreceiver still exists (beta, in core+contrib) for ingesting legacy Jaeger-protocol spans during a migration; don't call it removed.
5. Always include the safety/ops basics
memory_limiterprocessor in every pipeline that can be flooded.health_checkextension (and wire it intoservice.extensions).- For secrets/endpoints use env expansion:
endpoint: ${env:OTLP_ENDPOINT}(note theenv:prefix — bare${VAR}is deprecated syntax). - Set
service.telemetry.logs.leveland, if needed, expose the Collector's own metrics.
6. Validate before declaring done
Never hand back a config you haven't validated. The Collector has a built-in validator that loads and type-checks the full config without starting it:
otelcol validate --config=config.yaml # core
otelcol-contrib validate --config=config.yaml # contrib
The bundled scripts/validate.sh wraps this (auto-detects the contrib binary,
falls back to a Docker run if no local binary). If otelcol isn't available,
say so explicitly rather than claiming the config is validated.
Fixing an existing config — run this checklist
When handed a broken or "it starts but nothing arrives" config, check in order:
- Unwired components — every receiver/processor/exporter defined but not referenced in any
service.pipelines.*? (silent no-op) - Undefined references — a name used in a pipeline that isn't defined above? (validation error)
- Signal mismatch — a metrics-only receiver in a traces pipeline, etc.
- core vs contrib — a contrib-only component on the core image? (crash on start)
- Processor order —
batchbeforememory_limiter?memory_limiternot first? - pull vs push exporter —
prometheuswhereprometheusremotewritewas meant (or vice-versa)? - Removed components —
loggingexporter (→debug),lokiexporter (→otlphttp),jaegerexporter (→otlp; the jaeger receiver is still fine), bare${VAR}env syntax (→${env:VAR}). - Endpoint/protocol — gRPC vs HTTP port mismatch (4317 vs 4318);
tls/insecureset correctly for the environment. - Endpoint binding — receiver bound to
localhostwhen traffic comes from other containers (needs0.0.0.0), or bound to0.0.0.0in a context where that's a security concern.
State the diagnosis as "what was wrong → why it failed → the fix", then output the corrected config and validate it.
The hard parts — get these right every time (see references/advanced.md)
A capable model already handles the basics above. These six are the ones it gets
inconsistently right; treat them as load-bearing and pull the exact patterns
from references/advanced.md:
- OTTL statement form (
transform/filterprocessors). The function syntax itself —set(attributes["x"], "y") where <cond>,delete_key(...),replace_pattern(...)— is usually recalled correctly. What isn't: there are two forms with different path syntax, and mixing them fails to parse. Flat list → prefix every path (span.attributes[...]); inside acontext: spanblock → write paths bare (attributes[...]). Pick one per statement list. Seereferences/advanced.md§1. Always seterror_mode; regexes are RE2 (no lookahead). - spanmetrics (and any connector) is wired in TWO pipelines — as an exporter in the source pipeline and a receiver in the destination pipeline. Wiring it once silently produces no metrics (or a validation error).
- Sampling must sit downstream of metric generation. If
tail_sampling(or any sampler) runs before thespanmetricsconnector, RED metrics are computed from only the surviving fraction — request rate is wrong by the sampling ratio and nothing looks broken. Fork instead: the ingest pipeline exports to[spanmetrics, forward/sampled]; a second traces pipeline receivesforward/sampled, appliestail_sampling, and exports to the trace backend. spanmetrics must see 100% of spans. resource_to_telemetry_conversionis a trade-off, not a default-on. Off (the default), resource attributes land only on thetarget_infoseries — sosum by (service_name)returns nothing and you must join (... * on (job, instance) group_left(service_name) target_info). On, it copies every resource attribute onto every series — includingk8s.pod.name,host.id,container.id, multiplying series count per pod. Rule: enable it only after dropping the high-cardinality resource attributes; otherwise leave it off and keeptarget_info: enabled: truefor the join. State which you chose and why.tail_samplingat >1 replica needs aloadbalancingtier keyed bytraceID, so all spans of a trace reach the same sampler instance. Tail-sampling directly behind a plain load balancer is broken sampling.tail_samplinggoes beforebatch.- Exporter reliability — and the queue key is per-exporter. Production exporters
need
retry_on_failureplus a queue, but the key differs:otlp/otlphttpusesending_queue(addstorage: file_storage/...when data loss is unacceptable), whileprometheusremotewriteusesremote_write_queueand rejectssending_queueat startup. If validation rejects a queue key, look up that exporter's own key — do not conclude the exporter has no queue and ship it with retry only. Defaults drop data on a transient backend blip. Durability differs too:remote_write_queueis memory-only (it takes nostorage:), so a restart loses the backlog — but that does not mean the exporter has no on-disk option.prometheusremotewritehas its ownwal:(directory,buffer_size,truncate_frequency). For survive-a-restart, usewalhere, or move the path tootlphttp+sending_queue.storageagainst an OTLP-capable backend.
When the request touches any of these, encode the pattern explicitly rather than trusting recall — that's the difference this skill is for.
References
references/components.md— curated catalog of the most-used receivers / processors / exporters / connectors with the gotcha for each, and the core-vs-contrib split.references/advanced.md— the hard parts in depth: OTTL patterns, spanmetrics dual-wiring,resource_to_telemetry_conversion, tail_sampling load-balancing topology, exporter reliability.examples/— a before/after pair (broken → corrected, validated) andspanmetrics-config.yaml, a validated advanced config (spanmetrics connector + OTTL + remote-write labels + retry/queue).
Output discipline
- Emit valid YAML only inside config blocks — no
...placeholders that won't parse. - Before emitting, cross-check every name in
service.pipelinesagainst the blocks above it. Connectors are the usual miss: they appear in two pipelines, so it's easy to referenceforward/xorspanmetricswithout ever declaring it underconnectors:. A config that needs a "one thing to fix before you deploy" footnote is a failed answer. - Comment the non-obvious lines (why
memory_limiteris first, why this exporter). - Prefer the smallest config that satisfies the request; don't bolt on components the user didn't ask for.
- If a component is contrib-only, say so and name the image/build needed.
Architecte Docker Compose
DevOps
Concoit des configurations Docker Compose optimisees.
Rapport de Post-Mortem
DevOps
Rédige des rapports post-mortem d'incidents structurés et blameless.
Créateur de Runbooks
DevOps
Crée des runbooks opérationnels clairs pour les procédures DevOps courantes.