111 lines
3.9 KiB
Python
111 lines
3.9 KiB
Python
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,
|
|
"tzsp_kernel_udp_drops": 0,
|
|
"tzsp_queue_drops": 0,
|
|
"tzsp_truncated_datagrams": 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,
|
|
"log_auto_truncations": 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_tzsp_receiver(self, values: dict[str, Any]) -> None:
|
|
"""Replace Rust receiver counters from its compact telemetry sample."""
|
|
with self._lock:
|
|
for key, value in values.items():
|
|
if key == "last_packet_at":
|
|
if value:
|
|
self._data[key] = value
|
|
continue
|
|
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
self._data[key] = value
|
|
|
|
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 metrics_snapshot(self) -> dict:
|
|
"""Return raw in-memory counters without derived calculations."""
|
|
with self._lock:
|
|
data = dict(self._data)
|
|
data["suricata"] = dict(self._suricata)
|
|
data["suricata_stats_at"] = self._suricata_timestamp
|
|
return data
|
|
|
|
def snapshot(self) -> dict:
|
|
data = self.metrics_snapshot()
|
|
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
|