hetest / logging_setup.py
jkkim
Add classifier/anonymization/learning pipeline + Genesis design + Lucide icons
8485d6e
"""Logging โ€” ๋กœํ…Œ์ดํŒ… ํŒŒ์ผ + in-memory ring buffer + ๋ ˆ๋ฒจ ํ•„ํ„ฐ.
UI ์˜ "๋กœ๊ทธ" ํŽ˜์ด์ง€๊ฐ€ `recent()` ๊ฒฐ๊ณผ๋ฅผ ํด๋งํ•ด์„œ ํ‘œ์‹œ.
ํŒŒ์ผ์€ `logs/app.log` (1MB ๋‹จ์œ„ ๋กค๋ง, ์ตœ๊ทผ 3๊ฐœ ๋ณด์กด).
"""
from __future__ import annotations
import logging
import logging.handlers
import time
from collections import deque
from pathlib import Path
from threading import Lock
LOG_DIR = Path(__file__).resolve().parent / "logs"
LOG_DIR.mkdir(exist_ok=True)
LOG_FILE = LOG_DIR / "app.log"
_BUFFER: deque = deque(maxlen=600)
_buf_lock = Lock()
LEVEL_RANK = {"DEBUG": 0, "INFO": 1, "WARNING": 2, "ERROR": 3, "CRITICAL": 4}
class MemoryHandler(logging.Handler):
"""In-memory ring โ€” UI ๊ฐ€ ์ฆ‰์‹œ ๊ฐ€์ ธ๊ฐˆ ์ˆ˜ ์žˆ๊ฒŒ dict ํ˜•ํƒœ๋กœ ์ €์žฅ."""
def emit(self, record: logging.LogRecord) -> None:
try:
msg = record.getMessage()
if record.exc_info:
# ๊ฐ„๋‹จํžˆ ํ•œ ์ค„ ์š”์•ฝ๋งŒ
msg += f" | {record.exc_info[0].__name__}: {record.exc_info[1]}"
except Exception:
msg = "<format error>"
with _buf_lock:
_BUFFER.append({
"ts": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(record.created)) +
f".{int(record.msecs):03d}",
"ts_unix": record.created,
"level": record.levelname,
"logger": record.name,
"module": record.module,
"line": record.lineno,
"message": msg,
})
def setup(level: str = "INFO") -> None:
root = logging.getLogger()
root.setLevel(level.upper())
# ์ค‘๋ณต ๋“ฑ๋ก ๋ฐฉ์ง€
for h in list(root.handlers):
if isinstance(h, MemoryHandler) or (
isinstance(h, logging.handlers.RotatingFileHandler)
and getattr(h, "baseFilename", "") == str(LOG_FILE)
):
root.removeHandler(h)
fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")
fh = logging.handlers.RotatingFileHandler(
LOG_FILE, maxBytes=1024 * 1024, backupCount=3, encoding="utf-8",
)
fh.setFormatter(fmt)
root.addHandler(fh)
mh = MemoryHandler()
root.addHandler(mh)
logging.getLogger("werkzeug").setLevel("WARNING") # ์š”์ฒญ ๋กœ๊ทธ ์ค„์ž„
def set_level(level: str) -> str:
level = level.upper()
if level not in LEVEL_RANK:
raise ValueError(f"unknown level: {level}")
logging.getLogger().setLevel(level)
return level
def get_level() -> str:
return logging.getLevelName(logging.getLogger().level)
def recent(level: str | None = None, limit: int = 200, contains: str | None = None) -> list[dict]:
"""์ตœ๊ทผ ๋กœ๊ทธ ์กฐํšŒ. level= 'INFO' ๋ฉด INFO ์ด์ƒ, contains= ๋ถ€๋ถ„๋ฌธ์ž์—ด ํ•„ํ„ฐ."""
with _buf_lock:
items = list(_BUFFER)
if level:
threshold = LEVEL_RANK.get(level.upper(), 0)
items = [r for r in items if LEVEL_RANK.get(r["level"], 0) >= threshold]
if contains:
c = contains.lower()
items = [r for r in items if c in r["message"].lower() or c in r["logger"].lower()]
return items[-limit:]
def buffer_size() -> int:
return len(_BUFFER)