-
Notifications
You must be signed in to change notification settings - Fork 683
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
1 changed file
with
249 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()) |