Невозможно использовать дополнительный атрибут с помощью специального средства форматирования в Python с модулем ведения журнала

У меня есть новый регистратор, который я тестирую, чтобы он был более детальным в текущем проекте 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: поле форматирования не найдено в записи: «имя хоста».

Есть ли что-то, что мне не хватает в том, как использовать дополнительный атрибут? Судя по тому, что я прочитал, он должен работать довольно просто, но я ничего не могу с ним сделать. Огромное вам спасибо за помощь в выяснении этого вопроса.

возможно, вам следует назначить new_logger = logging.LoggerAdapter(...), а затем использовать new_logger.debug(). ИЛИ, может быть, вам следует создать класс MyAdaper(LoggerAdapter) и заменить функцию process() в этом классе. У меня нет minimal working code, чтобы проверить это.

furas 22.06.2024 14:32
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
1
55
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вам необходимо назначить адаптер переменной, а затем использовать эту переменную.

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

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

Адаптер — это всего лишь оболочка оригинального регистратора, поэтому, если вам нужен доступ к исходному регистратору, вам необходимо получить adapter.logger. И это позволяет устанавливать значения только для этого сообщения

adapter.logger.debug("Loading env...", extra = {"hostname": "other", "ip": "8.8.8.8"})

Это позволяет поставить другой адаптер на существующий адаптер - и тогда вам может понадобиться использовать новый адаптер для всех сообщений.


Я не проверял это на вашем коде, но создал минимальный рабочий код
так что каждый может просто скопировать и запустить его.

Он использует дополнительные значения, добавленные в адаптер (для форматирования вывода требуется новая строка)
но он не использует значения, явно добавленные в сообщение.

import logging
import socket
import subprocess

logging.basicConfig(level=logging.DEBUG)

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()
host_ip = subprocess.run([r"echo '127.0.0.1'"], capture_output=True, text=True, shell=True).stdout.splitlines()[0].strip()

#logger = logging.getLogger(__name__)  # doesn't work - it doesn't have handlers
logger = logging.getLogger()  # get it after `basicConfig` to get correct logger. OR create logger without using `basicConfig` 
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)

# explicitly add the extra - it doesn't use new values but get all values
adapter.debug("Loading env...", extra = {"hostname": "other", "ip": "8.8.8.8"})

# implicitly add the extra - 
adapter.debug("Loading env...") 

# using standard logger with explicitly added extra - it works and it uses new values
logger.debug("Loading env...", extra = {"hostname": "other", "ip": "8.8.8.8"})
#adapter.logger.debug("Loading env...", extra = {"hostname": "other", "ip": "8.8.8.8"})
 
# using standard logger without extra - it doesn't work because it needs values for `%(hostname)s %(ip)s`
#logger.debug("Loading env...")  # raise error because format_string has `%(hostname)s %(ip)s`
#adapter.logger.debug("Loading env...")  # raise error because format_string has `%(hostname)s %(ip)s`


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]
Loading env... [extra: hostname=other, IP=8.8.8.8]
division by zero [extra: hostname=notebook, IP=127.0.0.1]

Вот это вау. Моя голова, должно быть, была где-то в другом месте, я всегда читал эту часть как logger.LoggerAdapter, а не logging.Adapter. Спасибо за ясное и доходчивое объяснение и пример.

LaZoR_Bear 23.06.2024 15:09

Другие вопросы по теме