@@ -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"\n Warnings ({ len (self .warnings )} ):" )
0 commit comments