From ef6fe5340231eb3d23f3d6a7348a462e15c43007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BChler?= Date: Wed, 15 Nov 2023 17:29:59 +0100 Subject: [PATCH] add labels and conntrack stats to prometheus output --- src/capport/stats.py | 103 ++++++++++++++++++++++--------- stats-to-prometheus-collector.sh | 2 +- 2 files changed, 76 insertions(+), 29 deletions(-) diff --git a/src/capport/stats.py b/src/capport/stats.py index b7d396f..f0d8a34 100644 --- a/src/capport/stats.py +++ b/src/capport/stats.py @@ -1,7 +1,9 @@ from __future__ import annotations +import argparse +import dataclasses import ipaddress -import sys +import typing import trio @@ -11,18 +13,39 @@ import capport.utils.nft_set from . import cptypes -def print_metric(name: str, mtype: str, value, *, now: int | None = None, help: str | None = None): - # no labels in our names for now, always print help and type - if help: - print(f"# HELP {name} {help}") - print(f"# TYPE {name} {mtype}") - if now: - print(f"{name} {value} {now}") - else: - print(f"{name} {value}") +@dataclasses.dataclass(kw_only=True, slots=True) +class MetricPrinter: + args: CliArguments + now: int | None = None + _known_names: set[str] = dataclasses.field(default_factory=set, init=None) + _now_str: str = dataclasses.field(init=False) + _label_str: str = dataclasses.field(init=False) + + def __post_init__(self) -> None: + if not self.now is None: + self._now_str = f" {self.now}" + else: + self._now_str = "" + labels = [] + if self.args.instance: + labels.append(f"instance={self.args.instance!r}") + labels.append(f"interface={self.args.interface!r}") + self._label_str = "{" + ",".join(labels) + "}" + + def print_metric(self, *, name: str, mtype: str, value: typing.Any, help: str | None = None): + if not name in self._known_names: + self._known_names.add(name) + if help: + print(f"# HELP {name} {help}") + print(f"# TYPE {name} {mtype}") + print(f"{name}{self._label_str} {value} {self._now_str}") + + def print_gauge(self, name: str, value: int | float, help: str | None = None): + self.print_metric(name=name, mtype="gauge", value=value, help=help) -async def amain(client_ifname: str): +async def amain(args: CliArguments): + printer = MetricPrinter(args=args) ns = capport.utils.nft_set.NftSet() captive_allowed_entries: set[cptypes.MacAddress] = {entry["mac"] for entry in ns.list()} seen_allowed_entries: set[cptypes.MacAddress] = set() @@ -33,7 +56,7 @@ async def amain(client_ifname: str): unique_ipv6 = set() async with capport.utils.ipneigh.connect() as ipn: ipn.ip.strict_check = True - async for (mac, addr) in ipn.dump_neighbors(client_ifname): + async for (mac, addr) in ipn.dump_neighbors(args.interface): if mac in captive_allowed_entries: seen_allowed_entries.add(mac) unique_clients.add(mac) @@ -43,50 +66,74 @@ async def amain(client_ifname: str): else: total_ipv6 += 1 unique_ipv6.add(mac) - print_metric( + printer.print_gauge( "capport_allowed_macs", - "gauge", len(captive_allowed_entries), help="Number of allowed client mac addresses", ) - print_metric( + printer.print_gauge( "capport_allowed_neigh_macs", - "gauge", len(seen_allowed_entries), help="Number of allowed client mac addresses seen in neighbor cache", ) - print_metric( + printer.print_gauge( "capport_unique", - "gauge", len(unique_clients), help="Number of clients (mac addresses) in client network seen in neighbor cache", ) - print_metric( + printer.print_gauge( "capport_unique_ipv4", - "gauge", len(unique_ipv4), help="Number of IPv4 clients (unique per mac) in client network seen in neighbor cache", ) - print_metric( + printer.print_gauge( "capport_unique_ipv6", - "gauge", len(unique_ipv6), help="Number of IPv6 clients (unique per mac) in client network seen in neighbor cache", ) - print_metric( + printer.print_gauge( "capport_total_ipv4", - "gauge", total_ipv4, help="Number of IPv4 addresses seen in neighbor cache", ) - print_metric( + printer.print_gauge( "capport_total_ipv6", - "gauge", total_ipv6, help="Number of IPv6 addresses seen in neighbor cache", ) + with open('/proc/sys/net/netfilter/nf_conntrack_count') as f: + printer.print_gauge( + "nf_conntrack_count", + int(f.readline()), + help="Conntrack count", + ) + with open('/proc/sys/net/netfilter/nf_conntrack_max') as f: + printer.print_gauge( + "nf_conntrack_max", + int(f.readline()), + help="Conntrack max", + ) + + +@dataclasses.dataclass +class CliArguments: + interface: str + instance: str | None + + def __init__(self): + parser = argparse.ArgumentParser() + parser.add_argument( + "--interface", + required=True, + help="Name of network interface clients are connected to (to look for IP neighbors)", + ) + parser.add_argument("--instance", help="Name of instance (to tag metrics with)") + args = parser.parse_args() + + self.interface = args.interface + self.instance = args.instance def main(): - assert len(sys.argv) == 2, "Need name of client interface as argument" - trio.run(amain, sys.argv[1]) + args = CliArguments() + trio.run(amain, args) diff --git a/stats-to-prometheus-collector.sh b/stats-to-prometheus-collector.sh index 6b226a4..5fcab7e 100755 --- a/stats-to-prometheus-collector.sh +++ b/stats-to-prometheus-collector.sh @@ -16,7 +16,7 @@ fi targetname="/var/lib/prometheus/node-exporter/capport-${instance}.prom" tmpname="${targetname}.$$" -if ./stats.sh "${ifname}" > "${tmpname}"; then +if ./stats.sh --instance "${instance}" --interface "${ifname}" > "${tmpname}"; then mv "${tmpname}" "${targetname}" else rm "${tmpname}"