🎉 Initial commit

This commit is contained in:
Jonas Bengtsson
2026-10-07 10:38:21 +02:00
commit 1c64aeb071
38 changed files with 3391 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
"""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
@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")