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  }