Repository
Munin (contrib)
Last change
2020-10-14
Graph Categories
Family
auto
Capabilities
Keywords
Language
Python (3.x)

cronjobs

Name

cronjobs - Plugin to monitor the number of cronjobs running per user, gathering data from syslog.

Installation

Place in /etc/munin/plugins/ (or link it there using ln -s)

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.

Authors

Copyright (C) 2019 pcy <pcy.ulyssis.org>

Magic Markers

#%# family=auto
#%# capabilities=autoconf
#!/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()