OpenTelemetry Collector¶
Status: draft M0. Explains infra/otel/collector.yaml and the overlays under infra/otel/exporters/. Decision: ADR 0034.
1. Version and distribution¶
otel/opentelemetry-collector-contrib, version pinned in BOTHY_OTELCOL_VERSION. The
configuration was checked with otelcol-contrib 0.162.0 (otelcol-contrib validate, base alone,
with each overlay and with three combinations of two overlays). In this version the
components are named otlp_grpc, otlp_http, cumulative_to_delta, azure_monitor (output of
otelcol-contrib components) [verified].
2. Structure of the base¶
| Part | Component | Purpose |
|---|---|---|
| Receiving | otlp (gRPC :4317, HTTP :4318) |
Server, bothy-backupd, adapters |
| Receiving | postgresql |
Database metrics with the role bothy_monitor; deadlocks additionally enabled |
| Protection | memory_limiter |
Rejects under memory pressure instead of crashing; the server then discards and counts |
| Labeling | resource/instance |
Sets deployment.environment.name if the source does not set it |
| Privacy | transform/scrub |
Deletes url.query, url.full, client addresses, content attributes of the GenAI conventions, exception.message |
| Privacy | redaction/allowlist |
Spans and logs: only keys from signals.md 5.3 and the conventions; HMAC for suspicious values |
| Privacy | transform/scrub_metrics |
Metrics: delete person attributes of third-party sources |
| Sampling | tail_sampling |
Keeps all traces with an error, all slower than BOTHY_TRACE_SLOW_THRESHOLD_MS, and BOTHY_TRACE_SAMPLE_PERCENT of the rest |
| Batching | batch |
Fewer exports |
| Distribution | forward/traces, forward/metrics, forward/logs |
End of the inbound pipelines; overlays attach to them |
| Default target | nop |
Without an overlay, data is discarded |
| Own metrics | Prometheus reader on :8888 |
Prometheus scrapes them (alert BothyTelemetryExportFailing) |
Metrics do not pass through the allowlist: Bothy's own attributes are bounded in the code
(signals.md 2), and the processor is only alpha for metrics. For metrics of
third-party sources, transform/scrub_metrics deletes person attributes (user.*, enduser.*,
session.id, organization.id, client.address) on the resource and the data point.
3. Overlays¶
An overlay is a second --config file. The collector merges maps and replaces lists.
Therefore every overlay adds its own named pipelines (for example traces/newrelic) and changes
no list of the base. Exception: an overlay with an extension must list service.extensions in
full; this applies only to azure-monitor. Two overlays with extensions at the same time therefore
do not work; all other pairs work.
| Overlay | Target | Signals | Special feature |
|---|---|---|---|
none |
none | none | Default |
local |
Prometheus, Loki, Tempo in the Compose profile observability |
all | Metrics cumulative to the OTLP receiver of Prometheus |
newrelic |
New Relic OTLP | all | Delta temporality, attributes truncated |
grafana |
Grafana Cloud OTLP gateway | all | Whole Authorization header from a file |
datadog |
Datadog | all | Datadog connector for APM statistics |
azure-monitor |
Azure Monitor (native OTLP ingestion) | all | Entra authentication, delta temporality |
sentry |
Sentry OTLP | traces, logs | No metrics |
Evidence and limits per backend: backends.md.
Compose selects up to two overlays with BOTHY_OTEL_EXPORTER and BOTHY_OTEL_EXPORTER_2.
The test overlay file (infra/compose/test/otel/file.yaml) exists only for the Compose smoke
test (M1-73): it writes the three signals as OTLP JSON lines into a volume of the test overlay,
shortens tail_sampling.decision_wait and batch.timeout to one second, and is never part of an
installation or of the release bundle. tools/opscheck validates it with the base alone.
4. Secrets¶
The collector reads all keys with ${file:/run/bothy/secrets/<name>} from its
subdirectory of the volume secrets. Environment variables carry only endpoints and IDs.
5. Limits and operations¶
- Tail sampling needs all spans of a trace in the same collector. With several collectors
(cloud, several hosts) a load balancer by trace ID is needed in front of them (
load_balancingexporter) [derived]. - APM statistics at Datadog are based on the sample, because sampling runs before the overlay.
- If the collector fails, the server discards once its buffer is full; the message path stays
unaffected. The alert
BothyServerTelemetryMissingreports this. - Adapters on the same host send to
127.0.0.1:BOTHY_OTLP_PUBLISH_PORT; adapters on other hosts run their own collector with the same files (base plus overlay).
6. Check¶
make ops-check checks the configuration in the gate
(tools/opscheck) with otelcol-contrib in the version of the
image (check tool-versions keeps both equal):
- Check
collector:otelcol-contrib validatefor the base alone, the base with each overlay and the base with every ordered pair of two overlays exceptnone. Both orders count, because Compose setsBOTHY_OTEL_EXPORTERbeforeBOTHY_OTEL_EXPORTER_2and lists are replaced when merging. - Check
collector-allowlist: thebothy.*keys ofredaction/allowlistequal the table in signals.md 5.3, and the allowlist works:allow_all_keysis false, every trace and log pipeline with a receiver runs it, and no overlay redefines the processor or a pipeline of the base.
Before any validate, a reference to a config provider other than env, file and yaml
(${https:...}, ${s3:...}), also one inside another reference, is a finding: the collector
resolves it while it validates, and most of these providers fetch over the network (providers
such as s3 and googlesecretmanager:
confmap/provider,
retrieved 2026-10-12). Their clients may bypass the closed proxy of the validators: a sandbox run in
the red team saw ${https:...} connect past it (ruling M1-04/29). The check runs on a work copy:
instead of /run/bothy/secrets it reads placeholder files (telemetry_pseudonym_key needs at least
32 bytes), and the datadog exporter gets a fixed host name, so validate does not wait for host
name detection. It sets only the variables of COLLECTOR_ENV in tools/opscheck/opscheck.py, the
ones the overlays use without a default (Azure Monitor, Grafana and Sentry endpoints point at
example.com, the Azure IDs are zero UUIDs). A new overlay that reads a variable without a default
(${env:NAME} instead of ${env:NAME:-<default>}) needs NAME in COLLECTOR_ENV; otherwise the
value is empty and, for an endpoint, validate fails (with the otlphttp exporter: "at least one
endpoint must be specified"). The metadata query for GCP, which the exporter starts anyway, points
through GCE_METADATA_HOST to a closed port of 127.0.0.1, like the proxy of all validators
(tools/opscheck); the GCP metadata client honours that variable
and uses a transport of its own without a proxy
(metadata.go,
retrieved 2026-10-12; ruling M1-04/22). Check only the collector:
uv run --project tools --locked python tools/opscheck/opscheck.py --only collector collector-allowlist