github.com/netdata/go.d.plugin@v0.58.1/modules/proxysql/collect.go (about)

     1  // SPDX-License-Identifier: GPL-3.0-or-later
     2  
     3  package proxysql
     4  
     5  import (
     6  	"context"
     7  	"database/sql"
     8  	"fmt"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  )
    13  
    14  const (
    15  	queryVersion                    = "select version();"
    16  	queryStatsMySQLGlobal           = "SELECT * FROM stats_mysql_global;"
    17  	queryStatsMySQLMemoryMetrics    = "SELECT * FROM stats_memory_metrics;"
    18  	queryStatsMySQLCommandsCounters = "SELECT * FROM stats_mysql_commands_counters;"
    19  	queryStatsMySQLUsers            = "SELECT * FROM stats_mysql_users;"
    20  	queryStatsMySQLConnectionPool   = "SELECT * FROM stats_mysql_connection_pool;"
    21  )
    22  
    23  func (p *ProxySQL) collect() (map[string]int64, error) {
    24  	if p.db == nil {
    25  		if err := p.openConnection(); err != nil {
    26  			return nil, err
    27  		}
    28  	}
    29  
    30  	p.once.Do(func() {
    31  		v, err := p.doQueryVersion()
    32  		if err != nil {
    33  			p.Warningf("error on querying version: %v", err)
    34  		} else {
    35  			p.Debugf("connected to ProxySQL version: %s", v)
    36  		}
    37  	})
    38  
    39  	p.cache.reset()
    40  
    41  	mx := make(map[string]int64)
    42  
    43  	if err := p.collectStatsMySQLGlobal(mx); err != nil {
    44  		return nil, fmt.Errorf("error on collecting mysql global status: %v", err)
    45  	}
    46  	if err := p.collectStatsMySQLMemoryMetrics(mx); err != nil {
    47  		return nil, fmt.Errorf("error on collecting memory metrics: %v", err)
    48  	}
    49  	if err := p.collectStatsMySQLCommandsCounters(mx); err != nil {
    50  		return nil, fmt.Errorf("error on collecting mysql command counters: %v", err)
    51  	}
    52  	if err := p.collectStatsMySQLUsers(mx); err != nil {
    53  		return nil, fmt.Errorf("error on collecting mysql users: %v", err)
    54  	}
    55  	if err := p.collectStatsMySQLConnectionPool(mx); err != nil {
    56  		return nil, fmt.Errorf("error on collecting mysql connection pool: %v", err)
    57  	}
    58  
    59  	p.updateCharts()
    60  
    61  	return mx, nil
    62  }
    63  
    64  func (p *ProxySQL) doQueryVersion() (string, error) {
    65  	q := queryVersion
    66  	p.Debugf("executing query: '%s'", q)
    67  
    68  	var v string
    69  	if err := p.doQueryRow(q, &v); err != nil {
    70  		return "", err
    71  	}
    72  
    73  	return v, nil
    74  }
    75  
    76  func (p *ProxySQL) collectStatsMySQLGlobal(mx map[string]int64) error {
    77  	// https://proxysql.com/documentation/stats-statistics/#stats_mysql_global
    78  	q := queryStatsMySQLGlobal
    79  	p.Debugf("executing query: '%s'", q)
    80  
    81  	var name string
    82  	return p.doQuery(q, func(column, value string, rowEnd bool) {
    83  		switch column {
    84  		case "Variable_Name":
    85  			name = value
    86  		case "Variable_Value":
    87  			mx[name] = parseInt(value)
    88  		}
    89  	})
    90  }
    91  
    92  func (p *ProxySQL) collectStatsMySQLMemoryMetrics(mx map[string]int64) error {
    93  	// https://proxysql.com/documentation/stats-statistics/#stats_mysql_memory_metrics
    94  	q := queryStatsMySQLMemoryMetrics
    95  	p.Debugf("executing query: '%s'", q)
    96  
    97  	var name string
    98  	return p.doQuery(q, func(column, value string, rowEnd bool) {
    99  		switch column {
   100  		case "Variable_Name":
   101  			name = value
   102  		case "Variable_Value":
   103  			mx[name] = parseInt(value)
   104  		}
   105  	})
   106  }
   107  
   108  func (p *ProxySQL) collectStatsMySQLCommandsCounters(mx map[string]int64) error {
   109  	// https://proxysql.com/documentation/stats-statistics/#stats_mysql_commands_counters
   110  	q := queryStatsMySQLCommandsCounters
   111  	p.Debugf("executing query: '%s'", q)
   112  
   113  	var command string
   114  	return p.doQuery(q, func(column, value string, rowEnd bool) {
   115  		switch column {
   116  		case "Command":
   117  			command = value
   118  			p.cache.getCommand(command).updated = true
   119  		default:
   120  			mx["mysql_command_"+command+"_"+column] = parseInt(value)
   121  		}
   122  	})
   123  }
   124  
   125  func (p *ProxySQL) collectStatsMySQLUsers(mx map[string]int64) error {
   126  	// https://proxysql.com/documentation/stats-statistics/#stats_mysql_users
   127  	q := queryStatsMySQLUsers
   128  	p.Debugf("executing query: '%s'", q)
   129  
   130  	var user string
   131  	var used int64
   132  	return p.doQuery(q, func(column, value string, rowEnd bool) {
   133  		switch column {
   134  		case "username":
   135  			user = value
   136  			p.cache.getUser(user).updated = true
   137  		case "frontend_connections":
   138  			used = parseInt(value)
   139  			mx["mysql_user_"+user+"_"+column] = used
   140  		case "frontend_max_connections":
   141  			mx["mysql_user_"+user+"_frontend_connections_utilization"] = calcPercentage(used, parseInt(value))
   142  		}
   143  	})
   144  }
   145  
   146  func (p *ProxySQL) collectStatsMySQLConnectionPool(mx map[string]int64) error {
   147  	// https://proxysql.com/documentation/stats-statistics/#stats_mysql_connection_pool
   148  	q := queryStatsMySQLConnectionPool
   149  	p.Debugf("executing query: '%s'", q)
   150  
   151  	var hg, host, port string
   152  	var px string
   153  	return p.doQuery(q, func(column, value string, rowEnd bool) {
   154  		switch column {
   155  		case "hg", "hostgroup":
   156  			hg = value
   157  		case "srv_host":
   158  			host = value
   159  		case "srv_port":
   160  			port = value
   161  			p.cache.getBackend(hg, host, port).updated = true
   162  			px = "backend_" + backendID(hg, host, port) + "_"
   163  		case "status":
   164  			mx[px+"status_ONLINE"] = boolToInt(value == "1")
   165  			mx[px+"status_SHUNNED"] = boolToInt(value == "2")
   166  			mx[px+"status_OFFLINE_SOFT"] = boolToInt(value == "3")
   167  			mx[px+"status_OFFLINE_HARD"] = boolToInt(value == "4")
   168  		default:
   169  			mx[px+column] = parseInt(value)
   170  		}
   171  	})
   172  }
   173  
   174  func (p *ProxySQL) updateCharts() {
   175  	for k, m := range p.cache.commands {
   176  		if !m.updated {
   177  			delete(p.cache.commands, k)
   178  			p.removeMySQLCommandCountersCharts(m.command)
   179  			continue
   180  		}
   181  		if !m.hasCharts {
   182  			m.hasCharts = true
   183  			p.addMySQLCommandCountersCharts(m.command)
   184  		}
   185  	}
   186  	for k, m := range p.cache.users {
   187  		if !m.updated {
   188  			delete(p.cache.users, k)
   189  			p.removeMySQLUserCharts(m.user)
   190  			continue
   191  		}
   192  		if !m.hasCharts {
   193  			m.hasCharts = true
   194  			p.addMySQLUsersCharts(m.user)
   195  		}
   196  	}
   197  	for k, m := range p.cache.backends {
   198  		if !m.updated {
   199  			delete(p.cache.backends, k)
   200  			p.removeBackendCharts(m.hg, m.host, m.port)
   201  			continue
   202  		}
   203  		if !m.hasCharts {
   204  			m.hasCharts = true
   205  			p.addBackendCharts(m.hg, m.host, m.port)
   206  		}
   207  	}
   208  }
   209  
   210  func (p *ProxySQL) openConnection() error {
   211  	db, err := sql.Open("mysql", p.DSN)
   212  	if err != nil {
   213  		return fmt.Errorf("error on opening a connection with the proxysql instance [%s]: %v", p.DSN, err)
   214  	}
   215  
   216  	db.SetConnMaxLifetime(10 * time.Minute)
   217  
   218  	if err := db.Ping(); err != nil {
   219  		_ = db.Close()
   220  		return fmt.Errorf("error on pinging the proxysql instance [%s]: %v", p.DSN, err)
   221  	}
   222  
   223  	p.db = db
   224  	return nil
   225  }
   226  
   227  func (p *ProxySQL) doQueryRow(query string, v any) error {
   228  	ctx, cancel := context.WithTimeout(context.Background(), p.Timeout.Duration)
   229  	defer cancel()
   230  
   231  	return p.db.QueryRowContext(ctx, query).Scan(v)
   232  }
   233  
   234  func (p *ProxySQL) doQuery(query string, assign func(column, value string, rowEnd bool)) error {
   235  	ctx, cancel := context.WithTimeout(context.Background(), p.Timeout.Duration)
   236  	defer cancel()
   237  
   238  	rows, err := p.db.QueryContext(ctx, query)
   239  	if err != nil {
   240  		return err
   241  	}
   242  	defer func() { _ = rows.Close() }()
   243  
   244  	return readRows(rows, assign)
   245  }
   246  
   247  func readRows(rows *sql.Rows, assign func(column, value string, rowEnd bool)) error {
   248  	columns, err := rows.Columns()
   249  	if err != nil {
   250  		return err
   251  	}
   252  
   253  	values := makeValues(len(columns))
   254  
   255  	for rows.Next() {
   256  		if err := rows.Scan(values...); err != nil {
   257  			return err
   258  		}
   259  		for i, l := 0, len(values); i < l; i++ {
   260  			assign(columns[i], valueToString(values[i]), i == l-1)
   261  		}
   262  	}
   263  	return rows.Err()
   264  }
   265  
   266  func valueToString(value any) string {
   267  	v, ok := value.(*sql.NullString)
   268  	if !ok || !v.Valid {
   269  		return ""
   270  	}
   271  	return v.String
   272  }
   273  
   274  func makeValues(size int) []any {
   275  	vs := make([]any, size)
   276  	for i := range vs {
   277  		vs[i] = &sql.NullString{}
   278  	}
   279  	return vs
   280  }
   281  
   282  func parseInt(value string) int64 {
   283  	v, _ := strconv.ParseInt(value, 10, 64)
   284  	return v
   285  }
   286  
   287  func calcPercentage(value, total int64) (v int64) {
   288  	if total == 0 {
   289  		return 0
   290  	}
   291  	if v = value * 100 / total; v < 0 {
   292  		v = -v
   293  	}
   294  	return v
   295  }
   296  
   297  func boolToInt(v bool) int64 {
   298  	if v {
   299  		return 1
   300  	}
   301  	return 0
   302  }
   303  
   304  func backendID(hg, host, port string) string {
   305  	hg = strings.ReplaceAll(strings.ToLower(hg), " ", "_")
   306  	host = strings.ReplaceAll(host, ".", "_")
   307  	return hg + "_" + host + "_" + port
   308  }