All clean. Here is the complete report, Sir Charles of St Cheese.

---

**Status: DONE**

---

**Files modified**

- `/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/otel-collector/otelconfig.tf` — complete rewrite (lines 1–130). Introduced `_otel_base`, `_exporters_direct`, `_exporters_downstream`, plus pre-serialised JSON locals (`_exporters_direct_json`, `_exporters_downstream_json`) to work around Terraform's structural type-checking on ternary branches with different object attribute names. `otel_config` is assembled via `merge(_otel_base, jsondecode(selected_json))`. The pipeline exporter list locals (`traces_exporters`, `logs_exporters`, `metrics_exporters`) drive the swap cleanly.

- `/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/otel-collector/main.tf` — two targeted edits:
  - Line 35: `count = var.downstream_endpoint == "" ? 1 : 0` added to `kubernetes_manifest.otel-collector`
  - Lines 238–242: `extraEnvsFrom` gated with a ternary — list with secret ref when direct mode, empty list in downstream mode

- `/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/otel-collector/lbconfig.tf` — auto-reformatted by `terraform fmt` only (pre-existing spacing issues, no semantic change).

---

**`terraform fmt` result**: EXIT 0 (clean after auto-format run)

**`terraform validate` result**: EXIT 0 — `Success! The configuration is valid.`

---

**Ad-hoc plan verification**

**Case A** (`downstream_endpoint = ""`, default): Plan shows 5 resources to create. Confirmed in rendered values:
- `kubernetes_manifest.otel-collector[0]` present (count=1)
- `exporters.datadog/exporter` with `api.key: "${env:DD_API_KEY}"`, `api.site: "datadoghq.com"`, `sending_queue.storage: "file_storage/persistent_queue"`
- `extraEnvsFrom` contains `secretRef.name: "otel-collector"`
- Traces: `["datadog/connector", "datadog/exporter"]`; logs/metrics: `["datadog/exporter"]`

**Case B** (`downstream_endpoint = "http://vector.vector.svc.cluster.local:4317"`): Plan shows 4 resources to create. Confirmed:
- `kubernetes_manifest.otel-collector` — **absent from plan** (count=0, resource counted out)
- `exporters.otlp.endpoint: "http://vector.vector.svc.cluster.local:4317"` present with all retry/queue config
- `exporters.datadog/exporter` — **absent from rendered values**
- `extraEnvsFrom: []` — empty list, no secret mount
- Traces: `["datadog/connector", "otlp"]`; logs: `["otlp"]`; metrics: `["otlp"]`

---

**Self-review findings**

- **Backwards compat (Case A)**: Confirmed byte-identical behaviour. The `_otel_base` + `_exporters_direct` merge produces exactly the same structure as the original inlined map. The `jsondecode(jsonencode())` round-trip on the direct exporter block is transparent — all values, including the `$${env:DD_API_KEY}` escape, survive correctly.

- **Forward path (Case B)**: Swap is clean. All five required fields on the OTLP exporter are present in the rendered YAML. Pipeline lists are correct in all three pipelines.

- **Connectors/processors/receivers untouched**: `datadog/connector` is present in both cases under `connectors` and as a receiver in the `metrics` pipeline. The `batch` processor, `health_check`/`file_storage` extensions, receiver config, and `service.telemetry` are all identical across both plan outputs.

- **One implementation note**: The `jsonencode`/`jsondecode` pre-serialisation pattern was required because Terraform 1.9.x performs structural type-checking on ternary branches at parse time, before any functions are applied — the simpler `jsondecode(jsonencode(ternary))` form does not escape this check. Pre-encoding each branch to a string (`_exporters_direct_json`, `_exporters_downstream_json`) first makes the ternary operate on `string` vs `string`, which passes type checking cleanly.