diff options
-rw-r--r-- | overlays/cake-prometheus-exporter/cake-prometheus-exporter.py | 124 | ||||
-rw-r--r-- | overlays/cake-prometheus-exporter/default.nix | 29 |
2 files changed, 153 insertions, 0 deletions
diff --git a/overlays/cake-prometheus-exporter/cake-prometheus-exporter.py b/overlays/cake-prometheus-exporter/cake-prometheus-exporter.py new file mode 100644 index 00000000..07197149 --- /dev/null +++ b/overlays/cake-prometheus-exporter/cake-prometheus-exporter.py | |||
@@ -0,0 +1,124 @@ | |||
1 | #!@python@/bin/python | ||
2 | |||
3 | from os import environ | ||
4 | import sys | ||
5 | |||
6 | from http.server import BaseHTTPRequestHandler, HTTPServer | ||
7 | |||
8 | import subprocess | ||
9 | import json | ||
10 | |||
11 | from urllib.parse import urlparse | ||
12 | |||
13 | |||
14 | def _format_prom_attrs(**attrs): | ||
15 | if not attrs: | ||
16 | return '' | ||
17 | |||
18 | return '{' + ','.join(map(lambda k: f'{k}="{attrs[k]}"', attrs)) + '}' | ||
19 | |||
20 | def _format_prom_metrics(metricName, metricType, metrics, metricHelp=''): | ||
21 | metricStr = dedent(f''' | ||
22 | # HELP {metricName} {metricHelp} | ||
23 | # TYPE {metricName} {metricType} | ||
24 | ''').lstrip() | ||
25 | for (attrs, val) in metrics: | ||
26 | attrs_str = _format_prom_attrs(**attrs) | ||
27 | metricStr += dedent(f''' | ||
28 | {metricName}{attrs_str} {val} | ||
29 | ''').lstrip() | ||
30 | return metricStr | ||
31 | |||
32 | |||
33 | class CAKEMetrics: | ||
34 | _instance = None | ||
35 | |||
36 | @classmethod | ||
37 | def instance(cls): | ||
38 | if cls._instance is None: | ||
39 | cls._instance = cls.__new__(cls) | ||
40 | cls._instance.attrs = None | ||
41 | return cls._instance | ||
42 | |||
43 | def __init__(self): | ||
44 | raise RuntimeError('Call instance() instead') | ||
45 | |||
46 | def update(self): | ||
47 | attrs = dict() | ||
48 | |||
49 | tc_output = None | ||
50 | with subprocess.Popen(['tc', '-s', '-j', 'qdisc', 'show'], stdout=subprocess.PIPE) as proc: | ||
51 | tc_output = json.load(proc.stdout) | ||
52 | |||
53 | for qdisc in tc_output: | ||
54 | if 'kind' not in qdisc or qdisc['kind'] != 'cake': | ||
55 | continue | ||
56 | |||
57 | tin_names = [] | ||
58 | if len(qdisc['tins']) == 4: | ||
59 | tin_names = ['bulk', 'best-effort', 'video', 'voice'] | ||
60 | tins = {} | ||
61 | for tin_name, tin_data in zip(tin_names, qdics['tins']): | ||
62 | tins[tin_name] = { | ||
63 | 'bytes': tin_data['sent_bytes'], | ||
64 | 'packets': tin_data['sent_packets'] | ||
65 | } | ||
66 | |||
67 | attrs[qdisc['dev']] = { | ||
68 | 'bytes': qdisc['bytes'], | ||
69 | 'packets': qdisc['packets'], | ||
70 | 'tins': tins | ||
71 | } | ||
72 | |||
73 | self.attrs = attrs | ||
74 | |||
75 | def json_text(self): | ||
76 | return json.dumps(self.attrs) | ||
77 | |||
78 | def prometheus(self): | ||
79 | metrics = '' | ||
80 | |||
81 | metrics += _format_prom_metrics('cake_bytes', 'counter', [({'dev': dev}, self.attrs[dev]['bytes']) for dev in self.attrs]) | ||
82 | metrics += _format_prom_metrics('cake_packets', 'counter', [({'dev': dev}, self.attrs[dev]['packets']) for dev in self.attrs]) | ||
83 | |||
84 | metrics += _format_prom_metrics('cake_tin_bytes', 'counter', [({'dev': dev, 'tin': tin}, self.attrs[dev]['tins'][tin]['bytes']) for tin in self.attrs[dev]['tins'] for dev in self.attrs]) | ||
85 | metrics += _format_prom_metrics('cake_tin_packets', 'counter', [({'dev': dev, 'tin': tin}, self.attrs[dev]['tins'][tin]['packets']) for tin in self.attrs[dev]['tins'] for dev in self.attrs]) | ||
86 | |||
87 | return metrics.encode('utf-8') | ||
88 | |||
89 | |||
90 | class CAKEMetricsServer(BaseHTTPRequestHandler): | ||
91 | def log_message(self, format, *args): | ||
92 | pass | ||
93 | |||
94 | def do_GET(self): | ||
95 | cake_metrics = CAKEMetrics.instance() | ||
96 | cake_metrics.update() | ||
97 | |||
98 | url = urlparse(self.path) | ||
99 | |||
100 | match url.path: | ||
101 | case '/metrics.json': | ||
102 | self.send_response(200) | ||
103 | self.send_header("Content-type", "application/json") | ||
104 | self.end_headers() | ||
105 | |||
106 | self.wfile.write(cake_metrics.json_text().encode('utf-8')) | ||
107 | case '/metrics': | ||
108 | self.send_response(200) | ||
109 | self.send_header("Content-type", "text/plain") | ||
110 | self.end_headers() | ||
111 | |||
112 | self.wfile.write(cake_metrics.prometheus()) | ||
113 | case _: | ||
114 | self.send_response(404) | ||
115 | self.end_headers() | ||
116 | |||
117 | |||
118 | def main(): | ||
119 | webServer = HTTPServer((str(environ.get('CAKE_HOSTNAME')), int(environ.get('CAKE_PORT'))), CAKEMetricsServer) | ||
120 | |||
121 | webServer.serve_forever() | ||
122 | |||
123 | if __name__ == "__main__": | ||
124 | sys.exit(main()) | ||
diff --git a/overlays/cake-prometheus-exporter/default.nix b/overlays/cake-prometheus-exporter/default.nix new file mode 100644 index 00000000..3d0acc2d --- /dev/null +++ b/overlays/cake-prometheus-exporter/default.nix | |||
@@ -0,0 +1,29 @@ | |||
1 | { final, prev, ... }: | ||
2 | let | ||
3 | inpPython = final.python310.override {}; | ||
4 | in { | ||
5 | cake-prometheus-exporter = prev.stdenv.mkDerivation rec { | ||
6 | pname = "cake-prometheus-exporter"; | ||
7 | version = "0.0.0"; | ||
8 | |||
9 | src = ./cake-prometheus-exporter.py; | ||
10 | |||
11 | phases = [ "buildPhase" "checkPhase" "installPhase" ]; | ||
12 | |||
13 | python = inpPython.withPackages (ps: with ps; []); | ||
14 | |||
15 | buildPhase = '' | ||
16 | substituteAll $src cake-prometheus-exporter | ||
17 | ''; | ||
18 | |||
19 | doCheck = true; | ||
20 | checkPhase = '' | ||
21 | ${python}/bin/python -m py_compile cake-prometheus-exporter | ||
22 | ''; | ||
23 | |||
24 | installPhase = '' | ||
25 | install -m 0755 -D -t $out/bin \ | ||
26 | cake-prometheus-exporter | ||
27 | ''; | ||
28 | }; | ||
29 | } | ||