You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python Logging模块extra属性与自定义格式化器兼容问题排查

解决Python logging模块extra参数不生效问题

问题描述

在Python项目中测试自定义logger时,extra参数始终无法生效:既无法在格式字符串中配置该属性,自定义格式化器也无法读取它。手动传入extra或依赖LoggerAdapter自动添加的场景均失效,在配置文件中添加hostname/ip字段时会抛出ValueError: Formatting field not found in record: 'hostname'错误,其余日志功能正常。

错误点分析

  1. LoggerAdapter未正确赋值
    创建LoggerAdapter后没有将其赋值给原logger变量,后续日志调用仍然使用原始的logging.Logger实例,而非带额外参数的Adapter。
  2. 自定义Formatter对extra的处理逻辑错误
    LoggerAdapter会将extra中的键值对直接合并到LogRecord的__dict__属性中,而非存入record.extra字段。原代码中通过getattr(record, "extra", {})无法获取到传入的额外参数。
  3. 格式字符串未处理缺失字段
    配置文件中直接使用%(hostname)s这类字段时,若存在未携带该参数的日志记录,会触发字段缺失错误。

修正步骤

1. 正确初始化LoggerAdapter

修改main.py中的logger初始化代码,将创建的LoggerAdapter赋值给logger变量:

import logging
import socket
import subprocess

# 初始化logger并包装为LoggerAdapter
logger = logging.getLogger(__name__)
hostname = socket.gethostname()
host_ip = subprocess.run(
    [r"ip -4 addr show ztjlhzlhyj | grep -oP '(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'"],
    capture_output=True, text=True, shell=True
).stdout.splitlines()[0].strip()
# 将LoggerAdapter赋值给logger,后续调用使用该实例
logger = logging.LoggerAdapter(logger, extra={"hostname": hostname, "ip": host_ip})

# 手动传入extra的示例(此时可省略,Adapter会自动添加,若需覆盖可保留)
logger.debug("Loading env...")

# 依赖LoggerAdapter自动添加extra的示例
try:
    raise Exception("Test error")
except Exception as e:
    logger.error(e)

2. 修改自定义Formatter的extra处理逻辑

调整maxlogger.py中MaxJSONFormatter的_prepare_log_dict方法,直接读取LogRecord.__dict__中的非内置属性:

import json
from datetime import datetime, timezone
import logging
from typing import Optional

LOG_RECORD_BUILTIN_ATTRS = {
    "args", "asctime", "created", "exc_info", "exc_text", "filename",
    "funcName", "levelname", "levelno", "lineno", "module", "msecs",
    "message", "msg", "name", "pathname", "process", "processName",
    "relativeCreated", "stack_info", "thread", "threadName", "taskName",
}

class MaxJSONFormatter(logging.Formatter):
    def __init__(self, *, fmt_keys: Optional[dict[str, str]] = None):
        super().__init__()
        self.fmt_keys = fmt_keys if fmt_keys is not None else dict()

    def format(self, record: logging.LogRecord) -> str:
        message = self._prepare_log_dict(record)
        return json.dumps(message, default=str)

    def _prepare_log_dict(self, record: logging.LogRecord) -> dict:
        always_fields = {
            "message": record.getMessage(),
            "timestamp": datetime.fromtimestamp(
                record.created, tz=timezone.utc
            ).isoformat()
        }

        if record.exc_info is not None:
            always_fields["exc_info"] = self.formatException(record.exc_info)

        if record.stack_info is not None:
            always_fields["stack_info"] = self.formatStack(record.stack_info)

        # 构建基础字段
        message = {
            key: msg_val
            if (msg_val := always_fields.pop(val, None)) is not None
            else getattr(record, val)
            for key, val in self.fmt_keys.items()
        }

        # 添加始终字段
        message.update(always_fields)

        # 添加所有非内置的额外属性(包括extra传入的字段)
        for key, val in record.__dict__.items():
            if key not in LOG_RECORD_BUILTIN_ATTRS and key not in message:
                message[key] = val

        return message

3. 配置文件中处理缺失字段

在格式字符串中为hostname/ip添加默认值,避免因日志未携带该字段报错:

{
  "version": 1,
  "disable_existing_loggers": false,
  "formatters": {
    "simple": {
      "format": "[%(levelname)s]: %(message)s"
    },
    "detailed": {
      "format": "[%(levelname)s|%(module)s|%(lineno)d|%(hostname)s:-|%(ip)s:-] - %(asctime)s: %(message)s",
      "datefmt": "%Y-%m-%dT%H:%M:%S%z"
    },
    "json": {
      "()": "maxlogger.MaxJSONFormatter",
      "fmt_keys": {
        "level": "levelname",
        "message": "message",
        "timestamp": "timestamp",
        "logger": "name",
        "module": "module",
        "function": "funcName",
        "line": "lineno",
        "thread_name": "threadName"
      }
    }
  },
  "handlers": {
    "stderr": {
      "class": "logging.StreamHandler",
      "level": "WARNING",
      "formatter": "detailed",
      "stream": "ext://sys.stderr"
    },
    "file": {
      "class": "logging.handlers.RotatingFileHandler",
      "level": "DEBUG",
      "formatter": "detailed",
      "filename": "logs/debug.log",
      "maxBytes": 104857600,
      "backupCount": 3
    },
    "db": {
      "()": "maxlogger.MaxDBHandler",
      "level": "WARNING",
      "formatter": "json"
    },
    "queue": {
      "()": "maxlogger.MaxQueueHandler",
      "handlers": [
        "stderr",
        "file",
        "db"
      ],
      "respect_handler_level": true
    }
  },
  "loggers": {
    "root": {
      "level": "DEBUG",
      "handlers": [
        "queue"
      ]
    }
  }
}

关键说明

  • LoggerAdapter的作用是为所有通过它发起的日志调用自动添加extra参数,无需每次手动传入。
  • LogRecord的额外属性会直接存入__dict__,无需从record.extra读取。
  • 格式字符串中%(field)s:-的语法表示当field不存在时,使用-作为默认值,避免报错。

内容的提问来源于stack exchange,提问作者LaZoR_Bear

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 05:07:22