1414import argparse
1515import sys
1616import os
17+ import psycopg2
18+ import psycopg2 .extras
1719
1820
1921class PostgresReportGenerator :
20- def __init__ (self , prometheus_url : str = "http://localhost:9090" ):
22+ def __init__ (self , prometheus_url : str = "http://sink-prometheus:9090" ,
23+ postgres_sink_url : str = "postgresql://pgwatch:pgwatchadmin@sink-postgres:5432/measurements" ):
2124 """
2225 Initialize the PostgreSQL report generator.
2326
2427 Args:
25- prometheus_url: URL of the Prometheus instance
28+ prometheus_url: URL of the Prometheus instance (default: http://sink-prometheus:9090)
29+ postgres_sink_url: Connection string for the Postgres sink database
30+ (default: postgresql://pgwatch:pgwatchadmin@sink-postgres:5432/measurements)
2631 """
2732 self .prometheus_url = prometheus_url
2833 self .base_url = f"{ prometheus_url } /api/v1"
34+ self .postgres_sink_url = postgres_sink_url
35+ self .pg_conn = None
2936
3037 def test_connection (self ) -> bool :
3138 """Test connection to Prometheus."""
@@ -36,6 +43,59 @@ def test_connection(self) -> bool:
3643 print (f"Connection failed: { e } " )
3744 return False
3845
46+ def connect_postgres_sink (self ) -> bool :
47+ """Connect to Postgres sink database."""
48+ if not self .postgres_sink_url :
49+ return False
50+
51+ try :
52+ self .pg_conn = psycopg2 .connect (self .postgres_sink_url )
53+ return True
54+ except Exception as e :
55+ print (f"Postgres sink connection failed: { e } " )
56+ return False
57+
58+ def close_postgres_sink (self ):
59+ """Close Postgres sink connection."""
60+ if self .pg_conn :
61+ self .pg_conn .close ()
62+ self .pg_conn = None
63+
64+ def get_index_definitions_from_sink (self ) -> Dict [str , str ]:
65+ """
66+ Get index definitions from the Postgres sink database.
67+
68+ Returns:
69+ Dictionary mapping index names to their definitions
70+ """
71+ if not self .pg_conn :
72+ if not self .connect_postgres_sink ():
73+ return {}
74+
75+ index_definitions = {}
76+
77+ try :
78+ with self .pg_conn .cursor (cursor_factory = psycopg2 .extras .DictCursor ) as cursor :
79+ # Query the index_definitions table for the most recent data
80+ query = """
81+ SELECT DISTINCT ON (data->>'indexname')
82+ data->>'indexname' as indexname,
83+ data->>'index_definition' as index_definition
84+ FROM public.index_definitions
85+ WHERE data ? 'indexname' AND data ? 'index_definition'
86+ ORDER BY data->>'indexname', time DESC
87+ """
88+ cursor .execute (query )
89+
90+ for row in cursor .fetchall ():
91+ if row ['indexname' ]:
92+ index_definitions [row ['indexname' ]] = row ['index_definition' ]
93+
94+ except Exception as e :
95+ print (f"Error fetching index definitions from Postgres sink: { e } " )
96+
97+ return index_definitions
98+
3999 def query_instant (self , query : str ) -> Dict [str , Any ]:
40100 """
41101 Execute an instant PromQL query.
@@ -315,6 +375,21 @@ def generate_h002_unused_indexes_report(self, cluster: str = "local", node_name:
315375 # Get all databases
316376 databases = self .get_all_databases (cluster , node_name )
317377
378+ # Query postmaster uptime to get startup time
379+ postmaster_uptime_query = f'last_over_time(pgwatch_db_stats_postmaster_uptime_s{{cluster="{ cluster } ", node_name="{ node_name } "}}[10h])'
380+ postmaster_uptime_result = self .query_instant (postmaster_uptime_query )
381+
382+ postmaster_startup_time = None
383+ postmaster_startup_epoch = None
384+ if postmaster_uptime_result .get ('status' ) == 'success' and postmaster_uptime_result .get ('data' , {}).get ('result' ):
385+ uptime_seconds = float (postmaster_uptime_result ['data' ]['result' ][0 ]['value' ][1 ]) if postmaster_uptime_result ['data' ]['result' ] else None
386+ if uptime_seconds :
387+ postmaster_startup_epoch = datetime .now ().timestamp () - uptime_seconds
388+ postmaster_startup_time = datetime .fromtimestamp (postmaster_startup_epoch ).isoformat ()
389+
390+ # Get index definitions from Postgres sink database
391+ index_definitions = self .get_index_definitions_from_sink ()
392+
318393 unused_indexes_by_db = {}
319394 for db_name in databases :
320395 # Query stats_reset timestamp for this database
@@ -353,10 +428,14 @@ def generate_h002_unused_indexes_report(self, cluster: str = "local", node_name:
353428 {}).get (
354429 'result' ) else 0
355430
431+ # Get index definition from collected metrics
432+ index_definition = index_definitions .get (index_name , 'Definition not available' )
433+
356434 index_data = {
357435 "schema_name" : schema_name ,
358436 "table_name" : table_name ,
359437 "index_name" : index_name ,
438+ "index_definition" : index_definition ,
360439 "reason" : reason ,
361440 "idx_scan" : idx_scan ,
362441 "index_size_bytes" : index_size_bytes ,
@@ -381,7 +460,9 @@ def generate_h002_unused_indexes_report(self, cluster: str = "local", node_name:
381460 "stats_reset" : {
382461 "stats_reset_epoch" : stats_reset_epoch ,
383462 "stats_reset_time" : stats_reset_time ,
384- "days_since_reset" : days_since_reset
463+ "days_since_reset" : days_since_reset ,
464+ "postmaster_startup_epoch" : postmaster_startup_epoch ,
465+ "postmaster_startup_time" : postmaster_startup_time
385466 }
386467 }
387468
@@ -1764,8 +1845,10 @@ def make_request(api_url, endpoint, request_data):
17641845
17651846def main ():
17661847 parser = argparse .ArgumentParser (description = 'Generate PostgreSQL reports using PromQL' )
1767- parser .add_argument ('--prometheus-url' , default = 'http://localhost:9090' ,
1768- help = 'Prometheus URL (default: http://localhost:9090)' )
1848+ parser .add_argument ('--prometheus-url' , default = 'http://sink-prometheus:9090' ,
1849+ help = 'Prometheus URL (default: http://sink-prometheus:9090 for Docker, use http://localhost:59090 for external access)' )
1850+ parser .add_argument ('--postgres-sink-url' , default = 'postgresql://pgwatch:pgwatchadmin@sink-postgres:5432/measurements' ,
1851+ help = 'Postgres sink connection string (default: postgresql://pgwatch:pgwatchadmin@sink-postgres:5432/measurements for Docker, use postgresql://pgwatch:pgwatchadmin@localhost:55433/measurements for external access)' )
17691852 parser .add_argument ('--cluster' , default = 'local' ,
17701853 help = 'Cluster name (default: local)' )
17711854 parser .add_argument ('--node-name' , default = 'node-01' ,
@@ -1785,7 +1868,7 @@ def main():
17851868
17861869 args = parser .parse_args ()
17871870
1788- generator = PostgresReportGenerator (args .prometheus_url )
1871+ generator = PostgresReportGenerator (args .prometheus_url , args . postgres_sink_url )
17891872
17901873 # Test connection
17911874 if not generator .test_connection ():
@@ -1852,6 +1935,9 @@ def main():
18521935 print (f"Error generating reports: { e } " )
18531936 raise e
18541937 sys .exit (1 )
1938+ finally :
1939+ # Clean up postgres connection
1940+ generator .close_postgres_sink ()
18551941
18561942
18571943if __name__ == "__main__" :
0 commit comments