summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--overlays/cake-prometheus-exporter/cake-prometheus-exporter.py124
-rw-r--r--overlays/cake-prometheus-exporter/default.nix29
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
3from os import environ
4import sys
5
6from http.server import BaseHTTPRequestHandler, HTTPServer
7
8import subprocess
9import json
10
11from urllib.parse import urlparse
12
13
14def _format_prom_attrs(**attrs):
15 if not attrs:
16 return ''
17
18 return '{' + ','.join(map(lambda k: f'{k}="{attrs[k]}"', attrs)) + '}'
19
20def _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
33class 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
90class 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
118def main():
119 webServer = HTTPServer((str(environ.get('CAKE_HOSTNAME')), int(environ.get('CAKE_PORT'))), CAKEMetricsServer)
120
121 webServer.serve_forever()
122
123if __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, ... }:
2let
3 inpPython = final.python310.override {};
4in {
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}