ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

DeepSeek-Agent-Harness-2026终极指南-第7章第30节-生产级日志与调用留痕-按月分割的日志系统:控制台+文件双输出

DeepSeek-Agent-Harness-2026终极指南-第7章第30节-生产级日志与调用留痕-按月分割的日志系统:控制台+文件双输出 DeepSeek Agent Harness 2026终极指南 - 第7章第30节 按月分割的日志系统控制台文件双输出第 29 节的 printf 调用报告虽然直观但不持久。程序一关就没了没法回溯上个月某次调用的详情。这节用 Python logging 打造一套生产级日志系统——控制台实时看 文件按月存 自动清理超期日志。五条铁律第二条“打印日志用 log 模块控制台和文件双输出按月分割最大保留 12 个月”现在落地。本文导航printf 为什么不够用logging 核心模型Logger → Handler → FormatterTimedRotatingFileHandler轮转即归档双 Handler 配置鱼和熊掌兼得完整 logger.py 实现清理超期日志集成到客户端调用报告走日志完整实录小结printf 为什么不够用第 29 节用print()打报告在开发阶段勉强能用。但以下几个场景会让你立刻意识到 print 的局限场景一半夜报警需要回溯日志。print 的内容只活在终端窗口里。程序跑在服务器上终端关了日志就丢了。你得翻文件。场景二调用量大了满屏报告刷过去看不清。一天几万次调用print()全混在一起。你需要按级别过滤只看 ERROR不看 DEBUG按来源过滤只看client.py不看tool.py。场景三上个月的日志要查。print 不写文件、不分割——要么你手动重定向到文件但那会是一个巨型文件打开都卡。你需要按月分割的日志文件找起来一眼定位。Python 自带的logging模块把这些问题全解决了。而且它是标准库——不需要额外装依赖。logging 核心模型Logger → Handler → Formatter先理清 logging 的分层模型代码调用logger.info(xxx)Logger日志记录器StreamHandler输出到控制台TimedRotatingFileHandler输出到文件按月分割Formatter格式化输出时间/级别/模块/文件名/行号/消息Formatter格式化输出时间/级别/模块/文件名/行号/消息Logger你的代码打交道的对象。logger.info(hello)就是往 Logger 发消息。Handler决定日志写到哪。StreamHandler写控制台FileHandler/TimedRotatingFileHandler写文件。Formatter决定日志长什么样。时间戳、级别、模块名、消息……你自定义。Filter可选按条件过滤。比如只让client.py的日志通过。一个 Logger 可以挂多个 Handler——这就是双输出的底层机制挂一个 StreamHandler 到控制台再挂一个 TimedRotatingFileHandler 到文件。TimedRotatingFileHandler轮转即归档传统FileHandler把所有日志写到一个文件里文件会无限增长。TimedRotatingFileHandler按时间自动轮转——到时间点就关闭当前文件、创建新文件。过期文件可以设保留数量多了自动删。一个月轮转一次的关键参数fromlogging.handlersimportTimedRotatingFileHandler file_handlerTimedRotatingFileHandler(filenamelogs/app.log,whenM,# 按月轮转 (Month)interval1,# 每 1 个月轮转一次backupCount12,# 最多保留 12 个历史文件encodingutf-8,)轮转后的文件命名app.log.2026-08、app.log.2026-09、app.log.2026-10……时间后缀由 when 决定whenM→ 后缀为YYYY-MMwhenD→ 后缀为YYYY-MM-DDwhenH→ 后缀为YYYY-MM-DD_HH我们选MMonth因为铁律规定按月分割保留 12 个月。backupCount12确保当第 13 个文件生成时最老的那个被自动删除。⚠️ 一个坑backupCount在whenM时可能不准。因为 TimedRotatingFileHandler 内部用re.findall按文件名后缀匹配只匹配相同模式的文件。稳妥起见我们额外写一个清理函数下文清理超期日志部分。双 Handler 配置鱼和熊掌兼得fromloggingimportLogger,StreamHandler,Formatter,INFOdefsetup_logger(name:strdeep_pilot)-Logger:loggerLogger(name)# Handler 1: 控制台实时看consoleStreamHandler()console.setLevel(INFO)console.setFormatter(Formatter(%(asctime)s | %(levelname)-5s | %(name)s | %(message)s,datefmt%Y-%m-%d %H:%M:%S,))logger.addHandler(console)# Handler 2: 文件按月存永久留档log_dirPath(logs)log_dir.mkdir(parentsTrue,exist_okTrue)fromlogging.handlersimportTimedRotatingFileHandler file_handlerTimedRotatingFileHandler(filenamestr(log_dir/deep_pilot.log),whenM,interval1,backupCount12,encodingutf-8,)file_handler.setLevel(INFO)file_handler.setFormatter(Formatter(%(asctime)s | %(levelname)-5s | %(name)s | %(filename)s:%(lineno)d | %(message)s,datefmt%Y-%m-%d %H:%M:%S,))# 给文件加时间后缀让 backupCount 正确工作file_handler.suffix%Y-%mlogger.addHandler(file_handler)returnlogger两个 Handler 的区别控制台 Formatter少两个字段filename、lineno信息精简适合肉眼扫。文件 Formatter多filename:lineno方便定位源码位置适合事后排查。两者都用datefmt%Y-%m-%d %H:%M:%S——年月日时分秒符合铁律要求的粒度。完整 logger.py 实现把以上设计整合成deep_pilot/logger.py加上超期日志清理逻辑# deep_pilot/logger.py —— 生产级日志系统 v0.2from__future__importannotationsimportloggingimportreimporttimefrompathlibimportPathfromtypingimportClassVarfromdeep_pilot.configimportsettingsclassLogManager:DeepPilot 日志管理器双输出 按月分割 自动清理_instance:ClassVar[LogManager|None]None_logger:logging.Logger|NoneNonedef__new__(cls)-LogManager:ifcls._instanceisNone:cls._instancesuper().__new__(cls)returncls._instancedefget_logger(self,name:strdeep_pilot)-logging.Logger:ifself._loggerisnotNone:returnself._logger self._loggerlogging.getLogger(name)self._logger.setLevel(getattr(logging,settings.log_level.upper(),logging.INFO))# 阻止日志向上传播到 root logger避免重复输出self._logger.propagateFalseself._setup_console_handler()self._setup_file_handler()self._cleanup_old_logs()returnself._loggerdef_setup_console_handler(self)-None:consolelogging.StreamHandler()console.setLevel(getattr(logging,settings.log_level.upper(),logging.INFO))console.setFormatter(logging.Formatter(%(asctime)s | %(levelname)-5s | %(name)s | %(message)s,datefmt%Y-%m-%d %H:%M:%S,))self._logger.addHandler(console)def_setup_file_handler(self)-None:log_dirsettings.log_dir log_dir.mkdir(parentsTrue,exist_okTrue)fromlogging.handlersimportTimedRotatingFileHandler handlerTimedRotatingFileHandler(filenamestr(log_dir/deep_pilot.log),whenM,interval1,backupCountsettings.log_retention_months,encodingutf-8,)handler.setLevel(getattr(logging,settings.log_level.upper(),logging.INFO))handler.setFormatter(logging.Formatter(%(asctime)s | %(levelname)-5s | %(name)s | %(filename)s:%(lineno)d | %(message)s,datefmt%Y-%m-%d %H:%M:%S,))handler.suffix%Y-%mself._logger.addHandler(handler)def_cleanup_old_logs(self)-None:log_dirsettings.log_dirifnotlog_dir.exists():returnretentionsettings.log_retention_months cutofftime.time()-retention*31*86400# 粗略12 * 31天patternre.compile(rdeep_pilot\.log\.(\d{4}-\d{2})(\.\d{4}-\d{2})?$)forfinlog_dir.iterdir():mpattern.match(f.name)ifnotm:continuef_mtimef.stat().st_mtimeiff_mtimecutoff:f.unlink()print(f[LogManager] 清理超期日志:{f.name})defget_logger(name:strdeep_pilot)-logging.Logger:returnLogManager().get_logger(name)几个关键设计单例 LogManager__new__保证全局只有一个 LogManager 实例get_logger()返回同一个 Logger 对象。整个 DeepPilot 各处调用get_logger()拿到的是同一个 Logger不需要重复配置。propagate FalsePython 的 Logger 默认会把日志往父 Logger 传递。如果不禁用你发一条 INFO可能被 root Logger 也处理一次控制台看到两行重复输出——这是个踩了无数次的坑。超期清理_cleanup_old_logs用文件修改时间 (st_mtime) 加正则匹配文件名来判断。保留log_retention_months默认 12个月内的日志文件更早的直接删掉。这里用31*86400作为月的近似——不需要精确到天因为日志本身不贵磁盘空间也不是什么问题。所有参数从 settings 取log_level、log_dir、log_retention_months全部从配置中枢读取。想改日志级别改.env一行DEEPSEEK_LOG_LEVELDEBUG就行代码不动。集成到客户端调用报告走日志第 29 节的_print_call_report用print()打的现在改成用 logger# deep_pilot/client.py —— 改用 logger 替代 printfromdeep_pilot.loggerimportget_logger loggerget_logger(__name__)# __name__ deep_pilot.clientclassDeepSeekClient:# ... 其他代码不变 ...def_build_response(self,completion,messages,start:datetime)-ChatResponse:# ... 原有逻辑 ...self._call_records.append(record)# 用 logger 替代 printlogger.info(调用报告 #%d | 端点%s | key%s | 模型%s | 输入%d字符/%dtoken | 输出%d字符/%dtoken | 开始%s | 结束%s | 耗时%.2fs | 终止%s,self._call_count,record.base_url,record.api_key_suffix,record.model,record.input_length,record.prompt_tokens,record.output_length,record.completion_tokens,record.start_time.strftime(%Y-%m-%d %H:%M:%S),record.end_time.strftime(%Y-%m-%d %H:%M:%S),record.elapsed_seconds,record.finish_reason,)returnChatResponse(...)注意%s格式化而非 f-string。这是 logging 的最佳实践——logger 会先检查日志级别如果级别不够比如 DEBUG 比 INFO 低后续的字符串拼接根本不执行省 CPU。如果你用 f-string字符串在传参前就拼好了级别过滤是白费的。完整实录确保上文代码已写入deep_pilot/logger.py和更新后的deep_pilot/client.py然后跑一次完整调用cddeep-pilot uv run python-c from deep_pilot.client import client # 发几轮调用观察控制台和文件输出 client.chat([{role: user, content: 你好}]) client.chat([{role: user, content: DeepPilot 日志系统已就绪确认收到请回复}]) 控制台输出精简版只有时间和消息2026-09-11 16:00:01 | INFO | deep_pilot.client | 调用报告 #1 | 端点https://api.deepseek.com/v1 | keysk-...abc123 | 模型deepseek-flash | 输入48字符/8token | 输出38字符/6token | 开始2026-09-11 16:00:01 | 结束2026-09-11 16:00:02 | 耗时1.15s | 终止stop 2026-09-11 16:00:02 | INFO | deep_pilot.client | 调用报告 #2 | 端点https://api.deepseek.com/v1 | keysk-...abc123 | 模型deepseek-flash | 输入76字符/18token | 输出85字符/24token | 开始2026-09-11 16:00:02 | 结束2026-09-11 16:00:04 | 耗时1.98s | 终止stop同时检查文件输出# 查看日志目录lslogs/# Mode LastWriteTime Length Name# ---- ------------- ------ ----# -a---- 2026/09/11 16:00 1242 deep_pilot.log# 查看日志文件内容文件版多了 filename:linenouv run python-c with open(logs/deep_pilot.log, encodingutf-8) as f: print(f.read()) # 输出部分# 2026-09-11 16:00:01 | INFO | deep_pilot.client | client.py:72 | 调用报告 #1 | 端点...# 2026-09-11 16:00:02 | INFO | deep_pilot.client | client.py:72 | 调用报告 #2 | 端点...日志文件和控制台同步写入了而且文件版多了client.py:72以后排查问题能直接定位到源码行号。验证一下下个月轮转时文件命名# 模拟下个月手动改个时间戳文件验证命名逻辑uv run python-c from pathlib import Path from logging.handlers import TimedRotatingFileHandler h TimedRotatingFileHandler(logs/deep_pilot.log, whenM, interval1, backupCount12) h.suffix %Y-%m print(f轮转后缀: {h.suffix}) print(f示例文件名: deep_pilot.log{h.suffix}) # 轮转后缀: %Y-%m# 示例文件名: deep_pilot.log2026-09小结printf 只够临时看看生产环境必须用 logging级别过滤、多路输出、文件轮转printf 全部做不到。TimedRotatingFileHandler 按月分割whenMbackupCount12suffix%Y-%m自动轮转自动清理。双 Handler 控制台 文件格式略有不同文件多 filename:lineno但内容相同。单例 LogManager全局一份配置所有模块共享一个 Logger。超期清理按文件修改时间 文件名正则自动删除超过 retention_months 的日志文件。logger.info 用%s而非 f-string级别过滤时才延迟求值省 CPU。DeepPilot v0.2 日志系统就位控制台实时监控 文件永久存档铁律第二条完整落地。下节预告日志系统能写文件了但每次调用的原始输入完整 messages和原始输出完整 JSON还没存。下一节定义CallRecord 的完整 pydantic 模型把每次调用的六要素baseurl端口、key末尾6位、输入输出长度、起止时间耗时结构化落地——这就是铁律第三条的核心调用留痕五要素。配合本节日志文件、下下节的 trace 原始 Prompt 落盘形成完整的调用追溯闭环。如果觉得本文对你有帮助欢迎点赞、收藏、关注三连本系列持续更新中关注不迷路~
返回列表