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