Skip to content

Commit 91423e0

Browse files
committed
test(grafana): verify dashboard panels display non-zero data
Add verify_dashboard_panels_data() that: - Executes actual dashboard panel queries through Grafana API - Validates data is returned (not empty) - Checks values meet minimum thresholds (e.g., DB size > 1MB) - Verifies transaction commits, tuples returned/inserted are non-zero This ensures dashboards would actually visualize real data, not just that metrics exist in Prometheus.
1 parent efaf3fa commit 91423e0

File tree

1 file changed

+171
-35
lines changed

1 file changed

+171
-35
lines changed

tests/grafana/test_grafana_metrics.py

Lines changed: 171 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -355,35 +355,27 @@ def verify_grafana_dashboards(self) -> bool:
355355
self.errors.append(f"Grafana dashboards check failed: {e}")
356356
return False
357357

358-
def verify_dashboard_query(self) -> bool:
359-
"""Test that a dashboard panel query returns data via Grafana."""
360-
self.log("Testing dashboard query execution via Grafana...")
361-
362-
# Query the prometheus datasource through Grafana's proxy
358+
def get_prometheus_datasource_uid(self) -> str | None:
359+
"""Get the UID of the Prometheus datasource."""
363360
try:
364-
# First, get the prometheus datasource ID
365361
response = requests.get(
366362
f"{self.grafana_url}/api/datasources",
367363
auth=self.grafana_auth,
368364
timeout=10,
369365
)
370366
response.raise_for_status()
371-
datasources = response.json()
372-
373-
prometheus_ds = None
374-
for ds in datasources:
367+
for ds in response.json():
375368
if ds.get("type") == "prometheus":
376-
prometheus_ds = ds
377-
break
378-
379-
if not prometheus_ds:
380-
self.log("No Prometheus datasource for query test", "warn")
381-
return True # Not a hard failure
382-
383-
ds_uid = prometheus_ds.get("uid")
384-
385-
# Query through Grafana's datasource proxy
386-
query = f'pgwatch_db_size_size_b{{datname="{self.test_dbname}"}}'
369+
return ds.get("uid")
370+
except Exception:
371+
pass
372+
return None
373+
374+
def execute_grafana_query(
375+
self, expr: str, ds_uid: str, instant: bool = True
376+
) -> list[dict] | None:
377+
"""Execute a PromQL query through Grafana and return results."""
378+
try:
387379
response = requests.post(
388380
f"{self.grafana_url}/api/ds/query",
389381
auth=self.grafana_auth,
@@ -392,8 +384,11 @@ def verify_dashboard_query(self) -> bool:
392384
{
393385
"refId": "A",
394386
"datasource": {"uid": ds_uid, "type": "prometheus"},
395-
"expr": query,
396-
"instant": True,
387+
"expr": expr,
388+
"instant": instant,
389+
"range": not instant,
390+
"intervalMs": 15000,
391+
"maxDataPoints": 100,
397392
}
398393
],
399394
"from": "now-5m",
@@ -403,22 +398,159 @@ def verify_dashboard_query(self) -> bool:
403398
)
404399
response.raise_for_status()
405400
result = response.json()
406-
407-
# Check if we got results
408401
frames = result.get("results", {}).get("A", {}).get("frames", [])
409-
if frames:
410-
self.log("Grafana datasource proxy query returned data", "ok")
411-
return True
402+
return frames
403+
except Exception as e:
404+
self.log(f"Query execution failed: {e}", "warn")
405+
return None
406+
407+
def extract_numeric_values_from_frames(self, frames: list[dict]) -> list[float]:
408+
"""Extract numeric values from Grafana response frames."""
409+
values = []
410+
for frame in frames:
411+
schema = frame.get("schema", {})
412+
data = frame.get("data", {})
413+
414+
# Get field values
415+
field_values = data.get("values", [])
416+
fields = schema.get("fields", [])
417+
418+
for i, field in enumerate(fields):
419+
field_type = field.get("type", "")
420+
if field_type == "number" and i < len(field_values):
421+
for v in field_values[i]:
422+
if v is not None:
423+
try:
424+
values.append(float(v))
425+
except (ValueError, TypeError):
426+
pass
427+
return values
428+
429+
def verify_dashboard_panels_data(self) -> bool:
430+
"""
431+
Verify actual dashboard panels return non-empty, non-zero data.
432+
433+
This fetches real dashboard panel queries and executes them through
434+
Grafana to ensure the visualizations would show actual data.
435+
"""
436+
self.log("Verifying dashboard panels display real data...")
437+
438+
ds_uid = self.get_prometheus_datasource_uid()
439+
if not ds_uid:
440+
self.log("No Prometheus datasource found", "error")
441+
self.errors.append("Cannot verify panels: no Prometheus datasource")
442+
return False
443+
444+
# Define panel queries that should return non-zero data
445+
# These are actual queries used in the dashboards with variables substituted
446+
panel_queries = [
447+
{
448+
"name": "Database Size",
449+
"expr": f'pgwatch_db_size_size_b{{cluster="{self.cluster_name}", node_name="{self.node_name}", datname="{self.test_dbname}"}}',
450+
"min_value": 1_000_000, # At least 1MB
451+
"description": "Database size in bytes",
452+
},
453+
{
454+
"name": "Transaction Commits",
455+
"expr": f'pgwatch_db_stats_xact_commit{{cluster="{self.cluster_name}", node_name="{self.node_name}", datname="{self.test_dbname}"}}',
456+
"min_value": 1, # At least 1 commit
457+
"description": "Total transaction commits",
458+
},
459+
{
460+
"name": "Tuples Returned",
461+
"expr": f'pgwatch_db_stats_tup_returned{{cluster="{self.cluster_name}", node_name="{self.node_name}", datname="{self.test_dbname}"}}',
462+
"min_value": 1, # At least 1 tuple
463+
"description": "Total tuples returned by queries",
464+
},
465+
{
466+
"name": "Tuples Inserted",
467+
"expr": f'pgwatch_db_stats_tup_inserted{{cluster="{self.cluster_name}", node_name="{self.node_name}", datname="{self.test_dbname}"}}',
468+
"min_value": 1, # At least 1 insert
469+
"description": "Total tuples inserted",
470+
},
471+
]
472+
473+
panels_ok = 0
474+
panels_failed = 0
475+
476+
for panel in panel_queries:
477+
name = panel["name"]
478+
expr = panel["expr"]
479+
min_value = panel["min_value"]
480+
481+
frames = self.execute_grafana_query(expr, ds_uid, instant=True)
482+
483+
if frames is None:
484+
self.log(f"Panel '{name}': query failed", "error")
485+
panels_failed += 1
486+
continue
487+
488+
if not frames:
489+
self.log(f"Panel '{name}': no data returned (empty)", "error")
490+
self.errors.append(f"Panel '{name}' returned no data")
491+
panels_failed += 1
492+
continue
493+
494+
# Extract numeric values
495+
values = self.extract_numeric_values_from_frames(frames)
496+
497+
if not values:
498+
self.log(f"Panel '{name}': no numeric values found", "error")
499+
self.errors.append(f"Panel '{name}' has no numeric values")
500+
panels_failed += 1
501+
continue
502+
503+
max_value = max(values)
504+
if max_value < min_value:
505+
self.log(
506+
f"Panel '{name}': value {max_value} below minimum {min_value}",
507+
"error",
508+
)
509+
self.errors.append(
510+
f"Panel '{name}' value too low: {max_value} < {min_value}"
511+
)
512+
panels_failed += 1
513+
continue
514+
515+
# Format value for display
516+
if max_value >= 1_000_000:
517+
display_value = f"{max_value / 1_000_000:.2f} MB"
518+
elif max_value >= 1_000:
519+
display_value = f"{max_value / 1_000:.1f} K"
412520
else:
413-
self.log("Grafana query returned no data", "warn")
414-
self.warnings.append("Grafana proxy query returned empty results")
415-
return True
521+
display_value = f"{max_value:.0f}"
416522

417-
except Exception as e:
418-
self.log(f"Dashboard query test failed: {e}", "warn")
419-
self.warnings.append(f"Dashboard query test failed: {e}")
523+
self.log(f"Panel '{name}': {display_value}", "ok")
524+
panels_ok += 1
525+
526+
self.log(f"Panel verification: {panels_ok} OK, {panels_failed} failed")
527+
528+
if panels_failed > 0:
529+
return False
530+
531+
return True
532+
533+
def verify_dashboard_query(self) -> bool:
534+
"""Test that a dashboard panel query returns data via Grafana."""
535+
self.log("Testing dashboard query execution via Grafana...")
536+
537+
ds_uid = self.get_prometheus_datasource_uid()
538+
if not ds_uid:
539+
self.log("No Prometheus datasource for query test", "warn")
420540
return True # Not a hard failure
421541

542+
# Query through Grafana's datasource proxy
543+
query = f'pgwatch_db_size_size_b{{datname="{self.test_dbname}"}}'
544+
frames = self.execute_grafana_query(query, ds_uid, instant=True)
545+
546+
if frames:
547+
self.log("Grafana datasource proxy query returned data", "ok")
548+
return True
549+
else:
550+
self.log("Grafana query returned no data", "warn")
551+
self.warnings.append("Grafana proxy query returned empty results")
552+
return True
553+
422554
def cleanup(self):
423555
"""Clean up test resources."""
424556
self.log("Cleaning up...")
@@ -463,12 +595,16 @@ def run(self) -> bool:
463595
dash_ok = self.verify_grafana_dashboards()
464596
query_ok = self.verify_dashboard_query()
465597

598+
# Verify dashboard panels show real data (not zeros/empty)
599+
print("\n--- Dashboard Panel Data Verification ---")
600+
panels_ok = self.verify_dashboard_panels_data()
601+
466602
# Summary
467603
print("\n" + "=" * 60)
468604
print("Test Summary")
469605
print("=" * 60)
470606

471-
all_passed = all([db_size_ok, tx_ok, tuple_ok, ds_ok, dash_ok, query_ok])
607+
all_passed = all([db_size_ok, tx_ok, tuple_ok, ds_ok, dash_ok, query_ok, panels_ok])
472608

473609
if self.warnings:
474610
print(f"\nWarnings ({len(self.warnings)}):")

0 commit comments

Comments
 (0)