如果你正在找一个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参数设为"DEBUG":
logger = acrilog.getLogger("debug_demo", level="DEBUG")如果你只想给某个业务模块开启更细的日志,另外建一个带独立级别的Logger会更方便,这样不会把其他模块的信息刷得铺天盖地。
3. 核心参数解析:到底有哪些关键配置
3.1 高频参数对照表
如果只是无脑用默认配置,其实已经比print好用了。但要发挥这个包的价值,还是要看懂几个核心参数。下面这个表我按自己使用的频率整理出来,不一定包含包的全部参数,但覆盖面已经能应对大多数项目。
| 参数名 | 类型 | 默认值 | 作用 |
|---|---|---|---|
| name | str | 必传 | Logger的名称,位置参数 |
| level | str/int | "INFO" | 日志级别,控制输出下限 |
| fmt | str | 内置模板 | 日志输出格式模板 |
| use_colors | bool | True | 是否启用颜色输出 |
| json_output | bool | False | 是否输出为JSON格式 |
| handlers | list | None | 自定义Handler列表 |
| propagate | bool | False | 是否向父Logger传播日志 |
| ctx | dict | None | 绑定的默认上下文信息 |
| file_path | str | None | 直接把日志写入文件 |
| file_mode | str | "a" | 写文件时的模式 |
初次接触的人最容易忽略的是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_output=True,每条日志会以单行JSON形式输出,这样ELK、Loki或者自建日志平台解析起来更省事:
logger = acrilog.getLogger( "json_demo", json_output=True, 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 案例1:FastAPI接口日志
在Web开发里,日志不止是给程序员调试用,更多时候要记录请求来源、接口耗时、响应状态和异常信息。我用FastAPI搭服务的时候,习惯在中间件里统一记录日志,这样不用每个路由重复写。
下面是一个很典型的中间件日志场景:
import time from fastapi import FastAPI, Request import acrilog logger = acrilog.getLogger( "api_access", level="INFO", json_output=True, 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还是会以key=value的形式拼接在message后面,信息不丢,只是展示方式不同。
4.2 案例2:多线程任务跟踪
用print调试多线程程序是最痛苦的,因为多个线程的日志会穿插在一起,分不清先后顺序,也不知道每行日志来自哪个线程。acrilog的context机制在这里就能发挥很好的作用。
拿一个简单爬虫任务举例,我用ThreadPoolExecutor同时抓取多个页面,每个线程需要一个唯一ID来关联日志:
import threading import time from concurrent.futures import ThreadPoolExecutor import acrilog logger = acrilog.getLogger("crawler", level="INFO", use_colors=False) 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_workers=4) 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_path="logs/app.log", file_mode="a", level="INFO" )但这个写法只适合日志量小的场景。大量日志会撑爆磁盘,所以生产环境我一般会加上RotatingFileHandler,按文件大小滚动切分:
import logging from logging.handlers import RotatingFileHandler import acrilog file_handler = RotatingFileHandler( "logs/app.log", maxBytes=10 * 1024 * 1024, backupCount=5, encoding="utf-8" ) file_handler.setLevel("INFO") logger = acrilog.getLogger( "file_demo", level="INFO", handlers=[file_handler] )这样单个日志文件达到10MB就会自动切一个备份,最多保留5个历史文件。手动的Handler和acrilog自带的配置并不冲突,它会把这个Handler挂到Logger上,再用自己的Formatter去格式化输出。如果你希望文件里的格式和终端里的格式不一样,后续自定义一个Formatter覆盖到handler上就行。
对异常信息,我建议用logger.exception代替logger.error。exception方法会自动把当前异常堆栈打出来,排错省很多时间。不过example里要展示的话,我还是用error加exc_info=True更通用:
try: risky_call() except Exception: logger.error("something broken", exc_info=True)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_colors=False)这样确实失去了一点视觉上的级别区分,但至少信息不会乱掉。在Windows上还经常遇到中文乱码,特别是日志写入文件的时候。写入文件如果没指定encoding,默认可能是系统本地编码,有些环境不支持中文,导致乱码。我用RotatingFileHandler时都会带上encoding="utf-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这两个功能,在真实项目里会逐渐显出价值。