munin-contrib/plugins/user/cronjobs

156 lines
3.8 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- python -*-
"""
=head1 NAME
cronjobs - Plugin to monitor the number of cronjobs running per user, gathering data from syslog.
=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:
[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
The plugin needs to run as root in order to read from syslog.
=head1 AUTHORS
Copyright (C) 2019 pcy <pcy.ulyssis.org>
=head1 MAGIC MARKERS
#%# family=auto
#%# capabilities=autoconf
=cut
"""
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:
# <date> <hostname> CRON[<pid>]: (<user>) 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()