feat(cryptify): Grafana usage dashboard, pinned to the /metrics exporter - #310
feat(cryptify): Grafana usage dashboard, pinned to the /metrics exporter#310dobby-coder[bot] wants to merge 4 commits into
Conversation
…exporter The /metrics exporter has been in place since cryptify#102, and metrics.rs line 9 already points readers at `docs/grafana/` for the dashboard that goes with it. That directory never existed, so the reference dangled and the dashboard side of postguard#305 stayed unbuilt. Adds cryptify-usage.json: messages sent per channel and per client app, bytes uploaded, storage in use, files on disk, and uploads that expired before finalize. Staging and Procolix production split on an `env` label, which the Prometheus job supplies via static labels rather than the exporter, since one process has no way to know which deployment it is. `mod dashboard_tests` keeps the two honest. It reads the `# TYPE` lines out of a real Metrics::render() and compares them against the metric names in the committed panel queries, in both directions, so a rename fails cargo test instead of silently emptying a graph. It also requires every panel query to carry the env matcher, so no panel can quietly sum staging into production. Refs #305 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
VERDICT: request-changes
Rules + review — cycle 1
Rule sweep: no breaches. I selected the 12 rules that plausibly fire on a Rust + Grafana-JSON + docs diff (the Rust fmt/clippy/test gates, digest-pinned-file-needs-gitattributes, tests-required-on-fixes, the docs-claim rules, cross-repo-link-format, no-justification-paragraphs-for-simple-changes) and checked each against the diff. Two I want to record as explicitly cleared, because both look like breaches at a glance:
- The
cryptify/CLAUDE.mdmetrics hunk rewrites "unauthenticated by design" into Bearer-gated-when-metrics_token-is-set. That is not an overclaim: the guard is already on this branch (cryptify/src/main.rs:140–200,cryptify/src/config.rs:143, plus the startup warning atmain.rs:1603). The edit corrects stale docs rather than describing something this PR did not build. include_str!on a checked-in text file is the shape that normally needs a.gitattributes-textpin, and the repo's existing.gitattributespins exactly two files for that reason. Not needed here: nothing is byte-exact or a multi-line anchor — the JSON is parsed as JSON, and the only substring check is a single-linecontains(). CRLF-safe.
One blocking issue, handed over from review and confirmed here by reproduction, plus three non-blocking nits inline.
The blocker is worth stating plainly: the test module is the entire safety argument for committing a dashboard as a separate artefact, and it does not cover cryptify_storage_bytes — the one metric issue #305 names twice. I deleted panels 8 and 9 on this branch and all four tests stayed green. The fix is small and I have verified it both ways; the inline suggestion is rustfmt's formatting of it, not the shorter chain, because cargo fmt --check is a CI gate here.
Everything else holds up. The mutation table in the PR body is honest, the three claimed mutations do fail as described, and the "what I could not verify" section is the right call rather than a hedge — the PromQL genuinely has not been through a real Grafana.
The exported->dashboard test folded template-variable queries into the set it checked against, and the `env` variable is `label_values(cryptify_storage_bytes, env)`. So `cryptify_storage_bytes` certified itself: deleting both panels that display storage left all four tests green. That is the one metric #305 names explicitly, so the guard rail was missing exactly what it was built for. Split the panel-only set out and use it for that direction. The other direction keeps the combined set, since a variable querying a metric the exporter dropped is broken too. Also from review: - Panel 8 asked for a `graphMode: area` sparkline but ran an instant query, which returns one sample and draws nothing. - Panels 8, 9 and 10 selected the two unlabelled gauges raw while labelling the result `{{env}}`, so two targets sharing an env rendered as two identically-named series. They now aggregate with `max by (env)`, like panel 11 already did. - Reworded the env-matcher comment and assertion: a matcher filters, it does not stop panels 2, 4 and 6 aggregating `env` away. Refs #305
Updating the PR body with `gh pr edit --body-file` exits 0, prints what looks like a deprecation warning about projects classic, and does not apply the edit. Cost a cycle here; REST works.
…caveat Panels 3 and 5 are `sum by (channel) (...)`, which drops `env` just as surely as panel 2's bare `sum(...)` or panel 6's `by (app)`. The caveat listed only 2, 4 and 6, implying 3 and 5 were safe. Panels 2 through 6 are the whole "Messages sent" row, so the range says it exactly.
There was a problem hiding this comment.
VERDICT: approve
Rules + review, cycle 2
Cycle 1's blocker is fixed, and I reproduced the fix rather than taking it on trust.
panel_metrics() now feeds the coverage direction while referenced_metrics() (panels plus template variables) feeds the other. The env variable's label_values(cryptify_storage_bytes, env) query is datasource plumbing that puts nothing on a graph, so it no longer lets cryptify_storage_bytes certify itself. Probe: I deleted panels 8 and 9 from cryptify-usage.json and ran the suite.
cryptify_storage_bytes is exported but no dashboard panel queries it —
add a panel to cryptify/docs/grafana/cryptify-usage.json
test result: FAILED. 3 passed; 1 failed
The same deletion left all four tests green before the fix. The guard rail now guards the metric #305 names twice. Restored with the inverse edit, tree clean.
The max by (env) change on panels 8, 9 and 10 is right, and the README paragraph explaining it earns its place: which aggregator to use depends on whether a second target shares the volume, and that is a call the operator has to make. It says so and names the alternative.
Rule sweep on the 50e36a9..53493e5 delta: no breaches. Two worth recording as explicitly cleared:
include_str!on the newcryptify-usage.jsonwith no.gitattributespin. Not a finding: nothing is byte-exact or a multi-line anchor. The file is parsed as JSON, and the one substring check is a single-linecontains()onenv=~"$env", so a CRLF checkout is fine.- The new root
CLAUDE.mdbullet ongh pr edit. It fires only in this repo, and this repo has a writable rootCLAUDE.md, so that is where it belongs rather than in the shared rules index.
One nit, fixed on the branch
The ENV_SELECTOR caveat named panels 2, 4 and 6 as the ones that aggregate env away. Panels 3 and 5 do it too: both are sum by (channel) (...), and by (channel) drops env just as surely as a bare sum(...) does. Panels 2 through 6 are the whole Messages sent row, so the range says it exactly and is shorter than the list. This came from my own cycle-1 nit, which listed the same incomplete trio; the PR copied it faithfully.
Pushed as 3a88f54, comment text only.
Verification
On 3a88f54, from cryptify/:
| Check | Result |
|---|---|
cargo fmt --all -- --check |
clean |
cargo clippy --all-targets -- -D warnings |
clean |
cargo test --all-targets |
161 passed, 0 failed |
No blocking issues left. Flipping this out of draft.
What this is
The dashboard half of #305. The exporter half already landed:
GET /metricshas shipped per-channel upload counters and storage gauges since
encryption4all/cryptify#102, and came into this workspace with the cryptify
merge (#277). So this PR does not touch the exporter.
What was missing is the dashboard itself.
cryptify/src/metrics.rs:9has beentelling readers to "see
docs/grafana/for the reference dashboard JSON" whilethat directory did not exist. This adds it.
What is in it
cryptify/docs/grafana/cryptify-usage.json, importable as-is:website,staging-website,outlook,thunderbird,api,unknown.pg4ol,pg4tb,pg-js,pg-dotnet), from theappfield ofX-POSTGUARD-CLIENT-VERSION. This is the more reliable read onthe two add-ins than the User-Agent sniff.
The three panels behind these aggregate with
max by (env)rather thanselecting raw, because the two gauges carry no labels of their own.
The datasource is a dashboard variable rather than a baked-in UID, so the file
imports into any Grafana without editing.
Staging vs Procolix production
envis a Prometheus label, not an exporter one. Nothing in a cryptify processknows which deployment it is, and adding a config key for it would put the same
fact in two places. The scrape job attaches
env: staging/env: productionvia static labels;
cryptify/docs/grafana/README.mdhas the config, includingthe Bearer auth against
metrics_token.Test
mod dashboard_testsincryptify/src/metrics.rspins the dashboard to theexporter, since a dashboard querying a renamed metric renders as an empty graph
rather than an error. It reads the
# TYPElines out of a realMetrics::render()and compares them with the metric names in the committedpanel queries, both directions, and requires every panel query to carry
env=~"$env".Verified it catches drift rather than just passing, five mutations, each
reverted after:
env=~"$env"from one panelcryptify_storage_bytesenvvariable's query onlycargo test --manifest-path cryptify/Cargo.toml --all-targetsis 161 passed, 0failed.
cargo fmt --all -- --checkandcargo clippy --manifest-path cryptify/Cargo.toml --all-targets -- -D warningsare clean.Review cycle 1
One bug in the guard rail, found by the reviewer and reproduced here before
fixing: the exported->dashboard test folded the template-variable queries into
the set it checked against, and the
envvariable islabel_values(cryptify_storage_bytes, env). Socryptify_storage_bytescertified itself. I deleted both storage panels on the old code and all four
tests stayed green. The metric #305 names twice was the one thing the test did
not cover. The panel-only set is now split out for that direction; the other
direction keeps the combined set, since a variable querying a dropped metric is
broken too.
Also addressed: panel 8 asked for a sparkline but ran an instant query, the
three gauge panels labelled their series
{{env}}while selecting unlabelledmetrics raw, and the env-matcher comment and assertion message claimed a matcher
prevents aggregation when it only filters. All four threads have a reply and are
resolved.
What I could not verify
I have no Grafana or Prometheus in this container (no usable docker, no
network for a PromQL parser), so the JSON has not been through an actual
import and the PromQL is eyeballed, not machine-checked. I did check the file
structurally: 11 panels, unique ids, no
gridPosoverlap, every target has arefIdand the templated datasource. Worth a real import before merge.Left for a human
This is deliberately
Refs, notCloses. Creating the Scaleway Grafanadashboard and adding the scrape job are ops actions no PR can perform, so #305
should stay open until those are done:
docs/grafana/README.mdto the ScalewayPrometheus, with
envlabels for staging and Procolix production, and setmetrics_tokenon both deployments.cryptify-usage.json.@rubenhensen: two questions I could not answer from the repo. What are the real
hostnames of the two cryptify deployments (the README uses placeholders), and
is
/metricsreachable from the Scaleway Prometheus at all, or does Procolixneed a firewall rule first?
One gap worth naming: the Outlook and Thunderbird add-ins are separate repos,
and if they do not send
X-POSTGUARD-CLIENT-VERSIONorX-Cryptify-Sourcetoday, their traffic lands in
unknownrather than in their own row. Thedashboard shows that honestly instead of hiding it. Happy to open follow-ups on
those two repos if you want the split to be exact.
Refs #305