The dashboard¶
Clicking a tile¶
Every tile that counts a set of repos links to a table listing exactly what is behind that number — collapsed under Drill-down at the bottom, opened full screen by the click. Each drill-down's row count is the tile's value by construction, and each repo name links to that repo on its own forge: the URL comes from the API that reported it, so a GitLab row opens GitLab.
Two tiles have no drill-down, because neither is a set of repos. Repos monitored is the fleet itself, and Data age is the collector's own freshness.
The tiles are laid out in two rows because that is how the data splits. The top row is what the forge says about the repos — a headline count, the two workload counts (open PRs, open issues), then the three problem counts. The bottom row is what this machine says about the clones: not in sync, dirty, off default branch. A green top row with an amber bottom row means the fleet is healthy and your checkouts are stale — a different problem, and a much cheaper one.
Open PRs and Open issues are deliberately not status-coloured. They are workload, not verdicts; the status palette is reserved for things that actually mean good or bad, so colouring them would make a normal backlog read as a failure.
Editing it¶
The board is provisioned from grafana/dashboards/fleet.json and is read-only in
the UI on purpose — edits there would be silently reverted. Change the JSON and
Grafana reloads it within 30 seconds. To iterate visually instead: copy the
panel, tweak it in the UI, then Panel JSON → paste back into the file.
Colours follow a validated palette: sequential blue where the number is a magnitude (releases behind), and the reserved status colours only where a colour actually means good or bad. Status cells always carry a glyph and a word as well, so nothing depends on colour alone.
Size and cadence¶
One row per checkout: lines of code, lines of test, how long since the last commit, commits in the last 30 days, and commits since the newest release.
LOC and Tests are lines of tracked source in the working copy. Tracked, so a
stray virtualenv cannot dwarf the repo it sits in. Source, so the markdown, the
lockfiles and the CSV fixtures are out — which means a repo whose product is
configuration, like a workflow template, will read smaller here than it feels.
Working copy, so uncommitted work counts. A file is test code if it sits under
tests/, test/ or testing/, or if it is named test_* / *_test.* — both
conventions are in this fleet.
Coverage is different in kind from its neighbours. Every other column is
read off your working copy and refreshes within a minute. Coverage comes from
the newest coverage-report artifact CI built on the default branch, so it
lags a push by a CI run and says nothing about uncommitted work. Repos that
publish no such artifact read no report, which is not 0% — six of this
fleet are in that position.
Its denominator is whatever CI pointed --cov at, and that is not the LOC
column: rhiza reads 100% of 176 measured lines while LOC counts 1477. Both
are right; they answer different questions. jq_ci_coverage_lines carries the
denominator so the percentage can be read honestly. Note also that the branch
filter is load-bearing — artifacts come back newest-first across every ref, and
in a repo that tags releases the newest is usually a tag build.
The two commit counts are taken on the default branch, not on whatever
branch the clone is parked on: a repo's cadence is what landed on main, not what
you happen to have checked out. Unreleased counts commits since the newest tag
reachable in this clone, so a release published since your last fetch is not
reflected yet. Read it next to Last fetch on the panel above, the same way you
read ahead/behind. A repo that has never been tagged shows never tagged rather
than 0, because zero unreleased commits means "everything is shipped" and that
is the opposite of the truth.
None of the five columns is status-coloured. They are magnitude and workload, not verdicts — the same reason Open PRs is not coloured.
Why the numbers sit still¶
Counting lines means reading every tracked file, and the clones are bind mounts — cheap natively, much less so through Docker Desktop. So the whole row is measured only when the clone has actually moved: a new commit, an edit, a new untracked file, a new tag. Between those, the previous scan's numbers are carried over verbatim. A flat line here is a quiet repo, not a stuck collector.
Two consequences worth knowing:
- Tagging the current commit moves neither HEAD nor the tree, so the tag
mtimes under
.gitare part of what counts as movement. Without that, cutting a release would leave Unreleased showing its pre-release value on exactly the day you would look at it. - The 30-day window slides on its own. A repo that has gone quiet still has
to watch its old commits fall out of the count, so a carried-over reading
expires after
JQ_MEASURE_MAX_AGE(a day by default) regardless of movement.
Template drift is deliberately outside this cache. It is the one thing that changes while the clone stands still — because it is the upstream that moved — so the pointer is re-read on every pass.
Traps worth not re-introducing¶
JSON cannot carry comments, so these are recorded here instead. The first two were live in the first cut of this board.
Never write count(...) or vector(0) in a panel with a time range. The
fallback fires at every timestamp where the left side has no series, so the
chart paints a confident flat zero across all the time before the collector
existed — it asserts "nothing was wrong last week" about a week it knows nothing
about. Use the bool modifier instead: sum(jq_ci_last_run_success == bool 0)
evaluates per existing series, so it is 0 when the repos really are all green
and simply absent when there is no data. or vector(0) is only safe on a stat
tile reading a single instant.
Renaming a label starts a new series. The repo label changed from a bare
name to owner/name when the cvxgrp repos were added, so history from before
that point lives under the old names. Windows spanning the change show both
generations; they age out. This is inherent to Prometheus, not a misconfiguration.
The instance label is pinned on purpose. There is one collector, so
instance says only where the process happened to be running — and left alone
it is the scrape address. When the collector moved out of its container the
address changed, every series forked in two, and for one stale window each repo
appeared on the board twice. prometheus.yml relabels it to a constant so that
cannot recur; series from before the move keep the old value and age out with
retention.
Never group_left onto an _info metric without a topk guard. The
_info metrics carry a label that changes value — ref, branch,
conclusion, checks. When one changes, Prometheus returns the old and the
new series until the old is marked stale, and group_left then aborts the whole
query with found duplicate series for the match group. The panel goes blank
exactly while someone is doing the work the board exists to show — which is how
this was found, mid-rhiza bump. Wrap the right-hand side:
group_left(ref) topk by (repo) (1, jq_rhiza_template_ref_info). During the
overlap it may show the older label value; that self-corrects on the next
scrape, which is far better than an error. The same applies to and on(...),
which does not error but does double-count.
A state-timeline colours from thresholds, not from value mappings. With
color.mode: thresholds and no thresholds block, Grafana silently falls back
to its default steps (green up to 80), so 0 and 1 both render green and a
failing repo looks fine. Define the steps explicitly and let the mappings supply
only the words.
Alerting¶
Six rules are provisioned into the Jebel-Quant folder, each multi-dimensional
so you get one alert instance per repo:
| Rule | Fires when |
|---|---|
| CI red on the default branch | last completed run failed, for 24h |
| Repo behind the template | one or more releases behind, for 7d |
| Open pull request with red checks | a PR's checks are red, for 24h |
| Working copy left dirty | uncommitted changes, for 3d |
| Clone parked off its default branch | non-default branch checked out, for 3d |
| Collector is not refreshing | no successful refresh in 30m |
The long for windows are deliberate: this is a fleet board, not a pager, and a
repo that goes red for an hour during a normal push-fix cycle is not worth a
notification.
Notifications need one more step. The rules fire and show up in the Grafana UI as provisioned, but they route to Grafana's built-in email contact point, which has no SMTP configured — so nothing is delivered anywhere yet. To actually get notified, add a contact point (Slack webhook, email, ntfy) under Alerting → Contact points and point the default notification policy at it. Provisioning it here would mean committing a webhook URL to the repo.