github.com/mackerelio/mackerel-agent-plugins@v0.89.3/mackerel-plugin-postgres/lib/postgres.go (about) 1 package mppostgres 2 3 import ( 4 "errors" 5 "flag" 6 "fmt" 7 "os" 8 "regexp" 9 "strconv" 10 "strings" 11 12 "github.com/jmoiron/sqlx" 13 "golang.org/x/text/cases" 14 "golang.org/x/text/language" 15 // PostgreSQL Driver 16 _ "github.com/lib/pq" 17 mp "github.com/mackerelio/go-mackerel-plugin-helper" 18 "github.com/mackerelio/golib/logging" 19 ) 20 21 var logger = logging.GetLogger("metrics.plugin.postgres") 22 23 // PostgresPlugin mackerel plugin for PostgreSQL 24 type PostgresPlugin struct { 25 Host string 26 Port string 27 Username string 28 Password string 29 SSLmode string 30 Prefix string 31 Timeout int 32 Tempfile string 33 Option string 34 } 35 36 func fetchStatDatabase(db *sqlx.DB) (map[string]interface{}, error) { 37 db = db.Unsafe() 38 rows, err := db.Queryx(`SELECT * FROM pg_stat_database`) 39 if err != nil { 40 logger.Errorf("Failed to select pg_stat_database. %s", err) 41 return nil, err 42 } 43 44 type pgStat struct { 45 XactCommit uint64 `db:"xact_commit"` 46 XactRollback uint64 `db:"xact_rollback"` 47 BlksRead uint64 `db:"blks_read"` 48 BlksHit uint64 `db:"blks_hit"` 49 BlkReadTime *float64 `db:"blk_read_time"` 50 BlkWriteTime *float64 `db:"blk_write_time"` 51 TupReturned uint64 `db:"tup_returned"` 52 TupFetched uint64 `db:"tup_fetched"` 53 TupInserted uint64 `db:"tup_inserted"` 54 TupUpdated uint64 `db:"tup_updated"` 55 TupDeleted uint64 `db:"tup_deleted"` 56 Deadlocks *uint64 `db:"deadlocks"` 57 TempBytes *uint64 `db:"temp_bytes"` 58 } 59 60 totalStat := pgStat{} 61 for rows.Next() { 62 p := pgStat{} 63 if err := rows.StructScan(&p); err != nil { 64 logger.Warningf("Failed to scan. %s", err) 65 continue 66 } 67 totalStat.XactCommit += p.XactCommit 68 totalStat.XactRollback += p.XactRollback 69 totalStat.BlksRead += p.BlksRead 70 totalStat.BlksHit += p.BlksHit 71 if p.BlkReadTime != nil { 72 if totalStat.BlkReadTime == nil { 73 totalStat.BlkReadTime = p.BlkReadTime 74 } else { 75 *totalStat.BlkReadTime += *p.BlkReadTime 76 } 77 } 78 if p.BlkWriteTime != nil { 79 if totalStat.BlkWriteTime == nil { 80 totalStat.BlkWriteTime = p.BlkWriteTime 81 } else { 82 *totalStat.BlkWriteTime += *p.BlkWriteTime 83 } 84 } 85 totalStat.TupReturned += p.TupReturned 86 totalStat.TupFetched += p.TupFetched 87 totalStat.TupInserted += p.TupInserted 88 totalStat.TupUpdated += p.TupUpdated 89 totalStat.TupDeleted += p.TupDeleted 90 if p.Deadlocks != nil { 91 if totalStat.Deadlocks == nil { 92 totalStat.Deadlocks = p.Deadlocks 93 } else { 94 *totalStat.Deadlocks += *p.Deadlocks 95 } 96 } 97 if p.TempBytes != nil { 98 if totalStat.TempBytes == nil { 99 totalStat.TempBytes = p.TempBytes 100 } else { 101 *totalStat.TempBytes += *p.TempBytes 102 } 103 } 104 } 105 stat := make(map[string]interface{}) 106 stat["xact_commit"] = totalStat.XactCommit 107 stat["xact_rollback"] = totalStat.XactRollback 108 stat["blks_read"] = totalStat.BlksRead 109 stat["blks_hit"] = totalStat.BlksHit 110 if totalStat.BlkReadTime != nil { 111 stat["blk_read_time"] = *totalStat.BlkReadTime 112 } 113 if totalStat.BlkWriteTime != nil { 114 stat["blk_write_time"] = *totalStat.BlkWriteTime 115 } 116 stat["tup_returned"] = totalStat.TupReturned 117 stat["tup_fetched"] = totalStat.TupFetched 118 stat["tup_inserted"] = totalStat.TupInserted 119 stat["tup_updated"] = totalStat.TupUpdated 120 stat["tup_deleted"] = totalStat.TupDeleted 121 if totalStat.Deadlocks != nil { 122 stat["deadlocks"] = *totalStat.Deadlocks 123 } 124 if totalStat.TempBytes != nil { 125 stat["temp_bytes"] = *totalStat.TempBytes 126 } 127 return stat, nil 128 } 129 130 func fetchConnections(db *sqlx.DB, version version) (map[string]interface{}, error) { 131 var query string 132 133 if version.first > 9 || version.first == 9 && version.second >= 6 { 134 query = `select count(*), state, wait_event is not null from pg_stat_activity where state is not null group by state, wait_event is not null` 135 } else { 136 query = `select count(*), state, waiting from pg_stat_activity where state is not null group by state, waiting` 137 } 138 rows, err := db.Query(query) 139 if err != nil { 140 logger.Errorf("Failed to select pg_stat_activity. %s", err) 141 return nil, err 142 } 143 144 stat := map[string]interface{}{ 145 "active": 0.0, 146 "active_waiting": 0.0, 147 "idle": 0.0, 148 "idle_in_transaction": 0.0, 149 "idle_in_transaction_aborted": 0.0, 150 } 151 152 normalizeRe := regexp.MustCompile("[^a-zA-Z0-9_-]+") 153 154 for rows.Next() { 155 var count float64 156 var waiting bool 157 var state string 158 if err := rows.Scan(&count, &state, &waiting); err != nil { 159 logger.Warningf("Failed to scan %s", err) 160 continue 161 } 162 state = normalizeRe.ReplaceAllString(state, "_") 163 state = strings.TrimRight(state, "_") 164 if waiting { 165 state += "_waiting" 166 } 167 stat[state] = float64(count) 168 } 169 170 return stat, nil 171 } 172 173 func fetchDatabaseSize(db *sqlx.DB) (map[string]interface{}, error) { 174 rows, err := db.Query("select sum(pg_database_size(datname)) as dbsize from pg_database where has_database_privilege(datname, 'connect')") 175 if err != nil { 176 logger.Errorf("Failed to select pg_database_size. %s", err) 177 return nil, err 178 } 179 180 var totalSize float64 181 for rows.Next() { 182 var dbsize float64 183 if err := rows.Scan(&dbsize); err != nil { 184 logger.Warningf("Failed to scan %s", err) 185 continue 186 } 187 totalSize += dbsize 188 } 189 190 return map[string]interface{}{ 191 "total_size": totalSize, 192 }, nil 193 } 194 195 func fetchXlogLocation(db *sqlx.DB, version version) (map[string]interface{}, error) { 196 var recovery bool 197 { 198 rows, err := db.Query("SELECT pg_is_in_recovery()") 199 if err != nil { 200 logger.Errorf("Failed to select pg_is_in_recovery. %s", err) 201 return nil, err 202 } 203 for rows.Next() { 204 if err := rows.Scan(&recovery); err != nil { 205 logger.Warningf("Failed to scan %s", err) 206 } 207 } 208 } 209 210 var walLevel string 211 { 212 rows, err := db.Query("SELECT setting FROM pg_settings WHERE name = 'wal_level'") 213 if err != nil { 214 logger.Errorf("Failed to show wal_level. %s", err) 215 return nil, err 216 } 217 for rows.Next() { 218 if err := rows.Scan(&walLevel); err != nil { 219 logger.Warningf("Failed to scan %s", err) 220 } 221 } 222 } 223 224 stat := map[string]interface{}{ 225 "xlog_location_bytes": 0.0, 226 } 227 228 if !recovery && walLevel == "logical" { 229 var bytes float64 230 if version.first >= 10 { 231 rows, err := db.Query(`SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0')`) 232 if err != nil { 233 logger.Errorf("Failed to select pg_wal_lsn_diff. %s", err) 234 return nil, err 235 } 236 for rows.Next() { 237 if err := rows.Scan(&bytes); err != nil { 238 logger.Warningf("Failed to scan %s", err) 239 } 240 } 241 } else if version.first == 9 && version.second >= 2 { 242 rows, err := db.Query(`SELECT pg_xlog_location_diff(pg_current_xlog_location(), '0/0')`) 243 if err != nil { 244 logger.Debugf("Failed to select pg_xlog_location_diff. %s", err) 245 // pg_current_xlog_location() is currently not supported for Amazon Aurora 246 return nil, nil 247 } 248 for rows.Next() { 249 if err := rows.Scan(&bytes); err != nil { 250 logger.Warningf("Failed to scan %s", err) 251 } 252 } 253 } 254 stat["xlog_location_bytes"] = bytes 255 } 256 257 return stat, nil 258 } 259 260 var versionRe = regexp.MustCompile(`PostgreSQL (\d+)\.(\d+)(\.(\d+))?`) 261 262 type version struct { 263 first uint 264 second uint 265 third uint 266 } 267 268 func fetchVersion(db *sqlx.DB) (version, error) { 269 270 res := version{} 271 272 rows, err := db.Query("select version()") 273 if err != nil { 274 logger.Errorf("Failed to select version(). %s", err) 275 return res, err 276 } 277 278 for rows.Next() { 279 var versionStr string 280 var first, second, third uint64 281 if err := rows.Scan(&versionStr); err != nil { 282 return res, err 283 } 284 285 // ref. https://www.postgresql.org/support/versioning/ 286 287 submatch := versionRe.FindStringSubmatch(versionStr) 288 if len(submatch) >= 4 { 289 first, err = strconv.ParseUint(submatch[1], 10, 0) 290 if err != nil { 291 return res, err 292 } 293 second, err = strconv.ParseUint(submatch[2], 10, 0) 294 if err != nil { 295 return res, err 296 } 297 if len(submatch) == 5 && submatch[4] != "" { 298 third, err = strconv.ParseUint(submatch[4], 10, 0) 299 if err != nil { 300 return res, err 301 } 302 } 303 res = version{uint(first), uint(second), uint(third)} 304 return res, err 305 } 306 } 307 return res, errors.New("failed to select version()") 308 } 309 310 func mergeStat(dst, src map[string]interface{}) { 311 for k, v := range src { 312 dst[k] = v 313 } 314 } 315 316 // MetricKeyPrefix returns the metrics key prefix 317 func (p PostgresPlugin) MetricKeyPrefix() string { 318 if p.Prefix == "" { 319 p.Prefix = "postgres" 320 } 321 return p.Prefix 322 } 323 324 // FetchMetrics interface for mackerelplugin 325 func (p PostgresPlugin) FetchMetrics() (map[string]interface{}, error) { 326 327 cmd := fmt.Sprintf("user=%s host=%s port=%s sslmode=%s connect_timeout=%d %s", p.Username, p.Host, p.Port, p.SSLmode, p.Timeout, p.Option) 328 if p.Password != "" { 329 cmd = fmt.Sprintf("password=%s %s", p.Password, cmd) 330 } 331 db, err := sqlx.Connect("postgres", cmd) 332 333 if err != nil { 334 logger.Errorf("FetchMetrics: %s", err) 335 return nil, err 336 } 337 defer db.Close() 338 339 version, err := fetchVersion(db) 340 if err != nil { 341 logger.Warningf("FetchMetrics: %s", err) 342 return nil, err 343 } 344 345 statStatDatabase, err := fetchStatDatabase(db) 346 if err != nil { 347 return nil, err 348 } 349 statConnections, err := fetchConnections(db, version) 350 if err != nil { 351 return nil, err 352 } 353 statDatabaseSize, err := fetchDatabaseSize(db) 354 if err != nil { 355 return nil, err 356 } 357 statXlogLocation, err := fetchXlogLocation(db, version) 358 if err != nil { 359 return nil, err 360 } 361 362 stat := make(map[string]interface{}) 363 mergeStat(stat, statStatDatabase) 364 mergeStat(stat, statConnections) 365 mergeStat(stat, statDatabaseSize) 366 mergeStat(stat, statXlogLocation) 367 368 return stat, err 369 } 370 371 // GraphDefinition interface for mackerelplugin 372 func (p PostgresPlugin) GraphDefinition() map[string]mp.Graphs { 373 labelPrefix := cases.Title(language.Und, cases.NoLower).String(p.MetricKeyPrefix()) 374 375 var graphdef = map[string]mp.Graphs{ 376 "connections": { 377 Label: (labelPrefix + " Connections"), 378 Unit: "integer", 379 Metrics: []mp.Metrics{ 380 {Name: "active", Label: "Active", Diff: false, Stacked: true}, 381 {Name: "active_waiting", Label: "Active waiting", Diff: false, Stacked: true}, 382 {Name: "idle", Label: "Idle", Diff: false, Stacked: true}, 383 {Name: "idle_in_transaction", Label: "Idle in transaction", Diff: false, Stacked: true}, 384 {Name: "idle_in_transaction_aborted_", Label: "Idle in transaction (aborted)", Diff: false, Stacked: true}, 385 {Name: "fastpath_function_call", Label: "fast-path function call", Diff: false, Stacked: true}, 386 {Name: "disabled", Label: "Disabled", Diff: false, Stacked: true}, 387 }, 388 }, 389 "commits": { 390 Label: (labelPrefix + " Commits"), 391 Unit: "integer", 392 Metrics: []mp.Metrics{ 393 {Name: "xact_commit", Label: "Xact Commit", Diff: true, Stacked: false}, 394 {Name: "xact_rollback", Label: "Xact Rollback", Diff: true, Stacked: false}, 395 }, 396 }, 397 "blocks": { 398 Label: (labelPrefix + " Blocks"), 399 Unit: "integer", 400 Metrics: []mp.Metrics{ 401 {Name: "blks_read", Label: "Blocks Read", Diff: true, Stacked: false}, 402 {Name: "blks_hit", Label: "Blocks Hit", Diff: true, Stacked: false}, 403 }, 404 }, 405 "rows": { 406 Label: (labelPrefix + " Rows"), 407 Unit: "integer", 408 Metrics: []mp.Metrics{ 409 {Name: "tup_returned", Label: "Returned Rows", Diff: true, Stacked: false}, 410 {Name: "tup_fetched", Label: "Fetched Rows", Diff: true, Stacked: true}, 411 {Name: "tup_inserted", Label: "Inserted Rows", Diff: true, Stacked: true}, 412 {Name: "tup_updated", Label: "Updated Rows", Diff: true, Stacked: true}, 413 {Name: "tup_deleted", Label: "Deleted Rows", Diff: true, Stacked: true}, 414 }, 415 }, 416 "size": { 417 Label: (labelPrefix + " Data Size"), 418 Unit: "integer", 419 Metrics: []mp.Metrics{ 420 {Name: "total_size", Label: "Total Size", Diff: false, Stacked: false}, 421 }, 422 }, 423 "deadlocks": { 424 Label: (labelPrefix + " Dead Locks"), 425 Unit: "integer", 426 Metrics: []mp.Metrics{ 427 {Name: "deadlocks", Label: "Deadlocks", Diff: true, Stacked: false}, 428 }, 429 }, 430 "iotime": { 431 Label: (labelPrefix + " Block I/O time"), 432 Unit: "float", 433 Metrics: []mp.Metrics{ 434 {Name: "blk_read_time", Label: "Block Read Time (ms)", Diff: true, Stacked: false}, 435 {Name: "blk_write_time", Label: "Block Write Time (ms)", Diff: true, Stacked: false}, 436 }, 437 }, 438 "tempfile": { 439 Label: (labelPrefix + " Temporary file"), 440 Unit: "integer", 441 Metrics: []mp.Metrics{ 442 {Name: "temp_bytes", Label: "Temporary file size (byte)", Diff: true, Stacked: false}, 443 }, 444 }, 445 "xlog_location": { 446 Label: (labelPrefix + " Amount of Transaction location change"), 447 Unit: "bytes", 448 Metrics: []mp.Metrics{ 449 {Name: "xlog_location_bytes", Label: "Amount of Transaction location change (byte)", Diff: true, Stacked: false}, 450 }, 451 }, 452 } 453 454 return graphdef 455 } 456 457 // Do the plugin 458 func Do() { 459 optHost := flag.String("hostname", "localhost", "Hostname to login to") 460 optPort := flag.String("port", "5432", "Database port") 461 optUser := flag.String("user", "", "Postgres User") 462 optDatabase := flag.String("database", "", "Database name") 463 optPass := flag.String("password", os.Getenv("PGPASSWORD"), "Postgres Password") 464 optPrefix := flag.String("metric-key-prefix", "postgres", "Metric key prefix") 465 optSSLmode := flag.String("sslmode", "disable", "Whether or not to use SSL") 466 optConnectTimeout := flag.Int("connect_timeout", 5, "Maximum wait for connection, in seconds.") 467 optTempfile := flag.String("tempfile", "", "Temp file name") 468 flag.Parse() 469 470 if *optUser == "" { 471 logger.Warningf("user is required") 472 flag.PrintDefaults() 473 os.Exit(1) 474 } 475 option := "" 476 if *optDatabase != "" { 477 option = fmt.Sprintf("dbname=%s", *optDatabase) 478 } 479 480 var postgres PostgresPlugin 481 postgres.Host = *optHost 482 postgres.Port = *optPort 483 postgres.Username = *optUser 484 postgres.Password = *optPass 485 postgres.Prefix = *optPrefix 486 postgres.SSLmode = *optSSLmode 487 postgres.Timeout = *optConnectTimeout 488 postgres.Option = option 489 490 helper := mp.NewMackerelPlugin(postgres) 491 492 helper.Tempfile = *optTempfile 493 helper.Run() 494 }