diff --git a/plugins/nginx/nginx_vts b/plugins/nginx/nginx_vts new file mode 100755 index 00000000..655f76e5 --- /dev/null +++ b/plugins/nginx/nginx_vts @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 + +"""Munin plugin to monitor NGINX using nginx-module-vts statistics. + +=head1 NAME + +nginx_vts - monitor NGINX web server + +=head1 APPLICABLE SYSTEMS + +Systems with NGINX running and nginx-module-vts loaded. +See https://github.com/vozlt/nginx-module-vts for more information. + +=head1 CONFIGURATION + +This shows the default configuration of this plugin. You can override +the status URL. + + [nginx_vts] + env.url http://localhost/status/format/json + +=head1 LICENSE + +GPLv2 + +=head1 MAGIC MARKERS + + #%# family=auto + #%# capabilities=autoconf + +=cut +""" + +import json +import os +import pathlib +import sys +import time +import unicodedata +import requests + + +RESPONSES = ('1xx', '2xx', '3xx', '4xx', '5xx') + + +def safe_label(name): + """Return safe label name.""" + # Convert ä->a as isalpha('ä') is true + value = unicodedata.normalize('NFKD', name) + value = value.encode('ASCII', 'ignore').decode('utf-8') + + # Remove non-alphanumeric chars + value = ''.join(char.lower() if char.isalnum() else '_' for char in value) + + # Add leading "_" if it starts with number + if value[:1].isnumeric(): + return f'_{value}' + return value + + +def cleanup_name(name): + """Remove "unix:/" etc from name.""" + if name.startswith('unix:'): + name = name[5:] + if name.startswith('/'): + name = name.split('/')[-1] + return name + + +def read_data(): + """Get vts statistics from NGINX.""" + try: + response = requests.get(os.getenv( + 'url', 'http://localhost/status/format/json')) + data = response.json() + except (OSError, ValueError, TypeError): + return {} + + # Rename serverZones to vhost, easier to output graphs + data['vhost'] = data.get('serverZones', {}) + + # Group filters to single + data['filter'] = {} + for value in data.get('filterZones', {}).values(): + data['filter'].update(value) + + # Group upstreams to single + data['upstream'] = {} + for value in data.get('upstreamZones', {}).values(): + for server in value: + data['upstream'][server['server']] = server + + # Merge old seens from state file. Expire them in 30d if not seen + # again. NGINX restart will clear response items so keep track of + # old seen ones. + state = pathlib.Path(os.getenv('MUNIN_PLUGSTATE')) / 'nginx_vts.json' + try: + previous = json.loads(state.read_text('utf-8')) + except (FileNotFoundError, ValueError): + previous = {} + + now = int(time.time()) + for topic in ('vhost', 'filter', 'upstream'): + for key in data[topic]: + data[topic][key]['_last_seen'] = now + for key in previous.get(topic, {}): + if ( + key not in data[topic] and + previous[topic][key]['_last_seen'] > now - 86400 * 30 + ): + data[topic][key] = previous[topic][key] + + state.write_text(json.dumps(data, sort_keys=True, indent=4), 'utf-8') + return data + + +def config_group(data, topic): + """Print vhost/filter/upstream config.""" + if not data[topic]: + return + + # Same as nginx_requests plugin, but per vhost + total + print(f'multigraph nginx_vts_{topic}_requests') + print(f'graph_title NGINX {topic} requests') + print('graph_category webserver') + print('graph_vlabel Requests per ${graph_period}') + print('graph_args --base 1000') + for host in sorted(data[topic]): + if host == '*': + host = 'Total' + safe = safe_label(host) + print(f'{safe}.type DERIVE') + print(f'{safe}.min 0') + print(f'{safe}.label {cleanup_name(host)}') + print() + + # Similar to if_ plugin + print(f'multigraph nginx_vts_{topic}_traffic') + print(f'graph_title NGINX {topic} traffic') + print('graph_category webserver') + print('graph_vlabel bits in (-) / out (+) per ${graph_period}') + for host in sorted(data[topic]): + if host == '*': + host = 'Total' + safe = safe_label(host) + print(f'{safe}_down.label {host} received') + print(f'{safe}_down.type DERIVE') + print(f'{safe}_down.graph no') + print(f'{safe}_down.cdef {safe}_down,8,*') + print(f'{safe}_down.min 0') + print(f'{safe}_up.label {cleanup_name(host)[:15]}') + print(f'{safe}_up.type DERIVE') + print(f'{safe}_up.negative {safe}_down') + print(f'{safe}_up.cdef {safe}_up,8,*') + print(f'{safe}_up.min 0') + print() + + +def config(data): + """Print plugin config.""" + # Same as nginx_status plugin + print('multigraph nginx_vts_status') + print('graph_title NGINX status') + print('graph_category webserver') + print('graph_vlabel Connections') + print('graph_args --base 1000') + print('active.label Active connections') + print('reading.label Reading') + print('writing.label Writing') + print('waiting.label Waiting') + print() + + # Response codes + print('multigraph nginx_vts_responses') + print('graph_title NGINX responses') + print('graph_category webserver') + print('graph_vlabel Responses per ${graph_period}') + print('graph_args --base 1000') + for host in data['vhost']: + if host != '*': + continue + for key in RESPONSES: + safe = safe_label(key) + print(f'{safe}.type DERIVE') + print(f'{safe}.min 0') + print(f'{safe}.label Response {key}') + print() + + config_group(data, 'vhost') + config_group(data, 'filter') + config_group(data, 'upstream') + + if os.environ.get('MUNIN_CAP_DIRTYCONFIG') == '1': + fetch(data) + + +def fetch_group(data, topic): + """Print vhost/filter/upstream values.""" + if not data[topic]: + return + + print(f'multigraph nginx_vts_{topic}_requests') + for host, values in data[topic].items(): + if host == '*': + host = 'Total' + safe = safe_label(host) + print(f'{safe}.value {values["requestCounter"]}') + + print(f'multigraph nginx_vts_{topic}_traffic') + for host, values in data[topic].items(): + if host == '*': + host = 'Total' + safe = safe_label(host) + print(f'{safe}_up.value {values["outBytes"]}') + print(f'{safe}_down.value {values["inBytes"]}') + + +def fetch(data): + """Print values.""" + if 'connections' in data: + print('multigraph nginx_vts_status') + print(f'active.value {data["connections"]["active"]}') + print(f'reading.value {data["connections"]["reading"]}') + print(f'writing.value {data["connections"]["writing"]}') + print(f'waiting.value {data["connections"]["waiting"]}') + + print('multigraph nginx_vts_responses') + sums = {key: 0 for key in RESPONSES} + for host, values in data['vhost'].items(): + if host == '*': + continue + for key in sums: + sums[key] += values["responses"][key] + for key, value in sums.items(): + safe = safe_label(key) + print(f'{safe}.value {value}') + + fetch_group(data, 'vhost') + fetch_group(data, 'filter') + fetch_group(data, 'upstream') + + +if __name__ == '__main__': + if len(sys.argv) > 1 and sys.argv[1] == 'autoconf': + print('yes' if read_data() else 'no (no NGINX vts statistics)') + elif len(sys.argv) > 1 and sys.argv[1] == 'config': + config(read_data()) + else: + fetch(read_data())