Watch the month in Grafana
In this lesson you open the Grafana the cluster runs, point its four dashboards at the simulated cloud and at the simulated month, read what each of them shows, and find the one alert the dev stack fires by design in vmalert and in Alertmanager.
At the end you have a name for what each dashboard and each alert component reads, and the whole state of this track, written out, which the billing track continues from.
This lesson takes about 5 minutes.
Before you start
The state lesson 3 leaves: the kind cluster with the dev overlay, the simulator stack holding its 84 notifications, the month of July 2026 in the reporting database, and one completed run of that month in the engine database.
A browser.
tally-ca.crtat the repository root, for thecurlcalls beside the browser. A new shell writes the file again with:shmake -s ca > tally-ca.crtThe dashboards read only what lesson 2 wrote, the events and the pushed series, so the engine's run of lesson 3 changes nothing on them.
Open Grafana
Ask Grafana whether it is up:
shcurl --cacert tally-ca.crt https://grafana.tally.127-0-0-1.nip.io:8443/api/healthjson{ "database": "ok", "version": "13.1.3", "commit": "45a27d64b64a82d666b06aa5c5bb3521587edb0d" }databaseokhas to match.version13.1.3 is the image the manifests pin, so it matches at this commit and moves with the manifests.commitis Grafana's own build.Open
https://grafana.tally.127-0-0-1.nip.io:8443in the browser. The browser stops on a certificate warning: the certificate is signed by the dev CA of lesson 1, which the browser does not know, and that CA is not installed on purpose, because cert-manager creates a new one after everymake up. Proceed past the warning for this hostname, in Firefox under its Advanced button.No login follows. The dev overlay lets an anonymous viewer read every dashboard, which is what
GF_AUTH_ANONYMOUS_ENABLEDwith the Viewer role indeploy/kubernetes/overlays/dev/kustomization.yamlsets, and install the Grafana dashboards covers a deployment with logins.Go to the folder
Tally. It carries four dashboards,Tally / Fleet Overview,Tally / Ingestion Health,Tally / Project DrilldownandTally / Reconciliation Drift, in the order the folder lists them, and Grafana dashboards names every panel and the expression it reads.Read the same four through the API:
shcurl --cacert tally-ca.crt 'https://grafana.tally.127-0-0-1.nip.io:8443/api/search?type=dash-db' | jq '.[] | {folderTitle, title, uid}'json{"folderTitle":"Tally","title":"Tally / Fleet Overview","uid":"tally-fleet-overview"} {"folderTitle":"Tally","title":"Tally / Ingestion Health","uid":"tally-ingestion-health"} {"folderTitle":"Tally","title":"Tally / Project Drilldown","uid":"tally-project-drilldown"} {"folderTitle":"Tally","title":"Tally / Reconciliation Drift","uid":"tally-reconciliation-drift"}The call carries no credential, so this is the anonymous viewer again. The four titles and the four uids have to match. A 401 page in the browser or here means anonymous access is off, which is not the dev overlay.
Set the cloud and the month
Set the
cloudvariable toos-simon every dashboard and leaveplatformon All. Both variables read the clouds offtally_current_resources, soos-simis offered once the projection holds the month.Use two time ranges. A dashboard has one range at a time and its panels are split across the two, so you set one range, read the panels that fit it, then set the other.
The absolute range
2026-07-01 00:00:00to2026-07-31 23:59:59fits every panel that reads anopenstack_*or aceilometer_*series, because the simulator pushed those at simulated time, inside July 2026. It ends a second before August because the inventory is pushed every five minutes, so its last sample of the month is at 23:55:00, and an instant panel reads the last sample within the five minutes before the range's end, a window that for an end at 00:00:00 opens just after that sample.Last 3 hoursfits every panel that reads atally_series, because those were scraped off the Reporting API at the wall clock while the month went out, in the few minutes lesson 2 took. The dashboards open onLast 6 hoursrefreshing every minute. The next step names which panels are which per dashboard.Every dashboard file carries
"timezone": "utc", so both ranges are read in UTC and no time zone setting is needed.
Read the four dashboards
The four read one store from four angles, and each of them narrows to what the cloud variable is set to. What you see on them:
Tally / Fleet Overview, the fleet the events built. It counts the resources the projection holds by type and state, draws the same counts as a trend, and says how many clouds report; two of its panels read the pushed inventory instead, the number of tenants and the ten with the most instances.Tally / Ingestion Health, the way in. It draws the events ingested per cloud and source, the duplicates dropped and the items rejected, the collector's buffer and its oldest buffered event, the projection replays, and whether each scrape target answers.Tally / Project Drilldown, one tenant at a time. It counts that tenant's servers, volumes, floating IPs, routers, images and load balancers, sums the GB its volumes hold, and reads its usage of the three nova quotas. Its last panel is the cloud's ingest split by event type, the one panel here that does not narrow to the tenant.Tally / Reconciliation Drift, what a sync against the cloud's own API changed. It draws the resources reconciled by action, the sync errors, and the sync runs by status, beside a text panel on how to read the three.
Which panels carry a value in this lesson and which stay empty is what the four steps below say, and each of them names the time range its panels need.
Tally / Fleet Overview. WithLast 3 hours,Resources by type and stateshows 37 instancesactive, 630deletedand 1shelved, 16 volumesavailable, 12in-useand 141deleted, 9 floating IPsactiveand 7deleted, 7 imagesactiveand 2deleted, and 4 load balancersactiveand 1deleted. The whole month has ended, so most of the fleet is deleted.Resource count trenddraws the same counts as lines that jump once, when the month went out, andClouds reportingis 1.With the July range,
Projects (OpenStack)is 6 andTop 10 projects by instance countlists the six tenant ids, four of them at 3 instances at the month's end and the CI tenant and the second Gardener tenant at 0. Over the month the CI tenant peaked at 10 and the large Gardener tenant at 11.Tally / Ingestion Health. WithLast 3 hours,Event ingest rateshows one spike for cloudos-simand sourcecollectorover the minutes the month went out, 1728 events in all. How high the spike stands is your machine's, because it is those events over the span the collector took to drain its outbox: about five events per second where that took three minutes, and above 30 where it took half a minute.Dedup rateandRejected eventsdraw nothing, because the month carried no duplicate and no item was rejected, so those counters were never written. A panel over a counter that does not exist showsNo data, which here reads as 0.Collector buffer depthandOldest buffered event ageread the collector of the compose stack, which theopenstack-collectorjob scrapes by its alias on thekindnetwork while the stack runs. Both carry the legendos-sim tally-openstack-collector:8080. They rose while the month went out, by one event and about one second on the run, and both read 0 once the outbox has drained.Projection replaysdraws nothing.Scrape healthlists five targets.reporting-api,otel-collectorandopenstack-collectorare up (1), andceilometeris down (0), which is the designed dev state, a placeholder target for an exporter that runs beside a real control plane.openstack-db-exporteris up (1), because the holding simulator still serves its inventory, which that job scrapes over thekindnetwork. Between simulator runs it andopenstack-collectorread 0.Tally / Project Drilldown. Set theReporting APItextbox tohttps://api.tally.127-0-0-1.nip.io:8443, the hostname and port the dev overlay publishes the API under. The dashboard ships with the base's placeholderhttps://api.tally.example.com, a host that answers nothing. Pick005be5adeef3d87e280d03d9d57c38b4in theproject_idvariable, one of the six ids and the Gardener tenant with the largest statement of lesson 3.With the July range, at the end of July,
Resources by typeshows 3 servers, 8 volumes, 4 floating IPs, 2 routers, 1 image and 4 load balancers,Volume capacityreads 480 GB, and the threeQuota usagegauges read 3 % of the instance quota, 6 % of the vCPU quota and 6 % of the memory quota. WithLast 3 hours,Recent lifecycle activityshows the ingest spike split by event type. Its linkEvent content (Reporting API)openshttps://api.tally.127-0-0-1.nip.io:8443/api/v1/events?project_id=005be5adeef3d87e280d03d9d57c38b4in a new tab, and the API answers it with the 401 problem document of lesson 1, because a link carries no bearer token.curlwith the token of lesson 1 reads the project's events from the same URL.Tally / Reconciliation Drift. Every query panel is empty (No data), because no sync ran in this track, and theDrift interpretationtext panel says how to read them once one does. Reconcile the simulated cloud runs that loop against this same stack.
A panel reading No data where a value is named above has the wrong one of the two time ranges, or the cloud variable is not on os-sim, or the July range ends at 2026-08-01 00:00:00 instead of 2026-07-31 23:59:59. That end empties the five panels of the July range that read one instant, Projects (OpenStack), Top 10 projects by instance count and the three Quota usage gauges, because their last sample lies five minutes before it.
Read a panel without the browser
Ask Grafana for the value the
Projects (OpenStack)panel shows over the July range:shcurl --cacert tally-ca.crt -H 'Content-Type: application/json' https://grafana.tally.127-0-0-1.nip.io:8443/api/ds/query -d '{"from":"1782864000000","to":"1785542399000","queries":[{"refId":"A","datasource":{"uid":"victoriametrics"},"expr":"sum(openstack_identity_projects{cloud=\"os-sim\"})","instant":true}]}' | jq '.results.A.frames[0].data.values'json[ [ 1785542399000 ], [ 6 ] ]This is the route the dashboards' panels query through from the browser, with the query that panel sends, and the call carries no credential, so it is the anonymous viewer again.
fromandtoare the July range of step 2 of "Set the cloud and the month" in milliseconds,2026-07-01T00:00:00Zand2026-07-31T23:59:59Z. The first list is the instant the value was read at, the range's end, and the second is the value. The 6 has to match.The same call with
"to":"1785542400000", which is2026-08-01T00:00:00Z, prints[]: the frame carries no value, which is what the browser shows asNo data, with the causes the end of the section above names. An expression the store refuses answers with a.results.A.errorthat names the store's error and a.results.A.statusother than 200, 422 on the run for the expressionsum(. Its frame is empty as well, so the filter above prints[]for it too, andjq '.results.A'in place of that filter tells the two apart.Read the store through Grafana's datasource proxy instead:
shcurl --cacert tally-ca.crt -w '%{http_code}\n' 'https://grafana.tally.127-0-0-1.nip.io:8443/api/datasources/proxy/uid/victoriametrics/api/v1/query?query=up'textThe Grafana datasource proxy is not published. 403The body and the
403have to match. The Gateway answers before Grafana sees the request, and the dev and the prod overlay both carry that rule. What the route publishes says why the prefix is refused while/api/ds/queryis published.
Find the alerts
Read what vmalert holds as firing:
shcurl --cacert tally-ca.crt https://vmalert.tally.127-0-0-1.nip.io:8443/api/v1/alerts | jq '.data.alerts[] | {alertname: .labels.alertname, state, job: .labels.job}'json{ "alertname": "TallyScrapeTargetDown", "state": "firing", "job": "ceilometer" }This one alert has to be there, and nothing else should be firing.
TallyScrapeTargetDownfires about five minutes aftermake upfor theceilometerjob, whose target is a placeholder for an exporter that runs beside a real control plane, and that is the designed dev state rather than a fault to chase: TallyScrapeTargetDown is its runbook and alert rules states every rule.openstack-db-exporterandopenstack-collectordo not fire because the holding simulator serves its inventory and the collector keeps running beside it.TallyCloudEventsSilentfires foros-simonce no event has been ingested for over an hour from a cloud that reported in the last 24 hours, so a reader who reaches this step more than an hour after lesson 2 sees it beside the first. That one is the holding simulator publishing nothing rather than a fault.Read the same alert as Alertmanager holds it:
shcurl --cacert tally-ca.crt https://alertmanager.tally.127-0-0-1.nip.io:8443/api/v2/alerts | jq '.[] | {alertname: .labels.alertname, runbook: .annotations.runbook}'json{ "alertname": "TallyScrapeTargetDown", "runbook": "https://b42labs.github.io/tally/how-to/alerts/TallyScrapeTargetDown" }The runbook annotation
https://b42labs.github.io/tally/how-to/alerts/TallyScrapeTargetDownhas to match. vmalert is what evaluates the rules against the store, and Alertmanager is what routes what fires.The two UIs render the same in the browser:
https://vmalert.tally.127-0-0-1.nip.io:8443/vmalert/shows the rules, their state and their last evaluation, andhttps://alertmanager.tally.127-0-0-1.nip.io:8443shows the firing alerts as routed. Both sit behind the same certificate warning as Grafana.Read
/api/v1/ruleswhen an alert you expect is missing from the list:shcurl --cacert tally-ca.crt https://vmalert.tally.127-0-0-1.nip.io:8443/api/v1/rules | jq '.data.groups[].rules[] | {name, state, lastError}'json{"name":"TallyCloudEventsSilent","state":"inactive","lastError":""} {"name":"TallyEventsRejected","state":"inactive","lastError":""} {"name":"TallySyncErrors","state":"inactive","lastError":""} {"name":"TallySyncStale","state":"inactive","lastError":""} {"name":"TallyReconciliationDriftHigh","state":"inactive","lastError":""} {"name":"TallyCollectorBufferAging","state":"inactive","lastError":""} {"name":"tally:current_resources:sum","state":"","lastError":""} {"name":"TallyResourceCountAnomaly","state":"inactive","lastError":""} {"name":"TallyRecordedSeriesMissing","state":"inactive","lastError":""} {"name":"TallyScrapeTargetDown","state":"firing","lastError":""} {"name":"TallyExporterServiceSilent","state":"inactive","lastError":""} {"name":"TallyScrapeJobMissing","state":"inactive","lastError":""}Every
lastErroris empty. A non-empty one names the query that failed, andstatetellsfiringfrominactive.tally:current_resources:sumis a recording rule and carries no state.TallyScrapeJobMissingcomes last because it is the one rule of the second group,tally-scrape, which vmalert evaluates fromscrape-rules.yaml.
What you learned
- Grafana reads the store through a read-only proxy in its own pod rather than reaching the store directly: the datasource and the proxy.
- The dashboards query the store through
/api/ds/query, and the Gateway refuses the datasource proxy prefix before Grafana sees the request: what the route publishes. - The pushed series carry the simulated month's timestamps and the scraped ones the wall clock's, which is why the dashboards need two time ranges here: two paths into one store.
- vmalert evaluates the rules against the store and Alertmanager routes what fires: two components.
TallyScrapeTargetDownasks whether a configured scrape target answered: what the rules ask.- The drift dashboard reads a sync loop this track never ran, which is why its query panels are empty: reconciliation.
Where to go next
Discount a customer group is the first lesson of the billing track, which continues from the state this lesson leaves behind.
Tear down your local Tally is the way back to a clean machine, and it belongs to neither track: a reader going on to the billing track runs it after that track's last lesson rather than here.
This is the state this track leaves behind:
- the kind cluster
tallywith the dev overlay; - the compose stack with the simulator holding 84 notifications, so
GET /clockonhttp://127.0.0.1:8091/clockanswersholdingtrue; - the reporting database holding the month of seed 1 on
os-simminus the held share, with an empty project registry; - the engine database holding the pricing model
2026-03and one completed, not finalized run of2026-07; - the JSON export of that run under
~/tally-tutorial/2026-07; tally-ca.crtat the repository root;- the VictoriaMetrics port-forward on
127.0.0.1:8428; - in the shell:
TALLY_REPORTING_DB_URL,TALLY_API_TOKEN,TALLY_ENGINE_DB_URL,TALLY_ENGINE_REPORTING_DB_URL,TALLY_ENGINE_COUNTER_SOURCES,TALLY_ENGINE_VM_URLandRUN_ID.
The cluster's hourly tally-engine scheduler keeps ticking meanwhile. It moves the periods along and may bill 2026-08 on its own once that month's grace window has passed, which leaves a run of 2026-08 beside the one of 2026-07 and changes nothing above.