У меня есть новый регистратор, который я тестирую, чтобы он был более детальным в текущем проекте 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: поле форматирования не найдено в записи: «имя хоста».
Есть ли что-то, что мне не хватает в том, как использовать дополнительный атрибут? Судя по тому, что я прочитал, он должен работать довольно просто, но я ничего не могу с ним сделать. Огромное вам спасибо за помощь в выяснении этого вопроса.
Вам необходимо назначить адаптер переменной, а затем использовать эту переменную.
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. Спасибо за ясное и доходчивое объяснение и пример.
возможно, вам следует назначить
new_logger = logging.LoggerAdapter(...)
, а затем использоватьnew_logger.debug()
. ИЛИ, может быть, вам следует создать классMyAdaper(LoggerAdapter)
и заменить функциюprocess()
в этом классе. У меня нетminimal working code
, чтобы проверить это.