From fc1f2726a0914ea1363203c01d9b4c2757f43344 Mon Sep 17 00:00:00 2001 From: pavel Date: Mon, 23 Mar 2026 18:02:18 +0200 Subject: [PATCH] 1802 --- files/nats_registration_listener.py | 75 ++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/files/nats_registration_listener.py b/files/nats_registration_listener.py index 9f03dbf..444668c 100644 --- a/files/nats_registration_listener.py +++ b/files/nats_registration_listener.py @@ -185,6 +185,73 @@ 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 + + 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) + except Exception: + return None + return None + + # ========================= # NetBox lookup (urllib) # ========================= @@ -469,6 +536,7 @@ async def main(): "netbox_ms": None, "total_ms": None, "nb_problems": None, + "event_age_s": None, } # strict payload dedupe @@ -489,6 +557,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) except Exception: pass @@ -871,7 +940,11 @@ async def main(): event["decision_reason"] = "mac not found in netbox" await log_event(event) - line = f"nb_problems={nb_problems_snapshot} [{ts()}] product={product} mac={mac} fw={fw}{host_suffix}{action_suffix}" + 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}" if product == "fox100": line += f" netbox_ms={netbox_time_ms:.1f} total_ms={total_ms:.1f}" if args.include_subject: