Appearance
🛡️ adminadmin@example.com · Domain admin in Acme Corp
Chapter 12 — Watch it
Query metrics through the control plane
bash
plexctl metrics query \
--domain "$DOMAIN_ID" \
--query 'vector(1)' \
--time "$(date -u +%Y-%m-%dT%H:%M:%SZ)"text
STATUS_CODE BODY
200 {"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1782051335,"1"]}]}}Plain PromQL. You never talk to the metrics backend — the control plane proxies the query and stamps your Domain as the upstream tenant server-side.
There is no query you can write that reads another Domain's series.
Range queries, same path
bash
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
START=$(date -u -v-10M +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '-10 min' +%Y-%m-%dT%H:%M:%SZ)
plexctl metrics query \
--domain "$DOMAIN_ID" \
--query 'vector(1)' \
--start "$START" \
--end "$NOW" \
--step 2m \
--output json | jq -r '.body | fromjson | .data.result[0].values'text
[
[1782050843, "1"],
[1782050963, "1"],
[1782051083, "1"],
[1782051203, "1"],
[1782051443, "1"]
]vector(1) is synthetic — it proves the round trip without needing ingested series. On a real fleet you would swap in up or rate(node_cpu_seconds_total[5m]). The mechanism is identical.
A different kind of read
bash
plexctl metrics capacity --domain "$DOMAIN_ID"Not PromQL at all. The control plane samples this from its own datastore, and it answers what PromQL cannot: how close is this Domain to the ceilings the platform enforces on it?
text
DIMENSION USED TARGET RATIO UNIT
nodes 2 10000 0.0002 count
sse_fanout 0 1000 0 events_per_secondOn an idle Domain this returns
capacity_snapshot_unavailable— the collector only snapshots dimensions that carry a measurable level. The chapter 11 Node is what makes it answer.
Logs, with LogQL
bash
FROM=$(date -u -v-1H +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '-1 hour' +%Y-%m-%dT%H:%M:%SZ)
TO=$(date -u +%Y-%m-%dT%H:%M:%SZ)
plexctl logs query \
--domain "$DOMAIN_ID" \
--query '{app="plexd"}' \
--from "$FROM" \
--to "$TO" \
--output json \
| jq -r '.body | fromjson | .data.result[].values[] | "\(.[0][:10] | tonumber | todate) \(.[1])"'Explicit --from / --to rather than a relative duration, so the window is never ambiguous on the wire.
This probably prints nothing — a valid 200 with an empty envelope.
Empty is not broken
The proxy ran. The Domain scoping applied. The LogQL evaluated. There was simply nothing to match.
Put a line where a Node's pipeline would land it:
bash
kubectl run loki-push --rm -i --quiet --restart=Never \
--image=curlimages/curl:8.10.1 -- -sS -XPOST http://loki:3100/loki/api/v1/push \
-H 'Content-Type: application/json' \
-d "{\"streams\":[{\"stream\":{\"app\":\"plexd\",\"level\":\"error\"},\"values\":[[\"$(date +%s)000000000\",\"demo enrolment failed: bootstrap token expired\"]]}]}"And read it back
bash
plexctl logs query \
--domain "$DOMAIN_ID" \
--query '{app="plexd"}' \
--from "$FROM" \
--to "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--output json \
| jq -r '.body | fromjson | .data.result[].values[] | "\(.[0][:10] | tonumber | todate) \(.[1])"'text
2026-07-31T15:12:47Z demo enrolment failed: bootstrap token expiredThrough the mediated proxy, scoped to your Domain, audited.
Chapter 14 does this for real — a line typed into a file on a host, collected by the agent, and read back out through this same query.
Store a rule instead of watching by hand
bash
ALERT_ID=$(plexctl alert create \
--domain "$DOMAIN_ID" \
--name high-cpu \
--signal 'avg(rate(node_cpu_seconds_total[5m]))' \
--comparator gt \
--threshold 0.9 \
--severity warning \
--output json | jq -r '.id')
echo "$ALERT_ID"bash
plexctl alert list --domain "$DOMAIN_ID"text
NAME SIGNAL COMPARATOR THRESHOLD SEVERITY ENABLED ID
high-cpu avg(rate(node_cpu_seconds_total[5m])) gt 0.9 warning true 019eea8b-6dd9-7e4a-bd06-7a988ea37218--comparator and --severity are validated against their enums before the request leaves your machine.
Updates are patches
bash
plexctl alert update --domain "$DOMAIN_ID" --id "$ALERT_ID" --threshold 0.95text
NAME SIGNAL COMPARATOR THRESHOLD SEVERITY ENABLED ID
high-cpu avg(rate(node_cpu_seconds_total[5m])) gt 0.95 warning true 019eea8b-6dd9-7e4a-bd06-7a988ea37218Only the field you named moved. The signal, comparator, and severity are untouched — no read-modify-write race between two operators editing the same rule.