Найти тему
Блог o Python

Логгирование с модулем logging, настройка через конфиг файл. Лог-сообщения в Telegram.

Оглавление

Это статья возникла из моей компиляции информации по работе модуля logging.

Я кратко постараюсь описать логику работы с ним, настройку конфиг файла и приведу код обработчика для отправки лог-сообщения в Telegram без установки дополнительных библиотек.

В первую очередь, сформулирую, для чего я использую логгирование.

В процессе написания кода

Это очень удобная замена принтам.

logger.debug(f'сообщение {value=}')

Когда модуль закончен, просто меняем в конфиге один параметр. И все - в консоли больше нет текщих уведомлений. Не надо ползать по всему модулю и убирать ненужные принты. А если нужно вернуться к отладке этого модуля, снова замена всего-лишь одного параметра в конфиге. При этом, все ошибки продолжают писаться в лог-файл, и их можно увидеть в любой момент.

Отладка программы и поиск багов.

Ну тут все понятно. Если что-то произошло, лезем в лог-файл и смотрим ошибку.

Оперативное информирование о сбоях в работе программы.

Настраиваем удобный способ получения сообщения. Например, email или Telegram.

Основные моменты для понимания работы модуля logging.

  1. Вы можете отправлять сообщения разных видов(отладка, информирование, предупреждение, ошибка, критическая ошибка и исключение).
  2. В проекте может быть несколько логгеров, которые следят за разными частями проекта.
  3. Каждый из этих логгеров может выводить сообщение одновременно несколькими способами(в консоль, в лог-файл, информирование на email, в Telegram и т.д.). Например, логгер модуля для работы с базой данных пишет ошибки только в лог-файл.
    А вот логгер модуля для обработки информации выводит все сообщения в консоль, сообщения с ошибками пишет в лог, и в случае, если выскочит исключение, отправит сообщение в телеграм.
  4. К вашему сообщению, логгер может добавлять дополнительную информацию(время, модуль, номер строки и т.д.). При этом для каждого канала информации, формат сообщения и набор параметров может быть разным. Например, в консоли не нужна дата и время, если работаем с отладкой прямо сейчас. А вот в лог-файле без них не обойтись.

Теперь попробуем на примере немного разобраться, как это работает.

Уровни в logging

В модуле logging есть понятие уровня и каждому уровню соответсвует определенное число.

CRITICAL 50

ERROR 40

WARNING 30

INFO 20

DEBUG 10

NOTSET 0

Вокруг этого уровня и строится все взаимодействие:

- У сообщения, которое мы отправляем есть уровень.

- У каждого логгера есть свой уровень.

- У каждого канала передачи информации есть свой уровень.

Теперь небольшой пример для понимания.

У нас в программе есть два сообщения: уровня debug и error

logger.debug('сообщение 1')
logger.error('сообщение 2')

У нас есть логгер, которому мы задали уровень 'DEBUG'

И мы настроили этот логгер так, что у него есть два канала вывода информации со своими обработчиками:

  • - обработчик вывода в консоль, которой мы задали уровень 'DEBUG'
  • - обработчик записи в лог-файл, с уровнем 'INFO'

Происходит вызов сообщений:

сообщение 1: "я сообщение уровня DEBUG со значением 10"

сообщение 2: "я сообщение уровня ERROR со значением 40"

логгер: " получаю сообщение 1 и сообщение 2

мой уровень DEBUG со значением 10. Я буду обрабатывать только сообщения с уровнем не меньше чем мой.

10 >= 10. Сообщение 1 проходит. Отправляю сообщение 1 в обработчики

40 >= 10. Сообщение 2 проходит. Отправляю сообщение 2 в обработчики

обработчик вывода в консоль: "Получаю сообщение 1 и сообщение 2."

Мой уровень DEBUG со значением 10. Я буду обрабатывать только сообщения с уровнем не меньше чем мой.

10 >= 10. Сообщение 1 проходит - вывожу в консоль сообщение 1

40 >= 10. Сообщение 2 проходит - вывожу в консоль сообщение 2"

обработчик записи в лог-файл: "Получаю сообщение 1 и сообщение 2

Мой уровень INFO со значением 20. Я буду обрабатывать только сообщения с уровнем не меньше чем мой.

10 <= 20. Сообщение 1 не проходит - игнорирую сообщение 1

40 >= 20. Сообщение 2 проходит - записываю сообщение 2 в лог-файл"

В результате мы имеем на экране: соощение 1 и сообщение 2. А в лог-файле только сообщение 1.

Теперь можно начинать работать с logging.

1. Вам нужно разделить свою программу на блоки, за которыми вы хотите следить по отдельности. Т.е. выбирать сообщения из какого блока, где хотите видеть.
Каждому блоку - назначаете свой логгер. В проекте может быть сколько угодно логгеров.
Лично я делаю так: на каждый модуль(отдельный
*.py-файл) свой логгер.
Логгеры идентифицирются по имени. Т.к. у меня логгер привязан к модулю, я в качестве имени использую имя этого модуля, а именно переменную `__name__`.

logger = logging.getLogger(__name__)

Это позволяет мне быстро через конфиг указывать, информацию из каких блоков я хочу сейчас видеть в консоли.

2. Для каждого отдельного блока нужно определить, как он будет выводить информацию.

Я использую вывод в консоль, лог-файл и Telegram.

3. Определить формат вывода информации.

Каждый из каналов может иметь свой вариант форматирования сообщений и набор параметров с дополнительной информацией.

В модуле logging много параметров, которые вы можете вывести(полный список в документации:

  • дата и время
  • имя модуля
  • уровень сообщения
  • номер строки в коде

Ну и теперь приступаем к реализации в коде.

Начнем с конфиг файла.

Создадим файл `settings.py`

Настройки хранятся в виде словаря:

В нем в свою очередь находятся еще три словаря:

  • loggers - словарь с логгерами. Формирование этого словаря я вынес в начало файла.
  • handlers - словарь с обработчиками, т.е. с каналами вывода информации
  • formatters - в нем прописаны виды форматирования сообщения

formatters

Пойдем снизу вверх, и начнем с форматтеров. Потому что, форматтеры нужно будет указать в настройках обработчиков, а те в свою очередь в настройках логгеров.

в словаре 'formatters', в хранятся словари с настройками отдельных форматтеров.

Я использую два вида форматирования с датой и без. Вы можете форматировать сообщение под себя.

formatters = {
'file_formatter': {
'format': '[{levelname}: {asctime}] [{filename}:{funcName}:{lineno}]: "{message}"',
'style': '{'
},
'debug_formatter': {
'format': '[{levelname}] [{filename}:{funcName}:{lineno}]: "{message}"',
'style': '{'
},
}

здесь:

  • `levelname` - уровень сообщения('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
  • `filename` - имя файла откуда вызвано сообщение
  • `funcName` - имя функции. Речь идет о вашей функции, которую вы определили `def func():`
  • `lineno` - номер строчки в коде
  • `message` - ну и собственно само сообщение, которое вы написали.
  • Для вывода в лог файл и Telegram добавляются дата и время - `asctime`
  • Параметр 'style': '{' указывает, что форматирование вывода идет как f-строка. Если не указать, придется параметры задавать через устаревший вариант с `%`.

handlers

Теперь перейдем к обработчикам.

Я использую три вида обработчиков: два стандартных('logging.StreamHandler', 'logging.FileHandler') и третий Telegram

  • 'console_handler' - вместо принтов, для текущего написания кода и отладки.
  • 'file_handler' - пишет информационные сообщения и сообщения об ошибках в лог-файл. Сообщения 'DEBUG' в него не попадают.
  • c обработчиком Telegram немного посложнее. Его нужно прописать.

Для отправки телеграм-сообщения я делаю POST-запрос через встроенный модуль `urllib3` к TelegramAPI. Я видел в интернете релизацию через библиотеку `telebot`, но тогда придется устанавливать ее в каждый проект. Я считаю, что лучше обойтись встроенными библиотеками.

class TelegramBotHandler(Handler):
def __init__(self, token: str, chat_id: str):
super().__init__()
self.token = token
self.chat_id = chat_id
def emit(self, record: LogRecord) -> None:
url = f'https://api.telegram.org/bot{self.token}/sendMessage'
post_data = {'chat_id': self.chat_id,
'text': self.format(record)}
http = urllib3.PoolManager()
http.request(method='POST', url=url, fields=post_data)

Конфигурируем обработчики:

'handlers': {
'console_handler': { # имя обработчика
'level': 'DEBUG', # уровень сообщений на который начнет реагировать обработчик
'class': 'logging.StreamHandler',
'formatter': 'debug_formatter', # указываем как форматировать сообщение
'stream': 'ext://sys.stdout' # По умолчанию stderr. Этот параметр указан, чтобы в консоли вывод был простым белым шрифтом, как в print, а не красным, как будто это ошибки.
},
'file_handler': {
'level': 'INFO',
'class': 'logging.FileHandler',
'formatter': 'file_formatter',
'filename': 'temp/logs/debug.log', # путь до лог файла. Без разницы какая ОС: Windows, Linux записываем через '/'
'mode': 'a', # По умолчанию 'a'. Режим записи, все как у обычногого файла. 'a' дозапись в текущий файл. 'w' новый файл, предыдущие записи стираются
'encoding': 'utf-8', # кодировка
},
'telegram_handler': {
'class': 'settings.TelegramBotHandler', # здесь указываем созданный нами обработчик Telegram
'level': 'INFO',
'token': TELEGRAM_BOT_TOKEN,
'chat_id': TELEGRAM_CHAT_ID
}
}

  • `class` - это выбор канала информации: консоль, лог-файл и т.д. Обработчиков много и опять же ссылка на них в документации [logging.handlers — Logging handlers — Python 3.10.5 documentation](https://docs.python.org/3/library/logging.handlers.html).
  • `level` в настройках обработчика указывает нижний уровень сообщения, с которого обработчик начинает на него реагировать. Т.е. если в настройках поставить 'WARNING', то в сообщениях от этого обработчика вы не увидите сообщения уровня 'DEBUG', 'INFO', но увидите все с большим номером.
    Если вы хотите ограничить верхнюю границу, нужно будет использовать такую вещь как фильтры, но я с ними подробно не разбирался, для моих задач этого не требовалось.
    Именно здесь мы указываем, какие сообщения мы хотим видеть в консоли, а какие нужно писать в лог-файл.
  • В параметре `formatter` мы указываем имя конкретного форматтера, который мы прописали в предыдущем пункте.

loggers.

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

В самом начале файла кортеж с именами модулей и их уровнем сообщений.

Именно здесь я меняю настройки, что мне выводить в консоль, а что нет. Т.е. закончил отладку определенного модуля, поменял 'DEBUG' на 'INFO'. И больше в консоли я не вижу от него отладочных сообщений. Поэтому эта часть у меня в начале файла.

modules_and_level = (
('__main__', 'DEBUG'),
('db_modules.db_common', 'INFO'),
('epub.epub', 'INFO'),
('common.utils', 'DEBUG'),
('common.common', 'INFO'),
('execute', 'DEBUG')
)
loggers = {}
for logger in modules_and_level:
loggers[logger[0]] = {
'handlers': ['console_handler', 'file_handler'],
'level': logger[1],
'propagate': False}

Обратите внимание, как записывается имя модуля(как в import) - с учетом пути через точку, без расширения. Т.е. файл 'db_common.py' в папке 'db_modules' записывается как 'db_modules.db_common'

  • В `handlers` указываем список обработчиков, в которые отправляет информацию этот логгер. У меня все логгеры работают с одинаковым набором обработчиков. Но вы можете настраивать под свой проект.
  • `propagate` ставим в `False`. Этот параметр относится к иерархии логгеров и отвечает за передачу сообщений к логгерам родителям. Если `True`, то при определенных настройках может быть дублирование сообщений.

Теперь собираем все в один словарь с настройками. Можно было сразу все в нем прописывать, но для упрощения работы я сделал так.

LOG_CONFIG = {
'version': 1, # обязательный параметр
'formatters': formatters,
'handlers': handlers,
'loggers': loggers
}

С конфигом закончили. Переходим в код проекта.

В каждый модуль(.py- файл) добавляем логгер

import logging
logger = logging.getLogger(__name__)

`__name__` - возвращает имя нашего модуля.

А в основном файле(файл с `if __name__ == 'main'`) загружаем конфиг логгера.

import logging.config
from settings import LOG_CONFIG
logging.config.dictConfig(LOG_CONFIG)

В принципе это все, можно уже работать.

Если нужно отладочное сообщение вставляем в код:

logger.debug(f'сообщение {value=}')

Если ошибка

logger.error(f'сообщение')

Есть сообщения exception. Его используем в блоке `try/except`.

По уровню оно равно ERROR 40. Но еще дополнительно логгирует traceback исключения.

try:
some_func()
except:
logger.exception('Сообщение')

Еще вариант использования модуля logging. Он мне помог избавиться от warnings при работе с модулем BeautifulSoup.

У меня вылетал `XMLParsedAsHTMLWarning`. Я понимал, о чем он, и его можно спокойно игнорировать, но его вывод мешался в консоли.

Оказалось, что модуль logging может перехватывать предупреждения.

logging.captureWarnings(True)

И все. В консоли тишина и спокойствие. Естественно включать эту опцию нужно, если понимаете, что ничего важного вы не пропустите.