vk_hackathon/index/tcp_log_handler.py
q 6a25927813 Add in-process TCP log streaming to 185.33.228.73:9999 + logserver receiver
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 16:09:35 +03:00

90 lines
3 KiB
Python

"""
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