From a79472bacccffab64af8d7feb66a114b023e68f5 Mon Sep 17 00:00:00 2001 From: pavel Date: Mon, 23 Mar 2026 18:08:48 +0200 Subject: [PATCH] 1808 --- files/nats_registration_listener.py | 145 +++++++++++++++------------- 1 file changed, 77 insertions(+), 68 deletions(-) diff --git a/files/nats_registration_listener.py b/files/nats_registration_listener.py index 444668c..4db4ffe 100644 --- a/files/nats_registration_listener.py +++ b/files/nats_registration_listener.py @@ -185,73 +185,84 @@ def extract_fields(obj: Dict[str, Any]): return product, mac, fw_active -def extract_event_age_s(obj: Dict[str, Any]) -> Optional[int]: - """ - Best-effort age of the registration event in seconds, derived from a timestamp - embedded in the payload if present. Returns None when no sane timestamp is found. - Surgical addition: no external calls, no broker queries. - """ - candidates = [] - - def _walk(x): - if isinstance(x, dict): - for k, v in x.items(): - kl = str(k).strip().lower() - if kl in {"ts", "timestamp", "event_ts", "event_timestamp", "time", "created_at", "createdat", "published_at", "publishedat"}: - candidates.append(v) - _walk(v) - elif isinstance(x, list): - for item in x: - _walk(item) - - def _to_epoch(v) -> Optional[int]: - try: - if isinstance(v, bool) or v is None: - return None - if isinstance(v, (int, float)): - n = float(v) - elif isinstance(v, str): - s = v.strip() - if not s: - return None - if s.endswith('Z'): - s = s[:-1] + '+00:00' - try: - return int(datetime.fromisoformat(s).timestamp()) - except Exception: - n = float(s) - else: - return None - - # milliseconds / microseconds / nanoseconds -> seconds - if n > 1e18: - n = n / 1e9 - elif n > 1e15: - n = n / 1e6 - elif n > 1e12: - n = n / 1e3 - - if 946684800 <= n <= 4102444800: - return int(n) - return None - except Exception: - return None - +def _parse_possible_event_epoch(value: Any) -> Optional[int]: try: - _walk(obj) - now_epoch = int(time.time()) - for raw in candidates: - epoch = _to_epoch(raw) - if epoch is None: - continue - age = now_epoch - epoch - if 0 <= age <= 86400 * 30: - return int(age) + if value is None: + return None + if isinstance(value, bool): + return None + if isinstance(value, (int, float)): + v = float(value) + if v > 1e12: + v = v / 1000.0 + if v > 0: + return int(v) + return None + if isinstance(value, str): + s = value.strip() + if not s: + return None + if s.isdigit(): + v = float(s) + if v > 1e12: + v = v / 1000.0 + if v > 0: + return int(v) + return None + s2 = s.replace("Z", "+00:00") + try: + return int(datetime.fromisoformat(s2).timestamp()) + except Exception: + pass + for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S%z", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S%z"): + try: + dt = datetime.strptime(s, fmt) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return int(dt.timestamp()) + except Exception: + pass except Exception: return None return None +def extract_registration_age_s(obj: Dict[str, Any]) -> Optional[int]: + candidate_keys = { + "timestamp", "ts", "time", "event_time", "eventtime", + "event_ts", "eventtimestamp", "created_at", "createdat", + "published_at", "publishedat", "sent_at", "sentat", + "received_at", "receivedat", + } + + def walk(node: Any) -> Optional[int]: + if isinstance(node, dict): + for k, v in node.items(): + ks = str(k).strip().lower().replace("-", "_") + if ks in candidate_keys: + parsed = _parse_possible_event_epoch(v) + if parsed is not None: + return parsed + for v in node.values(): + parsed = walk(v) + if parsed is not None: + return parsed + elif isinstance(node, list): + for item in node: + parsed = walk(item) + if parsed is not None: + return parsed + return None + + epoch = walk(obj) + if epoch is None: + return None + age_s = int(time.time()) - int(epoch) + if age_s < 0: + return 0 + return age_s + + # ========================= # NetBox lookup (urllib) # ========================= @@ -536,7 +547,7 @@ async def main(): "netbox_ms": None, "total_ms": None, "nb_problems": None, - "event_age_s": None, + "reg_age_s": None, } # strict payload dedupe @@ -557,7 +568,7 @@ async def main(): text = payload.decode("utf-8", errors="replace") obj = json.loads(text) product, mac, fw = extract_fields(obj) - event["event_age_s"] = extract_event_age_s(obj) + event["reg_age_s"] = extract_registration_age_s(obj) except Exception: pass @@ -940,11 +951,9 @@ async def main(): event["decision_reason"] = "mac not found in netbox" await log_event(event) - lag_suffix = "" - if isinstance(event.get("event_age_s"), int): - lag_suffix = f" event_age_s={event['event_age_s']}" - - line = f"nb_problems={nb_problems_snapshot} [{ts()}] product={product} mac={mac} fw={fw}{host_suffix}{action_suffix}{lag_suffix}" + reg_age_s = event.get("reg_age_s") + reg_age_suffix = f" reg_age_s={reg_age_s}" if reg_age_s is not None else " reg_age_s=na" + line = f"nb_problems={nb_problems_snapshot} [{ts()}] product={product} mac={mac} fw={fw}{host_suffix}{action_suffix}{reg_age_suffix}" if product == "fox100": line += f" netbox_ms={netbox_time_ms:.1f} total_ms={total_ms:.1f}" if args.include_subject: