add rotation

This commit is contained in:
larssand
2026-06-21 15:00:13 +02:00
parent 4594d080c3
commit 395a86e157
5 changed files with 101 additions and 3 deletions

View File

@@ -181,7 +181,14 @@ def fetch_policies(args: argparse.Namespace) -> int:
def listen_syslog(args: argparse.Namespace) -> int:
listen_udp_syslog(args.host, args.port, args.output, quiet=args.quiet)
listen_udp_syslog(
args.host,
args.port,
args.output,
rotate_bytes=args.rotate_bytes,
rotate_count=args.rotate_count,
quiet=args.quiet,
)
return 0
@@ -267,6 +274,8 @@ def build_parser() -> argparse.ArgumentParser:
listener.add_argument("--host", default="0.0.0.0", help="Bind address")
listener.add_argument("--port", type=int, default=5514, help="UDP port. Use 514 only with sudo/capability.")
listener.add_argument("--output", default="logs/fg_syslog.jsonl", help="File to append received logs to")
listener.add_argument("--rotate-bytes", type=int, default=25 * 1024 * 1024, help="Rotate active log at this size; 0 disables rotation")
listener.add_argument("--rotate-count", type=int, default=14, help="Number of compressed log archives to retain")
listener.add_argument("--quiet", action="store_true", help="Do not print each received syslog message")
listener.set_defaults(func=listen_syslog)

View File

@@ -1,24 +1,70 @@
from __future__ import annotations
import gzip
import socket
from datetime import datetime, timezone
from pathlib import Path
def listen_udp_syslog(host: str, port: int, output: str, *, max_bytes: int = 65535, quiet: bool = False) -> None:
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
handle.write(f"{message}\n")
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}")