233 lines
5.9 KiB
Python
Executable File
233 lines
5.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# pylint: disable=invalid-name
|
|
# pylint: enable=invalid-name
|
|
# pylint: disable=consider-using-f-string
|
|
|
|
"""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': '',
|
|
},
|
|
'cookies': {
|
|
'title': 'cookies',
|
|
'vlabel': 'queries / second',
|
|
'info': '',
|
|
},
|
|
'rrl': {
|
|
'title': 'response rate limiting',
|
|
'vlabel': 'queries / 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', encoding='utf-8') as cache:
|
|
cache.write(output)
|
|
elif (
|
|
os.path.exists(cachename) and
|
|
os.stat(cachename).st_mtime > time.time() - 900
|
|
):
|
|
with open(cachename, 'rt', encoding='utf-8') as cache:
|
|
output = cache.read()
|
|
|
|
# Parse output. Keep graph labels in knotc-order.
|
|
values = defaultdict(dict)
|
|
for line in output.splitlines():
|
|
# Parse line to key1.key2 = value
|
|
if ' = ' not in line:
|
|
continue
|
|
key, value = line.split(' = ', 1)
|
|
if key.startswith('mod-stats.'):
|
|
# "mod-stats.server-operation[axfr] = 7"
|
|
key1, key2 = key[10:-1].split('[', 1)
|
|
elif key.startswith(('mod-cookies.', 'mod-rrl.')):
|
|
# "mod-cookies.presence = 94647"
|
|
key1, key2 = key[4:].split('.', 1)
|
|
else:
|
|
continue
|
|
|
|
# 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)
|