admin / Syanpse-Vanguard
public
Syanpse-Vanguard / synapse-vanguard-v3 / agents / linux / sv_agent_linux.py
4307 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | """Synapse-Vanguard Linux agent. Collectors (each on its own thread, appending to the shared disk buffer): • journald — `journalctl -o json -f` follow stream • syslog — tail of traditional syslog files • auditd — tail of /var/log/audit/audit.log Package as a standalone executable with: pyinstaller --onefile --name sv-agent-linux \ --collect-submodules agents agents/linux/sv_agent_linux.py """ from __future__ import annotations import json import logging import subprocess import threading import time from datetime import datetime, timezone from agents.common.buffer import DiskBuffer from agents.common.config import AgentConfig from agents.common.sender import Sender log = logging.getLogger("sv.agent.linux") def _emit(buffer: DiskBuffer, source_type: str, host: str, message: str, fields: dict, ts: datetime | None = None) -> None: buffer.append({ "source_type": source_type, "host": host, "ts": (ts or datetime.now(timezone.utc)).isoformat(), "message": message, "fields": fields, }) def collect_journald(cfg: AgentConfig, buffer: DiskBuffer) -> None: cmd = ["journalctl", "-o", "json", "-f", "--no-pager"] while True: try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True) for line in proc.stdout: # type: ignore[union-attr] line = line.strip() if not line: continue try: rec = json.loads(line) except json.JSONDecodeError: continue ts = _journald_ts(rec.get("__REALTIME_TIMESTAMP")) _emit(buffer, "journald", cfg.hostname, rec.get("MESSAGE", ""), rec, ts) except FileNotFoundError: log.warning("journalctl not found; journald collector disabled") return except Exception as exc: # restart the follow on error log.warning("journald collector error, retrying: %s", exc) time.sleep(3) def tail_file(cfg: AgentConfig, buffer: DiskBuffer, path: str, source_type: str) -> None: """Simple tail -F: follow appends, reopen on truncation/rotation.""" while True: try: with open(path, "r", encoding="utf-8", errors="replace") as fh: fh.seek(0, 2) # start at EOF while True: line = fh.readline() if not line: time.sleep(0.5) # detect rotation: file shrank if fh.tell() > _size(path): break continue _emit(buffer, source_type, cfg.hostname, line.rstrip("\n"), {"path": path}) except FileNotFoundError: time.sleep(5) except Exception as exc: log.warning("tail %s error: %s", path, exc) time.sleep(3) def main() -> None: logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s") cfg = AgentConfig.load() cfg.platform = "linux" if not cfg.channels: cfg.channels = ["journald", "syslog", "auditd"] buffer = DiskBuffer(cfg.buffer_path) threads = [threading.Thread(target=collect_journald, args=(cfg, buffer), daemon=True)] for p in cfg.syslog_paths: threads.append(threading.Thread(target=tail_file, args=(cfg, buffer, p, "syslog"), daemon=True)) threads.append(threading.Thread(target=tail_file, args=(cfg, buffer, cfg.auditd_path, "auditd"), daemon=True)) for t in threads: t.start() Sender(cfg, buffer).run_forever() def _journald_ts(us) -> datetime | None: try: return datetime.fromtimestamp(int(us) / 1_000_000, tz=timezone.utc) except (TypeError, ValueError): return None def _size(path: str) -> int: import os try: return os.path.getsize(path) except OSError: return 0 if __name__ == "__main__": main() |