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 }