diff options
author | Gregor Kleen <gkleen@yggdrasil.li> | 2022-01-01 16:51:10 +0100 |
---|---|---|
committer | Gregor Kleen <gkleen@yggdrasil.li> | 2022-01-01 16:51:10 +0100 |
commit | a806adad2017413071d20d519d9a5d9b6b937474 (patch) | |
tree | d6a23660977c0e78e770783058965d92de243dbd /overlays/nftables-prometheus-exporter | |
parent | c389674935494e1246d156515e25ead60551e705 (diff) | |
download | nixos-a806adad2017413071d20d519d9a5d9b6b937474.tar nixos-a806adad2017413071d20d519d9a5d9b6b937474.tar.gz nixos-a806adad2017413071d20d519d9a5d9b6b937474.tar.bz2 nixos-a806adad2017413071d20d519d9a5d9b6b937474.tar.xz nixos-a806adad2017413071d20d519d9a5d9b6b937474.zip |
vidhar: prometheus: nftables
Diffstat (limited to 'overlays/nftables-prometheus-exporter')
-rw-r--r-- | overlays/nftables-prometheus-exporter/default.nix | 27 | ||||
-rw-r--r-- | overlays/nftables-prometheus-exporter/nftables-prometheus-exporter.py | 151 |
2 files changed, 178 insertions, 0 deletions
diff --git a/overlays/nftables-prometheus-exporter/default.nix b/overlays/nftables-prometheus-exporter/default.nix new file mode 100644 index 00000000..452f160f --- /dev/null +++ b/overlays/nftables-prometheus-exporter/default.nix | |||
@@ -0,0 +1,27 @@ | |||
1 | final: prev: | ||
2 | let | ||
3 | inpPython = final.python310; | ||
4 | in { | ||
5 | nftables-prometheus-exporter = prev.stdenv.mkDerivation rec { | ||
6 | name = "nftables-prometheus-exporter"; | ||
7 | src = ./nftables-prometheus-exporter.py; | ||
8 | |||
9 | phases = [ "buildPhase" "checkPhase" "installPhase" ]; | ||
10 | |||
11 | python = inpPython.withPackages (ps: with ps; []); | ||
12 | |||
13 | buildPhase = '' | ||
14 | substituteAll $src nftables-prometheus-exporter | ||
15 | ''; | ||
16 | |||
17 | doCheck = true; | ||
18 | checkPhase = '' | ||
19 | ${python}/bin/python -m py_compile nftables-prometheus-exporter | ||
20 | ''; | ||
21 | |||
22 | installPhase = '' | ||
23 | install -m 0755 -D -t $out/bin \ | ||
24 | nftables-prometheus-exporter | ||
25 | ''; | ||
26 | }; | ||
27 | } | ||
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()) | ||