Files
ai_site/ai-service/generate_log.py
2026-07-31 10:31:17 +08:00

174 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""蓝图生成过程日志:控制台 + 落盘,便于对账。
环境变量:
- GENERATE_LOG_DIR日志目录默认仓库 .runtime/logs/generate
- GENERATE_LOG_LEVELDEBUG/INFO/WARNING默认 INFO
"""
from __future__ import annotations
import logging
import os
import threading
import uuid
from contextvars import ContextVar
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
_root = Path(__file__).resolve().parents[1]
_LOGGER_NAME = "aijianzhan.generate"
_setup_lock = threading.Lock()
_configured = False
_current: ContextVar["GenerateTrace | None"] = ContextVar("generate_trace", default=None)
def _log_dir() -> Path:
raw = (os.getenv("GENERATE_LOG_DIR") or "").strip()
if raw:
return Path(raw).expanduser().resolve()
return (_root / ".runtime" / "logs" / "generate").resolve()
def _level() -> int:
name = (os.getenv("GENERATE_LOG_LEVEL") or "INFO").strip().upper()
return getattr(logging, name, logging.INFO)
def ensure_logging() -> logging.Logger:
"""幂等配置stdout + 按日滚动的 generate.log。"""
global _configured
logger = logging.getLogger(_LOGGER_NAME)
if _configured:
return logger
with _setup_lock:
if _configured:
return logger
logger.setLevel(_level())
logger.propagate = False
fmt = logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
sh = logging.StreamHandler()
sh.setLevel(_level())
sh.setFormatter(fmt)
logger.addHandler(sh)
try:
d = _log_dir()
d.mkdir(parents=True, exist_ok=True)
fh = logging.FileHandler(d / "generate.log", encoding="utf-8")
fh.setLevel(_level())
fh.setFormatter(fmt)
logger.addHandler(fh)
except OSError as e:
logger.warning("无法写入生成日志目录: %s", e)
_configured = True
return logger
class GenerateTrace:
"""单次 /apps/generate 请求的对账轨迹。"""
def __init__(self, run_id: str | None = None) -> None:
self.run_id = run_id or uuid.uuid4().hex[:10]
self.started_at = datetime.now(timezone.utc).isoformat()
self.lines: list[dict[str, Any]] = []
self._token = None
self._logger = ensure_logging()
self._run_file: Path | None = None
try:
d = _log_dir()
d.mkdir(parents=True, exist_ok=True)
self._run_file = d / f"run_{self.run_id}.log"
except OSError:
self._run_file = None
def __enter__(self) -> "GenerateTrace":
self._token = _current.set(self)
self.info("generate.start", run_id=self.run_id)
return self
def __exit__(self, exc_type, exc, tb) -> None:
if exc_type is not None:
self.error(f"generate.failed: {exc}", error=str(exc))
else:
self.info("generate.done")
if self._token is not None:
_current.reset(self._token)
return False
def _emit(self, level: str, message: str, **fields: Any) -> None:
ts = datetime.now(timezone.utc).isoformat()
entry = {"ts": ts, "level": level, "message": message}
if fields:
# 仅保留可 JSON 序列化的简单字段
clean: dict[str, Any] = {}
for k, v in fields.items():
if v is None:
continue
if isinstance(v, (str, int, float, bool)):
clean[k] = v
elif isinstance(v, (list, tuple)) and all(
isinstance(x, (str, int, float, bool)) for x in v
):
clean[k] = list(v)
else:
clean[k] = str(v)[:500]
entry.update(clean)
self.lines.append(entry)
extra = " ".join(f"{k}={v}" for k, v in entry.items() if k not in {"ts", "level", "message"})
line = f"[{self.run_id}] {message}" + (f" | {extra}" if extra else "")
log_fn = getattr(self._logger, level if level in {"debug", "info", "warning", "error"} else "info")
log_fn(line)
if self._run_file is not None:
try:
with self._run_file.open("a", encoding="utf-8") as f:
f.write(f"{ts} [{level.upper()}] {line}\n")
except OSError:
pass
def debug(self, message: str, **fields: Any) -> None:
self._emit("debug", message, **fields)
def info(self, message: str, **fields: Any) -> None:
self._emit("info", message, **fields)
def warning(self, message: str, **fields: Any) -> None:
self._emit("warning", message, **fields)
def error(self, message: str, **fields: Any) -> None:
self._emit("error", message, **fields)
def stage(self, name: str, detail: str = "", **fields: Any) -> None:
msg = f"stage.{name}" + (f": {detail}" if detail else "")
self.info(msg, stage=name, **fields)
def note(self, text: str) -> None:
"""把 warnings/notes 同步进日志。"""
t = (text or "").strip()
if t:
self.info(t)
def notes(self, items: list[str] | None) -> None:
for n in items or []:
self.note(str(n))
def as_payload(self) -> dict[str, Any]:
return {
"run_id": self.run_id,
"started_at": self.started_at,
"log_file": str(self._run_file) if self._run_file else "",
"lines": list(self.lines),
}
def get_trace() -> GenerateTrace | None:
return _current.get()
def glog() -> GenerateTrace | None:
"""当前请求轨迹;无上下文时返回 None调用方需判空"""
return get_trace()