package postgres import ( "context" "database/sql" "time" _ "github.com/lib/pq" "bugs.watch/agent/internal/aggregate" ) // Collector queries PostgreSQL status each sample interval. // All queries run under a 3-second timeout; any failure is silently skipped. type Collector struct { db *sql.DB } func New(dsn string) (*Collector, error) { db, err := sql.Open("postgres", dsn) if err != nil { return nil, err } db.SetMaxOpenConns(2) db.SetMaxIdleConns(1) db.SetConnMaxLifetime(5 * time.Minute) return &Collector{db: db}, nil } func (c *Collector) Name() string { return "postgres" } func (c *Collector) Close() { c.db.Close() } func (c *Collector) Collect(window *aggregate.Window) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() // pg_stat_database is readable by any user for their own database. var numBackends, maxConn int err := c.db.QueryRowContext(ctx, `SELECT d.numbackends, s.setting::int FROM pg_stat_database d, pg_settings s WHERE d.datname = current_database() AND s.name = 'max_connections'`, ).Scan(&numBackends, &maxConn) if err == nil && maxConn > 0 { window.AddDBConnPct(float64(numBackends) / float64(maxConn) * 100) } // pg_stat_activity requires pg_monitor role (PG 10+); silently skip on error. var activeQueries int if err := c.db.QueryRowContext(ctx, `SELECT count(*) FROM pg_stat_activity WHERE state = 'active'`, ).Scan(&activeQueries); err == nil { window.AddDBThreadsRunning(float64(activeQueries)) } var sizeBytes int64 if err := c.db.QueryRowContext(ctx, `SELECT pg_database_size(current_database())`, ).Scan(&sizeBytes); err == nil { window.SetDBSizeBytes(uint64(sizeBytes)) } }