Files
vacuum-wall/webui/api/logs.py
T
mteehan 37039351be fix htmx refactor route mismatches and remaining TODO items
- wireguard: POST /peers with JSON encoding (was /add-peer)
- rules: delete by rule_id in URL path (was JSON body); pass rule objects with id from server; add hx-disable to initial render
- nat: port forward delete uses URL path params to match blueprint
- nat: masquerade toggle uses native hx-post/hx-vals (was inline fetch)
- app.js renderers updated to use URL path deletes for rules and forwards
- remove TODO.md
2026-05-17 01:17:12 +00:00

100 lines
2.8 KiB
Python

"""
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 <unit> --no-pager -n <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 %}
<div class="log-line{% if 'ERROR' in line %} log-error{% elif 'WARN' in line %} log-warn{% endif %}">{{ line | e }}</div>
{% 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)