#!/usr/bin/env python # CollectD MySQL plugin, designed for MySQL 5.5+ (specifically Percona Server) # # Pulls most of the same metrics as the Percona Monitoring Plugins for Cacti, # however is designed to be used on newer versions of MySQL and as such drops # all of the legacy compatibility, and favors metrics from SHOW GLOBAL STATUS # as opposed to SHOW ENGINE INNODB STATUS where possible. # # Configuration: # Import mysql # <Module mysql> # Host localhost # Port 3306 (optional) # User root # Password xxxx # HeartbeatTable percona.heartbeat (optional, if using pt-heartbeat) # Verbose true (optional, to enable debugging) # </Module> # # Requires "MySQLdb" for Python # # Author: Chris Boulton <chris@chrisboulton.com> # License: MIT (http://www.opensource.org/licenses/mit-license.php) # import sys COLLECTD_ENABLED=True try: import collectd except ImportError: # We're not running in CollectD, set this to False so we can make some changes # accordingly for testing/development. COLLECTD_ENABLED=False import re import MySQLdb MYSQL_CONFIG = { 'Host': 'localhost', 'Port': 3306, 'User': 'root', 'Password': '', 'HeartbeatTable': '', 'Verbose': False, } MYSQL_STATUS_VARS = { 'Aborted_clients': 'counter', 'Aborted_connects': 'counter', 'Binlog_cache_disk_use': 'counter', 'Binlog_cache_use': 'counter', 'Bytes_received': 'counter', 'Bytes_sent': 'counter', 'Connections': 'counter', 'Created_tmp_disk_tables': 'counter', 'Created_tmp_files': 'counter', 'Created_tmp_tables': 'counter', 'Innodb_buffer_pool_pages_data': 'gauge', 'Innodb_buffer_pool_pages_dirty': 'gauge', 'Innodb_buffer_pool_pages_free': 'gauge', 'Innodb_buffer_pool_pages_total': 'gauge', 'Innodb_buffer_pool_read_requests': 'counter', 'Innodb_buffer_pool_reads': 'counter', 'Innodb_checkpoint_age': 'gauge', 'Innodb_checkpoint_max_age': 'gauge', 'Innodb_data_fsyncs': 'counter', 'Innodb_data_pending_fsyncs': 'gauge', 'Innodb_data_pending_reads': 'gauge', 'Innodb_data_pending_writes': 'gauge', 'Innodb_data_read': 'counter', 'Innodb_data_reads': 'counter', 'Innodb_data_writes': 'counter', 'Innodb_data_written': 'counter', 'Innodb_deadlocks': 'counter', 'Innodb_history_list_length': 'gauge', 'Innodb_ibuf_free_list': 'gauge', 'Innodb_ibuf_merged_delete_marks': 'counter', 'Innodb_ibuf_merged_deletes': 'counter', 'Innodb_ibuf_merged_inserts': 'counter', 'Innodb_ibuf_merges': 'counter', 'Innodb_ibuf_segment_size': 'gauge', 'Innodb_ibuf_size': 'gauge', 'Innodb_lsn_current': 'counter', 'Innodb_lsn_flushed': 'counter', 'Innodb_max_trx_id': 'counter', 'Innodb_mem_adaptive_hash': 'gauge', 'Innodb_mem_dictionary': 'gauge', 'Innodb_mem_total': 'gauge', 'Innodb_mutex_os_waits': 'counter', 'Innodb_mutex_spin_rounds': 'counter', 'Innodb_mutex_spin_waits': 'counter', 'Innodb_os_log_pending_fsyncs': 'gauge', 'Innodb_pages_created': 'counter', 'Innodb_pages_read': 'counter', 'Innodb_pages_written': 'counter', 'Innodb_row_lock_time': 'counter', 'Innodb_row_lock_time_avg': 'gauge', 'Innodb_row_lock_time_max': 'gauge', 'Innodb_row_lock_waits': 'counter', 'Innodb_rows_deleted': 'counter', 'Innodb_rows_inserted': 'counter', 'Innodb_rows_read': 'counter', 'Innodb_rows_updated': 'counter', 'Innodb_s_lock_os_waits': 'counter', 'Innodb_s_lock_spin_rounds': 'counter', 'Innodb_s_lock_spin_waits': 'counter', 'Innodb_uncheckpointed_bytes': 'gauge', 'Innodb_unflushed_log': 'gauge', 'Innodb_unpurged_txns': 'gauge', 'Innodb_x_lock_os_waits': 'counter', 'Innodb_x_lock_spin_rounds': 'counter', 'Innodb_x_lock_spin_waits': 'counter', 'Key_blocks_not_flushed': 'gauge', 'Key_blocks_unused': 'gauge', 'Key_blocks_used': 'gauge', 'Key_read_requests': 'counter', 'Key_reads': 'counter', 'Key_write_requests': 'counter', 'Key_writes': 'counter', 'Max_used_connections': 'gauge', 'Open_files': 'gauge', 'Open_table_definitions': 'gauge', 'Open_tables': 'gauge', 'Opened_files': 'counter', 'Opened_table_definitions': 'counter', 'Opened_tables': 'counter', 'Qcache_free_blocks': 'gauge', 'Qcache_free_memory': 'gauge', 'Qcache_hits': 'counter', 'Qcache_inserts': 'counter', 'Qcache_lowmem_prunes': 'counter', 'Qcache_not_cached': 'counter', 'Qcache_queries_in_cache': 'counter', 'Qcache_total_blocks': 'counter', 'Questions': 'counter', 'Select_full_join': 'counter', 'Select_full_range_join': 'counter', 'Select_range': 'counter', 'Select_range_check': 'counter', 'Select_scan': 'counter', 'Slave_open_temp_tables': 'gauge', 'Slave_retried_transactions': 'counter', 'Slow_launch_threads': 'counter', 'Slow_queries': 'counter', 'Sort_merge_passes': 'counter', 'Sort_range': 'counter', 'Sort_rows': 'counter', 'Sort_scan': 'counter', 'Table_locks_immediate': 'counter', 'Table_locks_waited': 'counter', 'Table_open_cache_hits': 'counter', 'Table_open_cache_misses': 'counter', 'Table_open_cache_overflows': 'counter', 'Threadpool_idle_threads': 'gauge', 'Threadpool_threads': 'gauge', 'Threads_cached': 'gauge', 'Threads_connected': 'gauge', 'Threads_created': 'counter', 'Threads_running': 'gauge', 'Uptime': 'gauge', 'wsrep_apply_oooe': 'gauge', 'wsrep_apply_oool': 'gauge', 'wsrep_apply_window': 'gauge', 'wsrep_causal_reads': 'gauge', 'wsrep_cert_deps_distance': 'gauge', 'wsrep_cert_index_size': 'gauge', 'wsrep_cert_interval': 'gauge', 'wsrep_cluster_size': 'gauge', 'wsrep_commit_oooe': 'gauge', 'wsrep_commit_oool': 'gauge', 'wsrep_commit_window': 'gauge', 'wsrep_flow_control_paused': 'gauge', 'wsrep_flow_control_paused_ns': 'counter', 'wsrep_flow_control_recv': 'counter', 'wsrep_flow_control_sent': 'counter', 'wsrep_local_bf_aborts': 'counter', 'wsrep_local_cert_failures': 'counter', 'wsrep_local_commits': 'counter', 'wsrep_local_recv_queue': 'gauge', 'wsrep_local_recv_queue_avg': 'gauge', 'wsrep_local_recv_queue_max': 'gauge', 'wsrep_local_recv_queue_min': 'gauge', 'wsrep_local_replays': 'gauge', 'wsrep_local_send_queue': 'gauge', 'wsrep_local_send_queue_avg': 'gauge', 'wsrep_local_send_queue_max': 'gauge', 'wsrep_local_send_queue_min': 'gauge', 'wsrep_received': 'counter', 'wsrep_received_bytes': 'counter', 'wsrep_repl_data_bytes': 'counter', 'wsrep_repl_keys': 'counter', 'wsrep_repl_keys_bytes': 'counter', 'wsrep_repl_other_bytes': 'counter', 'wsrep_replicated': 'counter', 'wsrep_replicated_bytes': 'counter', } MYSQL_VARS = [ 'binlog_stmt_cache_size', 'innodb_additional_mem_pool_size', 'innodb_buffer_pool_size', 'innodb_concurrency_tickets', 'innodb_io_capacity', 'innodb_log_buffer_size', 'innodb_log_file_size', 'innodb_open_files', 'innodb_open_files', 'join_buffer_size', 'max_connections', 'open_files_limit', 'query_cache_limit', 'query_cache_size', 'query_cache_size', 'read_buffer_size', 'table_cache', 'table_definition_cache', 'table_open_cache', 'thread_cache_size', 'thread_cache_size', 'thread_concurrency', 'tmp_table_size', ] MYSQL_PROCESS_STATES = { 'closing_tables': 0, 'copying_to_tmp_table': 0, 'end': 0, 'freeing_items': 0, 'init': 0, 'locked': 0, 'login': 0, 'none': 0, 'other': 0, 'preparing': 0, 'reading_from_net': 0, 'sending_data': 0, 'sorting_result': 0, 'statistics': 0, 'updating': 0, 'writing_to_net': 0, 'creating_table': 0, 'opening_tables': 0, } MYSQL_INNODB_STATUS_VARS = { 'active_transactions': 'gauge', 'current_transactions': 'gauge', 'file_reads': 'counter', 'file_system_memory': 'gauge', 'file_writes': 'counter', 'innodb_lock_structs': 'gauge', 'innodb_lock_wait_secs': 'gauge', 'innodb_locked_tables': 'gauge', 'innodb_sem_wait_time_ms': 'gauge', 'innodb_sem_waits': 'gauge', 'innodb_tables_in_use': 'gauge', 'lock_system_memory': 'gauge', 'locked_transactions': 'gauge', 'log_writes': 'counter', 'page_hash_memory': 'gauge', 'pending_aio_log_ios': 'gauge', 'pending_buf_pool_flushes': 'gauge', 'pending_chkp_writes': 'gauge', 'pending_ibuf_aio_reads': 'gauge', 'pending_log_writes':'gauge', 'queries_inside': 'gauge', 'queries_queued': 'gauge', 'read_views': 'gauge', } MYSQL_INNODB_STATUS_MATCHES = { # 0 read views open inside InnoDB 'read views open inside InnoDB': { 'read_views': 0, }, # 5635328 OS file reads, 27018072 OS file writes, 20170883 OS fsyncs ' OS file reads, ': { 'file_reads': 0, 'file_writes': 4, }, # ibuf aio reads: 0, log i/o's: 0, sync i/o's: 0 'ibuf aio reads': { 'pending_ibuf_aio_reads': 3, 'pending_aio_log_ios': 6, 'pending_aio_sync_ios': 9, }, # Pending flushes (fsync) log: 0; buffer pool: 0 'Pending flushes (fsync)': { 'pending_buf_pool_flushes': 7, }, # 16086708 log i/o's done, 106.07 log i/o's/second " log i/o's done, ": { 'log_writes': 0, }, # 0 pending log writes, 0 pending chkp writes ' pending log writes, ': { 'pending_log_writes': 0, 'pending_chkp_writes': 4, }, # Page hash 2302856 (buffer pool 0 only) 'Page hash ': { 'page_hash_memory': 2, }, # File system 657820264 (812272 + 657007992) 'File system ': { 'file_system_memory': 2, }, # Lock system 143820296 (143819576 + 720) 'Lock system ': { 'lock_system_memory': 2, }, # 0 queries inside InnoDB, 0 queries in queue 'queries inside InnoDB, ': { 'queries_inside': 0, 'queries_queued': 4, }, # --Thread 139954487744256 has waited at dict0dict.cc line 472 for 0.0000 seconds the semaphore: 'seconds the semaphore': { 'innodb_sem_waits': lambda row, stats: stats['innodb_sem_waits'] + 1, 'innodb_sem_wait_time_ms': lambda row, stats: int(float(row[9]) * 1000), }, # mysql tables in use 1, locked 1 'mysql tables in use': { 'innodb_tables_in_use': lambda row, stats: stats['innodb_tables_in_use'] + int(row[4]), 'innodb_locked_tables': lambda row, stats: stats['innodb_locked_tables'] + int(row[6]), }, "------- TRX HAS BEEN": { "innodb_lock_wait_secs": lambda row, stats: stats['innodb_lock_wait_secs'] + int(row[5]), }, } def get_mysql_conn(): return MySQLdb.connect( host=MYSQL_CONFIG['Host'], port=MYSQL_CONFIG['Port'], user=MYSQL_CONFIG['User'], passwd=MYSQL_CONFIG['Password'] ) def mysql_query(conn, query): cur = conn.cursor(MySQLdb.cursors.DictCursor) cur.execute(query) return cur def fetch_mysql_status(conn): result = mysql_query(conn, 'SHOW GLOBAL STATUS') status = {} for row in result.fetchall(): status[row['Variable_name']] = row['Value'] # calculate the number of unpurged txns from existing variables if 'Innodb_max_trx_id' in status: status['Innodb_unpurged_txns'] = int(status['Innodb_max_trx_id']) - int(status['Innodb_purge_trx_id']) if 'Innodb_lsn_last_checkpoint' in status: status['Innodb_uncheckpointed_bytes'] = int(status['Innodb_lsn_current'])- int(status['Innodb_lsn_last_checkpoint']) if 'Innodb_lsn_flushed' in status: status['Innodb_unflushed_log'] = int(status['Innodb_lsn_current']) - int(status['Innodb_lsn_flushed']) return status def fetch_mysql_master_stats(conn): try: result = mysql_query(conn, 'SHOW BINARY LOGS') except MySQLdb.OperationalError: return {} stats = { 'binary_log_space': 0, } for row in result.fetchall(): if 'File_size' in row and row['File_size'] > 0: stats['binary_log_space'] += int(row['File_size']) return stats def fetch_mysql_slave_stats(conn): result = mysql_query(conn, 'SHOW SLAVE STATUS') slave_row = result.fetchone() if slave_row is None: return {} status = { 'relay_log_space': slave_row['Relay_Log_Space'], 'slave_lag': slave_row['Seconds_Behind_Master'] if slave_row['Seconds_Behind_Master'] != None else 0, } if MYSQL_CONFIG['HeartbeatTable']: query = """ SELECT MAX(UNIX_TIMESTAMP() - UNIX_TIMESTAMP(ts)) AS delay FROM %s WHERE server_id = %s """ % (MYSQL_CONFIG['HeartbeatTable'], slave_row['Master_Server_Id']) result = mysql_query(conn, query) row = result.fetchone() if 'delay' in row and row['delay'] != None: status['slave_lag'] = row['delay'] status['slave_running'] = 1 if slave_row['Slave_SQL_Running'] == 'Yes' else 0 status['slave_stopped'] = 1 if slave_row['Slave_SQL_Running'] != 'Yes' else 0 return status def fetch_mysql_process_states(conn): global MYSQL_PROCESS_STATES result = mysql_query(conn, 'SHOW PROCESSLIST') states = MYSQL_PROCESS_STATES.copy() for row in result.fetchall(): state = row['State'] if state == '' or state == None: state = 'none' state = re.sub(r'^(Table lock|Waiting for .*lock)$', "Locked", state) state = state.lower().replace(" ", "_") if state not in states: state = 'other' states[state] += 1 return states def fetch_mysql_variables(conn): global MYSQL_VARS result = mysql_query(conn, 'SHOW GLOBAL VARIABLES') variables = {} for row in result.fetchall(): if row['Variable_name'] in MYSQL_VARS: variables[row['Variable_name']] = row['Value'] return variables def fetch_mysql_response_times(conn): response_times = {} try: result = mysql_query(conn, """ SELECT * FROM INFORMATION_SCHEMA.QUERY_RESPONSE_TIME WHERE `time` != 'TOO LONG' ORDER BY `time` """) except MySQLdb.OperationalError: return {} for i in range(1, 14): row = result.fetchone() # fill in missing rows with zeros if not row: row = { 'count': 0, 'total': 0 } row = {key.lower(): val for key, val in row.items()} response_times[i] = { 'time': float(row['time']), 'count': int(row['count']), 'total': round(float(row['total']) * 1000000, 0), } return response_times def fetch_innodb_stats(conn): global MYSQL_INNODB_STATUS_MATCHES, MYSQL_INNODB_STATUS_VARS result = mysql_query(conn, 'SHOW ENGINE INNODB STATUS') row = result.fetchone() status = row['Status'] stats = dict.fromkeys(MYSQL_INNODB_STATUS_VARS.keys(), 0) for line in status.split("\n"): line = line.strip() row = re.split(r' +', re.sub(r'[,;] ', ' ', line)) if line == '': continue # ---TRANSACTION 124324402462, not started # ---TRANSACTION 124324402468, ACTIVE 0 sec committing if line.find("---TRANSACTION") != -1: stats['current_transactions'] += 1 if line.find("ACTIVE") != -1: stats['active_transactions'] += 1 # LOCK WAIT 228 lock struct(s), heap size 46632, 65 row lock(s), undo log entries 1 # 205 lock struct(s), heap size 30248, 37 row lock(s), undo log entries 1 elif line.find("lock struct(s)") != -1: if line.find("LOCK WAIT") != -1: stats['innodb_lock_structs'] += int(row[2]) stats['locked_transactions'] += 1 else: stats['innodb_lock_structs'] += int(row[0]) else: for match in MYSQL_INNODB_STATUS_MATCHES: if line.find(match) == -1: continue for key in MYSQL_INNODB_STATUS_MATCHES[match]: value = MYSQL_INNODB_STATUS_MATCHES[match][key] if type(value) is int: if value < len(row) and row[value].isdigit(): stats[key] = int(row[value]) else: stats[key] = value(row, stats) break return stats def log_verbose(msg): if not MYSQL_CONFIG['Verbose']: return if COLLECTD_ENABLED: collectd.info('mysql plugin: %s' % msg) else: print('mysql plugin: %s' % msg) def dispatch_value(prefix, key, value, type, type_instance=None): if not type_instance: type_instance = key log_verbose('Sending value: %s/%s=%s' % (prefix, type_instance, value)) if value is None: return try: value = int(value) except ValueError: value = float(value) if COLLECTD_ENABLED: val = collectd.Values(plugin='mysql', plugin_instance=prefix) val.type = type val.type_instance = type_instance val.values = [value] val.dispatch() def configure_callback(conf): global MYSQL_CONFIG for node in conf.children: if node.key in MYSQL_CONFIG: MYSQL_CONFIG[node.key] = node.values[0] MYSQL_CONFIG['Port'] = int(MYSQL_CONFIG['Port']) MYSQL_CONFIG['Verbose'] = bool(MYSQL_CONFIG['Verbose']) def read_callback(): global MYSQL_STATUS_VARS conn = get_mysql_conn() mysql_status = fetch_mysql_status(conn) for key in mysql_status: if mysql_status[key] == '': mysql_status[key] = 0 # collect anything beginning with Com_/Handler_ as these change # regularly between mysql versions and this is easier than a fixed # list if key.split('_', 2)[0] in ['Com', 'Handler']: ds_type = 'counter' elif key in MYSQL_STATUS_VARS: ds_type = MYSQL_STATUS_VARS[key] else: continue dispatch_value('status', key, mysql_status[key], ds_type) mysql_variables = fetch_mysql_variables(conn) for key in mysql_variables: dispatch_value('variables', key, mysql_variables[key], 'gauge') mysql_master_status = fetch_mysql_master_stats(conn) for key in mysql_master_status: dispatch_value('master', key, mysql_master_status[key], 'gauge') mysql_states = fetch_mysql_process_states(conn) for key in mysql_states: dispatch_value('state', key, mysql_states[key], 'gauge') slave_status = fetch_mysql_slave_stats(conn) for key in slave_status: dispatch_value('slave', key, slave_status[key], 'gauge') response_times = fetch_mysql_response_times(conn) for key in response_times: dispatch_value('response_time_total', str(key), response_times[key]['total'], 'counter') dispatch_value('response_time_count', str(key), response_times[key]['count'], 'counter') innodb_status = fetch_innodb_stats(conn) for key in MYSQL_INNODB_STATUS_VARS: if key not in innodb_status: continue dispatch_value('innodb', key, innodb_status[key], MYSQL_INNODB_STATUS_VARS[key]) if COLLECTD_ENABLED: collectd.register_read(read_callback) collectd.register_config(configure_callback) if __name__ == "__main__" and not COLLECTD_ENABLED: print "Running in test mode, invoke with" print sys.argv[0] + " Host Port User Password " MYSQL_CONFIG['Host'] = sys.argv[1] MYSQL_CONFIG['Port'] = int(sys.argv[2]) MYSQL_CONFIG['User'] = sys.argv[3] MYSQL_CONFIG['Password'] = sys.argv[4] MYSQL_CONFIG['Verbose'] = True from pprint import pprint as pp pp(MYSQL_CONFIG) read_callback() # vim:noexpandtab ts=8 sw=8 sts=8