Created
February 1, 2026 15:30
-
-
Save todeveni/39a82eea2a16d2784053765452e81f23 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment