from __future__ import annotations import gzip import socket from datetime import datetime, timezone from pathlib import Path def rotate_log_file(output_path: Path, *, max_archives: int) -> Path | None: """Archive the active syslog file as gzip and retain only recent archives.""" if not output_path.exists() or output_path.stat().st_size == 0: return None timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") archive = output_path.with_name(f"{output_path.name}.{timestamp}.gz") suffix = 1 while archive.exists(): archive = output_path.with_name(f"{output_path.name}.{timestamp}.{suffix}.gz") suffix += 1 with output_path.open("rb") as source, gzip.open(archive, "wb") as destination: while chunk := source.read(1024 * 1024): destination.write(chunk) output_path.unlink() archives = sorted(output_path.parent.glob(f"{output_path.name}.*.gz"), key=lambda path: path.stat().st_mtime, reverse=True) for stale_archive in archives[max(0, max_archives):]: stale_archive.unlink() return archive def listen_udp_syslog( host: str, port: int, output: str, *, max_bytes: int = 65535, rotate_bytes: int = 25 * 1024 * 1024, rotate_count: int = 14, quiet: bool = False, ) -> None: output_path = Path(output) output_path.parent.mkdir(parents=True, exist_ok=True) if rotate_bytes < 0 or rotate_count < 0: raise ValueError("rotation settings must be zero or positive") sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((host, port)) print(f"Listening for UDP syslog on {host}:{port}") print(f"Writing logs to {output_path}") if rotate_bytes: print(f"Rotation: {rotate_bytes} bytes, retaining {rotate_count} gzip archives") with output_path.open("a", encoding="utf-8", buffering=1) as handle: while True: data, address = sock.recvfrom(max_bytes) message = data.decode("utf-8", errors="replace").strip() if not message: continue line = f"{message}\n" line_bytes = len(line.encode("utf-8")) if rotate_bytes and output_path.stat().st_size + line_bytes > rotate_bytes: handle.close() archive = rotate_log_file(output_path, max_archives=rotate_count) if archive and not quiet: print(f"Rotated syslog to {archive}") handle = output_path.open("a", encoding="utf-8", buffering=1) handle.write(line) if not quiet: print(f"{address[0]}:{address[1]} {message}")