Представьте ситуацию: ваше приложение работает в продакшене, как вдруг происходит критическая ошибка. Вы узнаете о ней только через несколько часов, когда пользователи начинают массово жаловаться. Идёте проверять консоль, а тут всего лишь трассировка стека, которая мало что говорит о проблеме. Из-за кого и когда возникла это ошибка? Чтобы предотвратить такие сценарии, необходима активная система уведомлений. В этом руководстве мы создадим пользовательский обработчик, который предоставит возможность создания системы уведомлений об ошибках, которая гарантирует, что вы всегда будете в курсе состояния вашей системы.
Теоретическая основа
Модуль logging реализует иерархическую систему логирования, которая состоит из 4-х ключевых компонентов:
Logger — ключевой объект для работы с логами. Имеет имя и уровень логирования (
NOTSET
,DEBUG
,INFO
,WARNING
,ERROR
,CRITICAL
).Handler — определяет, как и куда отправлять сообщения (файл, консоль, сокет, email и т.д.). Один логгер может иметь несколько обработчиков.
Filter — используется для дополнительной фильтрации сообщений (например, пропускать только определённые уровни или сообщения с конкретным текстом).
Formatter — определяет, как будет выглядеть сообщение (дата, уровень, модуль, текст и т.д.).
Сперва при возникновении события формируем запись. Затем мы определяем назначение этой записи, используя её поля. При необходимости фильтруем их. И в завершение форматируем итог в читаемый вид.

Handler — это базовый класс для всех обработчиков логов, в котором некоторые методы определяются только в его потомках. Он отвечает за отправку сообщений из логгера в конечное место назначения.
StreamHandler — это тот самый потомок, предназначенный для логирования ошибок в заданные потоки, некоторая часть кода из него нам понадобиться.
LogRecord — структура данных, которая содержит всю информацию о конкретном событии логирования.
Основные методы класса Handler:
format — Преобразует объект LogRecord в строку и возвращает её в отформатированном виде.
emit — Определяет, как именно сообщение будет выведено (например, вывод в консоль, запись в файл).
handle — Проверяет фильтры и уровень записи, вызывает emit, если запись прошла проверки.
Чего мы хотим?
Для начала нам следует определить, что мы хотим от нашего пользовательского обработчика. Выделю несколько пунктов:
Получать достаточную информацию в консоли, отформатированная в читаемый вид, для определения и устранения проблем и отладки приложения (например, время, модуль, уровень и текст ошибки)
Иметь возможность получать уведомления в зависимости от уровня события (например, стандартные ошибки записывать в специальный файл, а при критических ошибках отправлять уведомление разработчику)
Пишем код
Начнём с определения нового класса и его конструктора. Он будет унаследован от Handler. Для реализации 2-го пункта нам нужны callback-функции, которые будут вызываться при обработке события определённого уровня. Сохраним их в словаре, где ключом будет уровень логирования.
LogCallback = Optional[Callable[[LogRecord], None]]
class LoggingHandler(Handler):
def __init__(
self,
on_critical: LogCallback = None,
on_error: LogCallback = None,
on_warning: LogCallback = None,
on_info: LogCallback = None,
on_debug: LogCallback = None
):
super().__init__()
self.callbacks = {
logging.CRITICAL: on_critical,
logging.ERROR: on_error,
logging.WARNING: on_warning,
logging.INFO: on_info,
logging.DEBUG: on_debug,
}
Перед тем как вывести текст, нам следует его отформатировать для вывода. Сделаем так, что для каждого уровня события будет применять особый формат.
def format(self, record: LogRecord):
log_fmt = LEVEL_FORMATS.get(record.levelno, LOG_FORMAT)
formatter = Formatter(log_fmt, datefmt=TIME_FORMAT)
return formatter.format(record)
Реализацию констант, приведённых выше, я вам предлагаю реализовать самостоятельно. Атрибуты для форматирования текста и временных меток вам в помощь. Ниже я приведу пример из своего приложения:
GREY = "\x1b[38;20m"
YELLOW = "\x1b[33;20m"
RED = "\x1b[31;20m"
BOLD_RED = "\x1b[31;1m"
RESET = "\x1b[0m"
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
LOG_FORMAT = (
"[%(asctime)s %(name)s %(levelname)s "
"%(filename)s:%(lineno)d/%(funcName)s] %(message)s"
)
LEVEL_FORMATS = {
logging.DEBUG: GREY + LOG_FORMAT + RESET,
logging.INFO: GREY + LOG_FORMAT + RESET,
logging.WARNING: YELLOW + LOG_FORMAT + RESET,
logging.ERROR: RED + LOG_FORMAT + RESET,
logging.CRITICAL: BOLD_RED + LOG_FORMAT + RESET,
}
Приступим теперь к логике нашего обработчика. Для этого нам следует переопределить метод handle. Делать он будет тоже, что и метод в супперклассе, однако в конце мы добавим вызов наших callback-функций.
def handle(self, record: LogRecord):
rv = super().handle(record)
if not rv:
return rv
callback = self.callbacks.get(record.levelno)
if callback:
callback(record)
return rv
Дополнительно я хочу, чтобы полоса загрузки в tqdm корректно отображалась в логах. Для этого копируем реализацию метода emit из класса StreamHandler и модифицируем его под наши нужды.
def emit(self, record: LogRecord):
try:
msg = self.format(record)
tqdm.write(msg, end=self.terminator)
except RecursionError:
raise
except Exception:
self.handleError(record)
Теперь нам нужно написать метод для его установки. В аргументы пойдёт всё нужное для класса Logger и методы, которые будут вызваны при определённом уровне события.
def setup_logger(
name: str,
level: int = logging.INFO,
on_critical: LogCallback = None,
on_error: LogCallback = None,
on_warning: LogCallback = None,
on_info: LogCallback = None,
on_debug: LogCallback = None,
) -> logging.Logger:
logger = logging.getLogger(name)
logger.setLevel(level)
if not logger.handlers:
handler = LoggingHandler(
on_critical=on_critical,
on_error=on_error,
on_warning=on_warning,
on_info=on_info,
on_debug=on_debug,
)
logger.addHandler(handler)
return logger
Обработчик готов! Теперь его можно установить куда угодно, где вы хотите отслеживать события. Методы on_error, on_critical и прочие пусть будут реализованы на ваше усмотрение. Для проверки их работоспособности просто выведем текст.
def on_error(record: logging.LogRecord):
print("Возникла ошибка")
# Реалзация
def on_critical(record: logging.LogRecord):
print("Возникла критическая ошибка")
# Реалзация
mylogger = setup_logger(
__name__, logging.INFO,
on_error=on_error,
on_critical=on_critical
)
Теперь вызовем событие ошибки. Приведу для этого примитивный код и его вывод:
try:
a = 1 / 0
except Exception as e:
mylogger.error(e)
# [2025-09-30 00:00:00 __main__ ERROR exapmle.py:89/<module>] division by zero
# Возникла ошибка
Мы видим, что получили достаточную информацию о нашей ошибке, отформатированную в читаемый вид. Также мы реализовали механизм уведомления о определённых событиях. Это удовлетворяет нашим поставленным желаниям, а следовательно реализация пользовательского обработчика завершена.
Таким образом, мы создали гибкую систему, которая не только улучшает читаемость логов в консоли, но и предоставляет механизм для мгновенного реагирования на критические события.