"""UDP JSON event publisher on ``127.0.0.1:7001`` (configurable). Contract: one serialized ``Event`` per UDP datagram. Events larger than ``max_datagram_bytes`` are dropped and counted in ``stats.udp_events_oversize`` (no fragmentation). """ from __future__ import annotations import asyncio import json import logging from typing import Any from ..config import UdpEventsCfg from ..core.bus import EventBus from ..core.models import Event from ..core.stats import Stats log = logging.getLogger(__name__) class UdpEventsPublisher: def __init__(self, cfg: UdpEventsCfg, bus: EventBus, stats: Stats) -> None: self._cfg = cfg self._bus = bus self._stats = stats self._transport: asyncio.DatagramTransport | None = None self._sub: asyncio.Queue[Event] | None = None async def run(self) -> None: loop = asyncio.get_running_loop() # Ephemeral sender socket; we only *send* to a fixed target. transport, _ = await loop.create_datagram_endpoint( lambda: asyncio.DatagramProtocol(), remote_addr=(self._cfg.host, self._cfg.port), ) self._transport = transport self._sub = self._bus.subscribe(maxsize=1024) log.info("udp_events publisher -> %s:%d", self._cfg.host, self._cfg.port) try: while True: ev = await self._sub.get() self._send(ev) except asyncio.CancelledError: raise finally: if self._sub is not None: self._bus.unsubscribe(self._sub) self._sub = None if self._transport is not None: self._transport.close() self._transport = None def _send(self, ev: Event) -> None: try: blob = json.dumps(ev.to_dict(), ensure_ascii=False, separators=(",", ":")).encode("utf-8") except Exception: self._stats.incr("udp_events_serialize_errors") return if len(blob) > self._cfg.max_datagram_bytes: self._stats.incr("udp_events_oversize") return try: assert self._transport is not None self._transport.sendto(blob) self._stats.incr("udp_events_sent") except Exception: self._stats.incr("udp_events_send_errors") log.debug("udp_events send failed", exc_info=True) __all__ = ["UdpEventsPublisher"]