From 9af38423245ff275fea2ec6d4123d07c475f4858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Canna=C3=B2?= Date: Sun, 25 Sep 2016 23:49:24 +0000 Subject: [PATCH] Added ProxySQL collector for diamond --- diamond/ProxySQLCollector.conf | 7 ++ diamond/proxysqlstat.py | 191 +++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 diamond/ProxySQLCollector.conf create mode 100644 diamond/proxysqlstat.py diff --git a/diamond/ProxySQLCollector.conf b/diamond/ProxySQLCollector.conf new file mode 100644 index 000000000..12ddccf00 --- /dev/null +++ b/diamond/ProxySQLCollector.conf @@ -0,0 +1,7 @@ +enabled=True +db = None +host = 127.0.0.1 +passwd = stats +port = 6032 +user = stats + diff --git a/diamond/proxysqlstat.py b/diamond/proxysqlstat.py new file mode 100644 index 000000000..e74413cd5 --- /dev/null +++ b/diamond/proxysqlstat.py @@ -0,0 +1,191 @@ +# coding=utf-8 + +import diamond.collector +from diamond.collector import str_to_bool +import re +import time + +try: + import MySQLdb + from MySQLdb import MySQLError +except ImportError: + MySQLdb = None + MySQLError = ValueError + + +class ProxySQLCollector(diamond.collector.Collector): + + _GAUGE_KEYS = [ + 'Active_Transactions', + 'Client_Connections_connected', + 'ConnPool_memory_bytes', + 'MySQL_Monitor_Workers', + 'MySQL_Thread_Workers', + 'Query_Cache_Entries', + 'Query_Cache_Memory_bytes', + 'SQLite3_memory_bytes', + 'Server_Connections_connected', + 'mysql_backend_buffers_bytes', + 'mysql_frontend_buffers_bytes', + 'mysql_session_internal_bytes', + ] + + def __init__(self, *args, **kwargs): + super(ProxySQLCollector, self).__init__(*args, **kwargs) + + def process_config(self): + super(ProxySQLCollector, self).process_config() + if self.config['hosts'].__class__.__name__ != 'list': + self.config['hosts'] = [self.config['hosts']] + + # Move legacy config format to new format + if 'host' in self.config: + hoststr = "%s:%s@%s:%s/%s" % ( + self.config['user'], + self.config['passwd'], + self.config['host'], + self.config['port'], + self.config['db'], + ) + self.config['hosts'].append(hoststr) + + self.db = None + + def get_default_config_help(self): + config_help = super(ProxySQLCollector, self).get_default_config_help() + config_help.update({ + 'publish': + "Which rows of 'SHOW MYSQL STATUS' you would " + + "like to publish. Leave unset to publish all", + 'hosts': 'List of hosts to collect from. Format is ' + + 'yourusername:yourpassword@host:port/db[/nickname]' + + 'use db "None" to avoid connecting to a particular db' + }) + return config_help + + def get_default_config(self): + """ + Returns the default collector settings + """ + config = super(ProxySQLCollector, self).get_default_config() + config.update({ + 'path': 'proxysql', + # Connection settings + 'hosts': [], + + }) + return config + + def get_db_stats(self, query): + cursor = self.db.cursor(cursorclass=MySQLdb.cursors.DictCursor) + + try: + cursor.execute(query) + return cursor.fetchall() + except MySQLError, e: + self.log.error('ProxySQLCollector could not get db stats', e) + return () + + def connect(self, params): + try: + self.db = MySQLdb.connect(**params) + self.log.debug('ProxySQLCollector: Connected to database.') + except MySQLError, e: + self.log.error('ProxySQLCollector couldnt connect to database %s', e) + return False + return True + + def disconnect(self): + self.db.close() + + def get_db_global_status(self): + return self.get_db_stats('SHOW MYSQL STATUS') + + def get_stats(self, params): + metrics = {'status': {}} + + if not self.connect(params): + return metrics + + rows = self.get_db_global_status() + for row in rows: + try: + metrics['status'][row['Variable_name']] = float(row['Value']) + except: + pass + + self.disconnect() + + return metrics + + def _publish_stats(self, nickname, metrics): + + for key in metrics: + for metric_name in metrics[key]: + metric_value = metrics[key][metric_name] + + if type(metric_value) is not float: + continue + + if metric_name not in self._GAUGE_KEYS: + metric_value = self.derivative(nickname + metric_name, + metric_value) + if key == 'status': + if (('publish' not in self.config or + metric_name in self.config['publish'])): + self.publish(nickname + metric_name, metric_value) + else: + self.publish(nickname + metric_name, metric_value) + + def collect(self): + + if MySQLdb is None: + self.log.error('Unable to import MySQLdb') + return False + + for host in self.config['hosts']: + matches = re.search( + '^([^:]*):([^@]*)@([^:]*):?([^/]*)/([^/]*)/?(.*)', host) + + if not matches: + self.log.error( + 'Connection string not in required format, skipping: %s', + host) + continue + + params = {} + + params['host'] = matches.group(3) + try: + params['port'] = int(matches.group(4)) + except ValueError: + params['port'] = 3306 + params['db'] = matches.group(5) + params['user'] = matches.group(1) + params['passwd'] = matches.group(2) + + nickname = matches.group(6) + if len(nickname): + nickname += '.' + + if params['db'] == 'None': + del params['db'] + + try: + metrics = self.get_stats(params=params) + except Exception, e: + try: + self.disconnect() + except MySQLdb.ProgrammingError: + pass + self.log.error('Collection failed for %s %s', nickname, e) + continue + + # Warn if publish contains an unknown variable + if 'publish' in self.config and metrics['status']: + for k in self.config['publish'].split(): + if k not in metrics['status']: + self.log.error("No such key '%s' available, issue " + + "'show global status' for a full " + + "list", k) + self._publish_stats(nickname, metrics)