github.com/netdata/go.d.plugin@v0.58.1/modules/mysql/collect.go (about) 1 // SPDX-License-Identifier: GPL-3.0-or-later 2 3 package mysql 4 5 import ( 6 "context" 7 "database/sql" 8 "errors" 9 "fmt" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/blang/semver/v4" 15 ) 16 17 func (m *MySQL) collect() (map[string]int64, error) { 18 if m.db == nil { 19 if err := m.openConnection(); err != nil { 20 return nil, err 21 } 22 } 23 if m.version == nil { 24 if err := m.collectVersion(); err != nil { 25 return nil, fmt.Errorf("error on collecting version: %v", err) 26 } 27 // https://mariadb.com/kb/en/user-statistics/ 28 m.doUserStatistics = m.isPercona || m.isMariaDB && m.version.GTE(semver.Version{Major: 10, Minor: 1, Patch: 1}) 29 } 30 31 mx := make(map[string]int64) 32 33 if err := m.collectGlobalStatus(mx); err != nil { 34 return nil, fmt.Errorf("error on collecting global status: %v", err) 35 } 36 37 if hasInnodbOSLog(mx) { 38 m.addInnoDBOSLogOnce.Do(m.addInnoDBOSLogCharts) 39 } 40 if hasInnodbDeadlocks(mx) { 41 m.addInnodbDeadlocksOnce.Do(m.addInnodbDeadlocksChart) 42 } 43 if hasQCacheMetrics(mx) { 44 m.addQCacheOnce.Do(m.addQCacheCharts) 45 } 46 if hasGaleraMetrics(mx) { 47 m.addGaleraOnce.Do(m.addGaleraCharts) 48 } 49 if hasTableOpenCacheOverflowsMetrics(mx) { 50 m.addTableOpenCacheOverflowsOnce.Do(m.addTableOpenCacheOverflowChart) 51 } 52 53 now := time.Now() 54 if now.Sub(m.recheckGlobalVarsTime) > m.recheckGlobalVarsEvery { 55 if err := m.collectGlobalVariables(); err != nil { 56 return nil, fmt.Errorf("error on collecting global variables: %v", err) 57 } 58 } 59 mx["max_connections"] = m.varMaxConns 60 mx["table_open_cache"] = m.varTableOpenCache 61 62 if m.isMariaDB || !strings.Contains(m.varDisabledStorageEngine, "MyISAM") { 63 m.addMyISAMOnce.Do(m.addMyISAMCharts) 64 } 65 if m.varLogBin != "OFF" { 66 m.addBinlogOnce.Do(m.addBinlogCharts) 67 } 68 69 // TODO: perhaps make a decisions based on privileges? (SHOW GRANTS FOR CURRENT_USER();) 70 if m.doSlaveStatus { 71 if err := m.collectSlaveStatus(mx); err != nil { 72 m.Warningf("error on collecting slave status: %v", err) 73 m.doSlaveStatus = errors.Is(err, context.DeadlineExceeded) 74 } 75 } 76 77 if m.doUserStatistics { 78 if err := m.collectUserStatistics(mx); err != nil { 79 m.Warningf("error on collecting user statistics: %v", err) 80 m.doUserStatistics = errors.Is(err, context.DeadlineExceeded) 81 } 82 } 83 84 if err := m.collectProcessListStatistics(mx); err != nil { 85 m.Errorf("error on collecting process list statistics: %v", err) 86 } 87 88 calcThreadCacheMisses(mx) 89 return mx, nil 90 } 91 92 func (m *MySQL) openConnection() error { 93 db, err := sql.Open("mysql", m.DSN) 94 if err != nil { 95 return fmt.Errorf("error on opening a connection with the mysql database [%s]: %v", m.safeDSN, err) 96 } 97 98 db.SetConnMaxLifetime(10 * time.Minute) 99 100 ctx, cancel := context.WithTimeout(context.Background(), m.Timeout.Duration) 101 defer cancel() 102 103 if err := db.PingContext(ctx); err != nil { 104 _ = db.Close() 105 return fmt.Errorf("error on pinging the mysql database [%s]: %v", m.safeDSN, err) 106 } 107 108 m.db = db 109 return nil 110 } 111 112 func calcThreadCacheMisses(collected map[string]int64) { 113 threads, cons := collected["threads_created"], collected["connections"] 114 if threads == 0 || cons == 0 { 115 collected["thread_cache_misses"] = 0 116 } else { 117 collected["thread_cache_misses"] = int64(float64(threads) / float64(cons) * 10000) 118 } 119 } 120 121 func hasInnodbOSLog(collected map[string]int64) bool { 122 // removed in MariaDB 10.8 (https://mariadb.com/kb/en/innodb-status-variables/#innodb_os_log_fsyncs) 123 _, ok := collected["innodb_os_log_fsyncs"] 124 return ok 125 } 126 127 func hasInnodbDeadlocks(collected map[string]int64) bool { 128 _, ok := collected["innodb_deadlocks"] 129 return ok 130 } 131 132 func hasGaleraMetrics(collected map[string]int64) bool { 133 _, ok := collected["wsrep_received"] 134 return ok 135 } 136 137 func hasQCacheMetrics(collected map[string]int64) bool { 138 _, ok := collected["qcache_hits"] 139 return ok 140 } 141 142 func hasTableOpenCacheOverflowsMetrics(collected map[string]int64) bool { 143 _, ok := collected["table_open_cache_overflows"] 144 return ok 145 } 146 147 func (m *MySQL) collectQuery(query string, assign func(column, value string, lineEnd bool)) (duration int64, err error) { 148 ctx, cancel := context.WithTimeout(context.Background(), m.Timeout.Duration) 149 defer cancel() 150 151 s := time.Now() 152 rows, err := m.db.QueryContext(ctx, query) 153 if err != nil { 154 return 0, err 155 } 156 duration = time.Since(s).Milliseconds() 157 defer func() { _ = rows.Close() }() 158 159 columns, err := rows.Columns() 160 if err != nil { 161 return duration, err 162 } 163 164 vs := makeValues(len(columns)) 165 for rows.Next() { 166 if err := rows.Scan(vs...); err != nil { 167 return duration, err 168 } 169 for i, l := 0, len(vs); i < l; i++ { 170 assign(columns[i], valueToString(vs[i]), i == l-1) 171 } 172 } 173 return duration, rows.Err() 174 } 175 176 func makeValues(size int) []any { 177 vs := make([]any, size) 178 for i := range vs { 179 vs[i] = &sql.NullString{} 180 } 181 return vs 182 } 183 184 func valueToString(value any) string { 185 v, ok := value.(*sql.NullString) 186 if !ok || !v.Valid { 187 return "" 188 } 189 return v.String 190 } 191 192 func parseInt(s string) int64 { 193 v, _ := strconv.ParseInt(s, 10, 64) 194 return v 195 } 196 197 func parseFloat(s string) float64 { 198 v, _ := strconv.ParseFloat(s, 64) 199 return v 200 }