""" Non-blocking TCP log handler. Sends JSON-lines to a remote server in a daemon background thread. Never blocks the main application — drops records when queue is full. """ import json import logging import queue import socket import threading import time from datetime import datetime, timezone class TCPLogHandler(logging.Handler): def __init__(self, host: str, port: int, service: str, timeout: float = 3.0): super().__init__() self.host = host self.port = port self.service = service self.timeout = timeout self._queue: queue.Queue[str] = queue.Queue(maxsize=2000) self._sock: socket.socket | None = None self._lock = threading.Lock() self._thread = threading.Thread(target=self._worker, daemon=True, name="tcp-log") self._thread.start() def emit(self, record: logging.LogRecord) -> None: try: entry = { "ts": datetime.now(tz=timezone.utc).isoformat(), "level": record.levelname, "service": self.service, "logger": record.name, "msg": self.format(record), } self._queue.put_nowait(json.dumps(entry, ensure_ascii=False) + "\n") except queue.Full: pass # drop — never block the caller def _connect(self) -> bool: try: sock = socket.create_connection((self.host, self.port), timeout=self.timeout) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) with self._lock: self._sock = sock return True except OSError: return False def _close_sock(self) -> None: with self._lock: if self._sock: try: self._sock.close() except OSError: pass self._sock = None def _worker(self) -> None: while True: line = self._queue.get() sent = False while not sent: with self._lock: sock = self._sock if sock is None: if not self._connect(): time.sleep(5) continue with self._lock: sock = self._sock try: sock.sendall(line.encode("utf-8")) # type: ignore[union-attr] sent = True except OSError: self._close_sock() time.sleep(2) def setup_tcp_logging(service: str, host: str, port: int) -> TCPLogHandler | None: """Attach TCP handler to root logger. Returns handler or None if disabled.""" if not host or not port: return None handler = TCPLogHandler(host=host, port=port, service=service) handler.setFormatter(logging.Formatter("%(message)s")) logging.getLogger().addHandler(handler) logging.getLogger().info("TCP log handler started → %s:%d", host, port) return handler