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