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  }