Skip to content

Commit b3e2576

Browse files
committed
kea: add Kea DHCP statistics collector
1 parent 7d6dc61 commit b3e2576

1 file changed

Lines changed: 71 additions & 0 deletions

File tree

igcollect/kea.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env python3
2+
"""igcollect - Kea DHCP
3+
4+
Copyright (c) 2026 InnoGames GmbH
5+
"""
6+
7+
import json
8+
import re
9+
from argparse import ArgumentParser, Namespace
10+
from time import time
11+
from urllib.request import Request, urlopen
12+
13+
14+
def parse_args() -> Namespace:
15+
parser = ArgumentParser()
16+
parser.add_argument('--prefix', default='kea')
17+
parser.add_argument('--host', default='localhost')
18+
parser.add_argument('--port', type=int, default=8000,
19+
help='Kea DHCP server HTTP port (default: 8000)')
20+
return parser.parse_args()
21+
22+
23+
def get_stats(host: str, port: int) -> dict:
24+
payload = json.dumps({
25+
'command': 'statistic-get-all',
26+
}).encode()
27+
req = Request(
28+
f'http://{host}:{port}/',
29+
data=payload,
30+
headers={'Content-Type': 'application/json'},
31+
)
32+
with urlopen(req, timeout=10) as response:
33+
result = json.loads(response.read())
34+
35+
# Kea supports sending commands for multiple services together, but we don't do it.
36+
# As a consequence, the responses are wrapped in a list with a single member.
37+
if isinstance(result, list):
38+
result = result[0]
39+
40+
if result.get('result') != 0:
41+
raise RuntimeError(f"Kea returned error: {result.get('text', 'unknown')}")
42+
43+
return result['arguments']
44+
45+
46+
def sanitize_metric(name: str) -> str:
47+
"""Convert a Kea stat name to a Graphite-compatible metric path.
48+
49+
Examples:
50+
pkt4-received -> pkt4_received
51+
subnet[1].assigned-addresses -> subnet.1.assigned_addresses
52+
subnet[1].pool[0].total-addresses -> subnet.1.pool.0.total_addresses
53+
subnet[1].pd-pool[0].total-pds -> subnet.1.pd_pool.0.total_pds
54+
"""
55+
name = re.sub(r'\[(\d+)\]', r'.\1', name)
56+
name = name.replace('-', '_')
57+
return name
58+
59+
60+
def main() -> None:
61+
args = parse_args()
62+
timestamp = int(time())
63+
64+
stats = get_stats(args.host, args.port)
65+
for name, samples in stats.items():
66+
value = samples[0][0]
67+
print(f'{args.prefix}.{sanitize_metric(name)} {value} {timestamp}')
68+
69+
70+
if __name__ == '__main__':
71+
main()

0 commit comments

Comments
 (0)