From 271e21d34e3ae1f5ad4c9d58558ba939b6377d24 Mon Sep 17 00:00:00 2001 From: pcy Date: Thu, 16 Jul 2020 00:19:52 +0200 Subject: [PATCH] [plugin/user/cronjobs] added plugin to monitor cronjobs Graphs the number of cronjobs active at a certain moment, per user --- plugins/user/cronjobs | 165 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100755 plugins/user/cronjobs diff --git a/plugins/user/cronjobs b/plugins/user/cronjobs new file mode 100755 index 00000000..12859d8f --- /dev/null +++ b/plugins/user/cronjobs @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +# -*- python -*- +""" +=head1 NAME + +Plugin to monitor the number of cronjobs running per user, gathering data from +syslog. + +=head1 INSTALLATION + +Usage: 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 + + [cronjobs] + user root + env.syslog_path /var/log/syslog # default value + env.cron_ident_regex crond? # for finding cron entries in the syslog, + case-insensitive (default value) + +=back + +The plugin needs to run as root in order to read from syslog. + +=head1 HISTORY + +2019-09-09: v 1.0 pcy : created + +=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 os +import re +import shutil +import socket +import sys +import struct +import time + + +syslog_path = os.getenv('syslog_path', '/var/log/syslog') +cron_ident_regex = os.getenv('cron_ident_regex', 'crond?') + +# expected format: +# CRON[]: () CMD ... +# example: +# Aug 16 22:00:01 zap CRON[23060]: (root) CMD (/usr/local/bin/dnsconfig -s) +cron_syslog_regex = re.compile('^.* %s %s\[[0-9]*\]: \((.*?)\)' # noqa: W605 + % (socket.gethostname(), cron_ident_regex), + re.I) + +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 extrcronuser(line: str): + t = cron_syslog_regex.search(line) + if t is None: + return None + gr = t.groups() + return gr[0] if len(gr) == 1 else None + + +def getcronlines(): + with open(syslog_path, 'r') as f: + for x in f.readlines(): + user = extrcronuser(x) + if user is not None: + yield (x, user) + + +def getcronusers(lines): + return set(x[1] for x in lines) + + +def autoconf(): + if shutil.which('crontab') is None: + print("no (need cron installed)") + elif not os.access(syslog_path, os.R_OK): + print("no (cannot access syslog file '%s')" % syslog_path) + else: + print("yes") + + +def config(): + usernames = getcronusers(getcronlines()) + print("""\ +graph_title Cron jobs per user +graph_vlabel jobs +graph_category processes""") + for n in usernames: + print(n + ".label " + n) + print(n + ".info jobs of user " + n) + + +def fetch(): + STATE = loadstate() + # why is there no stdlib function for this?! + localtz = timezone(timedelta(seconds=time.localtime().tm_gmtoff)) + now = datetime.now() + now_withtz = datetime.now(tz=localtz) + yearsfx = ' ' + str(now.year) + pyearsfx = ' ' + str(now.year - 1) + cronlines = list(getcronlines()) + allnames = getcronusers(cronlines) + counts = {} + + hostname = socket.gethostname() + for ln, name in cronlines: + datestr = ln[:ln.index(hostname)].strip() + logdate = datetime.strptime(datestr + yearsfx, "%b %d %H:%M:%S %Y") + if logdate > now: + logdate = datetime.strptime(datestr + pyearsfx, "%b %d %H:%M:%S %Y") + # add timezone info (ugly hack), as strptime doesn't want to do this + logdate = now_withtz + (logdate - now) + if STATE is None or logdate > STATE: + counts[name] = (counts[name] + 1) if name in counts else 1 + + for n in allnames: + if n in counts: + print("%s.value %d" % (n, counts[n])) + else: + print("%s.value 0" % n) + + savestate(now_withtz) + + +if len(sys.argv) >= 2: + if sys.argv[1] == 'autoconf': + autoconf() + elif sys.argv[1] == 'config': + config() + else: + fetch() +else: + fetch()