mirror of
https://github.com/munin-monitoring/contrib.git
synced 2025-07-25 10:28:36 +00:00
Sometimes after reboot munin-async + munin-node runs before knot is ready. This will result missing knot stats, as there is no static config in plugin. Cache results and use them instead if knot's output is empty.
216 lines
5.3 KiB
Python
Executable file
216 lines
5.3 KiB
Python
Executable file
#!/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.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)
|