README.md103 lines · main
1# observability stack
2
3phase 0 close-out — grafana + loki + prometheus + promtail on a single dokploy compose project. lives on the same docker network as `apps/api`, `apps/runtime`, `apps/realtime` so prometheus scrapes container names directly. grafana is exposed via traefik behind cloudflare access; everything else is internal-only.
4
5## what's here
6
7```
8infra/observability/
9├── compose.yml # the stack
10├── loki/config.yaml
11├── promtail/config.yaml
12├── prometheus/
13│ ├── prometheus.yml
14│ └── rules/services.yml
15├── grafana/
16│ ├── grafana.ini
17│ ├── provisioning/datasources/datasources.yaml
18│ ├── provisioning/dashboards/dashboards.yaml
19│ └── dashboards/
20│ ├── api-requests.json
21│ ├── runtime-invocations.json
22│ ├── realtime-subs.json
23│ └── postgres-health.json
24└── README.md (this file)
25```
26
27## what's emitted today vs. needed
28
29prometheus scrape targets in `prometheus.yml`:
30
31| service | /metrics | metrics emitted today | needed for full dashboards |
32| ----------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
33| `api` | ✅ shipped | `http_requests_total{method,status,route}`, `http_request_duration_ms_bucket{method,route,le}` | already covered — see `apps/api/src/lib/metrics.ts` + `apps/api/src/middleware/metrics.ts` |
34| `runtime` | ✅ shipped | pool gauges, cold-start histogram, isolate kill counter | already covered — see `apps/runtime/src/metrics.ts` |
35| `realtime` | ✅ shipped | `briven_realtime_subscriptions_active`, `briven_realtime_channels_active`, `briven_realtime_notifies_total`, `briven_realtime_reinvoke_total{outcome}` | already covered — see `apps/realtime/src/metrics.ts` |
36| `postgres` | ⚙️ template ready | none until deployed | merge `./postgres-exporter.compose.yml` into the data-plane compose project + create a `briven_metrics` role (`GRANT pg_monitor`) + set `POSTGRES_EXPORTER_DATA_SOURCE_NAME`. exporter binds to `postgres-exporter:9187`, already in `prometheus.yml`. |
37
38dashboards reference these metric names and will show "no data" until the corresponding service starts emitting them. that's deliberate — the dashboards land first so adding a metric is a one-line change.
39
40logs are scraped by promtail using **file-based discovery** against `/var/lib/docker/containers/*/*-json.log` (read-only bind-mount). per `DOCKER.md` we never touch `/var/run/docker.sock` or `docker_sd_configs` — the daemon is shared with other projects on the host and every API call goes through internal locks. promtail reads the json log files directly, unwraps the docker envelope, then parses the `service` field from each briven service's own JSON log line (emitted by `@briven/shared/observability`). non-briven containers ship through with `service` absent and Loki queries scope by `{service=~"api|runtime|realtime|web|docs"}`.
41
42## deploy on dokploy
43
44```bash
45# on the kvm running the briven docker network:
46cd infra/observability
47docker network create briven 2>/dev/null || true # idempotent
48docker compose up -d
49```
50
51verify:
52
53```bash
54curl -fsS http://localhost:9090/-/ready # prometheus ready
55curl -fsS http://localhost:3100/ready # loki ready
56docker compose ps # all four containers up
57```
58
59then point cloudflare access at `grafana.briven.tech` (the traefik labels in `compose.yml` cover the rest).
60
61## scale-out path
62
63phase 0 ships single-binary loki + local-disk storage. when log ingest exceeds ~50gb/day or query latency degrades:
64
651. swap loki's `common.storage.filesystem` for `s3` (minio or cloudflare r2)
662. enable loki microservices mode (separate distributor / ingester / querier containers)
673. move prometheus to a clustered mode (vmagent → vmsingle, or thanos)
68
69the dashboards and scrape configs don't change.
70
71## alerting
72
73`prometheus/rules/services.yml` defines `ServiceDown`, `HighErrorRate`, `PostgresConnectionsHigh`. they route through alertmanager → `benjojo/alertmanager-discord` bridge → discord:
74
75- `severity=critical|warning` → `#briven-alerts` (page-worthy)
76- everything else (info) → `#briven-deploys`
77
78operator env on the dokploy project:
79
80```
81DISCORD_WEBHOOK_ALERTS=https://discord.com/api/webhooks/…
82DISCORD_WEBHOOK_DEPLOYS=https://discord.com/api/webhooks/…
83```
84
85create the channels in your discord server, generate webhook URLs, paste into the dokploy env. each bridge instance holds one URL; rotation = update env + restart that one service. existing alert state lives in the `alertmanager_data` volume — keep it across redeploys so silences carry through.
86
87smoke test (drop a rule's `for:` to 0s temporarily, observe a message land, revert):
88
89```
90docker compose exec prometheus promtool query instant 'up'
91docker compose restart prometheus
92# … wait for the rule to fire …
93# then revert the rule edit and `docker compose restart prometheus` again.
94```
95
96## the four dashboards
97
98uids stay stable across redeploys — bookmark them.
99
100- `briven-api-requests` — request rate, p99 latency, 5xx error rate, recent error logs
101- `briven-runtime-invocations` — invocations/sec by project, cold-start p50/p99, active isolates, kills by reason
102- `briven-realtime-subs` — active subscriptions, LISTEN channels, NOTIFY rate, listener reconnect events
103- `briven-postgres-health` — connections, transactions/sec, deadlocks, longest query, db size by schema
Preview

observability stack

phase 0 close-out — grafana + loki + prometheus + promtail on a single dokploy compose project. lives on the same docker network as apps/api, apps/runtime, apps/realtime so prometheus scrapes container names directly. grafana is exposed via traefik behind cloudflare access; everything else is internal-only.

what's here

infra/observability/
├── compose.yml                                   # the stack
├── loki/config.yaml
├── promtail/config.yaml
├── prometheus/
│   ├── prometheus.yml
│   └── rules/services.yml
├── grafana/
│   ├── grafana.ini
│   ├── provisioning/datasources/datasources.yaml
│   ├── provisioning/dashboards/dashboards.yaml
│   └── dashboards/
│       ├── api-requests.json
│       ├── runtime-invocations.json
│       ├── realtime-subs.json
│       └── postgres-health.json
└── README.md  (this file)

what's emitted today vs. needed

prometheus scrape targets in prometheus.yml:

service/metricsmetrics emitted todayneeded for full dashboards
api✅ shippedhttp_requests_total{method,status,route}, http_request_duration_ms_bucket{method,route,le}already covered — see apps/api/src/lib/metrics.ts + apps/api/src/middleware/metrics.ts
runtime✅ shippedpool gauges, cold-start histogram, isolate kill counteralready covered — see apps/runtime/src/metrics.ts
realtime✅ shippedbriven_realtime_subscriptions_active, briven_realtime_channels_active, briven_realtime_notifies_total, briven_realtime_reinvoke_total{outcome}already covered — see apps/realtime/src/metrics.ts
postgres⚙️ template readynone until deployedmerge ./postgres-exporter.compose.yml into the data-plane compose project + create a briven_metrics role (GRANT pg_monitor) + set POSTGRES_EXPORTER_DATA_SOURCE_NAME. exporter binds to postgres-exporter:9187, already in prometheus.yml.

dashboards reference these metric names and will show "no data" until the corresponding service starts emitting them. that's deliberate — the dashboards land first so adding a metric is a one-line change.

logs are scraped by promtail using file-based discovery against /var/lib/docker/containers/*/*-json.log (read-only bind-mount). per DOCKER.md we never touch /var/run/docker.sock or docker_sd_configs — the daemon is shared with other projects on the host and every API call goes through internal locks. promtail reads the json log files directly, unwraps the docker envelope, then parses the service field from each briven service's own JSON log line (emitted by @briven/shared/observability). non-briven containers ship through with service absent and Loki queries scope by {service=~"api|runtime|realtime|web|docs"}.

deploy on dokploy

# on the kvm running the briven docker network:
cd infra/observability
docker network create briven 2>/dev/null || true   # idempotent
docker compose up -d

verify:

curl -fsS http://localhost:9090/-/ready          # prometheus ready
curl -fsS http://localhost:3100/ready            # loki ready
docker compose ps                                 # all four containers up

then point cloudflare access at grafana.briven.tech (the traefik labels in compose.yml cover the rest).

scale-out path

phase 0 ships single-binary loki + local-disk storage. when log ingest exceeds ~50gb/day or query latency degrades:

  1. swap loki's common.storage.filesystem for s3 (minio or cloudflare r2)
  2. enable loki microservices mode (separate distributor / ingester / querier containers)
  3. move prometheus to a clustered mode (vmagent → vmsingle, or thanos)

the dashboards and scrape configs don't change.

alerting

prometheus/rules/services.yml defines ServiceDown, HighErrorRate, PostgresConnectionsHigh. they route through alertmanager → benjojo/alertmanager-discord bridge → discord:

  • severity=critical|warning#briven-alerts (page-worthy)
  • everything else (info) → #briven-deploys

operator env on the dokploy project:

DISCORD_WEBHOOK_ALERTS=https://discord.com/api/webhooks/…
DISCORD_WEBHOOK_DEPLOYS=https://discord.com/api/webhooks/…

create the channels in your discord server, generate webhook URLs, paste into the dokploy env. each bridge instance holds one URL; rotation = update env + restart that one service. existing alert state lives in the alertmanager_data volume — keep it across redeploys so silences carry through.

smoke test (drop a rule's for: to 0s temporarily, observe a message land, revert):

docker compose exec prometheus promtool query instant 'up'
docker compose restart prometheus
# … wait for the rule to fire …
# then revert the rule edit and `docker compose restart prometheus` again.

the four dashboards

uids stay stable across redeploys — bookmark them.

  • briven-api-requests — request rate, p99 latency, 5xx error rate, recent error logs
  • briven-runtime-invocations — invocations/sec by project, cold-start p50/p99, active isolates, kills by reason
  • briven-realtime-subs — active subscriptions, LISTEN channels, NOTIFY rate, listener reconnect events
  • briven-postgres-health — connections, transactions/sec, deadlocks, longest query, db size by schema