Loguru 结构化日志配置详解:从 trace_id 到日志轮转
1. 引言在实际项目开发中日志不仅是排查问题的依据更是系统可观测性的基础。loguru作为 Python 生态中轻量又强大的日志库通过简洁的 API 就能实现传统logging模块难以做到的结构化输出、动态上下文注入、自动轮转等能力。本文将通过解析一段典型的setup_logger()配置带你彻底掌握如何用loguru构建一套支持trace_id的日志体系。2. 环境准备pipinstallloguru注意loguru依赖 Python 3.6本文基于loguru 0.7编写。3. 核心配置逐行解析下面就是我们要解析的setup_logger()函数它同时配置了标准输出和文件输出两个 handler并在每条日志中自动注入trace_id。importsysfromloguruimportlogger trace_id_var:strdefsetup_logger():配置结构化日志标准输出 文件输出。logger.remove()logger.add(sys.stdout,format(green{time:YYYY-MM-DD HH:mm:ss}/green | level{level: 8}/level | cyan{extra[trace_id]}/cyan | cyan{name}/cyan:cyan{function}/cyan:cyan{line}/cyan - level{message}/level),levelINFO,filterlambdarecord:record[extra].setdefault(trace_id,trace_id_var.get()),)logger.add(logs/app_{time:YYYY-MM-DD}.log,rotation00:00,retention7 days,format({time:YYYY-MM-DD HH:mm:ss} | {level: 8} | {extra[trace_id]} | {name}:{function}:{line} - {message}),levelDEBUG,filterlambdarecord:record[extra].setdefault(trace_id,trace_id_var.get()),)3.1logger.remove()– 清空默认 handlerloguru在导入时会自动注册一个写入sys.stderr的默认 handler。如果你不想要那个最简单的格式第一步就是移除它logger.remove()传入None或留空会移除所有handler你也可以通过传入 handler id 精确移除某一个。3.2 标准输出 handler控制台高亮logger.add(sys.stdout,format(green{time:YYYY-MM-DD HH:mm:ss}/green | level{level: 8}/level | cyan{extra[trace_id]}/cyan | cyan{name}/cyan:cyan{function}/cyan:cyan{line}/cyan - level{message}/level),levelINFO,filterlambdarecord:record[extra].setdefault(trace_id,trace_id_var.get()),)输出目标sys.stdout也就是控制台。格式使用green、cyan等颜色标签在终端中呈现彩色日志。{time:YYYY-MM-DD HH:mm:ss}时间戳格式{level: 8}左对齐、宽度为 8 的日志级别1. 左对齐 右对齐 ^中对齐 2. 8个字符解释 {level: 8} 里的 8 强制这块内容固定占用 8 个字符的位置不够就补空格超过 8 个字符才会自动撑开。 日志级别文本本身长短不一样 INFO4 个字符 ERROR5 个字符 WARNING7 个字符 CRITICAL8 个字符 2.1 不加宽度限制会怎样排版歪掉 INFO | xxx ERROR | xxx WARNING | xxx CRITICAL | xxx 2.2 设置宽度 8 左对齐 {level: 8} NFO → INFO INFO 4 个空格合计 8 字符 ERROR → ERROR ERROR 3 个空格 WARNING → WARNING WARNING 1 个空格 CRITICAL → CRITICAL刚好 8 位不用补空格 INFO | xxx ERROR | xxx WARNING | xxx CRITICAL | xxx{extra[trace_id]}自定义上下文变量{name}:{function}:{line}代码位置{message}日志消息。最低级别levelINFO只输出 INFO 及以上WARNING、ERROR 等。filter作用loguru的filter除了返回布尔值还可以用来在记录处理之前补充extra字段。这里通过record[extra].setdefault(trace_id, trace_id_var.get())达到以下效果如果该条日志在bind()时已经提供了trace_id则保留原值否则从全局变量trace_id_var中读取当前 trace id通常是一个请求上下文变量填入extra。这样一来所有经过该 handler 的日志都会自动带上trace_id无需在每一条logger.info(...)中手动传递。3.3 文件 handler持久化与轮转logger.add(logs/app_{time:YYYY-MM-DD}.log,rotation00:00,retention7 days,format({time:YYYY-MM-DD HH:mm:ss} | {level: 8} | {extra[trace_id]} | {name}:{function}:{line} - {message}),levelDEBUG,filterlambdarecord:record[extra].setdefault(trace_id,trace_id_var.get()),)文件路径logs/app_{time:YYYY-MM-DD}.log文件名中嵌入了日期每天会生成一个新的日志文件。rotation00:00表示每天午夜 0 点自动切分新文件。你也可以写成500 MB按大小切分或1 week按时间间隔切分。retention7 days表示只保留最近 7 天的日志旧文件会被自动清理。级别levelDEBUG文件存储更详细包括 DEBUG控制台则保持INFO兼顾性能与排查需求。格式文件日志不需要颜色标签所以使用纯文本格式。filter同样注入trace_id确保文件和终端输出一致性。3.4trace_id的设计与注入注意到全局变量trace_id_var: str和filter中对trace_id_var.get()的调用。实际上在真实 Web 应用中trace_id_var通常是一个contextvars.ContextVar这样每个请求都有独立的 trace id而不会在多线程/异步场景下相互干扰。示例fromcontextvarsimportContextVar trace_id_var:ContextVar[str]ContextVar(trace_id,default)在请求中间件中设置trace_id_var.set(generate_trace_id())之后同一线程/协程中的所有日志都会自动携带该请求的trace_id实现全链路追踪。4. 完整可运行示例importsysimportuuidfromcontextvarsimportContextVarfromloguruimportlogger# 使用 ContextVar 保证线程/协程安全trace_id_var:ContextVar[str]ContextVar(trace_id,default)defgenerate_trace_id()-str:returnuuid.uuid4().hex[:12]defsetup_logger():logger.remove()logger.add(sys.stdout,format(green{time:YYYY-MM-DD HH:mm:ss}/green | level{level: 8}/level | cyan{extra[trace_id]}/cyan | cyan{name}/cyan:cyan{function}/cyan:cyan{line}/cyan - level{message}/level),levelINFO,filterlambdarecord:record[extra].setdefault(trace_id,trace_id_var.get()),)logger.add(logs/app_{time:YYYY-MM-DD}.log,rotation00:00,retention7 days,format({time:YYYY-MM-DD HH:mm:ss} | {level: 8} | {extra[trace_id]} | {name}:{function}:{line} - {message}),levelDEBUG,filterlambdarecord:record[extra].setdefault(trace_id,trace_id_var.get()),)defmain():setup_logger()# 模拟请求上下文设置 trace_idtrace_id_var.set(generate_trace_id())logger.info(服务启动)logger.debug(调试信息仅出现在文件中)try:1/0exceptZeroDivisionError:logger.exception(捕获异常)if__name____main__:main()运行后控制台会输出类似2026-06-25 13:04:17 | INFO | a1b2c3d4e5f6 | __main__:main:36 - 服务启动同时logs/目录下会生成app_2026-06-25.log文件里面包含 DEBUG 级别的信息。5. 进阶在 FastAPI 中传递 trace_id如果你使用 FastAPI可以借助中间件自动管理trace_idfromfastapiimportFastAPI,RequestfromcontextvarsimportContextVarimportuuid appFastAPI()trace_id_var:ContextVar[str]ContextVar(trace_id,default)app.middleware(http)asyncdeftrace_middleware(request:Request,call_next):trace_idrequest.headers.get(X-Trace-Id,uuid.uuid4().hex[:12])tokentrace_id_var.set(trace_id)logger.bind(trace_idtrace_id).info(请求开始)responseawaitcall_next(request)trace_id_var.reset(token)returnresponse这样无需在每个业务函数中传递trace_id所有日志都会自动携带正确的值。6. 总结通过上面这段不到 30 行的setup_logger()我们实现了控制台彩色日志聚焦重要信息文件日志持久化按天轮转、自动清理跨模块、跨协程的trace_id自动注入不同 handler 使用不同日志级别兼顾效率与详细度。基于这套模板你可以快速为任何 Python 项目搭建起可观测的日志系统。现在去集成到你的项目里吧