18 pages cut their per-endpoint shape, payload, port and window claims and
link [[frank-archon]] / [[frank-dragoon]] / [[frank-zealot]] instead. Several
of the cut claims had already drifted -- the stack page still described the
kingjones ROCmFP4 era on ROCmFPX, OpenWebUI carried the 27B/3-slot bench
narrative, Hermes named a retired max_model_len and the old service name, and
the OMP harness page's live-window block had drifted off frank-zealot's real
262,144 (it is now labelled a dated worked example whose authority is the
endpoint page).
Two facts gain a single owner instead of two: the up{job=~"frank-.*"} scope
lives in Infrastructure/Prometheus.md, and the blackbox/Anubis probe form in
Architecture/Stale Signal Audit.md, each cited from the other page.
Voice Packs and Frank TTS carried live-state claims that contradicted the
engine page's 2026-10-18 deploy record (nothing listening on 7772/8880, model
dir absent); they now point at the pages that own host state.
185 lines
10 KiB
Markdown
185 lines
10 KiB
Markdown
---
|
|
type: reference
|
|
title: Observability Provisioning
|
|
description: How Prometheus config/rules and Grafana datasources/dashboards are provisioned as code in serverAppConfigs — the config triad, deploy copiers, planner mappings, seam tests, entry points for each change type, and API verification.
|
|
tags: [observability, prometheus, grafana, provisioning, deployment]
|
|
timestamp: 2026-10-05T00:00:00-07:00
|
|
---
|
|
|
|
# Observability Provisioning
|
|
|
|
The monitoring stack is **provisioned as code**: dashboards, datasources,
|
|
scrape jobs and alert rules live in the repo and ride the normal deploy flow.
|
|
No hand-edits inside containers — they are wiped on recreate and never
|
|
committed.
|
|
|
|
## The config triad
|
|
|
|
| Repo path | What it holds |
|
|
|---|---|
|
|
| `shared/prometheus/prometheus.yml` | scrape jobs (global + per-host) |
|
|
| `shared/prometheus/rules/llm-fleet.yml` | alert rules + recording rules |
|
|
| `shared/grafana/provisioning/` | datasources + dashboards (provisioned tree) |
|
|
|
|
Targets and datasources reference **container names** (`grafana:3000`,
|
|
`loki:3100` — `shared/prometheus/prometheus.yml:52-61`), never
|
|
loopback-published ports; `alloy` is **not scraped at all** (no job in the
|
|
config — its `127.0.0.1:12345` UI is only probed by the homepage timer,
|
|
`shared/homepage/services.json:233`). The LLM endpoints split by topology: the
|
|
two same-host ones (`frank-dragoon` `:7770`, `frank-zealot` `:7771`) are
|
|
scraped as `host.containers.internal:<port>` because a bridge container cannot
|
|
reach a host-netns bind at the host's LAN IP (`prometheus.yml:89-99`), and only
|
|
the endpoint on another machine (`192.168.0.235:7770`) is addressed by LAN IP
|
|
(`prometheus.yml:128-131`). Prometheus 3 refuses HTML responses as metrics —
|
|
HTML-scraped "probes" are structurally inert and need a blackbox exporter (see
|
|
[[Stale Signal Audit]]).
|
|
|
|
## The deploy path
|
|
|
|
`scripts/deploy-remote.sh` carries one copier per config surface, all called
|
|
from `deploy_observability`:
|
|
|
|
- `copy_prometheus_config` → `/opt/prometheus/config/prometheus.yml` **and** `config/rules/llm-fleet.yml` (`:219-227`)
|
|
- `copy_alertmanager_config` → `/opt/alertmanager/config/alertmanager.yml` (`:232-235`)
|
|
- `install_signal_relay` → the user unit + iptables + the health wait on `192.168.0.143:18093/health` (`:243-274`)
|
|
- `copy_loki_config` → `/opt/loki/config/loki-config.yaml` (`:276-279`)
|
|
- `copy_alloy_config` → `/opt/alloy/config/config.alloy` (`:281-284`)
|
|
- `copy_grafana_provisioning` → `/opt/grafana/provisioning/` (`:286-300`)
|
|
|
|
`scripts/deploy-plan.py` maps `shared/prometheus/**` to `prometheus` (plus
|
|
`alertmanager` when the changed file is `alertmanager.yml`, `:251-256`),
|
|
`shared/loki/**` to `loki` and `shared/alloy/**` to `alloy` (`:257-260`), and
|
|
`shared/grafana/**` to `grafana` (`:261-264`); changes under
|
|
`dankclaw/observability/` select the touched unit and its dependency consumer
|
|
(`:321-328`), and a change to either relay file redeploys `alertmanager`
|
|
(`:399-402`). So `deploy.sh dankclaw --service grafana` (etc.) restarts only
|
|
what the change touched. Seam tests in `tests/observability_quadlet.bats` pin
|
|
the unit files exist and are boot-persistent/self-healing (`:36-62`), that no
|
|
unit publishes on a wildcard (`:63-81`), that the copiers install each unit
|
|
(`:250`), that grafana provisioning is declared in git and installed
|
|
(`:345`), and that the provisioned paths match the units' mounts.
|
|
|
|
## Entry points
|
|
|
|
**Add a scrape target** — edit `shared/prometheus/prometheus.yml`. Decide the
|
|
vantage first: container name if the exporter is a container on
|
|
`dankclaw_default`; `host.containers.internal:<port>` if it is a process in the
|
|
host's own network namespace **on this same machine** (a bridge container
|
|
cannot reach the host's LAN IP at an unpublished port); the host's LAN IP only
|
|
if it is a process on a **different** machine; a blackbox module if it is an
|
|
HTTP probe, not a metrics endpoint.
|
|
|
|
**Add an alert/recording rule** — edit `shared/prometheus/rules/llm-fleet.yml`.
|
|
Current delivered set: **4 alert rules + 2 recording rules** —
|
|
`LlmRequestsQueueing`, `LlmPrefixCacheCollapse`, `LlmSpecDecodeOff`,
|
|
`LlmEndpointDown`, plus `job:llm_prefix_cache_reuse:rate5m` and
|
|
`job:llm_spec_accept_length:rate5m` (`shared/prometheus/rules/llm-fleet.yml:32-110`),
|
|
confirmed live: `curl -s http://127.0.0.1:9090/api/v1/rules` (2026-10-18)
|
|
reports 4 alerting + 2 recording. Rules reload with the prometheus service.
|
|
|
|
**Add a datasource** — drop a YAML file into
|
|
`shared/grafana/provisioning/datasources/`. Point it at the container name of
|
|
the backend (`http://prometheus:9090`), not loopback.
|
|
|
|
**Add a dashboard** — drop the JSON into
|
|
`shared/grafana/provisioning/dashboards/`. Provisioned dashboards live in
|
|
unified storage, not (only) the SQLite table — grep the API, not the DB, when
|
|
verifying (see the verify section of [[Fleet Audit and Posture]]). *(unverified
|
|
2026-10-18: the storage-backend claim needs an admin password to confirm
|
|
against `grafana.db` and the API side by side)*
|
|
|
|
## Verify through the APIs, not the filesystem
|
|
|
|
```bash
|
|
# prometheus: rules and targets actually loaded
|
|
curl -s http://127.0.0.1:9090/api/v1/rules | python3 -m json.tool | head
|
|
curl -s "http://127.0.0.1:9090/api/v1/targets?state=active" | \
|
|
python3 -c "import json,sys; [print(t['labels'].get('job'), t['health'], (t['lastError'] or '')[:60]) for t in json.load(sys.stdin)['data']['activeTargets']]"
|
|
# grafana: datasource + dashboard provisioned
|
|
curl -s -u admin:<pw> http://127.0.0.1:3002/api/datasources | python3 -m json.tool | grep -E '"name"|"url"'
|
|
```
|
|
|
|
The two Prometheus queries were run on 2026-10-18 and are what every number on
|
|
this page comes from: 4 alerting + 2 recording rules, and `up` for
|
|
`prometheus`, `node-dankclaw`, `node-dankserver`, `grafana`, `loki`,
|
|
`frank-zealot`, `frank-dragoon`, `frank-archon` with the four structural downs
|
|
listed in [[Prometheus]]. The Grafana query needs the local admin password and
|
|
was not run here; the filesystem side (`ls /opt/grafana/provisioning/*`) is
|
|
proof of installation only — see the next paragraph.
|
|
|
|
Health answers from a stale process and provisioned resources can hide in
|
|
unified storage — a file existing on disk is not proof the running server
|
|
loaded it. See [[Stale Signal Audit]] for the never-green-target decision
|
|
table (refused = outage, 404/HTML = config defect).
|
|
|
|
## Current delivered state (delivery chain live 2026-09-17, re-verified 2026-10-18)
|
|
|
|
Prometheus → Alertmanager (`dankclaw/observability/quadlet/alertmanager.container`,
|
|
loopback `:9093`) → host-side Signal relay (`scripts/signal-relay.py`, user
|
|
unit, `192.168.0.143:18093` — the LAN IP, the one bind a container can reach
|
|
via `host.containers.internal`) → signal-cli, addressed by number
|
|
`+12062476336` (`scripts/signal-relay.py:36-43`). Every hop re-checked
|
|
2026-10-18: `ss -ltnp` shows `python3` on `192.168.0.143:18093`,
|
|
`curl -s http://192.168.0.143:18093/health` → `ok`, and
|
|
`journalctl --user -u signal-relay` ends with successful `sent to +12062476336:`
|
|
lines for `LlmEndpointDown`, `LlmRequestsQueueing`, `LlmSpecDecodeOff` and
|
|
`LlmPrefixCacheCollapse` (firing and resolved). Some targets read down for
|
|
structural reasons (HTML-scraped "probes", Gitea metrics off); the remedy is
|
|
the blackbox exporter. See [[Prometheus]] → Targets.
|
|
|
|
**A llama.cpp endpoint is invisible unless it opts in.** Until 2026-10-18,
|
|
`up{job="frank-zealot"}` was 0 for the entire retained history of the metric —
|
|
the oldest sample, 2026-10-11, is already 0 — while the service itself answered
|
|
`/health` 200 the whole time. Cause: llama.cpp only exports `/metrics` when it
|
|
is started with the flag, so the scrape was refused at the protocol layer (the
|
|
flag and its opt-out are on [[frank-zealot]]) and `LlmEndpointDown` (critical)
|
|
stayed firing:
|
|
the relay re-sent it on the 4 h `repeat_interval` for days (`journalctl --user
|
|
-u signal-relay` — firing sends at 01:37, 05:42, 09:47, 13:52, 17:57 and 22:02
|
|
on 2026-10-12). It finally resolved at `2026-10-18T01:53:21-07:00`, after the
|
|
metrics opt-in landed that day; the target
|
|
reports up since (`curl -s http://127.0.0.1:9090/api/v1/targets` 2026-10-18).
|
|
How many payload generations it spanned is not readable from any host
|
|
artifact. *(unverified 2026-10-18)*
|
|
|
|
Two laws fall out of it. A target can read down beside a healthy service, so
|
|
read `lastError` from `/api/v1/targets` before believing an outage. And rule
|
|
state lives in Prometheus' process, not on disk — a restart re-stamps
|
|
`activeAt` on a long-standing alert, so a "fresh" alert after a reboot is not
|
|
evidence of a fresh fault. (Alertmanager by contrast **does** persist:
|
|
`--storage.path=/alertmanager` on a host volume keeps silences and the
|
|
notification log — `dankclaw/observability/quadlet/alertmanager.container:39-42`,
|
|
`/opt/alertmanager/data`.)
|
|
|
|
Remaining known downs are the structural ones above (HTML-scraped probes, Gitea
|
|
metrics, ComfyUI), and `frank-dragoon` exports `/metrics` natively
|
|
([[frank-dragoon]]), which is why
|
|
`frank-dragoon` has always been green. The active-alert count is a
|
|
moment-in-time value and should never be documented as a constant:
|
|
`/api/v1/alerts` was empty at the 2026-10-18 morning census, held one firing
|
|
`LlmPrefixCacheCollapse` (`activeAt` 11:10:48Z, value 2.6e-4) when sampled
|
|
minutes later, and was empty again by 11:56Z; the relay logged the matching
|
|
firing and resolved sends at `04:26:21-07:00` and `04:41:21-07:00`.
|
|
|
|
**What that zealot scrape measured on 2026-10-18.** As of ~04:30 PDT the
|
|
production `frank-zealot` root unit was stopped **on purpose** for a benchmark
|
|
window on the 3090 — the repo Benchmarking Policy says stop the production
|
|
service before benching — and `:7771` answered from a manually booted candidate
|
|
arm (`systemctl is-active frank-zealot` → `failed` after
|
|
`stop-sigterm timed out. Killing`, which is llama.cpp ignoring SIGTERM past
|
|
`TimeoutStopSec`, the known shutdown shape for these engines). So
|
|
`up{job="frank-zealot"} == 1` that day measured the trial, not the production
|
|
shape in `scripts/frank-zealot.env` — that shape is [[frank-zealot]]'s to state,
|
|
and it is the repo contract.
|
|
The unit stayed `enabled`; `./scripts/deploy.sh dankclaw --service frank-zealot`
|
|
reinstalls and starts it, restoring boot persistence and self-healing. Never
|
|
quote a candidate arm's flags or VRAM as fleet state — the process and its shape
|
|
changed between two censuses ten minutes apart.
|
|
|
|
## Related
|
|
|
|
- [[Observability]] — stack topology and the alert delivery chain
|
|
- [[Stale Signal Audit]] — classifying down targets before "fixing" them
|
|
- [[Quadlet Lifecycle]] — the units that host these services
|
|
- [[Deploy State Triage]] — how the planner selects these services
|