Grafana Dashboards for GIS Workflows
In short: a spatial pipeline’s dashboard needs one panel per question, in the order the questions are asked during an incident — is it running, is it keeping up, is the output complete, and where is the problem. The last of those is what distinguishes a GIS dashboard from any other: the answer is frequently a shape on a map, and a dashboard that cannot draw one sends every investigation to a SQL client.
Most pipeline dashboards accumulate rather than being designed. A panel is added for each question someone once asked, nothing is ever removed, and after a year there are thirty panels of which four are ever read. The alternative is not fewer metrics but a stated order: the top row answers “is anything wrong”, the second row localises it, and everything below is there for the investigation that follows. That order is what lets someone who has never seen the dashboard use it at three in the morning.
There is a second reason spatial dashboards drift more than most. A GIS pipeline’s data has natural axes that are tempting to slice by — layer, zoom, region, source, projection — and each slice looks like a reasonable panel. Adding all of them produces a dashboard that is genuinely comprehensive and completely unreadable, because comprehensiveness is the wrong goal for a document read under time pressure. The slices belong in template variables and ad-hoc queries; the panels belong to the four questions.
Prerequisites & Architecture Baseline
Core Principles
1. One panel, one question. A panel whose title is a metric name — raster_pixels_processed_total — makes the reader do the translation. A panel titled “Is the mosaic keeping up with the delivery rate?” does not. Write the question, then find the query that answers it.
2. Order the rows by the order of the questions. Health first, then throughput, then completeness, then geography, then the detail nobody needs until they do. That order matches how an incident actually unfolds, and it means the top of the screen is always the right place to start.
3. Include a map, and make it queryable. The Geomap panel reading from PostGIS turns “which tiles failed” into a picture. In spatial work that panel resolves more investigations than any time series, because failure is so often geographically clustered.
4. Compare against the same period last week. A throughput number alone is uninterpretable; the same number against last Tuesday is a judgement. Grafana’s time-shift makes this one query option, and it converts most “is this normal?” questions into a glance.
5. Put the healthy range in the panel, not in someone’s head. Thresholds, a description saying what normal looks like, and a link to the runbook. A panel that requires tribal knowledge to interpret is a panel that will be misread during the one incident that matters.
6. Delete panels that nobody reads. A dashboard is a document with an audience, and thirty panels have no audience. If a panel has not been part of an investigation in six months, it is costing attention it does not earn — the same argument as retiring an unused metric.
The four-row structure has a property worth noticing: each row narrows the search space, and the narrowing is what makes the dashboard usable under pressure. Row one says whether to keep reading. Row two distinguishes “broken” from “behind”, which are different incidents with different responses. Row three distinguishes “behind” from “producing incomplete output”, which is the distinction that decides whether the product can be published. Row four turns whatever remains into a place. Each row can be read in a few seconds, and someone who stops after row one has still learned the most important thing.
Production Implementation
Dashboards belong in version control as JSON. The excerpt below shows the two panels that carry the most weight — the throughput comparison and the map — with the queries that back them.
{
"title": "Raster pipeline — $layer",
"templating": {
"list": [
{
"name": "layer",
"type": "query",
"datasource": "Prometheus",
"query": "label_values(raster_pixels_processed_total, layer)",
"current": { "text": "ortho", "value": "ortho" }
}
]
},
"panels": [
{
"title": "Is it keeping up? (Mpx/s, vs. same time last week)",
"description": "Healthy is 40-60 Mpx/s on the 12-worker pool. Below 25 means the run will miss the 06:00 deadline. Runbook: /runbooks/raster-throughput",
"type": "timeseries",
"targets": [
{
"expr": "sum(rate(raster_pixels_processed_total{layer=\"$layer\"}[5m])) / 1e6",
"legendFormat": "now"
},
{
"expr": "sum(rate(raster_pixels_processed_total{layer=\"$layer\"}[5m] offset 1w)) / 1e6",
"legendFormat": "last week"
}
],
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "red", "value": null },
{ "color": "orange", "value": 25 },
{ "color": "green", "value": 40 }
]
}
}
}
},
{
"title": "Where is the problem? (failed tiles, last 24 h)",
"type": "geomap",
"datasource": "PostGIS",
"targets": [
{
"format": "table",
"rawSql": "SELECT ST_Y(ST_Centroid(ST_Transform(ST_TileEnvelope(z, x, y), 4326))) AS latitude, ST_X(ST_Centroid(ST_Transform(ST_TileEnvelope(z, x, y), 4326))) AS longitude, count(*) AS failures, max(error_text) AS reason FROM tile_runs WHERE outcome = 'failed' AND ran_at > now() - interval '24 hours' AND layer = '$layer' GROUP BY z, x, y"
}
]
}
]
}
The map query is the part worth studying. Grafana’s Geomap wants latitude and longitude columns, so the tile index is turned into a centroid in WGS84 by PostGIS rather than in the panel — which keeps the transformation somewhere testable and means the same query works from psql during an investigation.
Step-by-Step Walkthrough
- Write the questions before opening Grafana. Four to six questions, phrased as a person would ask them. That list is the dashboard’s specification, and it is much easier to argue about than a screenshot.
- Add the
layervariable first. One dashboard per layer is how a team ends up with eleven near-identical dashboards that drift. A template variable costs nothing and prevents that outcome. - Put the comparison in the panel, not in the reader’s memory. The
offset 1wseries is one extra target and it turns every throughput panel into a judgement rather than a number. - Write the description as though for someone new. What is normal, what is bad, and where the runbook is. Grafana shows it on hover, and it is the only documentation anyone will read during an incident.
- Set thresholds that match the description. If the text says 25 Mpx/s is bad, the threshold should be at 25. A panel whose colours disagree with its description trains people to ignore both.
- Transform to WGS84 in SQL. The Geomap panel expects latitude and longitude. Doing the transform in PostGIS keeps it out of the panel definition and makes the query reusable outside Grafana.
- Collapse the detail rows. Anything below the fourth row should be in a collapsed row, so the default view is the four questions and nothing else.
Edge Cases & Failure Recovery
A map panel with too many points. Twenty thousand failed tiles rendered as individual markers will make the browser unusable. Aggregate server-side — group by a coarser zoom, or return a heatmap layer — so the panel receives hundreds of rows rather than tens of thousands.
A variable with unbounded values. A tile or scene template variable produces a dropdown with a million entries that times out on load. Variables should draw from the same bounded label vocabulary the metrics use.
Panels that query the production database directly. A dashboard that runs an unbounded tile_runs scan every thirty seconds is a load source of its own. Add an index for the query, bound it by time, and set a sensible minimum refresh interval — thirty seconds is rarely necessary for a batch pipeline.
Time ranges that hide the batch. A pipeline that runs at 02:00 shows nothing on a “last 6 hours” default at midday. Set the dashboard’s default range to cover at least one full cycle, and prefer “last 2 days” over anything shorter for a nightly pipeline.
A dashboard that is edited in the browser and never saved back. Changes made in the UI vanish on the next provisioning run, or worse, persist and diverge from the JSON in version control. Provision from the repository and treat the UI as read-only, with a documented path for proposing a change.
A dashboard that only works for the person who built it. The panels make sense to whoever chose the queries and are opaque to everyone else, which is invisible until that person is on holiday. The cheap test is to hand it to a colleague who has not seen it and ask them to answer the four questions. Whatever they cannot answer in a minute is a missing description, a badly titled panel, or a panel that should not exist.
A geomap centred on the wrong hemisphere. Grafana’s Geomap defaults to a world view, and a panel returning a handful of points in Norway renders as an almost-empty world map. Set the initial view from the layer’s own extent — the panel supports fitting to data — or the map is technically correct and practically useless at a glance.
Panels showing counts where ratios are meant. “412 failures” is alarming or trivial depending on the denominator. Where the question is about health, the panel should show a ratio and put the count in the tooltip.
Configuration Reference
| Setting | Value | Spatial context |
|---|---|---|
| row order | health → throughput → completeness → geography | Matches how an incident actually unfolds. |
layer variable |
from label values | One dashboard for every layer, rather than one per layer. |
| comparison | offset 1w |
Turns a number into a judgement without any extra storage. |
| geomap source | PostGIS | Transform to WGS84 in SQL, so the query is reusable outside Grafana. |
| map aggregation | server-side | Hundreds of rows to the browser, not tens of thousands. |
| default time range | ≥ 2 days | A nightly pipeline is invisible on a six-hour default. |
| refresh interval | ≥ 1 min | A batch pipeline does not change in thirty seconds, and the queries are not free. |
Provisioning deserves one more note, because it decides whether any of the above survives. Dashboards defined in the repository and applied by Grafana’s provisioning are reviewable, revertible and diffable — a panel that changed has a commit explaining why. Dashboards edited in the browser accumulate untracked changes until nobody is sure what the current definition is or who made it. The migration is a single export and a directory, and doing it before the dashboard has thirty panels is considerably easier than after.
Frequently Asked Questions
What does the first row actually contain?
Three panels, all of them single-value with thresholds. The last run’s outcome and how long ago it finished, which catches a pipeline that stopped running at all — the failure no other panel detects. The failure ratio over the last run, as a percentage rather than a count. And the dead-letter queue’s depth trend over a week, which is the only one of the three that reveals a slow degradation rather than an acute failure. Those three fit across one row and answer “should I keep reading” in about two seconds.
Should there be one dashboard or several?
One per pipeline, with a template variable for the layer, plus one fleet-level overview if there are several pipelines. Beyond that, dashboards multiply faster than they are maintained. A useful rule is that a new dashboard needs a new audience, not a new question — questions are panels.
How do the SQL panels avoid loading the database?
By being bounded and indexed, and by refreshing no faster than the data changes. A panel querying tile_runs for the last twenty-four hours with an index on (layer, ran_at) costs a few milliseconds; the same query without the index is a sequential scan every refresh, on a table that grows by millions of rows a week. The refresh interval matters as much: a nightly pipeline’s dashboard refreshing every thirty seconds runs those queries close to three thousand times a day to show a number that changes once.
Can Grafana replace QGIS for investigation?
For localisation, yes: a geomap that shows where the failures are usually answers the question. For anything requiring the geometry itself — is this polygon self-intersecting, does it align with the parcel boundary — a real GIS client is the right tool. The dashboard’s job is to get someone to the right extent quickly; it is not a substitute for looking at the data.
How do I show coverage as a percentage?
From the pipeline’s own tables rather than from metrics: count(*) FILTER (WHERE built_at > …) / count(*) over the expected tile set. Coverage is a property of the output, not of the process, so the query belongs against the ledger — and it is the number that belongs in a freshness SLO.
How do I keep the dashboard from rotting?
Review it after every incident, with one question: which panel answered this, and which panel would have answered it faster? That produces a small, specific change each time — a threshold moved, a panel promoted into the top row, a query narrowed — and it keeps the dashboard shaped by what actually happens rather than by what someone imagined would. It is also the only reliable mechanism for deletion, because an incident review is the one moment when everyone agrees which panels were useless.
Do the panels need to match the alerts exactly?
They should be derived from the same expressions, ideally literally — Grafana’s alert rules can reference a panel’s query. What matters is that an alert firing leads to a panel showing the same thing, because the alternative is an alert nobody can corroborate. Divergence between the two is subtle and common: someone tunes the alert during an incident and the panel keeps the old threshold, and from then on the dashboard says everything is fine while the pager disagrees.
What belongs in an alert versus a panel?
Alerts fire on the top row’s questions only — is it running, is the failure ratio climbing, is the queue growing. Everything else is context that a human reads after being alerted. A dashboard where every panel has an alert produces alert fatigue, and the fatigue arrives long before the useful alert does.
Related
- Building a raster pipeline Grafana dashboard — the panels in full
- Alerting on CRS validation failures in Grafana — turning a panel into an alert
- Visualizing tile coverage gaps on a geomap — the map panel in detail
- Prometheus metrics for raster throughput — the series these panels read
- Data-quality SLOs for spatial pipelines — where the completeness row’s numbers become promises