Почему логер для INFO использует Formatter для Debug и почему дублируются сообщения логера
Прошу помочь с вопросом. Уже всю голову сломал. Пишу логер, который бы при включении его в debug должен сохранять данные, которые ему передадут. Специально для этого создал класс который наследовал от Logger. Переписал debug под описанную задачу (в примере просто обрабатывается сообщение) Вызываю вот так:
test_class.py
from package_test import my_class_logger
my_logger = my_class_logger.MyLogger(__name__)
def starter():
my_logger.setLevel('DEBUG')
my_logger.debug('debug_msg')
my_logger.info("info_msg")
if __name__ == "__main__":
starter()
Класс с логером:
package_test/my_class_logger.py
import logging
import inspect
# _log_format = f"%(asctime)s - [%(levelname)s] - %(name)s - (%(filename)s).%(funcName)s(%(lineno)d) - %(message)s"
class MyLogger(logging.Logger):
def __init__(self, name, level=logging.INFO):
super().__init__(name, level)
self.propagate = True
# Add handlers (e.g., ConsoleHandler, FileHandler, etc.)
st_handler = logging.StreamHandler()
st_handler.setLevel(logging.INFO)
st_formatter = logging.Formatter(fmt="%(asctime)s - [%(levelname)s] - %(name)s - (%(filename)s).%(funcName)s(%(lineno)d) - %(message)s")
st_handler.setFormatter(st_formatter)
self.addHandler(st_handler)
ft_handler = logging.StreamHandler()
ft_handler.setLevel(logging.DEBUG)
_log_debug_format = '%(asctime)s - [%(levelname)s] - %(name)s - %(full_file_name)s - %(message)s'
ft_formatter = logging.Formatter(fmt=_log_debug_format)
ft_handler.setFormatter(ft_formatter)
self.addHandler(ft_handler)
def debug(self, msg, *args, data=None, **kwargs):
if self.getEffectiveLevel() != logging.DEBUG:
print(self.getEffectiveLevel())
return
# возьми текущий фрейм объект (frame object)
current_frame = inspect.currentframe()
# получи фрейм объект, который его вызвал
caller_frame = current_frame.f_back
# возьми у вызвавшего фрейма исполняемый в нём объект типа "код" (code object)
code_obj = caller_frame.f_code
# и получи его имя, строку откуда был вызван debug
code_obj_name = code_obj.co_name
line = caller_frame.f_lineno
full_file_name = code_obj.co_filename
filename = full_file_name[full_file_name.rfind('\\') +1:]
data_module = {'full_file_name': full_file_name}
if data:
print(data)
super().debug(msg, *args, extra=data_module, **kwargs)
Вот этот кусок формирует дополнительные данные из того какая функция и из какой строки вызвала debug и полученные данные передаю как переменные для форматера:
# возьми текущий фрейм объект (frame object)
current_frame = inspect.currentframe()
# получи фрейм объект, который его вызвал
caller_frame = current_frame.f_back
# возьми у вызвавшего фрейма исполняемый в нём объект типа "код" (code object)
code_obj = caller_frame.f_code
# и получи его имя, строку откуда был вызван debug
code_obj_name = code_obj.co_name
line = caller_frame.f_lineno
full_file_name = code_obj.co_filename
filename = full_file_name[full_file_name.rfind('\\') +1:]
data_module = {'full_file_name': full_file_name}
В результате запуска получаю ошибку при выполнении INFO о том что в шаблоне сообщения нет поля full_file_name. Но его там и не должно быть!!!
raise ValueError('Formatting field not found in record: %s' % e)
ValueError: Formatting field not found in record: 'full_file_name'
Если же в обоих хендлерах прописать одинаковые форматы, то после выполнения debug сообщение info начинает двоиться:
2024-08-24 02:36:14,599 - [DEBUG] - __main__ - (my_class_logger.py).debug(49) - debug_msg
2024-08-24 02:36:26,115 - [INFO] - __main__ - (test_class.py).starter(12) - info_msg
2024-08-24 02:36:26,115 - [INFO] - __main__ - (test_class.py).starter(12) - info_msg
Что тут не так??
Ответы (2 шт):
Сам немного экспериментировал с логгером, надеюсь мой пример вам окажется полезным. Добивал специально для вас комментариев и примеры использования:
import platform
import os
import inspect
import logging
import logging.handlers
from datetime import datetime
from typing import List, Optional
# Дополнительная проверка для ОС Windows
if platform.system() == "Windows":
os.system('color')
class CustomFormatter(logging.Formatter):
DEFAULT_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)"
COLORS = {
"black": "\x1b[30m",
"red": "\x1b[31m",
"green": "\x1b[32m",
"yellow": "\x1b[33m",
"blue": "\x1b[34m",
"magenta": "\x1b[35m",
"cyan": "\x1b[36m",
"white": "\x1b[37m",
"reset": "\x1b[0m"
}
LEVEL_COLORS = {
logging.DEBUG: COLORS["green"],
logging.INFO: COLORS["magenta"],
logging.WARNING: COLORS["yellow"],
logging.ERROR: COLORS["red"],
logging.CRITICAL: COLORS["cyan"]
}
CONSOLE_FORMATS = {
logging.DEBUG: LEVEL_COLORS[logging.DEBUG] + DEFAULT_FORMAT + COLORS["reset"],
logging.INFO: LEVEL_COLORS[logging.INFO] + DEFAULT_FORMAT + COLORS["reset"],
logging.WARNING: LEVEL_COLORS[logging.WARNING] + DEFAULT_FORMAT + COLORS["reset"],
logging.ERROR: LEVEL_COLORS[logging.ERROR] + DEFAULT_FORMAT + COLORS["reset"],
logging.CRITICAL: LEVEL_COLORS[logging.CRITICAL] + DEFAULT_FORMAT + COLORS["reset"]
}
FILE_FORMATS = {
logging.DEBUG: DEFAULT_FORMAT,
logging.INFO: DEFAULT_FORMAT,
logging.WARNING: DEFAULT_FORMAT,
logging.ERROR: DEFAULT_FORMAT,
logging.CRITICAL: DEFAULT_FORMAT
}
def __init__(self, is_file=False):
super().__init__()
self.is_file = is_file
def format(self, record: logging.LogRecord) -> str:
if self.is_file:
log_fmt = self.FILE_FORMATS.get(record.levelno, self.DEFAULT_FORMAT)
else:
log_fmt = self.CONSOLE_FORMATS.get(record.levelno, self.DEFAULT_FORMAT)
self._style._fmt = log_fmt
return super().format(record)
@classmethod
def set_file_format(cls, level: str, format_string: str):
"""Устанавливает форматирование для заданного уровня логирования для файлового вывода."""
log_level = getattr(logging, level.upper(), None)
if log_level is not None:
cls.FILE_FORMATS[log_level] = format_string
else:
raise ValueError(f"Некорректный уровень логирования: {level}")
@classmethod
def set_console_format(cls, level: str, format_string: str):
"""Устанавливает форматирование для заданного уровня логирования."""
log_level = getattr(logging, level.upper(), None)
if log_level is not None:
cls.CONSOLE_FORMATS[log_level] = format_string
else:
raise ValueError(f"Некорректный уровень логирования: {level}")
@classmethod
def set_color(cls, level: str, color: str):
"""Устанавливает цвет для заданного уровня логирования."""
log_level = getattr(logging, level.upper(), None)
if log_level in cls.LEVEL_COLORS:
cls.LEVEL_COLORS[log_level] = cls.COLORS[color]
cls.CONSOLE_FORMATS[log_level] = cls.LEVEL_COLORS[log_level] + cls.DEFAULT_FORMAT + cls.COLORS["reset"]
else:
raise ValueError(f"Некорректный уровень логирования: {level}")
class MultiLevelFilter(logging.Filter):
def __init__(self, levels: List[int]):
super().__init__()
self.levels = levels
def filter(self, record: logging.LogRecord) -> bool:
return record.levelno in self.levels
class LevelFilter(logging.Filter):
def __init__(self, level: int):
super().__init__()
self.level = level
def filter(self, record: logging.LogRecord) -> bool:
return record.levelno == self.level
class LoggerManager:
LEVEL_MAPPING = {
'DEBUG': logging.DEBUG,
'INFO': logging.INFO,
'WARNING': logging.WARNING,
'ERROR': logging.ERROR,
'CRITICAL': logging.CRITICAL
}
def __init__(self, *, name: str, level: int = logging.DEBUG, log_to_file: bool = False, file_name: str = 'app.log',
log_dir: str = 'logs', max_bytes: int = 1024 * 1024, backup_count: int = 5):
if not isinstance(name, str) or not name.strip():
print(f"Некорректное имя логгера: '{name}'. Устанавливается имя по умолчанию 'DefaultLogger'.")
name = "DefaultLogger"
self.max_bytes: int = max_bytes
self.backup_count: int = backup_count
self.logger = logging.getLogger(name)
self.logger.setLevel(level)
self.console_handler: Optional[logging.StreamHandler] = None
self.file_handler: Optional[logging.FileHandler] = None
self.log_filter: Optional[logging.Filter] = None
self.setup_console_handler(level)
if log_to_file:
self.enable_file_logging(file_name, log_dir)
def setup_console_handler(self, level: int):
"""Настройка консольного обработчика и форматтера"""
self.console_handler = logging.StreamHandler()
self.console_handler.setLevel(level)
self.console_handler.setFormatter(CustomFormatter())
self.logger.addHandler(self.console_handler)
def enable_console_logging(self):
"""Включение логирования в консоль"""
if self.console_handler:
self.console_handler.setLevel(self.logger.level)
self.logger.info("Логирование в консоль включено")
def disable_console_logging(self):
"""Отключение логирования в консоль"""
if self.console_handler:
self.console_handler.setLevel(
logging.CRITICAL + 1) # Уровень выше CRITICAL, чтобы игнорировать все сообщения
self.logger.info("Логирование в консоль отключено")
else:
self.logger.warning("Обработчик консольного логирования уже отключен или не был инициализирован")
def enable_logging(self):
"""Включение логирования"""
self.logger.disabled = False
self.logger.info("Логирование включено")
def disable_logging(self):
"""Отключение логирования"""
self.logger.disabled = True
self.logger.info("Логирование отключено")
def enable_file_logging(self, file_name: str = 'app.log', log_dir: str = 'logs'):
try:
if not os.path.exists(log_dir):
os.makedirs(log_dir)
except Exception as e:
self.logger.error(f"Ошибка при создании директории для логов: {e}")
raise
log_path = os.path.join(log_dir, file_name)
if self.file_handler:
self.logger.removeHandler(self.file_handler)
self.file_handler.close()
try:
self.file_handler = logging.handlers.RotatingFileHandler(
log_path,
maxBytes=self.max_bytes, # Максимальный размер файла
backupCount=self.backup_count # Количество ротируемых файлов
)
self.file_handler.setLevel(self.logger.level)
file_formatter = CustomFormatter(is_file=True)
self.file_handler.setFormatter(file_formatter)
self.logger.addHandler(self.file_handler)
self.logger.info(f"Логирование в файл '{log_path}' включено")
except Exception as e:
self.logger.error(f"Ошибка при настройке файлового логгера с ротацией: {e}")
raise
def disable_file_logging(self):
"""Отключение логирования в файл"""
if self.file_handler:
self.logger.removeHandler(self.file_handler)
self.file_handler.close()
self.file_handler = None
self.logger.info("Логирование в файл отключено")
else:
self.logger.warning("Обработчик файла логирования уже отключен или не был инициализирован")
def set_file_handler_params(self, max_bytes: int, backup_count: int):
"""Установка параметров для RotatingFileHandler."""
self.max_bytes = max_bytes
self.backup_count = backup_count
if self.file_handler:
self.file_handler.maxBytes = max_bytes
self.file_handler.backupCount = backup_count
self.logger.info(
f"Изменены параметры RotatingFileHandler: maxBytes={max_bytes}, backupCount={backup_count}")
else:
self.logger.warning(
"Файловый обработчик не инициализирован. Новые параметры будут применены при следующем включении логирования в файл.")
def set_name(self, name: str):
"""Установка нового имени логгера"""
if not isinstance(name, str) or not name.strip():
self.logger.error(f"Некорректное новое имя логгера: '{name}'. Имя остается без изменений.")
return
self.logger.name = name
self.logger.info(f"Имя логгера изменено на '{name}'")
def set_level(self, level: str):
"""Установка уровня логирования"""
log_level = self.LEVEL_MAPPING.get(level)
if log_level is not None:
self.logger.setLevel(log_level)
if self.console_handler:
self.console_handler.setLevel(log_level)
if self.file_handler:
self.file_handler.setLevel(log_level)
self.logger.info(f"Уровень логирования установлен на {level}")
else:
self.logger.error(
f"Некорректный уровень логирования: {level}. Используйте один из: {list(self.LEVEL_MAPPING.keys())}")
def set_filter(self, level: str):
"""Установка фильтра для логирования"""
log_level = self.LEVEL_MAPPING.get(level)
if log_level is not None:
self.clear_filter()
self.log_filter = LevelFilter(log_level)
self.logger.addFilter(self.log_filter)
self.logger.info(f"Фильтр уровня логирования установлен на {level}")
else:
self.logger.error(
f"Некорректный уровень фильтра: {level}. Используйте один из: {list(self.LEVEL_MAPPING.keys())}")
def set_filter_list(self, levels: List[str]):
"""Установка списка фильтров для логирования"""
if not levels:
self.logger.error("Список уровней фильтра не должен быть пустым.")
return
valid_levels = [self.LEVEL_MAPPING[level] for level in levels if level in self.LEVEL_MAPPING]
if not valid_levels:
self.logger.error(
f"Некорректные уровни фильтра: {levels}. Используйте один из: {list(self.LEVEL_MAPPING.keys())}")
return
self.clear_filter()
self.log_filter = MultiLevelFilter(valid_levels)
self.logger.addFilter(self.log_filter)
self.logger.info(f"Фильтр уровней логирования установлен на {levels}")
def reset_level(self):
"""Сброс уровня логирования до уровня DEBUG"""
self.set_level('DEBUG')
def clear_filter(self):
"""Очистка установленного фильтра"""
if self.log_filter:
self.logger.removeFilter(self.log_filter)
self.log_filter = None
self.logger.info("Фильтр логирования очищен")
def time_execution(self):
"""Контекстный менеджер для логирования времени выполнения блока кода"""
return TimeExecutionLogger(self.logger)
class TimeExecutionLogger:
def __init__(self, with_logger, level=logging.INFO):
self.logger = with_logger
self.level = level
self.start_time = datetime.now()
caller_frame = inspect.stack()[
2] # Получаем информацию о вызывающем коде (2 - это индекс фрейма с вызывающим кодом)
self.caller_file = caller_frame.filename
self.caller_line = caller_frame.lineno
self.logger.log(self.level,
f"Начало выполнения блока кода в {self.start_time.strftime('%Y-%m-%d %H:%M:%S.%f')}. Вызвано из файла '{self.caller_file}', строка {self.caller_line}")
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
end_time = datetime.now()
execution_time = (end_time - self.start_time).total_seconds()
if exc_type is None:
self.logger.log(self.level,
f"Завершение выполнения блока кода в {end_time.strftime('%Y-%m-%d %H:%M:%S.%f')}. Время выполнения: {execution_time:.6f} секунд. Вызвано из файла '{self.caller_file}', строка {self.caller_line}")
else:
self.logger.log(self.level,
f"Завершение выполнения блока кода с ошибкой {exc_val} в {end_time.strftime('%Y-%m-%d %H:%M:%S.%f')}. Время выполнения: {execution_time:.6f} секунд. Вызвано из файла '{self.caller_file}', строка {self.caller_line}")
return False # Возвращаем False, чтобы исключение продолжило подъём
if __name__ == "__main__":
import time
logger_manager = LoggerManager(name="TestLogger")
# Установим форматы для файлового логирования
CustomFormatter.set_file_format('DEBUG', "%(asctime)s - !DEBUG!: %(message)s (%(filename)s:%(lineno)d)")
CustomFormatter.set_file_format('INFO', "%(asctime)s - !INFO!: %(message)s")
CustomFormatter.set_file_format('WARNING', "%(asctime)s - !WARNING!: %(message)s")
CustomFormatter.set_file_format('ERROR', "%(asctime)s - !ERROR!: %(message)s")
# CustomFormatter.set_file_format('CRITICAL', "%(asctime)s - !CRITICAL!: %(message)s")
# CustomFormatter.set_console_format('INFO', f"%(asctime)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)") # Задать свой формат
# CustomFormatter.set_color('DEBUG', 'blue') # Задать цвет из доступных
# logger_manager.set_file_handler_params(max_bytes = 2048*1024, backup_count = 10) # Максимальный размер файла и количество ротируемых файлов
# logger_manager.enable_console_logging() # Включение логирования в консоль
# logger_manager.disable_console_logging() # Отключение логирования в консоль
# logger_manager.disable_logging() # Отключение логирования
# logger_manager.enable_logging() # Включение логирования
# logger_manager.set_level('INFO') # Установка уровня логирования на INFO
# logger_manager.set_filter_list(['DEBUG' ,'WARNING', 'ERROR' ]) # Установка списка фильтров для логирования
# logger_manager.set_filter('INFO') # Фильтрация сообщений только уровня INFO
# logger_manager.clear_filter() # Очистка установленного фильтра
# logger_manager.reset_level() # Сброс уровня логирования до уровня DEBUG
# logger_manager.set_name("NewNameLogger") # Установка нового имени логгера
with logger_manager.time_execution():
for i in range(6):
# Включение и отключение логирования в файл
logger_manager.enable_file_logging(f"{datetime.now().strftime('%Y-%m-%d %H-%M')}-{i}.log")
# logger_manager.disable_file_logging()
# Логирование сообщений
logger = logger_manager.logger
logger.debug(f"debug message {i}")
logger.info(f"info message {i}")
logger.warning(f"warning message {i}")
logger.error(f"error message {i}")
logger.critical(f"critical message {i}")
time.sleep(1)
Спасибо огромное за пример. Добавил к вашему коду переопределение дебагера с добавлением переменных:
class MyLogger(logging.Logger):
def __init__(self, name, level=logging.INFO):
super().__init__(name, level)
def debug(self, msg, *args, data=None, **kwargs):
current_frame = inspect.currentframe()
caller_frame = current_frame.f_back
code_obj = caller_frame.f_code
code_obj_name = code_obj.co_name
line = caller_frame.f_lineno
full_file_name = code_obj.co_filename
filename = full_file_name[full_file_name.rfind('\\') +1:]
data_module = {'full_file_name': full_file_name, 'code_obj_name': code_obj_name, 'line': line}
if data:
print(data)
super().debug(msg, *args, extra=data_module, **kwargs)
В class LoggerManager:
изменил self.logger = logging.getLogger(name)
на self.logger = MyLogger(name)
.
Добавил дополнительный формат для переопределенного debug:
DEFAULT_FORMAT = "%(asctime)s - [%(levelname)s] - %(name)s - (%(filename)s).%(funcName)s(%(lineno)d) - %(message)s"
DEBUG_FORMAT = '%(asctime)s - [%(levelname)s] - %(name)s - (%(full_file_name)s).%(code_obj_name)s(%(line)d) - %(message)s'
Иначе в лог прилетают данные из самого класса. Теперь работает как нужно. Можно в debug дополнительно передавать необходимые данные. И в логе корректно отображается откуда был вызван логер и из какой строки. Спасибо большое за пример!