package debugger import ( "context" "fmt" "runtime" "sort" "strings" "sync" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/rs/zerolog" ) type Debugger struct { callerCounts map[string]int activeConns map[*pgx.Conn]string callerMu sync.Mutex lastPrint *time.Time l *zerolog.Logger pool *pgxpool.Pool poolMu sync.Mutex } func NewDebugger(l *zerolog.Logger) *Debugger { return &Debugger{ callerCounts: make(map[string]int), activeConns: make(map[*pgx.Conn]string), l: l, } } func (d *Debugger) Setup(pool *pgxpool.Pool) { d.poolMu.Lock() defer d.poolMu.Unlock() d.pool = pool d.activeConns = make(map[*pgx.Conn]string) } func (d *Debugger) getPool() *pgxpool.Pool { d.poolMu.Lock() defer d.poolMu.Unlock() return d.pool } func (d *Debugger) BeforeAcquire(ctx context.Context, conn *pgx.Conn) bool { // if we don't have a pool set yet, skip if d.getPool() == nil { return true } _, file, line, ok := runtime.Caller(5) caller := "unknown" if ok { if strings.Contains(file, "tx.go") { _, file, line, _ = runtime.Caller(6) } caller = fmt.Sprintf("%s:%d", file, line) } d.callerMu.Lock() d.callerCounts[caller]++ d.activeConns[conn] = caller d.callerMu.Unlock() if d.lastPrint == nil || time.Since(*d.lastPrint) > 120*time.Second { d.printCallerCounts() } if d.pool.Stat().AcquiredConns() == d.pool.Config().MaxConns { d.printActiveCallers() } return true } func (d *Debugger) AfterRelease(conn *pgx.Conn) bool { if d.getPool() == nil { return true } d.callerMu.Lock() delete(d.activeConns, conn) d.callerMu.Unlock() return true } type callerCount struct { caller string count int } func (d *Debugger) printCallerCounts() { d.callerMu.Lock() defer d.callerMu.Unlock() var counts []callerCount for caller, count := range d.callerCounts { counts = append(counts, callerCount{caller, count}) } sort.Slice(counts, func(i, j int) bool { return counts[i].count > counts[j].count }) sl := d.l.Warn() for i, c := range counts { // print only the top 20 callers if i >= 20 { break } sl.Int( c.caller, c.count, ) } sl.Msg("top 20 callers") d.callerCounts = make(map[string]int) now := time.Now() d.lastPrint = &now } func (d *Debugger) printActiveCallers() { // print the active callers, grouped by caller d.callerMu.Lock() defer d.callerMu.Unlock() callerMap := make(map[string]int) for _, caller := range d.activeConns { callerMap[caller]++ } sl := d.l.Warn() for caller, count := range callerMap { sl.Int( caller, count, ) } sl.Msg("hit max database connections, showing active callers") }