github.com/netdata/go.d.plugin@v0.58.1/modules/pgbouncer/collect.go (about) 1 // SPDX-License-Identifier: GPL-3.0-or-later 2 3 package pgbouncer 4 5 import ( 6 "context" 7 "database/sql" 8 "fmt" 9 "regexp" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/blang/semver/v4" 15 "github.com/jackc/pgx/v4" 16 "github.com/jackc/pgx/v4/stdlib" 17 ) 18 19 // 'SHOW STATS;' response was changed significantly in v1.8.0 20 // v1.8.0 was released in 2015 - no need to complicate the code to support the old version. 21 var minSupportedVersion = semver.Version{Major: 1, Minor: 8, Patch: 0} 22 23 const ( 24 queryShowVersion = "SHOW VERSION;" 25 queryShowConfig = "SHOW CONFIG;" 26 queryShowDatabases = "SHOW DATABASES;" 27 queryShowStats = "SHOW STATS;" 28 queryShowPools = "SHOW POOLS;" 29 ) 30 31 func (p *PgBouncer) collect() (map[string]int64, error) { 32 if p.db == nil { 33 if err := p.openConnection(); err != nil { 34 return nil, err 35 } 36 } 37 if p.version == nil { 38 ver, err := p.queryVersion() 39 if err != nil { 40 return nil, err 41 } 42 p.Debugf("connected to PgBouncer v%s", ver) 43 if ver.LE(minSupportedVersion) { 44 return nil, fmt.Errorf("unsupported version: v%s, required v%s+", ver, minSupportedVersion) 45 } 46 p.version = ver 47 } 48 49 now := time.Now() 50 if now.Sub(p.recheckSettingsTime) > p.recheckSettingsEvery { 51 v, err := p.queryMaxClientConn() 52 if err != nil { 53 return nil, err 54 } 55 p.maxClientConn = v 56 } 57 58 // http://www.pgbouncer.org/usage.html 59 60 p.resetMetrics() 61 62 if err := p.collectDatabases(); err != nil { 63 return nil, err 64 } 65 if err := p.collectStats(); err != nil { 66 return nil, err 67 } 68 if err := p.collectPools(); err != nil { 69 return nil, err 70 } 71 72 mx := make(map[string]int64) 73 p.collectMetrics(mx) 74 75 return mx, nil 76 } 77 78 func (p *PgBouncer) collectMetrics(mx map[string]int64) { 79 var clientConns int64 80 for name, db := range p.metrics.dbs { 81 if !db.updated { 82 delete(p.metrics.dbs, name) 83 p.removeDatabaseCharts(name) 84 continue 85 } 86 if !db.hasCharts { 87 db.hasCharts = true 88 p.addNewDatabaseCharts(name, db.pgDBName) 89 } 90 91 mx["db_"+name+"_total_xact_count"] = db.totalXactCount 92 mx["db_"+name+"_total_xact_time"] = db.totalXactTime 93 mx["db_"+name+"_avg_xact_time"] = db.avgXactTime 94 95 mx["db_"+name+"_total_query_count"] = db.totalQueryCount 96 mx["db_"+name+"_total_query_time"] = db.totalQueryTime 97 mx["db_"+name+"_avg_query_time"] = db.avgQueryTime 98 99 mx["db_"+name+"_total_wait_time"] = db.totalWaitTime 100 mx["db_"+name+"_maxwait"] = db.maxWait*1e6 + db.maxWaitUS 101 102 mx["db_"+name+"_cl_active"] = db.clActive 103 mx["db_"+name+"_cl_waiting"] = db.clWaiting 104 mx["db_"+name+"_cl_cancel_req"] = db.clCancelReq 105 clientConns += db.clActive + db.clWaiting + db.clCancelReq 106 107 mx["db_"+name+"_sv_active"] = db.svActive 108 mx["db_"+name+"_sv_idle"] = db.svIdle 109 mx["db_"+name+"_sv_used"] = db.svUsed 110 mx["db_"+name+"_sv_tested"] = db.svTested 111 mx["db_"+name+"_sv_login"] = db.svLogin 112 113 mx["db_"+name+"_total_received"] = db.totalReceived 114 mx["db_"+name+"_total_sent"] = db.totalSent 115 116 mx["db_"+name+"_sv_conns_utilization"] = calcPercentage(db.currentConnections, db.maxConnections) 117 } 118 119 mx["cl_conns_utilization"] = calcPercentage(clientConns, p.maxClientConn) 120 } 121 122 func (p *PgBouncer) collectDatabases() error { 123 q := queryShowDatabases 124 p.Debugf("executing query: %v", q) 125 126 var db string 127 return p.collectQuery(q, func(column, value string) { 128 switch column { 129 case "name": 130 db = value 131 p.getDBMetrics(db).updated = true 132 case "database": 133 p.getDBMetrics(db).pgDBName = value 134 case "max_connections": 135 p.getDBMetrics(db).maxConnections = parseInt(value) 136 case "current_connections": 137 p.getDBMetrics(db).currentConnections = parseInt(value) 138 case "paused": 139 p.getDBMetrics(db).paused = parseInt(value) 140 case "disabled": 141 p.getDBMetrics(db).disabled = parseInt(value) 142 } 143 }) 144 } 145 146 func (p *PgBouncer) collectStats() error { 147 q := queryShowStats 148 p.Debugf("executing query: %v", q) 149 150 var db string 151 return p.collectQuery(q, func(column, value string) { 152 switch column { 153 case "database": 154 db = value 155 p.getDBMetrics(db).updated = true 156 case "total_xact_count": 157 p.getDBMetrics(db).totalXactCount = parseInt(value) 158 case "total_query_count": 159 p.getDBMetrics(db).totalQueryCount = parseInt(value) 160 case "total_received": 161 p.getDBMetrics(db).totalReceived = parseInt(value) 162 case "total_sent": 163 p.getDBMetrics(db).totalSent = parseInt(value) 164 case "total_xact_time": 165 p.getDBMetrics(db).totalXactTime = parseInt(value) 166 case "total_query_time": 167 p.getDBMetrics(db).totalQueryTime = parseInt(value) 168 case "total_wait_time": 169 p.getDBMetrics(db).totalWaitTime = parseInt(value) 170 case "avg_xact_time": 171 p.getDBMetrics(db).avgXactTime = parseInt(value) 172 case "avg_query_time": 173 p.getDBMetrics(db).avgQueryTime = parseInt(value) 174 } 175 }) 176 } 177 178 func (p *PgBouncer) collectPools() error { 179 q := queryShowPools 180 p.Debugf("executing query: %v", q) 181 182 // an entry is made for each couple of (database, user). 183 var db string 184 return p.collectQuery(q, func(column, value string) { 185 switch column { 186 case "database": 187 db = value 188 p.getDBMetrics(db).updated = true 189 case "cl_active": 190 p.getDBMetrics(db).clActive += parseInt(value) 191 case "cl_waiting": 192 p.getDBMetrics(db).clWaiting += parseInt(value) 193 case "cl_cancel_req": 194 p.getDBMetrics(db).clCancelReq += parseInt(value) 195 case "sv_active": 196 p.getDBMetrics(db).svActive += parseInt(value) 197 case "sv_idle": 198 p.getDBMetrics(db).svIdle += parseInt(value) 199 case "sv_used": 200 p.getDBMetrics(db).svUsed += parseInt(value) 201 case "sv_tested": 202 p.getDBMetrics(db).svTested += parseInt(value) 203 case "sv_login": 204 p.getDBMetrics(db).svLogin += parseInt(value) 205 case "maxwait": 206 p.getDBMetrics(db).maxWait += parseInt(value) 207 case "maxwait_us": 208 p.getDBMetrics(db).maxWaitUS += parseInt(value) 209 } 210 }) 211 } 212 213 func (p *PgBouncer) queryMaxClientConn() (int64, error) { 214 q := queryShowConfig 215 p.Debugf("executing query: %v", q) 216 217 var v int64 218 var key string 219 err := p.collectQuery(q, func(column, value string) { 220 switch column { 221 case "key": 222 key = value 223 case "value": 224 if key == "max_client_conn" { 225 v = parseInt(value) 226 } 227 } 228 }) 229 return v, err 230 } 231 232 var reVersion = regexp.MustCompile(`\d+\.\d+\.\d+`) 233 234 func (p *PgBouncer) queryVersion() (*semver.Version, error) { 235 q := queryShowVersion 236 p.Debugf("executing query: %v", q) 237 238 var resp string 239 ctx, cancel := context.WithTimeout(context.Background(), p.Timeout.Duration) 240 defer cancel() 241 if err := p.db.QueryRowContext(ctx, q).Scan(&resp); err != nil { 242 return nil, err 243 } 244 245 if !strings.Contains(resp, "PgBouncer") { 246 return nil, fmt.Errorf("not PgBouncer instance: version response: %s", resp) 247 } 248 249 ver := reVersion.FindString(resp) 250 if ver == "" { 251 return nil, fmt.Errorf("couldn't parse version string '%s' (expected pattern '%s')", resp, reVersion) 252 } 253 254 v, err := semver.New(ver) 255 if err != nil { 256 return nil, fmt.Errorf("couldn't parse version string '%s': %v", ver, err) 257 } 258 259 return v, nil 260 } 261 262 func (p *PgBouncer) openConnection() error { 263 cfg, err := pgx.ParseConfig(p.DSN) 264 if err != nil { 265 return err 266 } 267 cfg.PreferSimpleProtocol = true 268 269 db, err := sql.Open("pgx", stdlib.RegisterConnConfig(cfg)) 270 if err != nil { 271 return fmt.Errorf("error on opening a connection with the PgBouncer database [%s]: %v", p.DSN, err) 272 } 273 274 db.SetMaxOpenConns(1) 275 db.SetMaxIdleConns(1) 276 db.SetConnMaxLifetime(10 * time.Minute) 277 278 p.db = db 279 280 return nil 281 } 282 283 func (p *PgBouncer) collectQuery(query string, assign func(column, value string)) error { 284 ctx, cancel := context.WithTimeout(context.Background(), p.Timeout.Duration) 285 defer cancel() 286 rows, err := p.db.QueryContext(ctx, query) 287 if err != nil { 288 return err 289 } 290 defer func() { _ = rows.Close() }() 291 292 columns, err := rows.Columns() 293 if err != nil { 294 return err 295 } 296 297 values := makeNullStrings(len(columns)) 298 for rows.Next() { 299 if err := rows.Scan(values...); err != nil { 300 return err 301 } 302 for i, v := range values { 303 assign(columns[i], valueToString(v)) 304 } 305 } 306 return rows.Err() 307 } 308 309 func (p *PgBouncer) getDBMetrics(dbname string) *dbMetrics { 310 db, ok := p.metrics.dbs[dbname] 311 if !ok { 312 db = &dbMetrics{name: dbname} 313 p.metrics.dbs[dbname] = db 314 } 315 return db 316 } 317 318 func (p *PgBouncer) resetMetrics() { 319 for name, db := range p.metrics.dbs { 320 p.metrics.dbs[name] = &dbMetrics{ 321 name: db.name, 322 pgDBName: db.pgDBName, 323 hasCharts: db.hasCharts, 324 } 325 } 326 } 327 328 func valueToString(value any) string { 329 v, ok := value.(*sql.NullString) 330 if !ok || !v.Valid { 331 return "" 332 } 333 return v.String 334 } 335 336 func makeNullStrings(size int) []any { 337 vs := make([]any, size) 338 for i := range vs { 339 vs[i] = &sql.NullString{} 340 } 341 return vs 342 } 343 344 func parseInt(s string) int64 { 345 v, _ := strconv.ParseInt(s, 10, 64) 346 return v 347 } 348 349 func calcPercentage(value, total int64) int64 { 350 if total == 0 { 351 return 0 352 } 353 return value * 100 / total 354 }