156 lines
3.8 KiB
Python
Executable File
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()
|