nginx_vts: plugin to monitor nginx statistics with nginx-module-vts

This plugin requires https://github.com/vozlt/nginx-module-vts module
to be loaded. It provides lot of statistics.
This commit is contained in:
Kim B. Heino 2023-03-09 12:40:34 +02:00
parent 05487ba694
commit 8c30611df5
1 changed files with 249 additions and 0 deletions

249
plugins/nginx/nginx_vts Executable file
View File

@ -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())