Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
598 lines (540 sloc) 17.5 KB
#!/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