trails/openspec/changes/observability/design.md
Ullrich Schäfer 7d20dbb12f
Add observability and security-hardening proposals, gitignore settings.local
Two new OpenSpec changes:
- observability (30 tasks): health endpoints, Prometheus, Grafana+Loki,
  structured logging, dashboards, alerting
- security-hardening (24 tasks): Caddy headers, scanner blocking,
  gitleaks, pnpm audit, dependabot, non-root Docker, fail2ban

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 09:48:08 +01:00

92 lines
3.5 KiB
Markdown

## Context
The Hetzner CX21 runs Journal, Planner, BRouter, PostgreSQL, and Caddy. There
is no monitoring — the disk-full outage was only discovered when a user
reported errors. Sentry covers application errors but not infrastructure health,
performance trends, or log aggregation.
## Goals / Non-Goals
**Goals:**
- Health endpoints for uptime monitoring
- Structured JSON logging for searchability
- Prometheus metrics for request latency, DB, sessions, BRouter
- Grafana dashboards for at-a-glance status
- Loki for centralized log aggregation
- Alerts for disk full, app down, high error rate
**Non-Goals:**
- Distributed tracing (Sentry already does this)
- Custom business metrics (user signups, route counts — later)
- External uptime monitoring service (can add later)
- Monitoring for self-hosted instances (flagship only)
## Decisions
### D1: Grafana + Prometheus + Loki in Docker Compose
Add all three as services in the existing docker-compose.yml. They share the
Docker network with the app containers. Prometheus scrapes `/metrics` from
Journal and Planner. Loki collects logs via the Docker logging driver.
Grafana is only accessible via SSH tunnel (`ssh -L 3100:localhost:3100`) or
Caddy with basic auth on a subdomain (e.g., `grafana.trails.cool`).
### D2: Pino for structured logging
Replace `console.log`/`console.error` with Pino. JSON output in production,
pretty-print in dev. Pino is the standard Node.js structured logger — fast,
zero-dep in production, and Loki-compatible.
Create a shared logging utility in `packages/logging/` or keep it simple
with per-app `lib/logger.server.ts`.
### D3: prom-client for Prometheus metrics
Use `prom-client` to expose a `/metrics` endpoint on each app. Default Node.js
metrics (event loop lag, heap, GC) plus custom:
- `http_request_duration_seconds` (histogram, by route + method + status)
- `planner_active_sessions` (gauge)
- `planner_connected_clients` (gauge)
- `brouter_request_duration_seconds` (histogram)
- `db_pool_active_connections` (gauge)
### D4: Health endpoint checking DB connectivity
`GET /health` returns `{ status: "ok", db: "connected" }` or
`{ status: "degraded", db: "unreachable" }` with appropriate HTTP status.
Used by Docker healthcheck and external monitoring.
### D5: Caddy access logs to stdout (Loki picks them up)
Enable Caddy's `log` directive. Structured JSON access logs go to stdout,
Docker sends them to Loki via the logging driver. No sidecar needed.
### D6: Grafana provisioned dashboards
Ship dashboard JSON files in `infrastructure/grafana/dashboards/`. Grafana
auto-loads them via provisioning config. Dashboards:
- **Overview**: Request rate, error rate, latency p50/p95/p99
- **Planner**: Active sessions, connected clients, BRouter latency
- **Infrastructure**: CPU, memory, disk, DB connections
### D7: Alert rules via Grafana
Configure alert rules in provisioned dashboard or as code:
- Disk usage > 80% (fires 15 min)
- Any app returns 0 healthy responses for 2 min
- Error rate > 5% for 5 min
- DB connection pool exhausted
Notifications via email (Resend, once transactional-emails is implemented)
or webhook.
## Risks / Trade-offs
- **Memory pressure** → Grafana + Prometheus + Loki add ~500MB. CX21 has 4GB,
currently using ~2GB. Tight but workable. Monitor and upgrade to CX22 if
needed.
- **Disk usage** → Prometheus retention default 15 days, Loki retention
configurable. Set conservative limits (1GB each).
- **Complexity** → Three new services to maintain. Mitigated by using official
Docker images with minimal config.