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
125
126
127
128
129
130
|
#!@python@/bin/python
from os import environ
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
import subprocess
import json
from urllib.parse import urlparse
from textwrap import dedent
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, qdisc['tins']):
tins[tin_name] = {
'bytes': tin_data['sent_bytes'],
'packets': tin_data['sent_packets']
}
attrs[qdisc['dev']] = {
'bytes': qdisc['bytes'],
'packets': qdisc['packets'],
'drops': qdisc['drops'],
'overlimits': qdisc['overlimits'],
'requeues': qdisc['requeues'],
'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])
for packet_counter in ['packets', 'overlimits', 'requeues', 'drops']:
metrics += _format_prom_metrics(f'cake_{packet_counter}', 'counter', [({'dev': dev}, self.attrs[dev][packet_counter]) for dev in self.attrs])
metrics += _format_prom_metrics('cake_tin_bytes', 'counter', [({'dev': dev, 'tin': tin}, self.attrs[dev]['tins'][tin]['bytes']) for dev in self.attrs for tin in self.attrs[dev]['tins']])
metrics += _format_prom_metrics('cake_tin_packets', 'counter', [({'dev': dev, 'tin': tin}, self.attrs[dev]['tins'][tin]['packets']) for dev in self.attrs for tin in self.attrs[dev]['tins']])
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())
|