Repository
Munin (contrib)
Last change
2021-04-02
Graph Categories
Family
auto
Capabilities
Keywords
Language
Python (3.x)
License
GPL-2.0-only
Authors

knot

Name

knot - monitor Knot DNS server statistics

Applicable Systems

Systems with Knot DNS server installed.

Configuration

This plugin requires config:

[knot]
user root

Author

Kim B. Heino b@bbbs.net

License

GPLv2

Magic Markers

#%# family=auto
#%# capabilities=autoconf
#!/usr/bin/env python3
# pylint: disable=invalid-name
# pylint: enable=invalid-name

"""Munin plugin to monitor Knot DNS server.

=head1 NAME

knot - monitor Knot DNS server statistics

=head1 APPLICABLE SYSTEMS

Systems with Knot DNS server installed.

=head1 CONFIGURATION

This plugin requires config:

    [knot]
    user root

=head1 AUTHOR

Kim B. Heino <b@bbbs.net>

=head1 LICENSE

GPLv2

=head1 MAGIC MARKERS

 #%# family=auto
 #%# capabilities=autoconf

=cut
"""

import os
import subprocess
import sys
import time
from collections import defaultdict


CONFIG = {
    # 'edns-presence': {},
    # 'flag-presence': {},
    'query-size': {
        'title': 'query counts grouped by size',
        'vlabel': 'queries / second',
        'info': '',
    },
    'query-type': {
        'title': 'query types',
        'vlabel': 'queries / second',
        'info': '',
    },
    'reply-nodata': {
        'title': 'no-data replies',
        'vlabel': 'replies / second',
        'info': '',
    },
    'reply-size': {
        'title': 'reply counts grouped by size',
        'vlabel': 'replies / second',
        'info': '',
    },
    'request-bytes': {
        'title': 'request bytes',
        'vlabel': 'bytes / second',
        'info': '',
    },
    'request-protocol': {
        'title': 'request protocols',
        'vlabel': 'requests / second',
        'info': '',
    },
    'response-bytes': {
        'title': 'response bytes',
        'vlabel': 'bytes / second',
        'info': '',
    },
    'response-code': {
        'title': 'response codes',
        'vlabel': 'responses / second',
        'info': '',
    },
    'server-operation': {
        'title': 'operations',
        'vlabel': 'operations / second',
        'info': '',
    },
}


def _merge_replysize(values):
    """Merge reply-size 512..65535 stats."""
    if 'reply-size' not in values:
        return

    total = 0
    todel = []
    for key in values['reply-size']:
        if int(key.split('-')[0]) >= 512:
            total += values['reply-size'][key]
            todel.append(key)
    for key in todel:
        del values['reply-size'][key]
    values['reply-size']['512-65535'] = total


def get_stats():
    """Get statistics."""
    # Get status output
    try:
        output = subprocess.run(['knotc', '--force', 'stats'],
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE, check=False,
                                encoding='utf-8', errors='ignore').stdout
    except FileNotFoundError:
        return {}

    # After server reboot output can be almost empty. Use cached results
    # instead, needed for plugin config when using munin-async.
    cachename = os.path.join(os.getenv('MUNIN_PLUGSTATE'), 'knot.state')
    if len(output) > 2048:
        with open(cachename, 'wt') as cache:
            cache.write(output)
    elif (
            os.path.exists(cachename) and
            os.stat(cachename).st_mtime > time.time() - 900
    ):
        with open(cachename, 'rt') as cache:
            output = cache.read()

    # Parse output. Keep graph labels in knotc-order.
    values = defaultdict(dict)
    for line in output.splitlines():
        if not line.startswith('mod-stats.') or ' = ' not in line:
            continue

        # Parse key
        key, value = line.split(' = ', 1)
        key = key[10:-1]
        key1, key2 = key.split('[', 1)

        # Parse value
        try:
            values[key1][key2] = int(value)
        except ValueError:
            continue

    _merge_replysize(values)
    return values


def _clean_key(key):
    """Convert knotc key to Munin label."""
    key = key.lower().replace('-', '_')
    if key[0].isdigit():
        key = '_' + key
    return key


def print_config(values):
    """Print plugin config."""
    for key_graph in sorted(CONFIG):
        if key_graph not in values:
            continue

        # Basic data
        print('multigraph knot_{}'.format(key_graph.replace('-', '')))
        print('graph_title Knot {}'.format(CONFIG[key_graph]['title']))
        print('graph_vlabel {}'.format(CONFIG[key_graph]['vlabel']))
        info = CONFIG[key_graph]['info']
        if info:
            print('graph_info {}'.format(info))
        print('graph_category dns')
        print('graph_args --base 1000 --lower-limit 0')

        # Keys
        for key_raw in values[key_graph]:
            key_clean = _clean_key(key_raw)
            print('{}.label {}'.format(key_clean, key_raw))
            print('{}.type DERIVE'.format(key_clean))
            print('{}.min 0'.format(key_clean))

    if os.environ.get('MUNIN_CAP_DIRTYCONFIG') == '1':
        print_values(values)


def print_values(values):
    """Print plugin values."""
    for key_graph in sorted(CONFIG):
        if key_graph not in values:
            continue

        print('multigraph knot_{}'.format(key_graph.replace('-', '')))
        for key_raw in values[key_graph]:
            key_clean = _clean_key(key_raw)
            print('{}.value {}'.format(key_clean, values[key_graph][key_raw]))


def main(args):
    """Do it all main program."""
    values = get_stats()
    if len(args) > 1 and args[1] == 'autoconf':
        print('yes' if values else 'no (knot is not running)')
    elif len(args) > 1 and args[1] == 'config':
        print_config(values)
    else:
        print_values(values)


if __name__ == '__main__':
    main(sys.argv)