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  }