Grafana vs Looker Studio for Site Health Dashboards
Most teams do not choose a dashboard tool for their audit pipeline — they inherit whichever one the organisation already uses for something else. That is usually fine, right up until the dashboard quietly becomes the way people find out that something is broken. This page is part of Wiring Health Scores into Dashboards & Alerts and compares Grafana and Looker Studio for site-health work, with the operational question stated explicitly rather than left to emerge later.
Comparison Table
| Dimension | Grafana | Looker Studio |
|---|---|---|
| Dashboard definition | JSON, version-controlled and deployable | Edited in a web UI; no reviewable source of truth |
| Alerting | First-class; rules evaluate the same queries panels use | None; a reporting surface only |
| Warehouse connectivity | Datasource plugins for BigQuery, Postgres, Prometheus and more | Native BigQuery connector, plus community connectors |
| Refresh model | Per-panel interval, or push via a metrics endpoint | Cached extracts or live query, per data source |
| Access model | Accounts and teams you administer | Link sharing with the identity provider most staff already use |
| Scheduled delivery | Via reporting plugins or the paid tier | Built in — scheduled email delivery of a PDF |
| Cost shape | Self-hosted infrastructure, or a per-user cloud tier | Free at the point of use |
| Templating | Repeating panels over a variable, e.g. one row per section | Limited; usually one page per breakdown |
The rows that decide it are the first two. Everything else is a preference; alerting and version control are the properties that determine whether the dashboard is a component of the system or a description of it.
Two parity configurations
Both configurations below read the same serving layer described in the parent guide, so the number on the screen is the number the scoring pipeline computed, in both tools.
// grafana/dashboards/site-health.json — deployed from the repository.
{
"title": "Site health — composite score by section",
"refresh": "15m",
"templating": { "list": [
{ "name": "section", "type": "query", "datasource": "audit-warehouse",
"query": "SELECT DISTINCT section FROM serving.scores_current ORDER BY 1" }
]},
"panels": [
{ "type": "timeseries", "title": "Composite score — $section (all devices)",
"datasource": "audit-warehouse",
"targets": [{ "rawSql":
"SELECT run_date AS time, composite_score FROM serving.scores_history WHERE section = '$section' ORDER BY 1",
"format": "time_series" }],
"fieldConfig": { "defaults": { "min": 0, "max": 100 } } },
{ "type": "stat", "title": "Serving layer age",
"datasource": "audit-warehouse",
"targets": [{ "rawSql":
"SELECT EXTRACT(EPOCH FROM (now() - MAX(served_at))) AS age FROM serving.scores_current" }],
"fieldConfig": { "defaults": { "unit": "s",
"thresholds": { "steps": [
{ "color": "green", "value": null }, { "color": "red", "value": 7200 }]}}}}
]
}
The Serving layer age panel is not decoration. It is the tile that distinguishes a healthy site from a stopped pipeline, and in Grafana it can carry an alert rule off the identical query — which is exactly the property that makes the tool operational rather than descriptive.
For Looker Studio the equivalent is a BigQuery view, because the tool has no place to put a query that is reviewable:
-- Deployed to BigQuery; Looker Studio reads the view, never raw SQL in a panel.
CREATE OR REPLACE VIEW `audit.serving_scores_reporting` AS
SELECT
run_date,
section,
device,
composite_score,
coverage,
imputed_share,
scoring_version,
TIMESTAMP_DIFF(CURRENT_TIMESTAMP(), MAX(served_at) OVER (), SECOND) AS serving_age_s
FROM `audit.serving_scores_history`
WHERE run_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 180 DAY);
Pushing every expression into a view is the workaround for the missing source of truth: the dashboard becomes a thin rendering of something that is in version control, and a panel that quietly acquires its own arithmetic is visible as a diff on the view rather than invisible inside the editor.
Verification
set -euo pipefail
# 1. The Grafana dashboard in the repo matches what is deployed.
curl -sS -H "Authorization: Bearer $GRAFANA_TOKEN" "$GRAFANA_URL/api/dashboards/uid/site-health" | jq -S '.dashboard | del(.version, .id)' > /tmp/live.json
jq -S 'del(.version, .id)' grafana/dashboards/site-health.json > /tmp/repo.json
diff /tmp/repo.json /tmp/live.json && echo "PASS: deployed dashboard matches the repository"
# 2. No panel computes a composite of its own.
grep -oE '"rawSql": "[^"]+"' grafana/dashboards/*.json | grep -iE '(\*|\+)[[:space:]]*[0-9.]+[[:space:]]*(\*|\+)' && { echo "FAIL: arithmetic in a panel query"; exit 1; } || echo "PASS: no scoring arithmetic in any panel"
# 3. The staleness threshold matches the shared config.
[ "$(jq -r '.serving.stale_after_s' "$THRESHOLD_CONFIG")" = "$(jq -r '..|.thresholds?.steps?[1]?.value // empty' grafana/dashboards/site-health.json | head -n1)" ] && echo "PASS: staleness threshold matches the shared config"
Expected output is three PASS lines. The second check is the one worth keeping permanently — it is a cheap grep that catches the exact regression this comparison is about, a panel quietly acquiring its own version of the score.
Failure Modes
Looker Studio caches an extract and the dashboard is hours behind
The BigQuery connector defaults to caching, so a tile can render a stale snapshot with no indication. The serving_age_s column in the view above exists to make that visible; put it on the dashboard as a scorecard, not in a detail page nobody opens.
Grafana panels drift from the repository
Someone edits a panel in the UI to answer a question during an incident and never reverts it. The diff check above catches it on the next CI run; without it, the repository copy quietly becomes fiction. Set the instance to provisioned dashboards where the UI is read-only if the drift keeps recurring.
The two tools disagree about the same section
Almost always a scope difference rather than a data difference — one is filtered to a device class or excludes a section the other includes. Compare the populations before comparing the numbers, exactly as described in the parent guide, and render the scope on every tile in both tools.
FAQ
Which of the two should drive alerting?
Grafana, if either of them does. Its alert rules evaluate the same queries the panels use, so the chart and the page cannot disagree and both are reviewed in one change. Looker Studio has no alerting at all, which is not a defect — it is a reporting surface. The failure mode to avoid is letting a reporting surface become the de facto way people notice something is broken, because it only works while someone happens to be looking at it.
Does it matter that Looker Studio dashboards are not in version control?
It matters as much as the dashboard matters. For a quarterly stakeholder report, an unversioned dashboard is an acceptable trade for sharing that works with permissions the organisation already runs. For anything on the operational path it is a real cost: a panel can change without a diff, an expression can acquire arithmetic nobody reviewed, and after the person who built it leaves there is no source to read. Pushing every expression into a database view recovers most of that.
Can the same serving layer feed both tools?
Yes, and it should. The whole point of a pre-computed serving layer is that every consumer reads the same rows rather than re-deriving anything, so pointing Grafana at it over a datasource and Looker Studio at it through a BigQuery view gives two renderings of one number. What must not happen is either tool computing its own composite from component metrics, because that immediately reintroduces the disagreement the layer exists to prevent.
Related
- Wiring Health Scores into Dashboards & Alerts — the parent guide covering the serving layer both tools should read
- Choosing Dashboard Alert Thresholds from Score History — deriving the floors whichever tool ends up rendering them
- Configuring Alert Thresholds and Routing — the alerting layer that should own paging regardless of the dashboard choice