"""logging 配置:降级到 stderr,避免污染 stdout(stdout 留给结构化输出如 JSON)。 优先用 jiangchang_skill_core 提供的 logging;本地无该包时降级到 stderr。 v1.1.18:本地降级路径追加 RotatingFileHandler(按 LOG_FILE env 决定是否落文件)。 """ from __future__ import annotations import logging import logging.handlers import os import sys from pathlib import Path from util.constants import ( LOG_LOGGER_NAME, SKILL_SLUG, SKILL_VERSION, ) _configured = False def setup_skill_logging() -> logging.Logger: """初始化 skill logger(幂等)。 本地降级行为(无 jiangchang_skill_core 时): - 若 LOG_FILE 已设:追加 RotatingFileHandler(LOG_MAX_BYTES × LOG_BACKUP_COUNT) - 始终追加 StreamHandler(sys.stderr) - logger level 取自 LOG_LEVEL(默认 INFO) """ global _configured logger = logging.getLogger(LOG_LOGGER_NAME) if _configured: return logger # 优先用宿主提供的 skill logger 配置 try: from jiangchang_skill_core.logging_config import setup_skill_logging as _host_setup _host_setup(SKILL_SLUG, LOG_LOGGER_NAME) _configured = True return logging.getLogger(LOG_LOGGER_NAME) except ImportError: pass # logger 级别 level_name = os.environ.get("LOG_LEVEL", "INFO").upper() logger.setLevel(getattr(logging, level_name, logging.INFO)) formatter = logging.Formatter( fmt=f"%(asctime)s [{SKILL_SLUG}] %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) # 本地降级:可选 RotatingFileHandler log_file_str = os.environ.get("LOG_FILE") if log_file_str: log_path = Path(log_file_str) if not log_path.is_absolute(): from util.runtime_paths import get_logs_dir log_path = get_logs_dir() / log_file_str log_path.parent.mkdir(parents=True, exist_ok=True) max_bytes = int(os.environ.get("LOG_MAX_BYTES", str(5 * 1024 * 1024))) backup_count = int(os.environ.get("LOG_BACKUP_COUNT", "5")) file_handler = logging.handlers.RotatingFileHandler( log_path, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8", ) file_handler.setFormatter(formatter) logger.addHandler(file_handler) # 始终保留 stderr handler handler = logging.StreamHandler(sys.stderr) handler.setFormatter(formatter) logger.addHandler(handler) logger.propagate = False _configured = True logger.info("logging initialized (local fallback) v%s", SKILL_VERSION) return logger def get_skill_logger() -> logging.Logger: """获取 skill logger(不会重复 setup)。""" if not _configured: return setup_skill_logging() return logging.getLogger(LOG_LOGGER_NAME) def reset_for_tests() -> None: """测试用:重置 _configured flag,让 setup_skill_logging 重新初始化。 必须在每个修改日志配置的测试 fixture 里调用,否则 env 改动不会生效。 """ global _configured logger = logging.getLogger(LOG_LOGGER_NAME) for h in list(logger.handlers): try: h.close() except Exception: pass logger.removeHandler(h) _configured = False