Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for OpenTelemetry By James Joyner IV · · 9 min read

OpenTelemetry Error Guide: 'cannot merge config' — Fix Collector Config Merge Failures

Quick answer

Fix 'cannot merge config: '[processors]' expected a map, got 'string'' when the Collector merges multiple --config sources with conflicting types.

  • #opentelemetry
  • #observability
  • #troubleshooting
  • #errors
Free toolkit

Fixing errors like this? Get 500 free DevOps AI prompts

500 copy-paste AI prompts for the stack you actually run — one PDF, free.

Overview

The OpenTelemetry Collector accepts multiple configuration sources and deep-merges them in order with its confmap machinery. When two sources disagree on the shape of the same key — one supplies a map, another supplies a scalar or list — the merge cannot reconcile them and the Collector refuses to start:

Error: cannot merge config: '[processors]' expected a map, got 'string'
2026/07/12 14:22:10 collector server run finished with error: cannot merge config

A related variant appears when an environment-variable override collapses a whole block into a string:

Error: cannot merge config: '[exporters][otlp][sending_queue]' expected a map, got 'string'

The message means one --config source (a file, a URI, or an env-expanded value) provided a different YAML type for a key than another source did, so the layered merge has no consistent structure to produce.

Symptoms

  • The Collector exits immediately at startup before any pipeline is built.
  • Logs show cannot merge config naming a specific key path like [processors] or [exporters][otlp].
  • The failure only appears when two or more --config flags (or a base plus overrides) are supplied.
  • Running with a single config file succeeds, but the combined invocation fails.
  • An env var such as OTEL_PROCESSORS or an override snippet is set to a scalar where the base file has a block.
  • otelcol-contrib validate on the merged set reports the same type mismatch.

Common Root Causes

  • Scalar overriding a map — an override file sets processors: batch (a string) while the base defines processors: { batch: {...} } (a map).
  • Env-var expansion collapsing a block${PROCESSORS} expands to a plain string, replacing a structured block with a scalar.
  • List vs map confusion — one source uses exporters: [otlp] (list) where the other expects exporters: { otlp: {...} } (map) for that key.
  • Wrong merge order — a stripped-down override intended to patch one field instead redefines a parent key with an incompatible type.
  • Copy-paste indentation drift — a mis-indented key lands one level up, turning a nested map into a sibling scalar during merge.
  • Templated config injection — a config-map or Helm value renders an empty or quoted value ("") where the base has a map.

Diagnostic Workflow

First, list exactly what sources the Collector is merging. Order matters — later --config values win, and each is deep-merged onto the previous:

# Inspect the running unit's ExecStart to see every --config source
systemctl cat otelcol-contrib | grep -A3 ExecStart
# Show any env vars that feed config expansion
systemctl show otelcol-contrib -p Environment

Validate the merged result rather than each file alone; validation reproduces the merge and surfaces the offending key path:

otelcol-contrib validate \
  --config /etc/otelcol-contrib/base.yaml \
  --config /etc/otelcol-contrib/override.yaml

Compare the type of the named key across sources. Here the base is a map but the override made it a scalar:

# base.yaml — processors is a MAP of component configs
processors:
  batch:
    send_batch_size: 512
  memory_limiter:
    check_interval: 1s
    limit_mib: 512
# override.yaml — WRONG: this replaces the whole map with a string
processors: batch

Fix the override so it merges as a map, patching only the field you intend to change:

# override.yaml — CORRECT: still a map, so it deep-merges
processors:
  batch:
    send_batch_size: 1024

If the culprit is env expansion, confirm what the variable resolves to before the Collector reads it:

echo "$PROCESSORS"   # if this prints a bare word, it will collapse the block
journalctl -u otelcol-contrib --since '10 min ago' | grep -i 'cannot merge'

Example Root Cause Analysis

A team ran the Collector with a shared base.yaml plus a per-environment override.yaml injected by a config-map. Staging worked; production failed at boot with cannot merge config: '[processors]' expected a map, got 'string'. The base defined processors: as a map with batch and memory_limiter. The production override had been hand-edited to processors: batch — an attempt to “select” the batch processor — which turned the whole key into a scalar. Because production layered that override on top of the base, the merge saw a map on one side and a string on the other and aborted.

The fix had two parts. First, the override was rewritten to keep processors: as a map and only patch the field that actually differed (send_batch_size: 1024), so the deep-merge preserved the base structure. Second, a CI step was added that runs otelcol-contrib validate against the fully merged config for each environment, so a type-shape mismatch fails the pipeline instead of the pod. Production started cleanly and the class of error was caught before deploy thereafter.

Prevention Best Practices

  • Keep every override the same YAML type as the key it patches — a map stays a map, a list stays a list.
  • Validate the merged configuration (otelcol-contrib validate with all --config sources) in CI, not just individual files.
  • Prefer patching leaf fields in overrides rather than redefining parent blocks like processors: or exporters:.
  • Quote and sanity-check any env var feeding ${...} expansion so it never collapses a block to a scalar.
  • Pin --config ordering explicitly and document which source is the base and which are overrides.
  • Lint YAML for indentation so a nested key never drifts into a sibling scalar during merge.

Quick Command Reference

# Show every --config source the unit merges, in order
systemctl cat otelcol-contrib | grep -A3 ExecStart

# Validate the merged result and reveal the offending key path
otelcol-contrib validate --config base.yaml --config override.yaml

# Check what an expansion variable resolves to before startup
echo "$PROCESSORS"

# Watch the merge failure at boot
journalctl -u otelcol-contrib --since '10 min ago' | grep -i 'cannot merge'

Conclusion

cannot merge config is a type-shape conflict: two configuration sources hand the Collector different YAML types for the same key, so the deep-merge has nothing consistent to build. Find the key path in the message, compare that key across your base and override sources, and make the override match the expected type — usually a map that patches a single field rather than a scalar that replaces a block. Validating the merged configuration in CI turns this startup crash into a caught pull-request error.

Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.