diff options
| -rw-r--r-- | hosts/vidhar/prometheus/default.nix | 44 | ||||
| -rw-r--r-- | overlays/nftables-prometheus-exporter/default.nix | 27 | ||||
| -rw-r--r-- | overlays/nftables-prometheus-exporter/nftables-prometheus-exporter.py | 151 |
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 @@ | |||
| 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()) | ||
