如果你正在找一个Python日志库既不想花一天时间配logging模块又不希望输出像默认logging那样干巴巴的只有一行文字acrilog包会是一个值得试一下的选择。我最早注意到这个包是因为那天下午临时要接手一个写了一半的爬虫项目里面全是print和散落的日志压根没办法定位任务挂在哪一步。换上acrilog之后结构化输出、颜色区分、不同级别过滤这些事都变简单了整个排错过程舒服不少。这篇文章就把它的语法、常用参数和实际项目里的落地用法梳理一遍适合初学Python的读者也可以给已经在用logging想换换手感的人做参考。1. acrilog包到底是干什么的1.1 为什么不用print也不用默认logging很多人写脚本都喜欢用print调试这里打印一下那里打印一下好像挺方便。但项目一旦超过几百行或者在后台跑定时任务print的弊端就暴露得很明显没有时间戳、没有日志级别、不能统一开关、不好做文件输出更别提给日志染上颜色来区分错误了。Python标准库的logging功能其实很强但默认配置特别劝退新手。你需要记忆Handler、Formatter、Filter这些组件的用法还要搞清楚Logger、Handler和Propagate之间的关系。每次换项目都要重新写一遍配置代码复制来复制去稍不小心就会遇到日志重复输出、等级不生效这种玄学问题。我在很多教程里看到最后直接劝退。acrilog这个包做的事情很简单把logging常用的配置压缩成几个参数让日志系统开箱即用。它不是要取代标准库而是把底层细节包装得更好看一点。你用acrilog本质上还是在用logging的能力只是不再需要从零搭配置了。1.2 acrilog的设计思路和核心价值第一次看这个包的结构我感觉它最明显的特点是把“日志输出”拆成了两个层面一个是给自己的开发调试看的另一个是给业务分析用的。开发时你希望看到带颜色、带模块名、带行号的信息上线后你希望输出一段能被日志收集工具直接解析的JSON。acrilog恰恰允许你通过参数切换这两种模式不用改业务代码只在初始化Logger的时候调整配置就行。我之前在多个小项目里试过用它整体感受是省心。它解决的核心问题有三个第一格式化日志时间、级别、调用来源这些重复劳动第二提供更直观的API比如直接传ctx上下文信息不用手动拼字符串第三保留标准库logging的Handler体系所以原本你会用RotatingFileHandler、StreamHandler这些它也都支持。适合用它的人我觉得大概是两类。一类是刚学Python不久想快速给脚本加正规日志但又不想先啃一遍logging源码的初学者另一类是像我这样已经写了不少项目但希望日志模块的样板代码越少越好的开发者。2. 安装与基础语法先跑起来2.1 环境准备和安装步骤安装没什么特别的走pip就行。如果你已经在虚拟环境里直接执行下面这条命令pip install acrilog如果你用的是比较新的Python环境有些依赖包可能需要编译比如带颜色输出的平台相关模块。万一遇到安装失败先升级一下pip和setuptools再做尝试python -m pip install --upgrade pip setuptools wheel pip install acrilog我在macOS和Linux系统上都装过整个过程没碰到特别大的问题。Windows环境我没深入测过但考虑到它有颜色输出功能有条件的话建议在终端里先跑一条简单的日志试试确认不是所有日志都输出成乱码。装完之后可以看一下包的版本和主要对象import acrilog print(acrilog.__version__) print(dir(acrilog))如果正常打印出版本号和函数列表说明安装没问题。2.2 三行代码上手先跑起来我非常喜欢这个包的入门方式因为它真的可以做到三行代码出日志import acrilog logger acrilog.getLogger(demo) logger.info(hello acrilog)跑下来你会看到终端里不仅有时间、日志级别、Logger名还有带颜色的输出。准确颜色取决于你的终端是否支持ANSI转义序列VSCode终端和macOS的Terminal一般都可以。默认配置下acrilog.getLogger()会返回一个标准Logger所以函数名、括号参数这些语法和logging.getLogger的使用习惯是接近的。你不需要在每次调用时都指定格式Logger内部已经帮你配好了默认Formatter。这条最基础的用法适合单独脚本或者调试阶段。你只要保证在模块顶层初始化Logger后面所有地方都能import到同一个logger实例。2.3 五种日志级别怎么选acrilog保留了 logging 的标准级别从低到高分别是DEBUG、INFO、WARNING、ERROR、CRITICAL。每个级别对应一个同名方法调用方式跟标准库一致logger.debug(debug message) logger.info(info message) logger.warning(warning message) logger.error(error message) logger.critical(critical message)不同级别适合不同场合。在我的习惯里DEBUG级别我用来记录变量值、进入某个函数、连接状态这类细节INFO级别记录程序的主要流程比如任务开始、任务结束、请求总量WARNING记录可恢复的问题比如接口超时重试ERROR和CRITICAL分别对应单次失败和程序无法继续运行的情况。初始Logger默认显示级别是INFO所以DEBUG日志不会打出来。要让它显示DEBUG级详情可以在构造时把level参数设为DEBUGlogger acrilog.getLogger(debug_demo, levelDEBUG)如果你只想给某个业务模块开启更细的日志另外建一个带独立级别的Logger会更方便这样不会把其他模块的信息刷得铺天盖地。3. 核心参数解析到底有哪些关键配置3.1 高频参数对照表如果只是无脑用默认配置其实已经比print好用了。但要发挥这个包的价值还是要看懂几个核心参数。下面这个表我按自己使用的频率整理出来不一定包含包的全部参数但覆盖面已经能应对大多数项目。参数名类型默认值作用namestr必传Logger的名称位置参数levelstr/intINFO日志级别控制输出下限fmtstr内置模板日志输出格式模板use_colorsboolTrue是否启用颜色输出json_outputboolFalse是否输出为JSON格式handlerslistNone自定义Handler列表propagateboolFalse是否向父Logger传播日志ctxdictNone绑定的默认上下文信息file_pathstrNone直接把日志写入文件file_modestra写文件时的模式初次接触的人最容易忽略的是propagate参数。因为acrilog内部创建Logger后会默认把propagate设为False避免日志在根Logger上再打印一次造成重复。如果你自己额外加了一个根Logger配置就要特别注意这个行为否则可能要么没日志要么日志出现两遍。3.2 自定义输出格式和JSON格式fmt参数是控制日志模板的核心。它和logging的format概念类似但acrilog做了一点简化。你可以直接写常规的Formatter格式字符串logger acrilog.getLogger( custom_fmt, fmt%(asctime)s | %(levelname)s | %(name)s | %(message)s )这样每条日志会按照模板里的顺序输出。acrilog内部还是会用标准库的%格式语法所以你能用的字段名和logging是一致的。比如:%(asctime)s 时间%(levelname)s 级别%(name)s Logger名%(message)s 日志正文%(module)s 输出日志的模块名%(lineno)d 行号如果你不想记那么多格式符号直接把logfmt或者纯文本串塞进去也行关键是让它包含你排错需要的信息。我自己排错时最喜欢带模块和行号所以至少会在fmt里保留%(name)s和%(lineno)d。在微服务或者需要接入日志平台的场景下JSON输出会更有用。设置json_outputTrue每条日志会以单行JSON形式输出这样ELK、Loki或者自建日志平台解析起来更省事logger acrilog.getLogger( json_demo, json_outputTrue, fmt%(asctime)s | %(levelname)s | %(message)s )此时日志整体会被封装成一个JSON对象同时带上level、time、message这些字段。如果你把context传进去它也会成为JSON里的一个独立字段。这个模式对机器读取非常友好人工阅读虽然有少许牺牲但胜在结构清晰。3.3 上下文绑定与trace_id追踪我最喜欢acrilog的一点是它设计了上下文信息入口。日常写业务日志时我们经常需要把用户ID、订单号、请求ID之类的信息一起打出来。用print或者原生logging时最容易的做法是拼字符串logger.info(user %s pay %s amount %s, user_id, order_id, amount)拼是能拼但日志一多字段顺序容易乱后端要解析也不方便。acrilog可以在创建Logger时绑定统一的ctx字典也可以在一次日志调用里覆盖logger acrilog.getLogger( app_logger, ctx{app: web-server, env: prod} ) logger.info(payment success, ctx{user_id: 12345, order_id: A10086})这样输出的内容里会附带一份上下文信息不管是排查单个用户的问题还是做指标统计都比纯文本搜索方便得多。更进一步你可以把请求进入到网关时生成的trace_id绑定到ctx里这样一整条调用链上的日志都能通过同一个trace_id串起来logger.info(request started, ctx{trace_id: trace_id}) logger.info(db query done, ctx{trace_id: trace_id})如果是在异步框架或者多线程场景里手动传trace_id有点烦。通常我会在协程或线程入口处拿一下上下文里的trace_id然后把它塞到Logger的ctx中保证整段处理过程里trace_id始终一致。4. 实际应用案例从接口日志到多线程任务4.1 案例1FastAPI接口日志在Web开发里日志不止是给程序员调试用更多时候要记录请求来源、接口耗时、响应状态和异常信息。我用FastAPI搭服务的时候习惯在中间件里统一记录日志这样不用每个路由重复写。下面是一个很典型的中间件日志场景import time from fastapi import FastAPI, Request import acrilog logger acrilog.getLogger( api_access, levelINFO, json_outputTrue, ctx{app: shop-api} ) app FastAPI() app.middleware(http) async def access_log(request: Request, call_next): start time.time() try: response await call_next(request) logger.info( request handled, ctx{ method: request.method, path: request.url.path, status: response.status_code, duration_ms: round((time.time() - start) * 1000, 2) } ) return response except Exception as exc: logger.error( request failed, ctx{ method: request.method, path: request.url.path, exception: repr(exc) } ) raise这个例子用到了json_output和ctx每个路由的访问记录都会被包装成一行JSON里面带上了请求方法和状态码。后来我把日志接入日志收集平台解析这批JSON字段时基本没做额外清洗。如果你团队习惯看纯文本日志也可以把json_output关掉但ctx还是会以keyvalue的形式拼接在message后面信息不丢只是展示方式不同。4.2 案例2多线程任务跟踪用print调试多线程程序是最痛苦的因为多个线程的日志会穿插在一起分不清先后顺序也不知道每行日志来自哪个线程。acrilog的context机制在这里就能发挥很好的作用。拿一个简单爬虫任务举例我用ThreadPoolExecutor同时抓取多个页面每个线程需要一个唯一ID来关联日志import threading import time from concurrent.futures import ThreadPoolExecutor import acrilog logger acrilog.getLogger(crawler, levelINFO, use_colorsFalse) def fetch_page(page_id): thread_name threading.current_thread().name logger.info(start fetch, ctx{thread: thread_name, page_id: page_id}) time.sleep(0.5) if page_id % 3 0: logger.warning(timeout, retry, ctx{thread: thread_name, page_id: page_id}) else: logger.info(finish fetch, ctx{thread: thread_name, page_id: page_id}) with ThreadPoolExecutor(max_workers4) as pool: for pid in range(10): pool.submit(fetch_page, pid)运行后日志里的ctx会带上thread和page_id两个字段。即使多个线程交错打印只要过滤page_id或者thread字段就能把一趟任务的生命周期完全还原出来。这在排查并发请求超时或资源竞争问题的时候特别管用。4.3 案例3日志落盘与滚动开发环境看终端生产环境要落盘这是日志的基本要求。acrilog通过handlers参数和file_path参数可以很方便地接入标准libgging的文件Handler。最简单的做法是直接把日志写到指定文件logger acrilog.getLogger( file_demo, file_pathlogs/app.log, file_modea, levelINFO )但这个写法只适合日志量小的场景。大量日志会撑爆磁盘所以生产环境我一般会加上RotatingFileHandler按文件大小滚动切分import logging from logging.handlers import RotatingFileHandler import acrilog file_handler RotatingFileHandler( logs/app.log, maxBytes10 * 1024 * 1024, backupCount5, encodingutf-8 ) file_handler.setLevel(INFO) logger acrilog.getLogger( file_demo, levelINFO, handlers[file_handler] )这样单个日志文件达到10MB就会自动切一个备份最多保留5个历史文件。手动的Handler和acrilog自带的配置并不冲突它会把这个Handler挂到Logger上再用自己的Formatter去格式化输出。如果你希望文件里的格式和终端里的格式不一样后续自定义一个Formatter覆盖到handler上就行。对异常信息我建议用logger.exception代替logger.error。exception方法会自动把当前异常堆栈打出来排错省很多时间。不过example里要展示的话我还是用error加exc_infoTrue更通用try: risky_call() except Exception: logger.error(something broken, exc_infoTrue)5. 常见问题与排查技巧实录5.1 日志重复输出原因和解决思路这个问题几乎所有Python日志库都会遇到acrilog也不能免俗。日志重复输出通常有两个来源一个是自己又一次手动加了StreamHandler到Logger上另一个是Logger的父级也有Handler而且propagate被设为了True。acrilog默认propagate是False所以如果你只是单纯用getLogger不会莫名其妙重复。怕就怕你既用了acrilog又额外给同一个Logger添加Handler或者在项目里混用了原生logging根配置。我在一个老项目里就见过输出控制台的日志变成了两行一模一样的排查半天才发现是项目入口处配置了一次日志又在插件代码里重复配置。解决方式也很直白检查Logger.handlers如果列表里已经有想要的Handler就不要再次addHandler了。要看得直观一点可以在启动时打印一下logger acrilog.getLogger(probe) print(logger.handlers)如果看到两个StreamHandler删掉一个就好。再有就是确认propagate参数必要时直接设成False。5.2 中文乱码或者输出颜色在Windows下不对颜色输出和编码问题跨平台项目里基本绕不开。Linux和macOS终端对ANSI颜色支持得比较好Windows上如果是旧版cmd或者PowerShell颜色可能显示成[32m之类的转义字符看起来非常难受。最简单的处理方式是在acrilog初始化时关掉颜色logger acrilog.getLogger(win_demo, use_colorsFalse)这样确实失去了一点视觉上的级别区分但至少信息不会乱掉。在Windows上还经常遇到中文乱码特别是日志写入文件的时候。写入文件如果没指定encoding默认可能是系统本地编码有些环境不支持中文导致乱码。我用RotatingFileHandler时都会带上encodingutf-8参数基本能避免这个问题。如果终端里直接print中文乱码大概率不是acrilog的问题而是终端本身的编码设置。检查一下终端的编码改成UTF-8或者把环境变量PYTHONIOENCODING设为utf-8再试。5.3 API版本更新带来的兼容性问题在使用第三方库时最让人头大的就是版本升级后参数发生变化。我最早接触acrilog时很多配置都写在getLogger函数的关键字参数里后来版本调整后可能是统一的配置类接管了部分参数。如果你在升级包之后发现原来能跑的代码忽然报错多半就是参数名变了。遇到这种情况我一般的排查套路是三步运行help(acrilog.getLogger)看一下当前版本支持哪些参数。运行print(acrilog.__version__)记录版本号。把旧代码里的LEVEL_INFO替换成新参数等操作逐步调整。这里提醒一句如果项目的核心逻辑依赖某个特定版本的acrilog别急着追新版本。可以先在requirements.txt里锁住版本等兼容性确认后再升级。我吃过太多莫名奇妙的lib升级亏日志这种横切面组件尤其要稳。6. 我的几个实际使用心得写到这里最后分享几个我在真实项目里摸索出来、容易踩坑但又不常写进文档里的经验。第一不要在模块底层到处创建acrilog.getLogger。每个Python模块都新建一个Logger代码是能跑但后期做日志级别调整时你得改很多地方。我现在的习惯是项目里有一个logger.py把项目中需要用到的Logger统一创建好其他模块直接从这里import。这样所有Logger配置都集中在同一处要开DEBUG的时候改一个文件就够了。第二ctx字段尽量用小写加下划线。虽然包本身没有强制但日志平台对JSON字段命名通常有规范比如user_id、trace_id这种解析时才能保持统一。如果一会儿驼峰一会儿下划线后续做告警和指标会非常痛苦。第三日志格式里一定要有请求或业务标识。无论是trace_id还是order_id只要出现异常你能靠它把散落的日志连起来。没有这个字段日志越多越乱最后变成一堆难以定位的噪音。第四建议在测试环境先把日志跑上一两个小时。日志系统看起来简单但真正到了高并发、告警接入、磁盘配额这些场景很多小问题才会暴露出来。提前让日志在真实环境里转起来比上线后临时修要好太多。我这里说的pre-production验证也就是看看文件增长速率、错误日志是否完整、JSON是否能被正确解析这几件事。我自己用了acrilog之后最直接的改变是日志代码变短了排错效率提高了。以前写一套logging配置要几十行还要小心翼翼安排Handler现在大部分项目只需要初始化一个Logger然后在需要的地方调用就能持续得到整齐一致的日志。如果你也在找一个轻量又实用的Python日志方案不妨拿个十几分钟把acrilog的语法和参数试一遍尤其是ctx和json_output这两个功能在真实项目里会逐渐显出价值。
