# thermograph-observability — agent instructions

The **logging/metrics stack** for the Thermograph fleet: Loki + Grafana on beta,
with a Grafana Alloy agent on every node (prod, beta, dev) shipping container and
app logs over the WireGuard mesh. Grafana is fronted by beta's Caddy at
`dashboard.thermograph.org` (Google SSO, pre-provisioned users only).

This is **operational infra config, not application code** — there is no build.
It deploys by hand: `docker compose up -d` on beta for Loki+Grafana, and the
Alloy agent (`alloy/docker-compose.agent.yml`) on each node. See the README for
the full per-node procedure.

## Layout

- `docker-compose.yml` — the Loki + Grafana stack (runs on beta).
- `loki/config.yml` — Loki config (mesh-only, filesystem storage).
- `grafana/provisioning/` — datasource (Loki) + dashboard provider, auto-loaded
  at startup. `grafana/dashboards/*.json` — the dashboards themselves.
- `grafana/provisioning/alerting/` — the alert rules, the Discord contact point
  and the notification policy, also auto-loaded at startup. Same rule as the
  dashboards: **the repo is the only durable path**, UI edits get overwritten.
  Alerts go to Discord `#ops-alerts`, never email — beta's Grafana relays SMTP
  through prod's Postfix, so email dies exactly when prod does. Every rule is
  LogQL (there is no Prometheus anywhere in the fleet). Thresholds were derived
  from real Loki data and the working is in the comments beside each rule —
  re-derive before changing a number rather than guessing.
- `alloy/config.alloy` + `alloy/docker-compose.agent.yml` — the per-node log
  shipper. The node name comes from `ALLOY_NODE` (set per host).
- `caddy-grafana.conf` — the Caddy vhost for `dashboard.thermograph.org` (lives
  in beta's Caddy config, kept here for reference).
- `.env.example` — copy to `.env` on beta before `docker compose up`. `.env` is
  gitignored; never commit real OAuth secrets or the admin password.

## Conventions

- **Every artifact that ships to a node is CI-validated**
  (`.forgejo/workflows/observability-validate.yml`): both compose files parse, all
  dashboard JSON is valid, and the Loki/provisioning YAML parses. Keep new
  dashboards as valid JSON and new config as valid YAML or CI fails. The alerting
  config gets a stricter third step — every rule's `condition` must name a refId
  that exists, every policy must route to a receiver that exists, and a literal
  Discord webhook URL in the repo is a hard failure. (A rule pointing at a missing
  refId is valid YAML, provisions cleanly, and then never fires; that is precisely
  the silent-no-op this whole domain exists to prevent.) The Alloy config is still
  not CI-validated (needs the `alloy` binary).
- The public hostname is **`dashboard.thermograph.org`** everywhere — not
  `grafana.thermograph.org`. Match it in any new comment/config.
- Secrets (OAuth client id/secret, admin password) live only in the host `.env`,
  never in the repo.

## Branching

Single `main` branch, no protection. Open a PR against `main`; the validator
gates it. Deploying the change to beta / the nodes is a separate manual step
(this repo has no deploy automation — a known gap).
