GoogleCloudPlatform / GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK
feat: add a free public Grafana dashboard for BQAA
- Dominant language
- Python
- Stars
- 47
- Forks
- 21
- Avg merge
- 2d 13h
- Merged PRs (30d)
- 33
Description
## Summary
Add a public, free Grafana dashboard option for BigQuery Agent Analytics (BQAA), using the existing `dashboard_v2` experience as the reference information architecture.
`dashboard_v2` already gives users a React/Vercel dashboard for BigQuery-backed BQAA data (self-hosted per customer). A Grafana dashboard is a lower-friction alternative: no custom app hosting, native BigQuery querying, built-in time-range controls, and an easy way to share a redacted public demo link from Grafana Cloud Free.
This issue has been verified against the current SDK: the **actual `agent_events` schema**, the SDK's **per-event-type views** (`ViewManager`), and the **`bqaa seed-events`** synthetic-data CLI are documented below, so implementation can start without schema spelunking.
## Why this matters
Many teams already use Grafana for observability. For BQAA, a Grafana dashboard makes the first evaluation path much simpler:
1. Create or use a free Grafana Cloud account.
2. Connect the Google BigQuery data source.
3. Import a BQAA dashboard JSON file from this repo.
4. Point it at `PROJECT.DATASET.agent_events`.
5. Optionally share a public, redacted demo dashboard link.
This complements, not replaces, `dashboard_v2`.
## Current reference
- Product reference: [`dashboard_v2/`](https://github.com/GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK/tree/main/dashboard_v2) (React/TS/Vite, Vercel-deployed, per-customer; project/dataset/table selection, FinOps summary, session/audit exploration, trace/event visualization, time-range analytics).
- Grafana docs: [BigQuery data source plugin](https://grafana.com/grafana/plugins/grafana-bigquery-datasource/) · [Grafana Cloud Free](https://grafana.com/pricing/) · [Externally shared dashboards](https://grafana.com/docs/grafana/latest/dashboards/dashboard-public/)
## Verified repo facts (read this before implementing)
**1. The real `agent_events` schema** (from `src/bigquery_agent_analytics/seed_events.py::_EVENT_SCHEMA_FIELDS`):
| Column | Type | Notes |
| --- | --- | --- |
| `timestamp` | TIMESTAMP (REQUIRED) | use for `$__timeFilter` |
| `event_type` | STRING (REQUIRED) | `LLM_REQUEST`, `TOOL_STARTING`, `TOOL_COMPLETED`, `TOOL_ERROR`, `TOOL_PAUSED`, `AGENT_COMPLETED`, ... |
| `agent` | STRING | **not** `agent_name` |
| `session_id`, `invocation_id`, `user_id` | STRING | |
| `trace_id`, `span_id`, `parent_span_id` | STRING | trace timeline / future node graph |
| `status`, `error_message` | STRING | error panels |
| `is_truncated` | BOOLEAN | |
| `content` | JSON | payloads; on `LLM_RESPONSE` rows, token usage lives at `$.usage.prompt` / `$.usage.completion` / `$.usage.total` |
| `attributes` | JSON | `$.model` for LLM events |
| `latency_ms` | JSON | `$.total_ms`, `$.time_to_first_token_ms` |
There are **no top-level `model`, `tool_name`, token, or cost columns** — model/tokens/latency come from the JSON columns (or from the views below). There are **no cost fields anywhere** in the schema (see FinOps note).
**2. The SDK already ships typed views.** `ViewManager` (`src/bigquery_agent_analytics/views.py`) creates per-event-type views that unnest `agent_events` into typed columns — e.g. the `LLM_RESPONSE` view (`llm_responses`) exposes `usage_prompt_tokens`, `usage_completion_tokens`, `usage_total_tokens`, `total_ms`, `ttft_ms`, and `model_version`; the `LLM_REQUEST` view (`llm_requests`) exposes `model`, request content, config, and tools; the `TOOL_*` views expose `tool_name` and `total_ms` -- plus the identity headers on every view. **The dashboard should prefer these views**: panels become plain-SQL simple, and the views absorb schema evolution. Setup adds one documented step (`ViewManager(...).create_all_views()` or the CLI equivalent). Raw-table queries remain fine for panels that only need header columns (activity over time, session summary).
**3. Synthetic data already exists.** `bqaa seed-events` (#246) writes synthetic `agent_events`, including the `retail-returns` multi-agent scenario (#313) with token/latency telemetry and a `decision-realistic` scenario with failed/orphaned/truncated sessions. The test plan and the public demo both use it — no app instrumentation needed, and the public demo is redaction-safe **by construction**.
## Design constraint: Grafana public dashboards vs. template variables
Two documented risks in Grafana's [externally shared dashboards](https://grafana.com/docs/grafana/latest/dashboards/dashboard-public/) affect the headline "public demo" path:
1. **Template variables are not supported** in public dashboards. The interactive design below leans on `$project`/`$dataset`/`$session_id` variables, so the same JSON cannot simply be flipped public.
2. **Google BigQuery is listed under "Unconfirmed" data sources** for public dashboards -- Grafana does not guarantee the BigQuery plugin works unauthenticated. **A P0 validation spike is required**: prove a minimal variable-free BigQuery panel renders through a public share link on Grafana Cloud Free *before* building `bqaa-public-demo.json`. If it doesn't, the fallback for the public demo is a scheduled snapshot/export or a screenshot walkthrough -- decide at spike time, and the interactive (authenticated) dashboard is unaffected either way.
Deliverable therefore includes **two dashboard JSONs**:
- `bqaa-dashboard.json` — interactive, variable-driven (project, dataset, table, agent, session, event type, user).
- `bqaa-public-demo.json` — variable-free, pinned to a demo dataset seeded via `bqaa seed-events`, no raw-content panels. This is the one users share publicly.
## Proposed deliverables
Location (maintainer call — see open questions): `grafana/` at repo root (sibling of `dashboard/` and `dashboard_v2/`), or `examples/grafana_dashboard/`.
- `README.md` — end-to-end Grafana Cloud Free guide: BigQuery IAM, view setup (`create_all_views`), dashboard import, seed-data option, public-sharing safety.
- `bqaa-dashboard.json` — interactive dashboard (variables above).
- `bqaa-public-demo.json` — variable-free public demo variant.
- `queries/` — checked-in SQL per panel (reviewable, debuggable in BigQuery Studio / Grafana Explore).
- `datasource.example.yaml` — optional provisioning example for self-managed Grafana (Cloud users configure via UI).
## Dashboard requirements
Works against `PROJECT.DATASET.agent_events` directly for header-column panels; uses the `ViewManager` views for model/token/latency panels. No custom backend, no Vercel deployment.
Variables (interactive dashboard): `project`, `dataset`, `table` (default `agent_events`), `agent`, `session_id`, `event_type`, `user_id`, plus BigQuery location if the data source requires it.
Panels:
1. **FinOps summary** — total sessions, total events, LLM calls, prompt/completion/total tokens (from the `LLM_RESPONSE` view, or `JSON_VALUE(content, '$.usage.*')` on `LLM_RESPONSE` rows). **Cost is derived, not stored**: there are no cost fields in the schema, so estimated cost = tokens × a user-set price variable (e.g. `$price_per_1k_input` / `$price_per_1k_output`), clearly labeled as an estimate. Missing usage fields must render as `0`/`N/A`, not break the panel.
2. **Activity over time** — event count by time bucket, split by `event_type` or `agent`; respects the dashboard time range.
3. **Session audit table** — one row per session: `session_id`, `user_id`, first/last event time, event count, distinct agents, error count, tokens (when present). Data-link drills into the trace panel by setting `$session_id`.
4. **Trace/event details** — timeline for `$session_id`: `timestamp`, `event_type`, `agent`, model (`JSON_VALUE(attributes,'$.model')`), `status`, `error_message`, `JSON_VALUE(latency_ms,'$.total_ms')`, `span_id`, `parent_span_id`. Table panel first; node-graph is a follow-up.
5. **Errors and latency** — error count/rate via the SDK's canonical predicate (`event_semantics.py`): `ENDS_WITH(event_type, '_ERROR') OR error_message IS NOT NULL OR UPPER(status) = 'ERROR'` (catches `TOOL_ERROR`, `LLM_ERROR`, and future `*_ERROR` types); top error messages; p50/p95 of `SAFE_CAST(JSON_VALUE(latency_ms,'$.total_ms') AS INT64)` (or `total_ms` from the views).
6. **Agent/tool usage** — counts by `agent`; counts by `event_type`; tool usage from the `TOOL_*` views.
Public/demo dashboard: no raw `content` payloads, no `user_id` values beyond synthetic ones, no variables (see constraint above). README instructs validating with `bqaa seed-events` data before enabling public sharing.
**Public-demo cost guardrails** (Grafana warns that anyone with a public link triggers real datasource queries): fixed narrow time range (no viewer-adjustable range), a tiny dedicated demo table (hundreds of rows via `--sessions`), `maximumBytesBilled` on the data source where supported, the least-privilege service account scoped to the demo dataset only, and README guidance to pause/revoke the public link after the demo.
## Example queries (verified column names)
Session summary (raw table — header columns only):
```sql
SELECT
session_id,
ANY_VALUE(user_id) AS user_id,
MIN(timestamp) AS started_at,
MAX(timestamp) AS last_seen_at,
COUNT(*) AS event_count,
COUNT(DISTINCT agent) AS agent_count,
COUNTIF(
ENDS_WITH(event_type, '_ERROR')
OR error_message IS NOT NULL
OR UPPER(status) = 'ERROR'
) AS error_count
FROM `${project}.${dataset}.${table}`
WHERE $__timeFilter(timestamp)
GROUP BY session_id
ORDER BY last_seen_at DESC
LIMIT 100
```
Events over time (raw table):
```sql
SELECT
TIMESTAMP_TRUNC(timestamp, HOUR) AS time,
event_type,
COUNT(*) AS events
FROM `${project}.${dataset}.${table}`
WHERE $__timeFilter(timestamp)
GROUP BY time, event_type
ORDER BY time
```
Session event timeline (raw table + JSON extraction; simpler via the views):
```sql
SELECT
timestamp,
event_type,
agent,
JSON_VALUE(attributes, '$.model') AS model,
status,
error_message,
SAFE_CAST(JSON_VALUE(latency_ms, '$.total_ms') AS INT64) AS latency_total_ms,
span_id,
parent_span_id
FROM `${project}.${dataset}.${table}`
WHERE $__timeFilter(timestamp)
AND session_id = '$session_id'
ORDER BY timestamp ASC
LIMIT 500
```
Token totals (prefer the `LLM_RESPONSE` view; raw-table equivalent -- note `COALESCE(SUM(SAFE_CAST(...)), 0)` so missing usage fields degrade to `0` instead of erroring or going NULL):
```sql
SELECT
COALESCE(SUM(SAFE_CAST(JSON_VALUE(content, '$.usage.prompt') AS INT64)), 0) AS prompt_tokens,
COALESCE(SUM(SAFE_CAST(JSON_VALUE(content, '$.usage.completion') AS INT64)), 0) AS completion_tokens,
COALESCE(SUM(SAFE_CAST(JSON_VALUE(content, '$.usage.total') AS INT64)), 0) AS total_tokens
FROM `${project}.${dataset}.${table}`
WHERE $__timeFilter(timestamp)
AND event_type = 'LLM_RESPONSE'
```
All queries are time-bounded (`$__timeFilter(timestamp)`), which also prunes cost if the table is date-partitioned on `timestamp` (verify partitioning in the deployed DDL and note it in the README).
## Step-by-step test plan: Grafana Cloud Free
Complete only when a new user can follow these steps from a blank Grafana Cloud Free account.
### 1. Prepare BQAA data in BigQuery
Either run an app instrumented with BQAA, **or seed synthetic data (recommended for first-time setup and required for the public demo):**
```bash
bqaa seed-events --scenario retail-returns --project-id PROJECT --dataset-id DATASET
```
Then confirm rows and recent timestamps exist:
```sql
SELECT COUNT(*) AS row_count, MAX(timestamp) AS latest
FROM `PROJECT.DATASET.agent_events`;
```
Create the typed views (used by the token/latency panels):
```python
from bigquery_agent_analytics.views import ViewManager
ViewManager(project_id="PROJECT", dataset_id="DATASET").create_all_views()
```
### 2. Create a free Grafana Cloud account
1. Sign up for Grafana Cloud Free; use the hosted instance.
2. Confirm the BigQuery data source plugin is available (Grafana Cloud Free includes Enterprise plugin access per Grafana's pricing page — re-verify at implementation time).
### 3. Enable required Google Cloud APIs
BigQuery API and Cloud Resource Manager API (listed as required by the Grafana BigQuery plugin).
### 4. Create least-privilege BigQuery access
Service account, e.g. `bqaa-grafana-viewer@PROJECT.iam.gserviceaccount.com`, with:
- `roles/bigquery.jobUser` on the query project.
- `roles/bigquery.dataViewer` on the BQAA dataset (or narrower table/view grants).
No owner/editor roles. A JSON key is acceptable for a quick test — rotate/delete it after validation; prefer keyless (workload identity federation) for anything long-lived.
### 5. Configure the BigQuery data source in Grafana
Connections → Data sources → Google BigQuery → authenticate with the test service account → save & test → smoke query in Explore:
```sql
SELECT COUNT(*) FROM `PROJECT.DATASET.agent_events`
```
### 6. Import the BQAA dashboard
Dashboards → New → Import → upload `bqaa-dashboard.json` → select the BigQuery data source → set `project`/`dataset`/`table` variables → set a time range that contains events → confirm every panel loads without query errors.
### 7. Validate public sharing with the demo variant
Only with `bqaa seed-events` (or otherwise redacted) data:
1. Import `bqaa-public-demo.json` pointed at the demo dataset.
2. Share → externally shared/public dashboard → anyone with the link.
3. Open in an incognito window and confirm: loads unauthenticated, no raw payloads or real identifiers visible, time range works.
## Acceptance criteria
- Both dashboard JSONs committed (interactive + variable-free public demo).
- README covers the Grafana Cloud Free path end to end, including `bqaa seed-events` and `ViewManager.create_all_views()` setup, least-privilege IAM, and public-sharing safety.
- Works without deploying `dashboard_v2` or any custom backend.
- All queries use verified schema columns (`agent`, JSON extraction for model/tokens/latency) and are bounded by `$__timeFilter(timestamp)`; no unbounded scans in the default dashboard.
- Missing optional JSON fields degrade to `0`/`N/A`, never panel errors.
- Cost panel is clearly labeled as derived (tokens × price variables) since the schema stores no cost.
- The error predicate matches the SDK canonical form (`ENDS_WITH(event_type, '_ERROR') OR error_message IS NOT NULL OR UPPER(status) = 'ERROR'`).
- The P0 public-sharing spike result is recorded on this issue (BigQuery panel renders unauthenticated: yes/no, and the chosen fallback if no).
- The public demo ships with the cost guardrails above (fixed time range, tiny demo table, scoped service account, pause-after-demo guidance).
- A maintainer or contributor validates the full flow on a Grafana Cloud Free instance, including the incognito public-link check on seeded data.
## Suggested phasing
- **P0**: interactive dashboard with activity-over-time, session audit table, errors/latency panels (header columns only — no views dependency), README with IAM + import steps, **and the public-sharing validation spike** (unauthenticated BigQuery panel via a public link -- go/no-go for the P2 public demo).
- **P1**: FinOps tokens + derived cost, trace timeline panel, `ViewManager` views integration, `queries/` folder.
- **P2**: variable-free public demo dashboard + seeded demo walkthrough; node-graph trace visualization; optional Grafana catalog publication.
## Alternatives considered (and why Grafana is the primary target)
Other BigQuery-capable dashboard tools, evaluated against this issue's criteria (free, native BQ, public sharing, versionable artifact in the repo):
| Tool | BigQuery | Free | Public sharing | Repo-versionable artifact | Why not primary |
| --- | --- | --- | --- | --- | --- |
| **Looker Studio** | Native (first-party Google) | Fully free | Yes, link sharing with variables/date controls intact | Weak: no JSON export; templates ship as "make a copy" / Linking API URLs | No committable dashboard file; still the lowest-friction path overall -- see below |
| **Metabase** | Official driver | OSS self-hosted | Public links on OSS | Serialization (YAML) is a paid feature; API-scriptable on OSS | Needs hosting; artifact story weaker on the free tier |
| **Apache Superset** | Via `sqlalchemy-bigquery` | Fully OSS | Yes, with deliberate config (guest tokens) | Yes: dashboard export/import ZIP | Heaviest to self-host -- opposite of the low-friction goal |
| **Redash** | Native connector | OSS self-hosted | Public dashboard URLs | Partial (API export) | Project effectively in maintenance mode |
| **Evidence.dev / Streamlit** | Connectors / Python client | OSS + free hosting tiers | Static builds / public apps | Excellent (dashboards are code) | Reintroduces "build and host an app," which is what `dashboard_v2` already is |
| **Tableau / Power BI** | Native connectors | Trials only (Tableau Public makes data public) | Limited | Workbook files | Paid; wrong audience for an OSS SDK repo |
Grafana stays primary because it matches the observability-native audience, ships a committable `dashboard.json`, and has `$__timeFilter`-style time-bounded querying built in.
**Looker Studio as a zero-infrastructure companion path (optional, cheap).** Looker Studio is free, first-party Google, requires no hosting and no plugin setup, and its public link sharing keeps date controls and filters working -- exactly the capability Grafana's public dashboards lack (see the constraint section above). A companion deliverable would be small: a Looker Studio template report over `agent_events` (+ the `ViewManager` views) linked from the README, plus setup steps. It costs a fraction of the Grafana work, has none of the public-sharing spike risk, and preempts the obvious "why not Google's own free tool?" question. Trade-off to record: the template lives as a shared URL, not a reviewable file in the repo, and viewer queries bill the connected BigQuery project (same cost guardrails apply: demo dataset, scoped access).
## Non-goals for the first version
- Replacing `dashboard_v2`.
- Building a custom Grafana plugin.
- Requiring paid Grafana features, a public BigQuery dataset, or write-back to BigQuery.
- Showing raw prompts/model responses in the public default dashboard.
## Open questions
- Location: `grafana/` at repo root (sibling of `dashboard/`, `dashboard_v2/`) vs `examples/grafana_dashboard/`?
- Publish to Grafana's public dashboard catalog later, or keep the JSON repo-only?
- Minimum supported BQAA schema version (the schema above is current `main`; views absorb most drift).
- Ship the Looker Studio companion template (see Alternatives considered) alongside the Grafana P0, or defer it to a separate issue?
*(Previously-open questions now answered above: synthetic seed data exists — `bqaa seed-events`; standardized views exist — `ViewManager` per-event-type views, and the dashboard should prefer them.)*
Contributor guide
Research direction
Start with the Grafana Cloud Free validation spike: test whether a variable-free BigQuery panel works through a public share link. Then read dashboard_v2/, src/bigquery_agent_analytics/seed_events.py, views.py, and event_semantics.py, and use the documented seed-events command and ViewManager setup. Done means reviewed interactive and public dashboard JSONs, panel SQL, README setup, and the public-demo safety checks are included.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- google-cloud, grafana, python, sql
- Domain
- data-visualization, observability
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100