""" webui/api/logs.py - Log viewing API blueprint. Serves log content to the /logs page via HTMX endpoints: /api/logs/journal — systemd journal for vacuum-wall /api/logs/nginx/access — nginx access log tail /api/logs/nginx/error — nginx error log tail /api/logs/dnsmasq — systemd journal for dnsmasq /api/logs/app — Vacuum Wall application log file """ import logging import subprocess from pathlib import Path from flask import Blueprint, render_template_string logger = logging.getLogger(__name__) bp = Blueprint("logs", __name__) PROJECT_DIR = Path(__file__).resolve().parent.parent APP_LOG_FILE = PROJECT_DIR / "data" / "logs" / "vacuum-wall.log" _MAX_LINES = 200 def _tail_file(path: str, n: int = _MAX_LINES) -> str: """Return the last *n* lines of a file.""" try: with open(path) as f: lines = f.readlines() return "".join(lines[-n:]) except FileNotFoundError: return "(log file not found)\n" except PermissionError: return "(permission denied)\n" def _sudo_journalctl(unit: str, n: int = _MAX_LINES) -> str: """Run ``sudo journalctl -u --no-pager -n `` and return output.""" try: result = subprocess.run( ["sudo", "journalctl", "-u", unit, "--no-pager", "-n", str(n)], capture_output=True, text=True, timeout=10, ) output = result.stdout.strip() return output if output else f"(no journal entries for {unit})\n" except (subprocess.TimeoutExpired, FileNotFoundError) as exc: return f"(error reading journal: {exc})\n" _LOG_LINE_TEMPLATE = """\ {% for line in lines %}
{{ line | e }}
{% endfor %}""" def _render_log_lines(text: str) -> str: """Render raw log text into HTML fragment with line-by-line coloring.""" lines = text.rstrip("\n").split("\n") if text.strip() else [] return render_template_string(_LOG_LINE_TEMPLATE, lines=lines) # --------------------------------------------------------------------------- # Endpoints # --------------------------------------------------------------------------- @bp.route("/journal") def journal(): text = _sudo_journalctl("vacuum-wall") return _render_log_lines(text) @bp.route("/nginx/access") def nginx_access(): text = _tail_file("/var/log/nginx/access.log") return _render_log_lines(text) @bp.route("/nginx/error") def nginx_error(): text = _tail_file("/var/log/nginx/error.log") return _render_log_lines(text) @bp.route("/dnsmasq") def dnsmasq(): text = _sudo_journalctl("dnsmasq") return _render_log_lines(text) @bp.route("/app") def app_log(): text = _tail_file(str(APP_LOG_FILE)) return _render_log_lines(text)