无法在带有日志记录模块的 Python 中使用自定义格式化程序的额外属性

问题描述 投票:0回答:1

我有一个新的记录器,我正在测试它在当前的 Python 项目中更加充实,尽管我观看了有关记录 python 包的非常好的视频并且我已经阅读了有关它的文档,但我不能因为喜欢我让额外的论点发挥作用。我无法以我的格式设置它,我的自定义格式化程序也看不到它。我不确定此时我哪里出了问题,因为其他一切都按预期进行,而且实现起来非常有趣。

以下是我在 main.py 文件中初始化记录器的方法以及调用它来记录消息的两个示例行:

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()
logging.LoggerAdapter(logging.getLogger(__name__), extra={"hostname": hostname, "ip": host_ip})

# Truncated code
logger.debug("Loading env...", extra={"hostname": hostname, "ip": host_ip}) # Example where I implicitly add the extra on the debug call.

# Truncated code
logger.error(e) # Example where I don't add it manually but where the LoggerAdapter should take care of it.

这是我的 maxlogger.py 文件,它声明了我的自定义格式化程序和处理程序:

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__()
        with open("stupid_test.log", "a+") as f:
            f.write(json.dumps(fmt_keys))
            f.write("\n")
        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()
        }

        with open("stupid_test.log", "a+") as f:
            f.write(json.dumps(getattr(record, "extra"), default=None))
            f.write("\n")
            f.write(json.dumps(record))
            f.write("\n")

        # Need to figure out the extra tag not working
        message.update(always_fields)
        message.update(getattr(record, "extra", {}))

        for key, val in record.__dict__.items():
            if key not in LOG_RECORD_BUILTIN_ATTRS:
                message[key] = val

        with open("stupid_test.log", "a+") as f:
            f.write(json.dumps(message))
            f.write("\n")

        return message


class MaxDBHandler(logging.Handler):
    def __init__(self):
        super().__init__()
        load_dotenv()
        connection_string = f"DRIVER={{FreeTDS}};SERVERNAME={env('DB_SERVER')};DATABASE={env('DATABASE')};UID={env('DB_USERNAME')};PWD={env('DB_PASSWORD')};"
        # connectio_string ONLY FOR LOCAL TESTING ONLY
        # connection_string = f"DRIVER={{SQL Server}};SERVER={env('DB_SERVER')};DATABASE={env('DATABASE')};UID={env('DB_USERNAME')};PWD={env('DB_PASSWORD')};"
        self.db = pyodbc.connect(connection_string)

    def emit(self, record):
        with self.db.cursor() as cursor:
            cursor.execute("INSERT INTO app_log (log) VALUES (?)", (self.format(record),))


class MaxQueueHandler(QueueHandler):
    def __init__(self, handlers: list[str], respect_handler_level: bool):
        super().__init__(queue=multiprocessing.Queue(-1))
        self.handlers = handlers
        self.respect_handler_level = respect_handler_level

这是我的日志配置文件:

{
  "version": 1,
  "disable_existing_loggers": false,
  "formatters": {
    "simple": {
      "format": "[%(levelname)s]: %(message)s"
    },
    "detailed": {
      "format": "[%(levelname)s|%(module)s|%(lineno)d] - %(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",
        "extra": "extra"
      }
    }
  },
  "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"
      ]
    }
  }
}

在我的配置中,我无法添加 ip 和主机名属性,因为记录器会因未定义它们而向我抛出错误 -> ValueError: 在记录中找不到格式字段:'主机名'

关于如何使用额外属性,我是否缺少一些东西,从我读到的内容来看,它应该以非常简单的方式工作,但我没有用它做任何事情。非常感谢您为解决这个问题提供的所有帮助。

python python-3.x logging
1个回答
0
投票

您必须将适配器分配给变量,然后使用该变量

adapter = logging.LoggerAdapter(logger, ...) 

adapter.error("Loading env...") 

适配器只是原始记录器的包装 - 所以如果您需要访问原始记录器,那么您必须获得

adapter.logger

它允许在现有适配器上放置另一个适配器 - 然后您可能需要对所有消息使用新适配器。


我没有在你的代码上测试它,但我创建了最小的工作代码 - 所以每个人都可以简单地复制并运行它。

它使用适配器中添加的额外值(它需要新字符串来格式化输出),但它不使用显式添加到消息中的值。

import logging
import socket
import subprocess

logging.basicConfig()

hostname = socket.gethostname()
host_ip = subprocess.run([r"ip -4 addr show | 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()

logger = logging.getLogger()  # get it after `basicConfig` to get correct logger. OR create logger without using `basicCodnfig 
adapter = logging.LoggerAdapter(logger, extra={"hostname": hostname, "ip": host_ip})

# replace formatting in handler(s)
#format_string = '%(asctime)s - %(name)s - %(levelname)s - %(message)s - [extra: hostname=%(hostname)s, IP=%(ip)s]'
format_string = '%(message)s [extra: hostname=%(hostname)s, IP=%(ip)s]'
formatter = logging.Formatter(format_string)
adapter.logger.handlers[0].setFormatter(formatter)

# Example where I explicitly add the extra on the debug call. - it doesn't use new values
adapter.error("Loading env...", extra={"hostname": "other", "ip": "8.8.8.8"})

# Example where I implicitly add the extra on the debug call.
adapter.error("Loading env...") 

# Truncated code
try:
    1/0
except Exception as e:
    adapter.error(e)

结果:

Loading env... [extra: hostname=notebook, IP=127.0.0.1]
Loading env... [extra: hostname=notebook, IP=127.0.0.1]
division by zero [extra: hostname=notebook, IP=127.0.0.1]
© www.soinside.com 2019 - 2024. All rights reserved.