satdetect-dev / app /dda /applog.py
coderuday21's picture
Cursor
Deploy satdetect-dev with Priyanka audit/exception logs.
d70361b
Raw
History Blame Contribute Delete
3.67 kB
"""In-memory + file application log so the Logs page can view recent events."""
from __future__ import annotations
import logging
import threading
from collections import deque
from datetime import datetime, timezone
from pathlib import Path
from typing import List, Optional
from ..database import DATA_DIR
_LOCK = threading.Lock()
_BUFFER: deque = deque(maxlen=4000)
_HANDLER: Optional["RingLogHandler"] = None
LOG_DIR = DATA_DIR / "logs"
LOG_FILE = LOG_DIR / "app.log"
_LEVELS = {
"DEBUG": logging.DEBUG,
"INFO": logging.INFO,
"WARNING": logging.WARNING,
"ERROR": logging.ERROR,
"CRITICAL": logging.CRITICAL,
}
class RingLogHandler(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
try:
msg = self.format(record)
except Exception:
msg = record.getMessage()
created = datetime.fromtimestamp(record.created, tz=timezone.utc)
entry = {
"ts": created.isoformat(),
"level": record.levelname,
"logger": record.name,
"message": msg,
}
with _LOCK:
_BUFFER.append(entry)
def install_app_log_handler() -> None:
"""Attach once to the root logger (and uvicorn access) for the Logs UI."""
global _HANDLER
if _HANDLER is not None:
return
try:
LOG_DIR.mkdir(parents=True, exist_ok=True)
except OSError:
pass
handler = RingLogHandler(level=logging.INFO)
handler.setFormatter(logging.Formatter("%(message)s"))
_HANDLER = handler
root = logging.getLogger()
root.addHandler(handler)
if root.level > logging.INFO or root.level == logging.NOTSET:
root.setLevel(logging.INFO)
file_handler = None
try:
file_handler = logging.FileHandler(LOG_FILE, encoding="utf-8")
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(logging.Formatter(
"%(asctime)s %(levelname)s [%(name)s] %(message)s"
))
root.addHandler(file_handler)
except OSError:
file_handler = None
for name in ("uvicorn", "uvicorn.error", "uvicorn.access"):
logging.getLogger(name).addHandler(handler)
if file_handler:
logging.getLogger(name).addHandler(file_handler)
def query_logs(
*,
level: str = "",
q: str = "",
logger_name: str = "",
limit: int = 200,
offset: int = 0,
) -> dict:
min_level = _LEVELS.get((level or "").upper(), 0)
needle = (q or "").strip().lower()
logger_f = (logger_name or "").strip().lower()
with _LOCK:
rows = list(_BUFFER)
rows.reverse()
filtered: List[dict] = []
for row in rows:
if min_level and _LEVELS.get(row["level"], 0) < min_level:
continue
if logger_f and logger_f not in (row.get("logger") or "").lower():
continue
if needle and needle not in (row.get("message") or "").lower() and needle not in (row.get("logger") or "").lower():
continue
filtered.append(row)
total = len(filtered)
page = filtered[offset:offset + limit]
return {
"total": total,
"offset": offset,
"limit": limit,
"logs": page,
"file": str(LOG_FILE) if LOG_FILE.exists() else "",
}
def clear_logs() -> dict:
with _LOCK:
_BUFFER.clear()
truncated = False
try:
LOG_DIR.mkdir(parents=True, exist_ok=True)
LOG_FILE.write_text("", encoding="utf-8")
truncated = True
except OSError:
pass
return {"ok": True, "cleared": True, "fileTruncated": truncated}
def log_file_path() -> Path:
return LOG_FILE