ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

acrilog入门:用Python JSON日志告别排查噩梦

acrilog入门:用Python JSON日志告别排查噩梦 说实话刚接触 Python 日志这块时我一度觉得 acrilog 这名字挺冷门但它解决的是线上服务最让人头疼的问题之一日志不好查。先说结论如果你的 Python 服务部署到服务器以后还在用默认的 logging 输出“时间-级别-消息”这样的明文日志那在故障排查、日志采集、告警分析的时候大概率会被折腾到怀疑人生。尤其当请求量大起来以后日志里每一行都有换行、缩进、堆栈信息想从里面按字段筛选简直是灾难。acrilog 就是一个把 Python 标准 logging 输出改造成单行 JSON 的小封装轻量、无侵入几行代码就能让日志从“给人看”变成“既给人看也顺便给机器看”。这篇文章我会围绕标题里的三个关键词展开acrilog 的语法调用方式、init() 的核心参数拆解以及几个我在真实项目里用到的实际应用案例。还会附上自己踩过的坑和排查思路按我们写代码的惯例先给结论再给过程能帮你少走弯路。1. 初识 acrilog整体设计与核心思路1.1 acrilog 到底做了什么acrilog 的本质不是一个全新的日志框架而是对 Python 标准库 logging 的一次“格式化升级”。它主要做的事情可以拆成三块接收你的配置参数比如服务名、日志文件名、日志级别、是否同时输出到控制台在 logging 体系里注册一套 JSON Formatter把原本一行纯文本的日志记录转成一个 Python dict再通过 json.dumps 变成一行 JSON 字符串写入目标位置可以是文件也可以是控制台或者两者同时你可以把它理解为“给 logging 换了一身输出皮肤的皮肤包”。Python 的 logging 本身是分层架构Logger、Handler、Formatter 各司其职acrilog 做的事情就是帮你把 Handler 和 Formatter 装好让你不用手动去组装那一堆配置代码。为什么说它无侵入因为你在业务代码里依然是用 logging.getLogger(app.order) 这种标准写法根本不需要引入 acrilog 的日志类。acrilog 只负责在程序启动的时候初始化一次后面的所有日志调用照旧。这意味着你想在项目里引入它不需要把已有的日志调用全部改一遍风险很小。1.2 为什么 JSON 日志如此重要很多人觉得日志只要能打出来就行文本格式也挺清晰的。我举个实际例子你有一条访问日志明文可能是这样2024-11-11 10:00:00,123 - INFO - api access - user_id1024 cost230ms看起来还算整洁对吧但当你想统计今天的平均耗时、按 user_id 筛选某个用户的全部请求、或者把日志导入 Elasticsearch 做聚合分析的时候传统文本就麻烦了。你需要写正则从字符串里抠字段字段一多正则就变成天书而且只要日志内容稍微有点格式变化正则立刻失效。换成 JSON 之后日志变成这样{app: user-service, logger: app.api, level: INFO, time: 2024-11-11T10:00:0008:00, user_id: 1024, cost_ms: 230, msg: api access}Elasticsearch、Loki、ClickHouse 这些日志系统拿到 JSON 以后可以直接解析字段不需要 grok 规则不需要正则提取mapping 自动生成查询的时候点字段名就行。这就是结构化日志最大的价值让机器能读懂日志。而 acrilog 就是帮你把标准库输出快速改造成结构化格式的最省事方案。1.3 为什么选 acrilog 而不是自己封装有人可能会说JSON 日志我自己写个 Formatter 也不难何必用第三方包确实一个自定义 JSON Formatter 大概几十行就能写完。但自己封装容易忽略几个细节异常堆栈的格式化、时间戳时区处理、中文编码问题、多 handler 的重复输出问题、日志文件轮转和目录创建等。acrilog 把这些边界情况提前处理好了。更关键的是acrilog 走的是正统 logging 体系不是造轮子。它的配置入口只有一个函数使用成本低出问题的时候你可以随时翻源码或者退回标准库方案。对中小型项目来说这是性价比最高的选择。2. 安装与基础用法先把最小配置跑起来2.1 安装环境准备acrilog 是纯 Python 包安装方式和其他包没什么两样pip install acrilog建议在虚拟环境里安装避免污染系统 Python。我实测常见的 Python 3.8、3.9、3.10 环境都没问题Python 3.7 及以下版本我没有验证过如果你用的是老版本建议先升级解释器版本。装完以后验证一下是否成功python -c import acrilog; print(acrilog.__version__)如果能正常输出版本号说明安装没问题。2.2 最小可运行配置最基础的用法在程序入口处调用 acrilog.init() 就行import logging import acrilog acrilog.init( app_namedemo-service, file_namelogs/demo.log, levelINFO, ) logger logging.getLogger(app.main) logger.info(service started)这段代码做的事情是初始化 acrilog把日志写入 logs/demo.log 文件日志级别为 INFO然后测试输出一条消息。打开日志文件看到的应该是一行 JSON 格式的日志。这个文件里包含的时间、logger 名、级别、消息、代码调用位置等字段就是日志系统最喜欢的东西。这里要提醒的一点是如果你传的 file_name 带子目录先确认目录已经存在。我遇到过某些版本不会自动创建父目录直接报 FileNotFoundError排查时很容易忽略这个细节。安全起见可以先写一句import os os.makedirs(logs, exist_okTrue)2.3 让日志同时输出到控制台和文件默认情况下acrilog 只写文件。很多人在本地调试时希望控制台也能看到日志这时需要加一个 stream 参数acrilog.init( app_namedemo-service, file_namelogs/demo.log, streamTrue, levelDEBUG, )streamTrue 表示同时把日志输出到控制台。注意控制台输出的也是 JSON 格式因为 acrilog 的逻辑就是“统一格式”。如果你希望控制台保持人能看懂的明文、文件里存 JSON那单靠 acrilog 的默认配置做不到需要自定义 Handler后面我讲案例时会提到。关于日志文件的大小我曾经算过一笔账假设每个请求产生两条日志一条访问日志大约 300 字节单机 QPS 100一天下来日志量是 2 条×300字节×100×86400秒≈5.2GB。这个数字非常可观。所以日志文件的切割策略一定要提前想好否则磁盘被日志塞满是迟早的事。acrilog 默认的 filemode 是追加模式不会自动按天切割生产环境建议配合系统的 logrotate或者干脆用 Python 的 logging.handlers.TimedRotatingFileHandler 自己搭建切割方案。3. acrilog 语法与参数深度解析3.1 先理解 acrilog 的“语法”模式acrilog 不是一门新语言它没有自定义的 DSL。很多人第一次接触时以为要学一套新语法其实不需要。acrilog 对外暴露的用法就是“引入 init() 配置 标准 logging 调用”三段式。说它是语法不如说是一套“配置规则”。关键点在这几个在程序入口调用 acrilog.init() 并传入参数此时完成全局配置在业务模块里继续用 logging.getLogger() 获取 logger完全不用关心 acrilog 的存在日志调用方式不变logger.info()、logger.warning()、logger.error() 都照常使用这套规则非常简单没有语法糖卖弄学习成本几乎为零。但它有一个隐含的约定init() 必须在你的业务代码第一次写日志之前完成否则部分日志可能会在 acrilog 接管之前就已经按默认格式输出了。3.2 init() 核心参数逐个拆解init() 是 acrilog 最重要的接口大部分问题都出在参数理解上。我把常用参数整理成一张表结合我的使用经验逐个说明参数名类型默认值作用说明app_namestr无服务名写入 JSON 里的 app 字段用于区分日志来源module_namestr无模块名可叠加到日志字段中方便识别模块file_namestr有默认日志文件名日志文件路径建议显式指定避免找不到文件filemodestra文件写入模式a 追加生产环境不要用 wencodingstrutf-8文件编码日志含中文时必须设为 utf-8levelstr/intINFO日志级别可用 DEBUG/INFO/WARNING/ERROR 或对应数字streamboolFalse是否同时输出到控制台formatboolTrue是否输出 JSON 格式置为 False 时输出接近 logfmt 的文本datefmtstr无时间格式化字符串可按需自定义json_kwargsdict无传给 json.dumps 的额外参数例如 ensure_ascii、indentstacklevelint1记录日志调用位置时向上追溯的栈层数装饰器场景会用这里挑几个重点展开。level参数控制日志阈值。传字符串 INFO 还是数字 20 效果一样因为 logging 模块本身就把级别名映射到数值。我习惯用字符串代码可读性更高。注意 DEBUG 级别在线上慎开它会输出海量细节磁盘压力非常大。format参数是 acrilog 的灵魂。默认 True 表示输出 JSON。如果你把它设为 False日志会变成类似2024-11-11 10:00:00 INFO [app.main] service started这种接近标准库的格式。在一些还没有日志采集系统的项目里这个模式可以作为过渡方案。json_kwargs是一个容易被忽略但非常重要的参数。比如日志里有中文json.dumps 默认会做 ASCII 转义输出变成\u4e2d\u6587人眼完全没法看。解决办法是传acrilog.init( ..., json_kwargs{ensure_ascii: False} )这样中文就能正常显示。如果你对接的是日志系统建议不要传 indent 缩进保持单行输出方便 Filebeat 这类采集器按行读取。我在本地调试时会临时加 indent2人眼看结构方便但线上我一定去掉。3.3 参数组合与动态调整参数之间不是孤立的选型时要考虑实际场景。我总结了几组常用组合本地开发streamTruelevelDEBUGjson_kwargs 加 indent方便观察测试环境streamFalselevelINFOfile_name 指向独立的 test 日志目录生产环境streamFalselevelINFOfile_name 按天命名配合日志轮转有些项目需要动态调整日志级别比如线上出问题时想临时把某个模块切到 DEBUG。acrilog 自身不提供运行时改级别的接口但你可以直接操作标准库import logging logging.getLogger(app.order).setLevel(DEBUG)因为 acrilog 并没有脱离 logging 体系所以标准库的很多玩法它都天然支持。这个点很多人不知道遇到“项目里能否运行时调日志级别”的问题时直接用标准库方法就行。还有一个小技巧如果你想在日志里看到更多调用上下文比如是哪个业务函数写的日志可以调高 stacklevel。例如在装饰器或工具函数里记录日志时默认 stacklevel1 会指到工具函数本身改成 2 或 3 可以让日志文件里的代码位置指向真正调用业务逻辑的地方。3.4 自定义扩展点Formatter 与 Handleracrilog 不万能遇到定制化需求时你依然可以回到 logging 的生态里补刀。比如你想要控制台是明文、文件是 JSON就可以这么做import logging import acrilog from acrilog.formatter import JSONFormatter acrilog.init(app_namedemo, file_namelogs/demo.log, streamFalse) root_logger logging.getLogger() console_handler logging.StreamHandler() console_handler.setFormatter(logging.Formatter(%(levelname)s - %(message)s)) root_logger.addHandler(console_handler)核心思路是先用 acrilog 把 JSON 文件输出配好再额外挂一个 StreamHandler用标准 Formatter 输出明文。由于两个 Handler 的 Formatter 不同文件是 JSON、控制台是明文互不影响。如果你是重度定制用户建议花十分钟看一下 acrilog 源码里的 formatter.py弄清楚 JSON 字段是怎么组装起来的这样加自定义字段、改字段名、过滤字段都游刃有余。它不复杂就是遍历 LogRecord 的属性再 json.dumps 一下。4. acrilog 实际应用案例4.1 案例一FastAPI 接口服务接入FastAPI 是目前很火的 Web 框架我把 acrilog 接入过多个 FastAPI 服务流程很固定。先看代码import logging import os from fastapi import FastAPI import acrilog os.makedirs(logs, exist_okTrue) acrilog.init( app_nameuser-service, file_namelogs/user_service.log, levelINFO, json_kwargs{ensure_ascii: False}, ) logger logging.getLogger(app.api) app FastAPI(titleuser-service) app.get(/health) def health(): logger.info(health check called) return {status: ok} app.get(/users/{user_id}) def get_user(user_id: int): logger.info(fetch user info, extra{user_id: user_id}) return {user_id: user_id, name: test}接入点有两个入口处 init() 一次业务代码里照常用 logger。需要注意 extra 参数的问题。我在实际测试中发现extra 里的字段是否会出现在最终的 JSON 输出里取决于你安装的 acrilog 版本里 JSONFormatter 的实现。有些版本会把 extra 合并进 JSON有些版本不会。所以如果你依赖自定义字段做查询一定要先写一段代码验证一下logger.info(test extra, extra{request_id: abc123})然后去日志文件里 grep 一下 request_id。如果日志里没有这个字段有两个解决办法一是把字段拼进 message 字符串二是自定义一个 Formatter把 LogRecord 的dict全部展开到 JSON 里。我个人更推荐第二种因为这样日志字段更统一。4.2 案例二多模块多文件分离有的项目模块分工明确比如订单模块、用户模块、支付模块希望日志分开存文件。acrilog 默认是一个配置文件一把梭不会自动按模块区分离文件但你可以自己构建。方案是这样的先通过 acrilog 初始化默认配置然后对需要单独文件的 logger手动挂一个带 JSON Formatter 的 FileHandlerimport logging import acrilog from acrilog.formatter import JSONFormatter acrilog.init(app_nameshop, file_namelogs/main.log, levelINFO) order_logger logging.getLogger(app.order) order_file_handler logging.FileHandler(logs/order.log, encodingutf-8) order_file_handler.setFormatter(JSONFormatter()) order_logger.addHandler(order_file_handler)这样 order_logger 的日志会同时写到主日志和 order.log 里。如果你不希望重复写可以把 order_logger 的 propagate 设置为 False让它不再向 root 传递order_logger.propagate False这个操作相当于为某个 logger 建立了一条独立的日志通道适合专题类日志比如支付回调、第三方接口调用。这些日志单独存放排查时直接翻独立文件效率提高很多。4.3 案例三对接 ELK 或 Loki 日志采集服务跑起来以后日志最终要送到日志平台。最常见的是用 Filebeat 采集日志文件然后送到 Elasticsearch。acrilog 在这里的价值非常明显因为输出是单行 JSONFilebeat 不需要任何解析规则直接按 JSON 采集就行。一个最小可用的 filebeat.yml 配置片段长这样filebeat.inputs: - type: log enabled: true paths: - /data/logs/*.log json: keys_under_root: true add_error_key: true output.elasticsearch: hosts: [localhost:9200]注意 json.keys_under_root 这个参数它表示把 JSON 里的字段直接映射到文档的根级别查询时可以直接用 level、app、logger 这些字段做过滤。如果用的是 Loki Promtail配置也类似Promtail 对于 JSON 日志可以通过 json 解析器提取标签scrape_configs: - job_name: demo static_configs: - targets: [localhost] labels: job: demo __path__: /data/logs/*.log pipeline_stages: - json: expressions: level: level app: app这里的 json 表达式就是把 acrilog 输出的 JSON 字段映射成 Loki 的 label。没有 acrilog 的话这一层解析会痛苦得多尤其在字段多、嵌套深的情况下。4.4 案例四把第三方组件的访问日志也统一成 JSON我在实际项目里遇到一个很烦的情况业务日志通过 acrilog 全部 JSON 化了但 FastAPI 底层用的 uvicorn 访问日志还是老样子。日志平台里一半是 JSON一半是纯文本检索时体验割裂。解决思路是把 uvicorn 相关 logger 的默认 handler 清掉让它们把日志传播给 root logger由 root 上的 acrilog Handler 统一输出 JSON。import logging import acrilog acrilog.init(app_nameapi-gateway, file_namelogs/access.log, levelINFO) for name in (uvicorn, uvicorn.error, uvicorn.access): log logging.getLogger(name) log.handlers.clear() log.propagate True清掉 handler 以后uvicorn 的日志会向上传播到 root loggerroot logger 上挂着 acrilog 的 JSON Handler于是所有访问日志都被改造成 JSON 格式。实测下来效果很好日志平台里格式完全统一了。这里有一个注意点清 handler 的操作必须在 uvicorn 开始打印访问日志之前完成所以最好放在入口模块的最前面。另外如果你关闭了 uvicorn 的 access log上面的操作对访问日志无效因为根本没有日志产生。5. 常见问题与排查技巧实录5.1 六个高频问题速查现象可能原因解决办法日志文件没生成file_name 目录不存在或权限不足先创建目录确认进程对目录有写权限中文变成 \uXXXXjson.dumps 默认 ASCII 转义init 时传 json_kwargs{ensure_ascii: False}控制台输出乱糟糟的 JSON 串streamTrue 导致控制台也输出 JSON关掉 stream自定义一个明文 StreamHandler日志重复出现两遍init() 被调用多次root 挂了多个 handler保证 init 只调用一次重复 handler 时先清 root.handlers第三方库日志还是明文第三方用了自己的 logger 或 handler清除对应 logger 的 handlers设置 propagateTrue日志级别调整不生效init 之后又用 basicConfig 重置了 root不要在 init 后调用 basicConfig用 logger.setLevel 调节这里面有一个坑特别常见不少人习惯在代码某处顺手调了 logging.basicConfig()这个函数会在 root logger 上再注册一个 handler轻则导致日志重复输出重则把 acrilog 配置的 JSON Formatter 覆盖掉。排查时如果发现 JSON 格式突然失效优先怀疑是不是有人调了 basicConfig。还有一个关于 streamTrue 的体验问题。开发时开着 stream 确实爽但控制台输出的 JSON 字符串通常很长日志一多眼都花了。我的习惯是本地调试时把 json_kwargs 里加 indent2线上再改回单行。虽然多写几行配置但调试体验提升明显。5.2 排查思路与避坑笔记如果你已经接入 acrilog却发现日志行为不符合预期我的排查顺序通常是这样第一步用最精简的脚本复现。单独写一个 py 文件只做 init 和一条 INFO 输出看结果对不对。如果最小复现正常说明问题出在业务代码里比如 init 被多次调用、某个 logger 被单独设置了 handler。如果最小复现也不正常多半是版本问题或安装问题重新确认 acrilog 版本和 Python 版本。第二步检查 logger 的传播关系。Python logging 的日志传递符合“子 logger 没有 handler 就往父级传播”的规则。你写 logger logging.getLogger(app.order) 时如果 app.order 这个 logger 本身有 handler消息就不会往 root 走acrilog 配置的 JSON Handler 自然收不到。很多“JSON 格式只对部分模块生效”的问题都出在这里。第三步看 JSON 里的时间字段。默认输出的时间有时带微秒和时区信息这对接入 Elasticsearch 很友好但如果你的日志系统对时间格式有要求就需要自己调 datefmt。举个例子我希望日志时间精确到毫秒且带时区可以设置acrilog.init( ..., datefmt%Y-%m-%dT%H:%M:%S.%f%z )这个格式是 ISO 8601 风格Elasticsearch 可以直接识别不用另外做日期格式转换。再说一个容易被忽略的生产问题日志文件切割。acrilog 本身不做按大小或按天的自动轮转生产环境日志量一大日志文件会变成一个几十 GB 的巨型文件到时候别说采集了连打开文件排查都卡。我的做法是把 acrilog 输出的日志文件名设计成带日期的固定命名比如 logs/app-20241111.log然后交给系统的 logrotate 或其他日志轮转工具去处理。如果你不太想在系统层面配置可以在代码里手动调用 TimedRotatingFileHandler 完成轮转from logging.handlers import TimedRotatingFileHandler handler TimedRotatingFileHandler( logs/app.log, whenmidnight, backupCount7, encodingutf-8 )设置 backupCount7 表示只保留最近 7 天的日志。这个数值怎么定我建议根据你的磁盘容量和单日日志量算一下宁可多留两天也不要因为日志被清掉导致丢了关键排查线索。5.3 用 jq 快速排查 JSON 日志日志全部变成 JSON 之后日常排查其实变得更爽了因为你可以直接用 jq 命令做结构化筛选。这在服务器上没有 Kibana、没有 Loki 的情况下尤其好用。比如我想找出今天所有 ERROR 级别的日志cat logs/app.log | jq -c select(.level ERROR)想看某个用户 ID 相关的日志cat logs/app.log | jq -c select(.user_id 1024)按时间范围过滤cat logs/app.log | jq -c select(.time 2024-11-11T00:00:00) | {time, level, msg}这一套操作在线上应急排查时非常高效。以前我遇到问题都是 grep 一串字符串再对着多行明文日志猜结构现在直接 jq 一条命令结果一目了然。这也算是我用了 acrilog 之后意外收获的一个排查习惯。提示jq 是 Linux 环境下的 JSON 处理命令行工具没有的话用 yum install jq 或 apt install jq 装一下几十 KB 的大小排查日志非常值。写在最后的一点体会acrilog 这个包不大代码量也不算多但它在“让日志从人能看变成机器也能看”这件事上帮我省了非常多的时间。我在几个生产项目里接入之后最大的感受是去日志平台检索问题时不再依赖正则和字符串匹配直接按字段拖拽组合查询排查效率提升了一个量级。如果你只是一个小脚本、一个一次性的任务用不用 acrilog 都无所谓但只要你做的是长期运行的服务或者已经接入了日志采集平台我非常建议花十分钟把日志切换到 JSON 格式。acrilog 是一个不错的起点它的学习成本几乎为零退出成本也低——就算将来不满足需求你也能轻松回到标准 logging 生态里做自定义改造。最后再分享一个小技巧接入 acrilog 之后可以在项目的初始化脚本里加一句自检日志输出 acrilog 的版本号和当前配置。这样你排查问题的时候第一眼就能判断线上跑的是哪个版本的包、配置是否生效避免在版本不一致上花时间。项目越来越大以后这个习惯能帮你省掉很多无谓的猜测。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表