登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python logging Filter 怎样给记录补充字段

来源:17golang原创

时间:2026-10-18 14:18:13 426浏览 收藏

Python logging 的 Filter 不仅能决定记录是否保留,也能为 LogRecord 补充自定义字段。若字段只属于某个输出端,在 Python 3.12 及以上可以把 Filter 挂到该 Handler:复制原记录,给副本补字段,再返回副本。这能避免为了一个 Handler 的格式需求而原地改动其他 Handler 也会看到的记录。

官方文档:https://docs.python.org/3/library/logging.html#filter-objects;Handler 上下文隔离示例:https://docs.python.org/3/howto/logging-cookbook.html#imparting-contextual-information-in-handlers。下面只讨论 Filter、记录属性和 Handler 的关系,不扩展到日志收集平台。

先确定字段属于哪个输出端

设一个 Logger 同时有 rich_handler 和 raw_handler。前者要求组件名称 app_component 与请求标识 app_request_id,后者只保留消息。目标不是改写业务消息,而是让 rich_handler 的 Formatter 总能拿到约定字段,同时不把补充字段写回原记录。

因此 OutputFields 只挂到 rich_handler。若多个输出端都需要这些字段,可分别配置它们;若要在记录的共同入口补充,则要重新考虑 Logger 级 Filter 的适用范围。父 Logger 上挂 Filter 并不会自动处理子 Logger 传播来的所有事件;传播会将记录交给祖先的 Handler,不能把 Logger 的父子关系误当成过滤器的继承关系。

定义补字段的最小接口

import copy
import logging


class OutputFields(logging.Filter):
    def filter(self, record):
        # Python 3.12+:只给此 Handler 使用的副本补字段。
        enriched = copy.copy(record)
        enriched.__dict__.setdefault("app_component", "worker")
        enriched.__dict__.setdefault("app_request_id", "-")
        return enriched

filter(record) 的输入是 LogRecord,不是最终字符串。示例返回另一个 LogRecord,由当前 Handler 后续处理使用;如果目标是过滤掉记录,应返回 False。写了赋值却忘记返回值,隐式 None 会被当作不保留该记录,日志可能因此消失。

这里 app_component 默认 worker,app_request_id 默认短横线。它们只是本示例的业务字段,不代表真实服务或请求。copy.copy 创建浅副本后再修改其属性字典,原记录上的标量属性不会因此被替换;并未承诺副本中所有嵌套对象也独立。

Logger、两个Handler与字段过滤器的静态配置关系
图1:Handler 配置契约说明图。增强端绑定 OutputFields 与 Formatter,原始端不绑定补字段过滤器;连线表示配置关系,不是执行时序或运行截图。

让格式器与字段约定一致

rich_handler 的格式串引用 %(app_component)s 和 %(app_request_id)s,过滤器必须保证走到该格式器的记录具有对应属性。只给部分日志调用传 extra,其他调用不传,容易留下缺字段的格式化错误。

setdefault 表达“缺少才补”,因此调用方通过 extra 传入的 app_request_id 会被保留;它不会把已存在的 None 或空字符串换成默认值。如果业务要求这些值也降级,应另写明确判断,不能把“补缺省”与“清洗取值”混在一起。

自定义字段应避开 msg、args、levelname 等标准 LogRecord 属性。尤其 extra 与已有属性冲突时,记录创建阶段就可能失败,后面的 Handler Filter 无法替这个冲突兜底。不要用通用的 name 字段覆盖日志系统原有的 Logger 名称。

用双Handler示例表达隔离关系

完整示例将两路输出分别写入内存字符串流,不创建文件,也不改变 root Logger。InspectOriginal 只在 raw_handler 收集它所看到的属性名,从而说明“没有打印字段”与“根本没收到新增字段”不是同一个判断。

import copy
import io
import logging


class OutputFields(logging.Filter):
    def filter(self, record):
        # 浅复制后只新增或替换标量属性,不修改嵌套对象。
        enriched = copy.copy(record)
        enriched.__dict__.setdefault("app_component", "worker")
        enriched.__dict__.setdefault("app_request_id", "-")
        return enriched


class InspectOriginal(logging.Filter):
    def filter(self, record):
        # 原始输出端记录所见属性,便于理解是否发生字段泄漏。
        self.seen_keys.append(set(record.__dict__))
        return True

    def __init__(self):
        super().__init__()
        self.seen_keys = []


def main():
    # 使用独立 Logger 与内存流;不要清理全局 root 配置。
    logger = logging.getLogger("field_demo")
    logger.setLevel(logging.INFO)
    logger.propagate = False
    rich_buffer = io.StringIO()
    raw_buffer = io.StringIO()
    rich_handler = logging.StreamHandler(rich_buffer)
    raw_handler = logging.StreamHandler(raw_buffer)
    inspector = InspectOriginal()

    rich_handler.addFilter(OutputFields())
    rich_handler.setFormatter(logging.Formatter(
        "%(app_component)s %(app_request_id)s %(message)s"
    ))
    raw_handler.addFilter(inspector)
    raw_handler.setFormatter(logging.Formatter("%(message)s"))

    logger.addHandler(rich_handler)
    logger.addHandler(raw_handler)
    try:
        logger.info("任务已受理")
        # 调用方有请求标识时保留,不被默认值覆盖。
        logger.info("任务已受理", extra={"app_request_id": "req-demo"})
        print("增强输出:")
        print(rich_buffer.getvalue(), end="")
        print("原始输出:")
        print(raw_buffer.getvalue(), end="")
        print("原始记录含组件字段:", [
            "app_component" in keys for keys in inspector.seen_keys
        ])
    finally:
        # 只移除本示例创建的 Handler,不破坏其他日志配置。
        for handler in (rich_handler, raw_handler):
            logger.removeHandler(handler)
            handler.close()
        rich_buffer.close()
        raw_buffer.close()


if __name__ == "__main__":
    main()

将代码保存为 handler_fields.py,读者可在 Python 3.12 及以上自行运行:

# 用本机 Python 3.12 或更高版本运行独立示例。
python3 handler_fields.py

本文没有运行示例。按配置推导的预期输出如下;两路各收到两条记录,原始记录不含增强端新增的 app_component:

增强输出:
worker - 任务已受理
worker req-demo 任务已受理
原始输出:
任务已受理
任务已受理
原始记录含组件字段: [False, False]

第二条原始记录仍然可以包含调用方原本通过 extra 提供的 app_request_id。隔离的是 OutputFields 对副本的改动,不是剥离原记录中已有的一切自定义属性。此脚本按独立运行场景编写;服务集成时应通过应用统一配置避免重复挂载 Handler。

原始记录、副本和两个自定义字段的静态归属
图2:记录副本与属性归属说明图。重点看原记录与浅副本、标量字段及增强格式器的关系,不把不显示字段当作隔离证明;图本身不是执行证据。

区分旧版本原地修改与新版本替换

从 Filter 返回替换 LogRecord 是 Python 3.12 引入的行为。更早版本不能直接用上面的返回副本方法达到替换效果;普通原地赋值后返回 True 虽能补字段,但改的是共享记录,其他 Handler 可能受到影响。不要把“Filter 挂在某个 Handler 上”等同于“原地修改只对这个 Handler 可见”。

写法含义注意点
改 record,返回 True保留并修改原记录需考虑其他 Handler 可见的副作用
改副本,返回副本替换当前处理记录本示例要求 Python 3.12+
返回 False丢弃当前过滤范围内的记录不是仅跳过补字段

若旧环境必须做输出端隔离,可将复制职责放到专门的 Handler 包装逻辑中另行设计,或升级运行环境;不要不加说明地提供同一段跨版本“兼容”代码。另一个常见问题是级别门槛:被 Logger 或 Handler 级别排除的记录,不能指望靠补字段过滤器重新救回。

补字段时的常见疑问

可以在 Filter 里再次调用同一 Logger 报错吗?

不宜这么做,可能再次触发同一过滤器,造成递归。过滤器应尽量保持轻量,不在每条日志上发网络请求或进行复杂阻塞操作;字段来源故障需另设不会回到同一链路的处理策略。

请求标识应该存成 Filter 的共享可变属性吗?

并发请求下这样容易把不同请求的值混用。本例只填固定兜底值并保留 extra;需要动态请求上下文时,应由可靠的上下文机制提供,不能把这个固定默认值当请求追踪方案。

浅复制能隔离字典字段内部的修改吗?

不能保证。若记录已有某个字典或列表属性,浅副本仍可能引用同一个内部对象。本文只在副本属性字典里补标量值;如需改嵌套内容,应对目标属性单独复制并明确代价。

实际选择可压缩为三句话:先确定字段属于哪些 Handler;缺字段用显式兜底,已有字段按契约保留;要求输出端隔离时,在 Python 3.12+ 的 Handler Filter 中返回改好的记录副本,不原地污染共享记录。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>