trails/infrastructure/grafana/provisioning/alerting/alerts.yml
Ullrich Schäfer 9e598fb6a1
BRouter host observability + docs
Lands sections 6 and 8 of relocate-brouter-to-dedicated-host on top
of the host compose in #291.

## Observability (section 6)

- **Prometheus**: new `brouter-cadvisor` job scraping
  `10.0.1.10:8080` over the vSwitch, labeled `host="brouter"`.
- **cAdvisor sidecar** on the dedicated host's compose
  (`--docker_only --whitelisted_container_labels=trails.cool.service`)
  so metrics only cover trails containers, never the operator's
  unrelated workloads on the shared box.
- **Promtail sidecar** on the dedicated host, Docker SD with relabel-
  drop on missing `trails.cool.service` label, pushing to flagship
  Loki at `http://10.0.0.2:3100/loki/api/v1/push`.
- **Flagship compose**: Loki now publishes port 3100 on the vSwitch
  IP only (10.0.0.2:3100) — Hetzner Cloud firewall blocks it from
  the public internet.
- **Grafana dashboard**: `brouter.json` — scrape up/down, request
  rate (from Planner-side `brouter_request_duration_seconds`),
  p50/p95/p99, container memory/CPU, Loki logs panel.
- **Alert**: `brouter-scrape-down` fires on
  `up{job="brouter-cadvisor"} < 1 for 2m`; `noDataState: Alerting`
  so a total scrape failure still pages.

Operator needs one UFW rule on the dedicated host for the cAdvisor
port — documented in `infrastructure/brouter-host/README.md`.

## Documentation (section 8)

- `CLAUDE.md` — hosts table + updated deployment table with SSH
  targets per workflow; BRouter host SSH is `-p 2232 trails@...`,
  different key.
- `docs/architecture.md` — Hosting section rewritten to cover both
  hosts, vSwitch boundary, and the observability-scoping rationale
  for the shared dedicated host.
- `docs/deployment.md` (new) — full operator runbook: host layout,
  first-time BRouter provisioning, SOPS rotation (including the
  macOS `SOPS_AGE_KEY_FILE` gotcha), cutover procedure with
  rollback, manual workflow triggers.

Task 8.4 (infrastructure/README.md) skipped: that file doesn't
exist and the ground is covered by brouter-host/README.md +
docs/deployment.md.

## Validation

- `docker compose config` on both the flagship and brouter-host
  compose files — both validate.
- `pnpm typecheck`, `pnpm lint`, `pnpm test` — all clean (full
  turbo cache hits; no code changes in this commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 23:10:45 +02:00

244 lines
7.7 KiB
YAML

apiVersion: 1
groups:
- orgId: 1
name: trails.cool alerts
folder: trails.cool
interval: 1m
rules:
- uid: disk-usage-high
title: Disk usage > 80%
condition: B
data:
- refId: A
relativeTimeRange: { from: 600, to: 0 }
datasourceUid: prometheus
model:
expr: (1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100
instant: true
- refId: B
datasourceUid: __expr__
model:
type: threshold
expression: A
conditions:
- evaluator: { params: [80], type: gt }
operator: { type: and }
reducer: { type: last }
for: 15m
annotations:
summary: "Disk usage is above 80%"
- uid: app-health-failing
title: App health check failing
condition: B
data:
- refId: A
relativeTimeRange: { from: 300, to: 0 }
datasourceUid: prometheus
model:
expr: up{job=~"journal|planner"}
instant: true
- refId: B
datasourceUid: __expr__
model:
type: threshold
expression: A
conditions:
- evaluator: { params: [1], type: lt }
operator: { type: and }
reducer: { type: last }
for: 2m
annotations:
summary: "{{ $labels.job }} is down"
- uid: error-rate-high
title: Error rate > 5%
condition: B
noDataState: OK
data:
- refId: A
relativeTimeRange: { from: 600, to: 0 }
datasourceUid: prometheus
model:
expr: (sum(rate(caddy_http_request_duration_seconds_count{code=~"5.."}[5m])) or vector(0)) / clamp_min(sum(rate(caddy_http_request_duration_seconds_count[5m])), 0.001) * 100
instant: true
- refId: B
datasourceUid: __expr__
model:
type: threshold
expression: A
conditions:
- evaluator: { params: [5], type: gt }
operator: { type: and }
reducer: { type: last }
for: 5m
annotations:
summary: "Error rate is above 5%"
- uid: container-restart-loop
title: Container restart loop
condition: B
data:
- refId: A
relativeTimeRange: { from: 300, to: 0 }
datasourceUid: prometheus
model:
expr: changes(container_start_time_seconds{name=~"trails-cool.*"}[5m])
instant: true
- refId: B
datasourceUid: __expr__
model:
type: threshold
expression: A
conditions:
- evaluator: { params: [2], type: gt }
operator: { type: and }
reducer: { type: last }
for: 1m
annotations:
summary: "{{ $labels.name }} has restarted more than 2 times in 5 minutes"
- uid: db-connections-high
title: PostgreSQL connections > 80
condition: B
data:
- refId: A
relativeTimeRange: { from: 300, to: 0 }
datasourceUid: prometheus
model:
expr: sum(pg_stat_activity_count{datname="trails"})
instant: true
- refId: B
datasourceUid: __expr__
model:
type: threshold
expression: A
conditions:
- evaluator: { params: [80], type: gt }
operator: { type: and }
reducer: { type: last }
for: 5m
annotations:
summary: "PostgreSQL active connections above 80 — approaching default max_connections limit"
- uid: app-crash-log
title: Application crash detected
condition: C
data:
- refId: A
relativeTimeRange: { from: 300, to: 0 }
datasourceUid: loki
model:
expr: sum(count_over_time({service=~"journal|planner"} |~ "uncaughtException|unhandledRejection|ERR_|FATAL|segfault|OOMKilled"[5m]))
instant: true
- refId: B
datasourceUid: __expr__
model:
type: reduce
expression: A
reducer: last
- refId: C
datasourceUid: __expr__
model:
type: threshold
expression: B
conditions:
- evaluator: { params: [0], type: gt }
operator: { type: and }
reducer: { type: last }
for: 0s
noDataState: OK
annotations:
summary: "Crash signature detected in application logs"
- uid: background-job-failures
title: Background job failures
condition: C
noDataState: OK
data:
- refId: A
relativeTimeRange: { from: 3600, to: 0 }
datasourceUid: postgres
model:
rawSql: "SELECT count(*) AS failed FROM pgboss.job WHERE state = 'failed' AND completed_on > now() - interval '1 hour'"
format: table
- refId: B
datasourceUid: __expr__
model:
type: reduce
expression: A
reducer: last
- refId: C
datasourceUid: __expr__
model:
type: threshold
expression: B
conditions:
- evaluator: { params: [0], type: gt }
operator: { type: and }
reducer: { type: last }
for: 0s
annotations:
summary: "Background jobs have failed in the last hour — check Grafana Service Health dashboard"
- uid: brouter-scrape-down
title: BRouter host unreachable
condition: B
noDataState: Alerting
data:
- refId: A
relativeTimeRange: { from: 300, to: 0 }
datasourceUid: prometheus
model:
expr: up{job="brouter-cadvisor"}
instant: true
- refId: B
datasourceUid: __expr__
model:
type: threshold
expression: A
conditions:
- evaluator: { params: [1], type: lt }
operator: { type: and }
reducer: { type: last }
for: 2m
annotations:
summary: "BRouter host metrics scrape has been failing for 2+ minutes — the dedicated host, vSwitch, or cAdvisor may be down"
- uid: caddy-502-rate
title: Caddy 502 errors detected
condition: B
noDataState: OK
data:
- refId: A
relativeTimeRange: { from: 300, to: 0 }
datasourceUid: prometheus
model:
expr: sum(rate(caddy_http_request_duration_seconds_count{code="502"}[5m])) or vector(0)
instant: true
- refId: B
datasourceUid: __expr__
model:
type: threshold
expression: A
conditions:
- evaluator: { params: [0], type: gt }
operator: { type: and }
reducer: { type: last }
for: 2m
annotations:
summary: "Caddy is returning 502 errors — journal or planner upstream unreachable"
contactPoints:
- orgId: 1
name: email
receivers:
- uid: email-default
type: email
settings:
addresses: admin@trails.cool
policies:
- orgId: 1
receiver: email