diff --git a/src/common/logger.py b/src/common/logger.py index 0b434d084..1e281dd6c 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -1,11 +1,32 @@ import logging +import logging.handlers +from pathlib import Path from typing import Callable, Optional +import json import structlog -# TODO: customize the logger configuration as needed -# TODO: compress the log output -# TODO: output to a file with JSON format +# 创建logs目录 +LOG_DIR = Path("logs") +LOG_DIR.mkdir(exist_ok=True) + +# 配置标准logging以支持文件输出和压缩 +logging.basicConfig( + level=logging.INFO, + format="%(message)s", + handlers=[ + # 带压缩的轮转文件处理器 + logging.handlers.RotatingFileHandler( + LOG_DIR / "app.log.jsonl", + maxBytes=10 * 1024 * 1024, # 10MB + backupCount=5, + encoding="utf-8", + ), + # 控制台处理器 + logging.StreamHandler(), + ], +) + structlog.configure( processors=[ structlog.contextvars.merge_contextvars, @@ -13,23 +34,108 @@ structlog.configure( structlog.processors.StackInfoRenderer(), structlog.dev.set_exc_info, structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S", utc=False), - structlog.dev.ConsoleRenderer(), + # 根据输出类型选择不同的渲染器 + structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], - wrapper_class=structlog.make_filtering_bound_logger(logging.NOTSET), + wrapper_class=structlog.stdlib.BoundLogger, context_class=dict, - logger_factory=structlog.PrintLoggerFactory(), - cache_logger_on_first_use=False, + logger_factory=structlog.stdlib.LoggerFactory(), + cache_logger_on_first_use=True, ) +# 为文件输出配置JSON格式 +file_formatter = structlog.stdlib.ProcessorFormatter( + processor=structlog.processors.JSONRenderer(ensure_ascii=False), + foreign_pre_chain=[ + structlog.stdlib.add_logger_name, + structlog.stdlib.add_log_level, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.TimeStamper(fmt="iso"), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + ], +) + +# 为控制台输出配置可读格式 +console_formatter = structlog.stdlib.ProcessorFormatter( + processor=structlog.dev.ConsoleRenderer(colors=True), + foreign_pre_chain=[ + structlog.stdlib.add_logger_name, + structlog.stdlib.add_log_level, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S", utc=False), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + ], +) + +# 获取根logger并配置格式化器 +root_logger = logging.getLogger() +for handler in root_logger.handlers: + if isinstance(handler, logging.handlers.RotatingFileHandler): + handler.setFormatter(file_formatter) + else: + handler.setFormatter(console_formatter) + raw_logger = structlog.get_logger() binds: dict[str, Callable] = {} def get_logger(name: Optional[str]): + """获取logger实例,支持按名称绑定""" if name is None: return raw_logger logger = binds.get(name) if logger is None: binds[name] = logger = structlog.get_logger(name).bind(logger_name=name) return logger + + +def configure_logging( + level: str = "INFO", + max_bytes: int = 10 * 1024 * 1024, + backup_count: int = 5, + log_dir: str = "logs", +): + """动态配置日志参数""" + log_path = Path(log_dir) + log_path.mkdir(exist_ok=True) + + # 更新文件handler配置 + root_logger = logging.getLogger() + for handler in root_logger.handlers: + if isinstance(handler, logging.handlers.RotatingFileHandler): + handler.maxBytes = max_bytes + handler.backupCount = backup_count + handler.baseFilename = str(log_path / "app.log.jsonl") + + # 设置日志级别 + root_logger.setLevel(getattr(logging, level.upper())) + +def format_json_for_logging(data, indent=2, ensure_ascii=False): + """将JSON数据格式化为可读字符串 + + Args: + data: 要格式化的数据(字典、列表等) + indent: 缩进空格数 + ensure_ascii: 是否确保ASCII编码 + + Returns: + str: 格式化后的JSON字符串 + """ + if isinstance(data, str): + try: + # 如果是JSON字符串,先解析再格式化 + parsed_data = json.loads(data) + return json.dumps(parsed_data, indent=indent, ensure_ascii=ensure_ascii) + except json.JSONDecodeError: + # 如果不是有效JSON,直接返回 + return data + else: + # 如果是对象,直接格式化 + try: + return json.dumps(data, indent=indent, ensure_ascii=ensure_ascii) + except (TypeError, ValueError): + # 如果无法序列化,返回字符串表示 + return str(data) diff --git a/src/config/config.py b/src/config/config.py index c30e8fe0f..986b392cf 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -48,7 +48,7 @@ TEMPLATE_DIR = "template" # 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 # 对该字段的更新,请严格参照语义化版本规范:https://semver.org/lang/zh-CN/ -MMC_VERSION = "0.7.3-snapshot.1" +MMC_VERSION = "0.7.4-snapshot.1" def update_config():