fix: sync unified logging probe before file handler attach
All checks were successful
技能自动化发布 / release (push) Successful in 5s

This commit is contained in:
2026-06-02 15:18:59 +08:00
parent b7163f12a5
commit ae5466854f

View File

@@ -10,13 +10,16 @@ import os
import sys import sys
import uuid import uuid
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from typing import Optional from typing import Optional, Tuple
from .runtime_env import get_data_root, get_user_id from .runtime_env import get_data_root, get_user_id
_skill_slug: str = "" _skill_slug: str = ""
_logger_name: str = "" _logger_name: str = ""
# 避免 logging 内部错误再向 stderr 打印 "--- Logging error ---"
logging.raiseExceptions = False
def get_unified_logs_dir() -> str: def get_unified_logs_dir() -> str:
path = os.path.join(get_data_root(), get_user_id(), "logs") path = os.path.join(get_data_root(), get_user_id(), "logs")
@@ -41,6 +44,19 @@ def get_skill_log_file_path() -> str:
return os.path.join(get_unified_logs_dir(), "jiangchang.log") return os.path.join(get_unified_logs_dir(), "jiangchang.log")
def _can_open_log_file(log_path: str) -> Tuple[bool, str]:
"""探测日志文件是否可追加写入(真实 open非 os.access"""
try:
parent = os.path.dirname(os.path.abspath(log_path))
if parent:
os.makedirs(parent, exist_ok=True)
with open(log_path, "a", encoding="utf-8"):
pass
return True, ""
except Exception as exc:
return False, str(exc)
def ensure_trace_for_process() -> str: def ensure_trace_for_process() -> str:
"""本进程调用链 ID沿用 JIANGCHANG_TRACE_ID否则生成并写入环境子进程可继承""" """本进程调用链 ID沿用 JIANGCHANG_TRACE_ID否则生成并写入环境子进程可继承"""
existing = (os.getenv("JIANGCHANG_TRACE_ID") or "").strip() existing = (os.getenv("JIANGCHANG_TRACE_ID") or "").strip()
@@ -87,8 +103,17 @@ def _backup_count() -> int:
return 30 return 30
def _attach_fallback_handler(log: logging.Logger, fmt: logging.Formatter) -> None: def _clear_logger_handlers(log: logging.Logger) -> None:
"""文件日志不可用时仅挂内存 handler不向 stderr 打印 logging 异常。""" for h in list(log.handlers):
log.removeHandler(h)
try:
h.close()
except Exception:
pass
def _attach_fallback_handler(log: logging.Logger) -> None:
"""文件日志不可用时仅挂 NullHandler不向 stderr 打印 logging 异常。"""
log.addHandler(logging.NullHandler()) log.addHandler(logging.NullHandler())
@@ -96,37 +121,39 @@ def setup_skill_logging(skill_slug: str, logger_name: str) -> None:
""" """
幂等:为指定 logger 挂载统一文件日志与可选 stderr。 幂等:为指定 logger 挂载统一文件日志与可选 stderr。
须在进程早期调用(如 CLI main 在业务日志之前)。 须在进程早期调用(如 CLI main 在业务日志之前)。
文件日志目录无权限或不可写时降级为 no-op,不影响 CLI 返回码。 文件日志不可写时降级为 NullHandler,不影响 CLI 返回码。
""" """
global _skill_slug, _logger_name global _skill_slug, _logger_name
logging.raiseExceptions = False
ensure_trace_for_process() ensure_trace_for_process()
_skill_slug = skill_slug _skill_slug = skill_slug
_logger_name = logger_name _logger_name = logger_name
log = logging.getLogger(logger_name) log = logging.getLogger(logger_name)
if log.handlers: _clear_logger_handlers(log)
return
log.setLevel(_log_level_from_env()) log.setLevel(_log_level_from_env())
fmt = logging.Formatter(_FORMAT, datefmt=_DATEFMT) fmt = logging.Formatter(_FORMAT, datefmt=_DATEFMT)
filt = _SkillContextFilter() filt = _SkillContextFilter()
try: log_path = get_skill_log_file_path()
path = get_skill_log_file_path() ok, _err = _can_open_log_file(log_path)
fh = TimedRotatingFileHandler( if ok:
path, try:
when="midnight", fh = TimedRotatingFileHandler(
interval=1, log_path,
backupCount=_backup_count(), when="midnight",
encoding="utf-8", interval=1,
delay=True, backupCount=_backup_count(),
) encoding="utf-8",
fh.setFormatter(fmt) delay=False,
fh.addFilter(filt) )
log.addHandler(fh) fh.setFormatter(fmt)
except (OSError, PermissionError, ValueError): fh.addFilter(filt)
_attach_fallback_handler(log, fmt) log.addHandler(fh)
except Exception: except Exception:
_attach_fallback_handler(log, fmt) _attach_fallback_handler(log)
else:
_attach_fallback_handler(log)
if (os.getenv("JIANGCHANG_LOG_TO_STDERR") or "").strip().lower() in ( if (os.getenv("JIANGCHANG_LOG_TO_STDERR") or "").strip().lower() in (
"1", "1",
@@ -162,23 +189,25 @@ def attach_unified_file_handler(
独立子进程内追加同一日志文件(如 login 检测子进程),格式与主进程一致。 独立子进程内追加同一日志文件(如 login 检测子进程),格式与主进程一致。
""" """
global _skill_slug global _skill_slug
logging.raiseExceptions = False
ensure_trace_for_process() ensure_trace_for_process()
_skill_slug = skill_slug _skill_slug = skill_slug
lg = logging.getLogger(logger_name) lg = logging.getLogger(logger_name)
lg.handlers.clear() _clear_logger_handlers(lg)
lg.setLevel(level) lg.setLevel(level)
fmt = logging.Formatter(_FORMAT, datefmt=_DATEFMT) fmt = logging.Formatter(_FORMAT, datefmt=_DATEFMT)
try: filt = _SkillContextFilter()
parent = os.path.dirname(os.path.abspath(log_path))
if parent: ok, _err = _can_open_log_file(log_path)
os.makedirs(parent, exist_ok=True) if ok:
fh = logging.FileHandler(log_path, encoding="utf-8") try:
fh.setFormatter(fmt) fh = logging.FileHandler(log_path, encoding="utf-8", delay=False)
fh.addFilter(_SkillContextFilter()) fh.setFormatter(fmt)
lg.addHandler(fh) fh.addFilter(filt)
except (OSError, PermissionError, ValueError): lg.addHandler(fh)
lg.addHandler(logging.NullHandler()) except Exception:
except Exception: _attach_fallback_handler(lg)
lg.addHandler(logging.NullHandler()) else:
_attach_fallback_handler(lg)
lg.propagate = False lg.propagate = False
return lg return lg