修改日志结构
This commit is contained in:
122
app/logging_setup.py
Normal file
122
app/logging_setup.py
Normal file
@@ -0,0 +1,122 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# stdlib 级别 → PQLogGUI 标签
|
||||
_LEVEL_TO_GUI = {
|
||||
logging.DEBUG: "debug",
|
||||
logging.INFO: "info",
|
||||
logging.WARNING: "warning",
|
||||
logging.ERROR: "error",
|
||||
logging.CRITICAL: "error",
|
||||
}
|
||||
|
||||
# PQLogGUI 标签 → stdlib 级别
|
||||
GUI_LEVEL_TO_LOG = {
|
||||
"debug": logging.DEBUG,
|
||||
"info": logging.INFO,
|
||||
"success": logging.INFO,
|
||||
"warning": logging.WARNING,
|
||||
"error": logging.ERROR,
|
||||
"separator": logging.INFO,
|
||||
"blank": logging.DEBUG,
|
||||
}
|
||||
|
||||
# 用于在 LogRecord 上做 GUI 来源标记,避免回环
|
||||
_FROM_GUI_FLAG = "_from_gui"
|
||||
|
||||
# 文件日志:头一行元信息,第二行正文,记录之间空行隔开,方便阅读
|
||||
_FILE_LOG_FORMAT = "%(asctime)s [%(levelname)s] %(name)s\n%(message)s\n"
|
||||
_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
_initialized = False
|
||||
|
||||
|
||||
def _resolve_log_dir(log_dir: Optional[str]) -> str:
|
||||
if log_dir:
|
||||
return log_dir
|
||||
# 默认放在工作目录下的 log/,方便用户直接打开查看
|
||||
return os.path.join(os.getcwd(), "log")
|
||||
|
||||
|
||||
def setup_logging(
|
||||
level: int = logging.INFO,
|
||||
log_dir: Optional[str] = None,
|
||||
) -> str:
|
||||
"""配置全局 logging,返回日志目录。可重复调用,第二次起为空操作。
|
||||
|
||||
- 不再向终端输出,避免污染控制台。
|
||||
- 日志文件按日期命名:``log/YYYY-MM-DD.log``。
|
||||
"""
|
||||
global _initialized
|
||||
root = logging.getLogger()
|
||||
if _initialized:
|
||||
return _resolve_log_dir(log_dir)
|
||||
|
||||
resolved = _resolve_log_dir(log_dir)
|
||||
os.makedirs(resolved, exist_ok=True)
|
||||
|
||||
file_formatter = logging.Formatter(_FILE_LOG_FORMAT, datefmt=_DATE_FORMAT)
|
||||
|
||||
root.setLevel(level)
|
||||
|
||||
# 移除既有 handler,避免重复 / basicConfig 默认控制台输出
|
||||
for handler in list(root.handlers):
|
||||
root.removeHandler(handler)
|
||||
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
log_file = os.path.join(resolved, f"{today}.log")
|
||||
file_handler = logging.FileHandler(log_file, encoding="utf-8", delay=True)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
root.addHandler(file_handler)
|
||||
|
||||
_initialized = True
|
||||
logging.getLogger(__name__).info("日志系统初始化完成 dir=%s", resolved)
|
||||
return resolved
|
||||
|
||||
|
||||
class TkLogHandler(logging.Handler):
|
||||
"""把 stdlib logging 的记录转发到 ``PQLogGUI`` 面板。
|
||||
|
||||
带 ``_from_gui`` 标记的记录会被忽略,避免 GUI → file → GUI 回环。
|
||||
"""
|
||||
|
||||
def __init__(self, log_gui):
|
||||
super().__init__()
|
||||
self._log_gui = log_gui
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None: # noqa: D401
|
||||
if getattr(record, _FROM_GUI_FLAG, False):
|
||||
return
|
||||
try:
|
||||
message = self.format(record)
|
||||
except Exception:
|
||||
try:
|
||||
message = record.getMessage()
|
||||
except Exception:
|
||||
return
|
||||
gui_level = _LEVEL_TO_GUI.get(record.levelno, "info")
|
||||
try:
|
||||
# PQLogGUI.log 内部已处理跨线程
|
||||
self._log_gui.log(message, level=gui_level, _from_logging=True)
|
||||
except Exception:
|
||||
self.handleError(record)
|
||||
|
||||
|
||||
def attach_gui_handler(log_gui) -> TkLogHandler:
|
||||
"""把 ``PQLogGUI`` 注册为 root logger 的 handler。已存在则替换。"""
|
||||
root = logging.getLogger()
|
||||
# 移除旧的 TkLogHandler,保证只挂一个
|
||||
for h in list(root.handlers):
|
||||
if isinstance(h, TkLogHandler):
|
||||
root.removeHandler(h)
|
||||
handler = TkLogHandler(log_gui)
|
||||
handler.setLevel(logging.INFO)
|
||||
handler.setFormatter(logging.Formatter("%(name)s: %(message)s"))
|
||||
root.addHandler(handler)
|
||||
return handler
|
||||
@@ -23,6 +23,7 @@ import uuid
|
||||
from io import BytesIO
|
||||
from dataclasses import dataclass, asdict
|
||||
from typing import Callable, List, Optional
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
@@ -41,7 +42,7 @@ _SUPPORTED_IMG_EXT = (".png", ".jpg", ".jpeg", ".bmp", ".webp")
|
||||
# 测试环境后端
|
||||
API_BASE_URL = "http://10.201.44.70:9018/ai-agent/"
|
||||
API_PATH = "api/v1/pqtest/generate"
|
||||
API_TIMEOUT = 90.0 # 后端最长 60s,留余量
|
||||
API_TIMEOUT = 300.0 # 后端最长 60s,留余量
|
||||
|
||||
# 进程级会话 id(多轮对话需保持一致),可通过 ``reset_session`` 重置
|
||||
_session_id: str = str(uuid.uuid4())
|
||||
@@ -62,7 +63,6 @@ def set_session_id(session_id: str) -> str:
|
||||
with _session_lock:
|
||||
old = _session_id
|
||||
_session_id = sid
|
||||
logger.info("[AIImage] 会话切换 %s -> %s", _mask_sid(old), _mask_sid(_session_id))
|
||||
return _session_id
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@ def reset_session() -> str:
|
||||
with _session_lock:
|
||||
old = _session_id
|
||||
_session_id = str(uuid.uuid4())
|
||||
logger.info("[AIImage] 会话切换 %s -> %s", _mask_sid(old), _mask_sid(_session_id))
|
||||
return _session_id
|
||||
|
||||
|
||||
@@ -144,27 +143,66 @@ def _call_pqtest_generate(user_message: str, session_id: str, timeout: float = A
|
||||
"session_id": session_id},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8")
|
||||
request_headers = {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Accept": "application/json",
|
||||
"User-Agent": "pqAutomationApp/1.0",
|
||||
}
|
||||
endpoint = _api_endpoint()
|
||||
logger.info(
|
||||
"[AIImage] 请求生成 sid=%s prompt_len=%d prompt=%r",
|
||||
_mask_sid(session_id), len(user_message or ""), _truncate(user_message),
|
||||
)
|
||||
logger.debug("[AIImage] POST %s timeout=%.1fs", endpoint, timeout)
|
||||
logger.info(
|
||||
"[AIImage][REQUEST]\\nendpoint=%s\\nmethod=POST\\ntimeout=%.1fs\\nheaders=%s\\nbody=%s",
|
||||
endpoint,
|
||||
timeout,
|
||||
json.dumps(request_headers, ensure_ascii=False),
|
||||
payload.decode("utf-8", errors="replace"),
|
||||
)
|
||||
request = Request(
|
||||
endpoint,
|
||||
data=payload,
|
||||
method="POST",
|
||||
headers={
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Accept": "application/json",
|
||||
"User-Agent": "pqAutomationApp/1.0",
|
||||
},
|
||||
headers=request_headers,
|
||||
)
|
||||
t0 = time.monotonic()
|
||||
try:
|
||||
with urlopen(request, timeout=timeout) as response:
|
||||
raw = response.read()
|
||||
http_status = response.status
|
||||
response_headers = dict(response.headers.items())
|
||||
raw_text = raw.decode("utf-8", errors="replace")
|
||||
logger.info(
|
||||
"[AIImage][RESPONSE]\\nstatus=%s\\nheaders=%s\\nbody=%s",
|
||||
http_status,
|
||||
json.dumps(response_headers, ensure_ascii=False),
|
||||
raw_text,
|
||||
)
|
||||
except HTTPError as exc:
|
||||
elapsed = time.monotonic() - t0
|
||||
err_raw = b""
|
||||
try:
|
||||
err_raw = exc.read() or b""
|
||||
except Exception:
|
||||
err_raw = b""
|
||||
err_text = err_raw.decode("utf-8", errors="replace") if err_raw else ""
|
||||
err_headers = {}
|
||||
try:
|
||||
if exc.headers is not None:
|
||||
err_headers = dict(exc.headers.items())
|
||||
except Exception:
|
||||
err_headers = {}
|
||||
logger.error(
|
||||
"[AIImage][RESPONSE_ERROR] sid=%s elapsed=%.2fs status=%s reason=%s\\nheaders=%s\\nbody=%s",
|
||||
_mask_sid(session_id),
|
||||
elapsed,
|
||||
getattr(exc, "code", "?"),
|
||||
str(exc),
|
||||
json.dumps(err_headers, ensure_ascii=False),
|
||||
err_text,
|
||||
)
|
||||
raise
|
||||
except Exception as exc:
|
||||
elapsed = time.monotonic() - t0
|
||||
logger.error(
|
||||
@@ -173,15 +211,13 @@ def _call_pqtest_generate(user_message: str, session_id: str, timeout: float = A
|
||||
)
|
||||
raise
|
||||
elapsed = time.monotonic() - t0
|
||||
logger.debug(
|
||||
"[AIImage] HTTP %s 收到 %d bytes elapsed=%.2fs",
|
||||
http_status, len(raw), elapsed,
|
||||
)
|
||||
logger.info("[AIImage] HTTP %s 收到 %d bytes elapsed=%.2fs", http_status, len(raw), elapsed)
|
||||
try:
|
||||
result = json.loads(raw.decode("utf-8"))
|
||||
except Exception as exc:
|
||||
logger.error("[AIImage] 响应解析失败 sid=%s raw=%r", _mask_sid(session_id), raw[:200])
|
||||
raise RuntimeError(f"AI 接口返回非 JSON:{raw[:200]!r}") from exc
|
||||
raw_text = raw.decode("utf-8", errors="replace")
|
||||
logger.error("[AIImage] 响应解析失败 sid=%s raw=%s", _mask_sid(session_id), raw_text)
|
||||
raise RuntimeError(f"AI 接口返回非 JSON:{raw_text}") from exc
|
||||
|
||||
code = result.get("code")
|
||||
message = result.get("message") or ""
|
||||
|
||||
@@ -187,11 +187,12 @@ def toggle_ai_image_panel(self):
|
||||
# ---------------- 列表 / 选中 ----------------
|
||||
|
||||
|
||||
def reload_ai_image_list(self):
|
||||
def reload_ai_image_list(self, auto_select_first=True):
|
||||
"""重新扫描缓存并刷新列表。
|
||||
|
||||
按 ``session_id`` 分组:每个会话用一行不可选的分隔头(``── 会话 #N · 时间 ──``),
|
||||
其下列出该轮生成的所有图片。会话按"最近使用"倒序,组内按时间倒序。
|
||||
auto_select_first: 是否自动选中第一张图片(默认 True)。
|
||||
"""
|
||||
self.ai_image_records = _svc.list_records()
|
||||
self.ai_image_listbox.delete(0, tk.END)
|
||||
@@ -222,7 +223,7 @@ def reload_ai_image_list(self):
|
||||
flat.append(rec)
|
||||
# 替换为按显示顺序展平后的列表,便于其它逻辑(前一张/后一张等)
|
||||
self.ai_image_records = flat
|
||||
if self.ai_image_records:
|
||||
if self.ai_image_records and auto_select_first:
|
||||
# 选中第一张实际记录
|
||||
for row, ridx in enumerate(self._ai_image_row_map):
|
||||
if ridx is not None:
|
||||
@@ -333,7 +334,7 @@ def _start_new_session(self):
|
||||
return
|
||||
_svc.reset_session()
|
||||
self.ai_image_status_var.set("已开启新对话")
|
||||
reload_ai_image_list(self)
|
||||
reload_ai_image_list(self, auto_select_first=False)
|
||||
|
||||
|
||||
def _session_id_for_row(self, row: int) -> str:
|
||||
@@ -372,8 +373,7 @@ def _update_request_progress(self):
|
||||
if not getattr(self, "_ai_image_requesting", False):
|
||||
self._ai_image_progress_job = None
|
||||
return
|
||||
phases = ["后端处理中…", "正在生成图片…", "正在下载结果…", "即将完成…"]
|
||||
self.ai_image_status_var.set(phases[self._ai_image_progress_phase % len(phases)])
|
||||
self.ai_image_status_var.set("正在生成图片…")
|
||||
self._ai_image_progress_phase += 1
|
||||
self._ai_image_progress_job = self.root.after(900, lambda: _update_request_progress(self))
|
||||
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
import logging
|
||||
import threading
|
||||
from datetime import datetime
|
||||
import tkinter as tk
|
||||
import ttkbootstrap as ttk
|
||||
|
||||
|
||||
# 与 app.logging_setup 共享的映射;放在这里避免循环 import
|
||||
_GUI_LEVEL_TO_LOG = {
|
||||
"debug": logging.DEBUG,
|
||||
"info": logging.INFO,
|
||||
"success": logging.INFO,
|
||||
"warning": logging.WARNING,
|
||||
"error": logging.ERROR,
|
||||
"separator": logging.INFO,
|
||||
"blank": logging.DEBUG,
|
||||
}
|
||||
|
||||
|
||||
class PQLogGUI(ttk.Frame):
|
||||
VALID_LEVELS = {"info", "success", "warning", "error", "debug", "separator", "blank"}
|
||||
|
||||
@@ -10,6 +24,8 @@ class PQLogGUI(ttk.Frame):
|
||||
super().__init__(parent)
|
||||
self._line_count = 0
|
||||
self._max_lines = 1500
|
||||
# 与 stdlib logging 联通的 logger,由 logging_setup 配置 file handler
|
||||
self._logger = logging.getLogger("pq.gui")
|
||||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
@@ -62,13 +78,25 @@ class PQLogGUI(ttk.Frame):
|
||||
self._configure_tags()
|
||||
self.log_text.config(state=tk.DISABLED)
|
||||
|
||||
def log(self, message, level="info"):
|
||||
def log(self, message, level="info", _from_logging=False):
|
||||
if threading.current_thread() is not threading.main_thread():
|
||||
self.after(0, self.log, message, level)
|
||||
self.after(0, self.log, message, level, _from_logging)
|
||||
return
|
||||
|
||||
text = "" if message is None else str(message)
|
||||
normalized_level = self._normalize_level(level, text)
|
||||
|
||||
# 转发到 stdlib logging,落到文件(_from_logging=True 表示来源已是
|
||||
# logging,不再回写避免重复)。带 _from_gui 标记,TkLogHandler 会跳过。
|
||||
if not _from_logging and normalized_level != "blank":
|
||||
log_level = _GUI_LEVEL_TO_LOG.get(normalized_level, logging.INFO)
|
||||
try:
|
||||
self._logger.log(
|
||||
log_level, text, extra={"_from_gui": True}
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.log_text.config(state=tk.NORMAL)
|
||||
self._append_message(text, normalized_level)
|
||||
self.log_text.see(tk.END)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ttkbootstrap as ttk
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox, filedialog
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
@@ -23,6 +22,7 @@ from app.views.panels import cct_panel as _ccp
|
||||
from app.views.panels import main_layout as _main
|
||||
from app.views.panels import ai_image_panel as _aip
|
||||
from app.views import panel_manager as PM
|
||||
from app.logging_setup import setup_logging, attach_gui_handler
|
||||
|
||||
# Step 0/1 重构:资源工具和纯算法已迁移到 app/ 包,这里重新导入以保持
|
||||
# 对原函数名/方法名的向后兼容(老代码内部仍用 self.calculate_* 调用)。
|
||||
@@ -877,17 +877,13 @@ class PQAutomationApp:
|
||||
|
||||
def main():
|
||||
try:
|
||||
# 全局日志:默认 INFO 输出到 stderr,便于排查 AI 接口等关键事件
|
||||
import logging as _logging
|
||||
if not _logging.getLogger().handlers:
|
||||
_logging.basicConfig(
|
||||
level=_logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
setup_logging()
|
||||
# root = tk.Tk()
|
||||
root = ttk.Window(themename="yeti")
|
||||
app = PQAutomationApp(root)
|
||||
# GUI 创建完成后,把 logging 记录同步到日志面板
|
||||
if hasattr(app, "log_gui"):
|
||||
attach_gui_handler(app.log_gui)
|
||||
root.mainloop()
|
||||
except Exception as e:
|
||||
print("程序发生错误:", e)
|
||||
|
||||
@@ -32,13 +32,7 @@
|
||||
"timing": "DMT 1920x 1080 @ 60Hz",
|
||||
"color_format": "RGB",
|
||||
"bpc": 8,
|
||||
"colorimetry": "sRGB",
|
||||
"cct_params": {
|
||||
"x_ideal": 0.3127,
|
||||
"x_tolerance": 0.003,
|
||||
"y_ideal": 0.329,
|
||||
"y_tolerance": 0.003
|
||||
}
|
||||
"colorimetry": "sRGB"
|
||||
},
|
||||
"hdr_movie": {
|
||||
"name": "HDR Movie测试",
|
||||
@@ -52,17 +46,11 @@
|
||||
"timing": "DMT 1920x 1080 @ 60Hz",
|
||||
"color_format": "RGB",
|
||||
"bpc": 8,
|
||||
"colorimetry": "sRGB",
|
||||
"cct_params": {
|
||||
"x_ideal": 0.3127,
|
||||
"x_tolerance": 0.003,
|
||||
"y_ideal": 0.329,
|
||||
"y_tolerance": 0.003
|
||||
}
|
||||
"colorimetry": "sRGB"
|
||||
}
|
||||
},
|
||||
"device_config": {
|
||||
"ca_com": "COM3",
|
||||
"ca_com": "COM1",
|
||||
"ucd_list": "0: UCD-323 [2128C209]",
|
||||
"ca_channel": "0"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user