# ADR 0006 — OpenTelemetry with built-in ILogger, not Serilog - Status: accepted - Date: 2026-07-28 ## Context The server is self-hosted by people who did not write it and who will debug it themselves. It also handles data it cannot read, which changes what observability can and should capture. ## Decision **Logging:** the built-in `ILogger` with source-generated `[LoggerMessage]` partial methods. No Serilog. `AddJsonConsole` outside Development; self-hosters capture container stdout. **Telemetry:** OpenTelemetry via `OpenTelemetry.Extensions.Hosting` with ASP.NET Core, HttpClient and runtime instrumentation, Npgsql's own `ActivitySource`, and custom sources `DodoSSH.Api`, `DodoSSH.Sync`, `DodoSSH.Relay`. OTLP exporter honouring the standard `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_SERVICE_NAME` variables, because self-hosters expect them. Sampling `ParentBased(TraceIdRatioBased(0.1))` by default, with tail sampling for errors done in the collector. Optional Prometheus scrape on a separate port, off by default. **Redaction is a hard requirement, not a nicety.** `Microsoft.Extensions.Compliance.Redaction` with `[PrivateData]` on DTO properties, plus an `ActivityProcessor` that strips `url.query` — the relay ticket fallback lives there ([ADR 0004](0004-relay-authorization.md)) — and drops `Authorization` and `Idempotency-Key`. Sync request bodies are never logged: they are ciphertext, but size and shape still leak. **Health:** `/healthz/live` checks the process only. `/healthz/ready` additionally checks PostgreSQL, OIDC discovery and JWKS reachability, the Data Protection key ring, and that no migrations are pending. ## Consequences - One telemetry pipeline instead of two configuration systems. OTel logs are first-class in .NET 10, so Serilog's usual payoff — sinks and enrichers — is redundant once a collector is in the compose file, and dual configuration is a real support burden ("why is my log level being ignored?"). If someone wants file logs, that is the collector's file exporter, not a second logging framework. - `[LoggerMessage]` is allocation-free and produces structured events by construction. CA1848 is a warning during early development and is raised to error when the logging pass lands in M4. - **Liveness deliberately excludes dependencies.** A transient PostgreSQL outage must not cause the orchestrator to restart the container, because that would kill every live relay session for a fault that has nothing to do with them. Readiness only removes the instance from load balancing, which is the correct response. - **Audit is a separate concern from logs.** `audit_event` is a partitioned, append-only table (`REVOKE UPDATE, DELETE` from the application role) with a per-day hash chain. It records who, when, which item id, which operation, outcome and source — plus non-secret shape data such as `fieldsChanged` and `payloadBytes`. Its `detail` jsonb must contain ids, counts and *field names* only, enforced by a serializer whitelist and a test. - Given no plaintext is visible, audit is nonetheless strong on the thing that matters: every ciphertext fetch is recorded, and there is no server-side path that reads a secret without a client fetch. Bulk fetch of an entire vault is a high-signal exfiltration indicator and should alert. - Honest limit on tamper evidence: a compromised server can rewrite the chain from any point forward unless heads are anchored externally. Publishing the daily head to clients, which cache it, makes truncation detectable — the same trick as the key log in [ADR 0001](0001-e2ee-trust-model.md). - Human-readable audit is a **client** concern: rows carry `subject_id`, and the client, which holds the keys, resolves ids to names. - Behind a reverse proxy, `UseForwardedHeaders` needs `KnownProxies`/`KnownNetworks` configured. **Log a startup warning if forwarded headers are enabled with no known proxies**, because self-hosters will get this wrong and it silently turns per-user rate limiting into per-proxy. ### Rejected - **Serilog.** Excellent library; the second configuration system is the problem, not the code. - **Application Insights or another vendor SDK.** Wrong for a self-hosted product. OTLP lets the operator point at whatever they already run. - **Logging request bodies for sync.** Ciphertext, so it looks harmless, and it leaks item sizes and access patterns while ballooning log volume.