Repository
Munin (contrib)
Last change
2020-10-06
Graph Categories
Capabilities
Keywords
Language
Python (3.x)
License
GPL-3.0-only
Authors

znc_logs.py

Example graph: day

Name

znc_logs

Description

Shows lines/minute in today’s znc-logs

Configuration

[znc_logs]
user znc                              # or any other user/group that can read the znclog-folder
group znc
env.logdir /var/lib/znc/moddata/log/  # path to the GLOBAL log-folder with a "/" at the end
env.expire 0                          # Keep channel names forever  - OR -
env.expire 1                          # Forget channel names from last run

GNU General Public License v3.0 only

SPDX-License-Identifier: GPL-3.0-only

Author

Thor77 <thor77[at]thor77.org>

#!/usr/bin/python3
# -*- coding: utf-8 -*-
'''
=head1 NAME

znc_logs

=head1 DESCRIPTION

Shows lines/minute in today's znc-logs

=head2 CONFIGURATION

 [znc_logs]
 user znc                              # or any other user/group that can read the znclog-folder
 group znc
 env.logdir /var/lib/znc/moddata/log/  # path to the GLOBAL log-folder with a "/" at the end
 env.expire 0                          # Keep channel names forever  - OR -
 env.expire 1                          # Forget channel names from last run

=head1 COPYRIGHT

GNU General Public License v3.0 only

SPDX-License-Identifier: GPL-3.0-only

=head1 AUTHOR

Thor77 <thor77[at]thor77.org>

=cut
'''
import json
import os, sys, time
import re
import stat
import traceback

logdir = os.environ.get('logdir')
expire = os.environ.get('expire', 0)

if not logdir:
    raise Exception('You have to set the logdir with env.logdir <path to log> in the plugin-conf!')

date = time.strftime('%Y%m%d')
longdate = time.strftime('%Y-%m-%d')
last_values_file = os.environ['MUNIN_PLUGSTATE'] + '/znc_logs_last'

def get_last():
    try:
        d = {}
        with open(last_values_file, 'r') as f:
            d = json.load(f)
        return d
    except FileNotFoundError:
        return {}

def tail_open(filename, position=0):
    # Based on tail_open from perls' Munin::Plugin
    filereset = 0
    size = os.stat(filename)[stat.ST_SIZE]
    if size is None:
        return (undef, undef)
    f = open(filename, 'r', encoding='utf-8', errors='replace')
    if position > size:
        filereset = 1
    else:
        f.seek(position, 0)
        newpos = f.tell()
        if newpos != position:
            raise Exception
    return (f, filereset)

def tail_close(fh):
    position = fh.tell()
    fh.close()
    return position


last = get_last()
if "users" in last:
    user_list = last["users"]
else:
    user_list = {}
if "channels" in last:
    channel_list = last["channels"]
else:
    channel_list = {}
if "log_pos" in last:
    log_pos = last["log_pos"]
else:
    log_pos = {}

channel_stats = {}
user_stats = {}

def read_data(savestate=True):
    # Version 1.6 will change to directory-based filing, so walk recursively
    for (dirpath, dirnames, filenames) in os.walk(logdir):
        for filename in filenames:
            filename_ = filename.replace('.log', '')

            user, network, channel, file_date = (None, None, None, None)

            try:
                if len(dirpath) > len(logdir):
                    # We're below the log path, so this is a 1.6-style log
                    reldir = dirpath.replace(logdir + "/", '', 1)
                    try:
                        network, channel = reldir.split(os.sep)
                    except ValueError as e:
                        user, network, channel = reldir.split(os.sep)
                    file_date = filename_
                else:
                    try:
                        network, channel, file_date = filename_.split('_')
                    except ValueError as e:
                        user, network, channel, file_date = filename_.split('_')
            except ValueError as e:
                continue
            network_channel = '{}@{}'.format(channel, network)
            if network.lower() not in channel_list:
                channel_list[network.lower()] = {}
            if channel.startswith('#'):
                channel_list[network.lower()][channel.lower()] = network_channel
            user_list[user.lower()] = user
            # check if log is from today
            if (file_date == date or file_date == longdate):
                # current lines in the file
                (fh, r) = tail_open(os.path.join(dirpath,filename), log_pos.get(os.path.join(dirpath, filename), 0))
                current_value = 0
                while True:
                    where = fh.tell()
                    line = fh.readline()
                    if line.endswith('\n'):
                        current_value += 1
                    else:
                        # Incomplete last line
                        fh.seek(where, 0)
                        log_pos[os.path.join(dirpath, filename)] = tail_close(fh)
                        break

                if network_channel.lower() in channel_stats and channel.startswith('#'):
                    channel_stats[network_channel.lower()] += current_value
                else:
                    channel_stats[network_channel.lower()] = current_value

                if user is not None and user.lower() in user_stats:
                    user_stats[user.lower()] += current_value
                else:
                    user_stats[user.lower()] = current_value
    if savestate:
        savedata = {}
        if int(expire) == 0:
            savedata["users"] = user_list
            savedata["channels"] = channel_list
        savedata["log_pos"] = log_pos
        with open(last_values_file, 'w') as f:
            json.dump(savedata,f)


def emit_config():
    print('graph_title Lines in the ZNC-log')
    print('graph_category chat')
    print('graph_vlabel lines / ${graph_period}')
    print('graph_scale no')
    print('graph_args --base 1000 --lower-limit 0')
    print('graph_period minute')
    graph_order = []

    if os.getenv('MUNIN_CAP_DIRTYCONFIG') == "1":
        read_data(1)
    else:
        read_data(0)

    for network in channel_list.keys():
        for channel in channel_list[network].keys():

            # print things to munin
            network_channel = "{}_{}".format(network,channel).replace('.', '').replace('#', '').replace('@','_')
            print('{network_channel}.label {label}'.format(network_channel=network_channel, label=channel_list[network][channel]))
            print('{network_channel}.type  ABSOLUTE'.format(network_channel=network_channel))
            print('{network_channel}.min   0'.format(network_channel=network_channel))
            print('{network_channel}.draw  AREASTACK'.format(network_channel=network_channel))

            graph_order.append(network_channel)
    for user in user_list.keys():
            fuser = re.sub(r'^[^A-Za-z_]', '_', user)
            fuser = re.sub(r'[^A-Za-z0-9_]', '_', fuser)
            print('{fuser}.label User {user}'.format(fuser=fuser, user=user))
            print('{fuser}.type  ABSOLUTE'.format(fuser=fuser))
            print('{fuser}.min   0'.format(fuser=fuser))
            print('{fuser}.draw  LINE1'.format(fuser=fuser))

    print('graph_order {}'.format(" ".join(sorted(graph_order, key=str.lower))))

def emit_values():
    read_data(1)
    for network in channel_list.keys():
        for channel in channel_list[network].keys():

            # print things to munin
            key = channel_list[network][channel]
            network_channel = "{}_{}".format(network,channel).replace('.', '').replace('#', '').replace('@','_')
            if key.lower() in channel_stats:
                print('{network_channel}.value {value}'.format(network_channel=network_channel, value=channel_stats[key.lower()]))
            else:
                print('{network_channel}.value U'.format(network_channel=network_channel))
    for user in user_list.keys():
        fuser = re.sub(r'^[^A-Za-z_]', '_', user)
        fuser = re.sub(r'[^A-Za-z0-9_]', '_', fuser)
        if user.lower() in user_stats:
            print('{fuser}.value {value}'.format(fuser=fuser, value=user_stats[user.lower()]))
        else:
            print('{fuser}.value U'.format(fuser=fuser))


if len(sys.argv) > 1 and sys.argv[1] == 'config':
    emit_config()
    sys.exit(0)

emit_values()