From 10ca12569acda3ccc36467a5b9943569cd3d8401 Mon Sep 17 00:00:00 2001 From: pcy Date: Mon, 20 Jul 2020 02:18:20 +0200 Subject: [PATCH] [plugins/mysql/mysql_audit] Added plugin for monitoring the audit log Shows the number of active over time --- plugins/mysql/mysql_audit | 189 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100755 plugins/mysql/mysql_audit diff --git a/plugins/mysql/mysql_audit b/plugins/mysql/mysql_audit new file mode 100755 index 00000000..1d5343a6 --- /dev/null +++ b/plugins/mysql/mysql_audit @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +# -*- python -*- + +""" +=head1 INTRODUCTION + +Plugin to monitor the MySQL audit log connection count + +=head1 APPLICABLE SYSTEMS + +MySQL needs to be configured manually in order to enable the audit log. This +can be done as follows: + +=over 2 + + plugin-load = server_audit=server_audit.so + server_audit_events = connect + server_audit_output_type=file + server_audit_file_path = /var/log/mysql/audit.log + server_audit_file_rotate_size = 512M + server_audit_logging = ON + server_audit_file_rotations = 5 + +=back + +=head1 INSTALLATION + +Place in /etc/munin/plugins/ (or link it there using ln -s) + +=head1 CONFIGURATION + +Add this to your /etc/munin/plugin-conf.d/munin-node: + +=over 2 + + [mysql_audit] + user mysql + env.logfile /var/log/mysql/audit.log + env.sorted 1 # sort output (if not: unsorted) + env.toplist 10 # only the top 10 (if unset: all of them). implies sorted + +=back + +=head1 HISTORY + +2017-11-03: v 1.0 Bert Van de Poel : created +2020-07-19: v 1.1 pcy : added config options + +=head1 USAGE + +Parameters understood: + + config (required) + autoconf (optional - used by munin-config) + +=head1 MAGIC MARKERS + +#%# family=auto +#%# capabilities=autoconf +""" + + +from datetime import datetime, timedelta, timezone +import operator +import os +import struct +import sys + + +def weakbool(x): + return x.lower().strip() in {'true', 'yes', 'y', '1'} + + +logfile = os.getenv('logfile', '/var/log/mysql/audit.log') +toplist = int(os.getenv('toplist', '0')) +sortlist = weakbool(os.getenv('sorted', 'N')) or toplist > 0 + +STATEFILE = os.getenv('MUNIN_STATEFILE') + + +def loadstate(): + if not os.path.isfile(STATEFILE): + return None + + with open(STATEFILE, 'rb') as f: + tstamp = struct.unpack('d', f.read())[0] + return datetime.fromtimestamp(tstamp, tz=timezone.utc) + + +def savestate(state): + with open(STATEFILE, 'wb') as f: + f.write(struct.pack('d', state.timestamp())) + + +def reverse_lines(filename, BUFSIZE=4096): + with open(filename, "r") as f: + f.seek(0, 2) + p = f.tell() + remainder = "" + while True: + sz = min(BUFSIZE, p) + p -= sz + f.seek(p) + buf = f.read(sz) + remainder + if '\n' not in buf: + remainder = buf + else: + i = buf.index('\n') + for L in buf[i + 1:].split("\n")[::-1]: + yield L + remainder = buf[:i] + if p == 0: + break + yield remainder + + +def get_data(do_save=True): + occurrences = {} + begin = datetime.now(timezone.utc) + begin_local = datetime.now() + + state = loadstate() + if state is None: + # need to do something here to prevent reading indefinitely + state = begin - timedelta(minutes=5) + + for line in reverse_lines(logfile): + if ',CONNECT,' not in line: + continue + + splitted = line.split(',') + key = splitted[2] + date = datetime.strptime(splitted[0], '%Y%m%d %H:%M:%S') + # hack to add timezone data to the datetime + date = begin + (date - begin_local) + + if date < state: + break + + occurrences[key] = occurrences.get(key, 0) + 1 + + if do_save: + savestate(begin) + + return occurrences + + +def autoconf(): + print("no (logfile not found)" if os.path.isfile(logfile) else "yes") + + +def configure(): + print('graph_title MySQL Audit connect count') + print('graph_vlabel Connections') + print('graph_category mysql') + + occurrences = get_data(False) + occitems = occurrences.items() + occitems = sorted(occitems, key=operator.itemgetter(1 if sortlist else 0), + reverse=sortlist) + if toplist > 0: + occitems = occitems[:toplist] + + for key, value in occitems: + print('{}.label {}'.format(key.lower(), key)) + print('{}.type GAUGE'.format(key.lower())) + print('{}.draw AREASTACK'.format(key.lower())) + + +def fetch(): + occurrences = get_data() + occitems = occurrences.items() + occitems = sorted(occitems, key=operator.itemgetter(1 if sortlist else 0), + reverse=sortlist) + if toplist > 0: + occitems = occitems[:toplist] + + for key, value in occitems: + print('{}.value {}'.format(key, value)) + + +if len(sys.argv) == 2 and sys.argv[1] == "autoconf": + autoconf() +elif len(sys.argv) == 2 and sys.argv[1] == "config": + configure() +else: + fetch() + +# flake8: noqa: E265