summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--hosts/vidhar/prometheus/default.nix44
-rw-r--r--overlays/nftables-prometheus-exporter/default.nix27
-rw-r--r--overlays/nftables-prometheus-exporter/nftables-prometheus-exporter.py151
3 files changed, 222 insertions, 0 deletions
diff --git a/hosts/vidhar/prometheus/default.nix b/hosts/vidhar/prometheus/default.nix
index f915fc68..87035d5d 100644
--- a/hosts/vidhar/prometheus/default.nix
+++ b/hosts/vidhar/prometheus/default.nix
@@ -142,6 +142,13 @@ in {
142 relabel_configs = relabelHosts; 142 relabel_configs = relabelHosts;
143 scrape_interval = "1s"; 143 scrape_interval = "1s";
144 } 144 }
145 { job_name = "nftables";
146 static_configs = [
147 { targets = ["localhost:9901"]; }
148 ];
149 relabel_configs = relabelHosts;
150 scrape_interval = "1s";
151 }
145 ]; 152 ];
146 }; 153 };
147 users.users.${config.services.prometheus.exporters.unbound.user} = { 154 users.users.${config.services.prometheus.exporters.unbound.user} = {
@@ -193,5 +200,42 @@ in {
193 format = "binary"; 200 format = "binary";
194 sopsFile = ./zte_10.141.1.3; 201 sopsFile = ./zte_10.141.1.3;
195 }; 202 };
203
204 systemd.services."prometheus-nftables-exporter" = {
205 wantedBy = [ "multi-user.target" ];
206 after = [ "network.target" ];
207 serviceConfig = {
208 Restart = "always";
209 PrivateTmp = true;
210 WorkingDirectory = "/tmp";
211 DynamicUser = true;
212 CapabilityBoundingSet = [""];
213 DeviceAllow = [""];
214 LockPersonality = true;
215 MemoryDenyWriteExecute = true;
216 NoNewPrivileges = true;
217 PrivateDevices = true;
218 ProtectClock = true;
219 ProtectControlGroups = true;
220 ProtectHome = true;
221 ProtectHostname = true;
222 ProtectKernelLogs = true;
223 ProtectKernelModules = true;
224 ProtectKernelTunables = true;
225 ProtectSystem = "strict";
226 RemoveIPC = true;
227 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
228 RestrictNamespaces = true;
229 RestrictRealtime = true;
230 RestrictSUIDSGID = true;
231 SystemCallArchitectures = "native";
232 UMask = "0077";
233 AmbientCapabilities = [ "CAP_NET_ADMIN" ];
234
235 Type = "simple";
236 ExecStart = "${pkgs.nftables-prometheus-exporter}/bin/nftables-prometheus-exporter";
237 Environment = "ZTE_HOSTNAME=localhost ZTE_PORT=9901";
238 };
239 };
196 }; 240 };
197} 241}
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 @@
1final: prev:
2let
3 inpPython = final.python310;
4in {
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
3import json
4
5from os import environ
6import sys
7
8from http.server import BaseHTTPRequestHandler, HTTPServer
9
10from urllib.parse import urlparse
11
12from textwrap import dedent
13
14import subprocess
15
16
17def _format_prom_attrs(**attrs):
18 if not attrs:
19 return ''
20
21 return '{' + ','.join(map(lambda k: f'{k}="{attrs[k]}"', attrs)) + '}'
22
23def _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
36class 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
120class 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
145def main():
146 webServer = HTTPServer((str(environ.get('NFT_HOSTNAME')), int(environ.get('NFT_PORT'))), NFTMetricsServer)
147
148 webServer.serve_forever()
149
150if __name__ == "__main__":
151 sys.exit(main())