From a84c5f05251efbcda9b996225012cc0073c89d82 Mon Sep 17 00:00:00 2001 From: jsvisa Date: Mon, 23 May 2016 21:13:00 +0800 Subject: [PATCH] add pgbouncer plugin --- plugins/inputs/all/all.go | 1 + plugins/inputs/pgbouncer/README.md | 62 ++++++++ plugins/inputs/pgbouncer/pgbouncer.go | 206 ++++++++++++++++++++++++++ 3 files changed, 269 insertions(+) create mode 100644 plugins/inputs/pgbouncer/README.md create mode 100644 plugins/inputs/pgbouncer/pgbouncer.go diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go index ddb7d4039..dacbff644 100644 --- a/plugins/inputs/all/all.go +++ b/plugins/inputs/all/all.go @@ -46,6 +46,7 @@ import ( _ "github.com/influxdata/telegraf/plugins/inputs/nstat" _ "github.com/influxdata/telegraf/plugins/inputs/ntpq" _ "github.com/influxdata/telegraf/plugins/inputs/passenger" + _ "github.com/influxdata/telegraf/plugins/inputs/pgbouncer" _ "github.com/influxdata/telegraf/plugins/inputs/phpfpm" _ "github.com/influxdata/telegraf/plugins/inputs/ping" _ "github.com/influxdata/telegraf/plugins/inputs/postgresql" diff --git a/plugins/inputs/pgbouncer/README.md b/plugins/inputs/pgbouncer/README.md new file mode 100644 index 000000000..31e883f11 --- /dev/null +++ b/plugins/inputs/pgbouncer/README.md @@ -0,0 +1,62 @@ +# Pgbouncer plugin + +This pgbouncer plugin provides metrics for your pgbouncer connection information. + +### Configuration: + +```toml +# Description +[[inputs.pgbouncer]] + ## specify address via a url matching: + ## postgres://[pqgotest[:password]]@localhost:port[/dbname]\ + ## ?sslmode=[disable|verify-ca|verify-full] + ## or a simple string: + ## host=localhost user=pqotest port=... password=... sslmode=... dbname=... + ## + ## All connection parameters are optional, except for dbname, + ## you need to set it always as pgbouncer. + address = "host=localhost user=postgres port=6432 sslmode=disable dbname=pgbouncer" + + ## A list of databases to pull metrics about. If not specified, metrics for all + ## databases are gathered. + # databases = ["app_production", "testing"] +` +``` + +### Measurements & Fields: + +Pgbouncer provides two measurement named "pgbouncer_pools" and "pgbouncer_stats", each have the fields as below: + +#### pgbouncer_pools + +- cl_active +- cl_waiting +- maxwait +- pool_mode +- sv_active +- sv_idle +- sv_login +- sv_tested +- sv_used + +### pgbouncer_stats + +- avg_query +- avg_recv +- avg_req +- avg_sent +- total_query_time +- total_received +- total_requests +- total_sent + +More information about the meaning of these metrics can be found in the [PgBouncer usage](https://pgbouncer.github.io/usage.html) + +### Example Output: + +``` +$ ./telegraf -config telegraf.conf -input-filter pgbouncer -test +> pgbouncer_pools,db=pgbouncer,host=localhost,pool_mode=transaction,server=host\=localhost\ user\=elena\ port\=6432\ dbname\=pgbouncer\ sslmode\=disable,user=elena cl_active=1500i,cl_waiting=0i,maxwait=0i,sv_active=0i,sv_idle=5i,sv_login=0i,sv_tested=0i,sv_used=5i 1466594520564518897 +> pgbouncer_stats,db=pgbouncer,host=localhost,server=host\=localhost\ user\=elena\ port\=6432\ dbname\=pgbouncer\ sslmode\=disable avg_query=1157i,avg_recv=36727i,avg_req=131i,avg_sent=23359i,total_query_time=252173878876i,total_received=55956189078i,total_requests=193601888i,total_sent=36703848280i 1466594520564825345 +``` + diff --git a/plugins/inputs/pgbouncer/pgbouncer.go b/plugins/inputs/pgbouncer/pgbouncer.go new file mode 100644 index 000000000..df4179cd6 --- /dev/null +++ b/plugins/inputs/pgbouncer/pgbouncer.go @@ -0,0 +1,206 @@ +package pgbouncer + +import ( + "bytes" + "database/sql" + "regexp" + "strings" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/plugins/inputs" + + "github.com/lib/pq" +) + +type Pgbouncer struct { + Address string + Databases []string + OrderedColumns []string + AllColumns []string + sanitizedAddress string +} + +var ignoredColumns = map[string]bool{"pool_mode": true, "database": true, "user": true} + +var sampleConfig = ` + ## specify address via a url matching: + ## postgres://[pqgotest[:password]]@localhost:port[/dbname]\ + ## ?sslmode=[disable|verify-ca|verify-full] + ## or a simple string: + ## host=localhost user=pqotest port=6432 password=... sslmode=... dbname=pgbouncer + ## + ## All connection parameters are optional, except for dbname, + ## you need to set it always as pgbouncer. + address = "host=localhost user=postgres port=6432 sslmode=disable dbname=pgbouncer" + + ## A list of databases to pull metrics about. If not specified, metrics for all + ## databases are gathered. + # databases = ["app_production", "testing"] +` + +func (p *Pgbouncer) SampleConfig() string { + return sampleConfig +} + +func (p *Pgbouncer) Description() string { + return "Read metrics from one or many pgbouncer servers" +} + +func (p *Pgbouncer) IgnoredColumns() map[string]bool { + return ignoredColumns +} + +var localhost = "host=localhost port=6432 sslmode=disable dbname=pgbouncer" + +func (p *Pgbouncer) Gather(acc telegraf.Accumulator) error { + if p.Address == "" || p.Address == "localhost" { + p.Address = localhost + } + + db, err := sql.Open("postgres", p.Address) + if err != nil { + return err + } + + defer db.Close() + + queries := map[string]string{"pools": "SHOW POOLS", "stats": "SHOW STATS"} + + for metric, query := range queries { + rows, err := db.Query(query) + if err != nil { + return err + } + + defer rows.Close() + + // grab the column information from the result + p.OrderedColumns, err = rows.Columns() + if err != nil { + return err + } else { + p.AllColumns = make([]string, len(p.OrderedColumns)) + copy(p.AllColumns, p.OrderedColumns) + } + + for rows.Next() { + err = p.accRow(rows, metric, acc) + if err != nil { + return err + } + } + } + return nil +} + +type scanner interface { + Scan(dest ...interface{}) error +} + +var passwordKVMatcher, _ = regexp.Compile("password=\\S+ ?") + +func (p *Pgbouncer) SanitizedAddress() (_ string, err error) { + var canonicalizedAddress string + if strings.HasPrefix(p.Address, "postgres://") || strings.HasPrefix(p.Address, "postgresql://") { + canonicalizedAddress, err = pq.ParseURL(p.Address) + if err != nil { + return p.sanitizedAddress, err + } + } else { + canonicalizedAddress = p.Address + } + p.sanitizedAddress = passwordKVMatcher.ReplaceAllString(canonicalizedAddress, "") + + return p.sanitizedAddress, err +} + +func (p *Pgbouncer) accRow(row scanner, metric string, acc telegraf.Accumulator) error { + var columnVars []interface{} + var tags = make(map[string]string) + var dbname, user, poolMode bytes.Buffer + + // this is where we'll store the column name with its *interface{} + columnMap := make(map[string]*interface{}) + + for _, column := range p.OrderedColumns { + columnMap[column] = new(interface{}) + } + + // populate the array of interface{} with the pointers in the right order + for i := 0; i < len(columnMap); i++ { + columnVars = append(columnVars, columnMap[p.OrderedColumns[i]]) + } + + // deconstruct array of variables and send to Scan + err := row.Scan(columnVars...) + + if err != nil { + return err + } + + // extract the database name from the column map + dbnameChars := (*columnMap["database"]).([]uint8) + for i := 0; i < len(dbnameChars); i++ { + dbname.WriteString(string(dbnameChars[i])) + } + + if p.ignoreDatabase(dbname.String()) { + return nil + } + + tags["db"] = dbname.String() + + if columnMap["user"] != nil { + userChars := (*columnMap["user"]).([]uint8) + for i := 0; i < len(userChars); i++ { + user.WriteString(string(userChars[i])) + } + tags["user"] = user.String() + } + + if columnMap["pool_mode"] != nil { + poolChars := (*columnMap["pool_mode"]).([]uint8) + for i := 0; i < len(poolChars); i++ { + poolMode.WriteString(string(poolChars[i])) + } + tags["pool_mode"] = poolMode.String() + } + + var tagAddress string + tagAddress, err = p.SanitizedAddress() + if err != nil { + return err + } else { + tags["server"] = tagAddress + } + + fields := make(map[string]interface{}) + for col, val := range columnMap { + _, ignore := ignoredColumns[col] + if !ignore { + fields[col] = *val + } + } + acc.AddFields("pgbouncer_"+metric, fields, tags) + + return nil +} + +func (p *Pgbouncer) ignoreDatabase(db string) bool { + if len(p.Databases) == 0 { + return false + } + + for _, dbName := range p.Databases { + if db == dbName { + return false + } + } + return true +} + +func init() { + inputs.Add("pgbouncer", func() telegraf.Input { + return &Pgbouncer{} + }) +}