diff options
Diffstat (limited to 'overlays/nftables-prometheus-exporter/nftables-prometheus-exporter.py')
-rw-r--r-- | overlays/nftables-prometheus-exporter/nftables-prometheus-exporter.py | 151 |
1 files changed, 151 insertions, 0 deletions
diff --git a/overlays/nftables-prometheus-exporter/nftables-prometheus-exporter.py b/overlays/nftables-prometheus-exporter/nftables-prometheus-exporter.py new file mode 100644 index 00000000..c5c5139d --- /dev/null +++ b/overlays/nftables-prometheus-exporter/nftables-prometheus-exporter.py | |||
@@ -0,0 +1,151 @@ | |||
1 | #!@python@/bin/python | ||
2 | |||
3 | import json | ||
4 | |||
5 | from os import environ | ||
6 | import sys | ||
7 | |||
8 | from http.server import BaseHTTPRequestHandler, HTTPServer | ||
9 | |||
10 | from urllib.parse import urlparse | ||
11 | |||
12 | from textwrap import dedent | ||
13 | |||
14 | import subprocess | ||
15 | |||
16 | |||
17 | def _format_prom_attrs(**attrs): | ||
18 | if not attrs: | ||
19 | return '' | ||
20 | |||
21 | return '{' + ','.join(map(lambda k: f'{k}="{attrs[k]}"', attrs)) + '}' | ||
22 | |||
23 | def _format_prom_metrics(metricName, metricType, metrics, metricHelp=''): | ||
24 | metricStr = dedent(f''' | ||
25 | # HELP {metricName} {metricHelp} | ||
26 | # TYPE {metricName} {metricType} | ||
27 | ''').lstrip() | ||
28 | for (attrs, val) in metrics: | ||
29 | attrs_str = _format_prom_attrs(**attrs) | ||
30 | metricStr += dedent(f''' | ||
31 | {metricName}{attrs_str} {val} | ||
32 | ''').lstrip() | ||
33 | return metricStr | ||
34 | |||
35 | |||
36 | class NFTMetrics: | ||
37 | _instance = None | ||
38 | |||
39 | @classmethod | ||
40 | def instance(cls): | ||
41 | if cls._instance is None: | ||
42 | cls._instance = cls.__new__(cls) | ||
43 | cls._instance.attrs = None | ||
44 | return cls._instance | ||
45 | |||
46 | |||
47 | def __init__(self): | ||
48 | raise RuntimeError('Call instance() instead') | ||
49 | |||
50 | def update(self): | ||
51 | attrs = dict() | ||
52 | queries = dict() | ||
53 | |||
54 | for query_name in ['ruleset', 'counters', 'maps', 'meters', 'sets']: | ||
55 | process = subprocess.run( | ||
56 | ('nft', '--json', 'list', query_name), | ||
57 | capture_output = True, check = True, text = True | ||
58 | ) | ||
59 | data = json.loads(process.stdout) | ||
60 | version = data['nftables'][0]['metainfo']['json_schema_version'] | ||
61 | if version != 1: | ||
62 | raise RuntimeError(f'nftables json schema v{version} is not supported') | ||
63 | queries[query_name] = data['nftables'][1:] | ||
64 | |||
65 | |||
66 | def extract_query(query_name, type_name): | ||
67 | return [ | ||
68 | item[type_name] | ||
69 | for item in queries[query_name] | ||
70 | if type_name in item | ||
71 | ] | ||
72 | |||
73 | attrs['rules_count'] = len(extract_query('ruleset', 'rule')) | ||
74 | attrs['chain_count'] = len(extract_query('ruleset', 'chain')) | ||
75 | attrs['counters'] = extract_query('counters', 'counter') | ||
76 | attrs['maps'] = extract_query('maps', 'map') | ||
77 | attrs['meters'] = extract_query('meters', 'meter') | ||
78 | attrs['sets'] = extract_query('sets', 'set') | ||
79 | |||
80 | self.attrs = attrs | ||
81 | |||
82 | def json_text(self): | ||
83 | return json.dumps(self.attrs) | ||
84 | |||
85 | def prometheus(self): | ||
86 | metrics = '' | ||
87 | |||
88 | metrics += _format_prom_metrics('nftables_rules_count', 'gauge', [({}, self.attrs['rules_count'])], 'Number of nftables rules') | ||
89 | metrics += _format_prom_metrics('nftables_chains_count', 'gauge', [({}, self.attrs['chain_count'])], 'Number of nftables chains') | ||
90 | |||
91 | counter_bytes = [] | ||
92 | counter_packets = [] | ||
93 | for counter in self.attrs['counters']: | ||
94 | labels = { k: v for k, v in counter.items() if k not in set(['bytes', 'packets']) } | ||
95 | counter_bytes += [(labels, counter['bytes'])] | ||
96 | counter_packets += [(labels, counter['packets'])] | ||
97 | metrics += _format_prom_metrics('nftables_counter_bytes', 'counter', counter_bytes) | ||
98 | metrics += _format_prom_metrics('nftables_counter_packets_count', 'counter', counter_packets) | ||
99 | |||
100 | map_counts = [] | ||
101 | for meter in self.attrs['maps']: | ||
102 | labels = { k: v for k, v in counter.items() if k not in set(['elem']) } | ||
103 | map_counts += [(labels, len(meter['elem']))] | ||
104 | metrics += _format_prom_metrics('nftables_map_elem_count', 'gauge', map_counts) | ||
105 | |||
106 | meter_counts = [] | ||
107 | for meter in self.attrs['meters']: | ||
108 | labels = { k: v for k, v in counter.items() if k not in set(['elem']) } | ||
109 | meter_counts += [(labels, len(meter['elem']))] | ||
110 | metrics += _format_prom_metrics('nftables_meter_elem_count', 'gauge', meter_counts) | ||
111 | |||
112 | set_counts = [] | ||
113 | for meter in self.attrs['sets']: | ||
114 | labels = { k: v for k, v in counter.items() if k not in set(['elem']) } | ||
115 | set_counts += [(labels, len(meter['elem']))] | ||
116 | metrics += _format_prom_metrics('nftables_set_elem_count', 'gauge', set_counts) | ||
117 | |||
118 | return metrics.encode('utf-8') | ||
119 | |||
120 | class NFTMetricsServer(BaseHTTPRequestHandler): | ||
121 | def do_GET(self): | ||
122 | zte_metrics = NFTMetrics.instance() | ||
123 | zte_metrics.update() | ||
124 | |||
125 | url = urlparse(self.path) | ||
126 | |||
127 | match url.path: | ||
128 | case '/metrics.json': | ||
129 | self.send_response(200) | ||
130 | self.send_header("Content-type", "application/json") | ||
131 | self.end_headers() | ||
132 | |||
133 | self.wfile.write(zte_metrics.json_text().encode('utf-8')) | ||
134 | case '/metrics': | ||
135 | self.send_response(200) | ||
136 | self.send_header("Content-type", "text/plain") | ||
137 | self.end_headers() | ||
138 | |||
139 | self.wfile.write(zte_metrics.prometheus()) | ||
140 | case _: | ||
141 | self.send_response(404) | ||
142 | self.end_headers() | ||
143 | |||
144 | |||
145 | def main(): | ||
146 | webServer = HTTPServer((str(environ.get('NFT_HOSTNAME')), int(environ.get('NFT_PORT'))), NFTMetricsServer) | ||
147 | |||
148 | webServer.serve_forever() | ||
149 | |||
150 | if __name__ == "__main__": | ||
151 | sys.exit(main()) | ||