from __future__ import annotations import threading from datetime import datetime, timezone from typing import Any class RuntimeStats: def __init__(self) -> None: self._lock = threading.Lock() self._data = self._new_counters() self._suricata: dict[str, int | float] = {} self._suricata_timestamp: str | None = None @staticmethod def _new_counters() -> dict[str, Any]: return { "tzsp_datagrams": 0, "tzsp_decode_errors": 0, "tzsp_unsupported": 0, "frames_injected": 0, "inject_errors": 0, "eve_events": 0, "eve_alerts": 0, "eve_parse_errors": 0, "alerts_filtered": 0, "alerts_filtered_low_priority": 0, "alerts_filtered_ignored_sid": 0, "alerts_filtered_ignored_category": 0, "alerts_deduplicated": 0, "block_attempts": 0, "block_success": 0, "block_errors": 0, "last_packet_at": None, "last_alert_at": None, } def inc(self, key: str, amount: int = 1) -> None: with self._lock: self._data[key] = int(self._data.get(key, 0)) + amount def stamp(self, key: str) -> None: with self._lock: self._data[key] = datetime.now(timezone.utc).isoformat() def update_suricata(self, stats: dict[str, Any], timestamp: str | None = None) -> None: flattened: dict[str, int | float] = {} _flatten_numeric("", stats, flattened, 240) with self._lock: self._suricata = flattened self._suricata_timestamp = timestamp or datetime.now(timezone.utc).isoformat() def reset(self) -> None: with self._lock: last_packet = self._data.get("last_packet_at") last_alert = self._data.get("last_alert_at") self._data = self._new_counters() self._data["last_packet_at"] = last_packet self._data["last_alert_at"] = last_alert def snapshot(self) -> dict: with self._lock: data = dict(self._data) data["suricata"] = dict(self._suricata) data["suricata_stats_at"] = self._suricata_timestamp datagrams = int(data.get("tzsp_datagrams", 0)) frames = int(data.get("frames_injected", 0)) data["tzsp_to_tap_loss"] = max(datagrams - frames, 0) attempts = int(data.get("block_attempts", 0)) success = int(data.get("block_success", 0)) data["block_success_rate"] = round((success / attempts) * 100.0, 2) if attempts else None return data def _flatten_numeric( prefix: str, value: Any, output: dict[str, int | float], limit: int, ) -> None: if len(output) >= limit: return if isinstance(value, dict): for key, child in value.items(): name = f"{prefix}.{key}" if prefix else str(key) _flatten_numeric(name, child, output, limit) if len(output) >= limit: return elif isinstance(value, (int, float)) and not isinstance(value, bool): output[prefix] = value