"""Event model: DiagEvent, Severity, EventKind (SPECIFICATION §8). Every anomaly report is one :class:`DiagEvent`. ``time_utc`` is always the **local client receive time** (wall clock), so events stay comparable even when the remote host clock is wrong; device timestamps travel inside ``detail``, clearly labeled (SPECIFICATION §8). """ from __future__ import annotations import json from dataclasses import asdict, dataclass, field from datetime import datetime, timezone from enum import Enum from typing import Any def utc_now() -> datetime: """Timezone-aware current UTC time (the local receive clock).""" return datetime.now(timezone.utc) class Severity(str, Enum): INFO = "INFO" WARN = "WARN" CRITICAL = "CRITICAL" class EventKind(str, Enum): """Event kinds — one per detection rule D1–D15 (SPECIFICATION §6).""" CONNECTED = "CONNECTED" DISCONNECTED = "DISCONNECTED" # D1 RECONNECTED = "RECONNECTED" # D2 RECONNECT_FLAPPING = "RECONNECT_FLAPPING" # D3 SLOW_POLL = "SLOW_POLL" # D4 SLOW_POLL_STREAK = "SLOW_POLL_STREAK" # D5 LATENCY_TREND = "LATENCY_TREND" # D6 POLL_HANG = "POLL_HANG" # D7 QUALITY_BAD = "QUALITY_BAD" # D8 QUALITY_UNCERTAIN = "QUALITY_UNCERTAIN" # D9 QUALITY_BAD_PERSISTENT = "QUALITY_BAD_PERSISTENT" # D10 QUALITY_FLAPPING = "QUALITY_FLAPPING" # D11 STALE_TIMESTAMP = "STALE_TIMESTAMP" # D12 FROZEN_VALUE = "FROZEN_VALUE" # D13 CONTROL_LINK_DEGRADED = "CONTROL_LINK_DEGRADED" # D14 HEARTBEAT = "HEARTBEAT" # D15 DEVICE_LINK_DOWN = "DEVICE_LINK_DOWN" # D16 DEVICE_LINK_UP = "DEVICE_LINK_UP" # D16 recovery @dataclass(frozen=True) class EventContext: """Snapshot counters attached to every event (SPECIFICATION §8).""" uptime_s: float reconnects_total: int rtt_p95_ms: float | None = None tags_bad: int = 0 tags_total: int = 0 @dataclass(frozen=True) class DiagEvent: """One structured diagnostic record — JSONL-ready via :meth:`to_json`.""" time_utc: datetime kind: EventKind severity: Severity detail: dict[str, Any] = field(default_factory=dict) context: EventContext | None = None def to_json(self) -> str: """Serialize as one JSON line (SPECIFICATION §8, JSONL append-safe).""" if self.time_utc.tzinfo is None: raise ValueError("time_utc must be timezone-aware (use events.utc_now())") payload: dict[str, Any] = { "time_utc": _utc_z(self.time_utc), "kind": self.kind.value, "severity": self.severity.value, "detail": self.detail, "context": asdict(self.context) if self.context is not None else None, } return json.dumps(payload, ensure_ascii=False) def _utc_z(dt: datetime) -> str: """ISO-8601 UTC with millisecond precision and a Z suffix.""" return dt.astimezone(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")