В переводе с английского log — это «журнал». Логирование — это ведение «бортового журнала», автоматическая запись событий в специальный файл или вывод таких записей в терминал. Логи — это дополнительная система мониторинга, и разработчик может настроить логирование в тех местах, за которыми нужно присматривать.
Зачастую для отладки кода и вывода служебных сообщений применяют print()
, но логи позволяют делать то же самое гибче и удобнее.
Более того, с помощью логов можно не просто выводить сообщения об ошибке, но и сортировать сообщения по степени важности, записывать время события или другую служебную информацию о нём. В логах могут быть многие тысячи записей, но если известно, в какой период времени случилась ошибка — можно найти записи за этот период и выяснить, что случилось.
Всегда лучше работать через логи, а не выводить на печать через print()
.
Библиотека logging в Python
В Python есть встроенная библиотека для логирования. Она умеет всё, что может потребоваться для работы с логами — включать и выключать логирование, сортировать логи по важности, настраивать их внешний вид, записывать сообщения в файл и избавляться от старых записей, которые зря занимают место на диске.
«Внимание, красный уровень!»
По умолчанию в библиотеке logging задано пять «уровней» сообщений, по степени важности. Обычно их бывает достаточно, но при необходимости можно создать свой уровень.
Запустите файл с таким кодом:
import logging
logging.debug('123') # Когда нужна отладочная информация
logging.info('Сообщение отправлено') # Когда нужна дополнительная информация
logging.warning('Большая нагрузка, хелп') # Когда что-то идёт не так, но работает
logging.error('Бот не смог отправить сообщение') # Когда что-то сломалось
logging.critical('Всё упало! Зовите админа!1!111') # Когда всё совсем плохо
Логи будут выведены так:
УРОВЕНЬ_ВАЖНОСТИ:текущий_пользователь:сообщение
WARNING:root:Большая нагрузка, хелп
ERROR:root:Бот не смог отправить сообщение
CRITICAL:root:Всё упало! Зовите админа!1!111
Первые два уровня по умолчанию не будут выводиться в консоль: они предназначены для текущего информирования и нужны разработчику при отладке кода. В сообщения этого уровня не принято выводить информацию непосредственно о работе программы.
Разные уровни логирования нужны для сортировки сообщений по важности. Это облегчает поиск и работу с сообщениями.
DEBUG — уровень отладки: на этом уровне выводится всякая служебная информация: «Произошёл запуск функции», «переменная содержит такое-то значение». Это сообщения о том, что происходит в коде, информация для разработчика.
INFO — информация о текущих событиях: этот уровень применяют, если нужно убедиться, что всё идёт по плану: «Письмо отправлено», «Запись в базе создана».
WARNING — «тревожный звоночек»: проблемы нет, но есть что-то, что может привести к проблеме; что-то, на что следует обратить внимание.
ERROR — это ошибка: что-то работает не так, как нужно. Требуется вмешательство и исправление ошибки.
CRITICAL — случилось что-то совсем критичное: надо всё бросать и бежать к компьютеру; всё сломалось. Не очень часто используется на практике, обычно бывает достаточно ERROR.
Настройка логов
По умолчанию в терминал выводятся только наиболее важные логи, от уровня WARNING и выше: WARNING → ERROR → CRITICAL.
Сообщения с уровнями ниже WARNING, то есть DEBUG и INFO, по умолчанию отключены и никуда не выводятся.
Эти настройки можно изменить, вызвав метод для конфигурации логов basicConfig()
и передав в параметр level
уровень, с которого нужно фиксировать сообщения:
logging.basicConfig(level=logging.DEBUG)
Форматирование логов
Без предварительной настройки логи записываются в таком формате:
УРОВЕНЬ ВАЖНОСТИ:текущий пользователь:сообщение
Этот формат можно изменить: в метод basicConfig()
передаётся параметр format
, а в нём описывается содержимое лога:
logging.basicConfig(format='%(asctime)s, %(levelname)s, %(name)s, %(message)s')
asctime — это время события,
levelname — уровень важности,
name — имя логера,
message — текст сообщения.
Для описания атрибутов используется «%-форматирование»: атрибут берётся в скобки, перед скобками ставится символ %
, а после скобок указывают тип данных, например:
s
— строка (string),d
— число (digit).
Помимо времени и уровня есть и другие полезные атрибуты для форматирования логов:
filename — имя файла, из которого отправлено сообщение в лог;
funcName — имя функции, из которой отправлено сообщение в лог;
lineno — номер строки в том файле, из которого отправлено сообщение в лог.
Полный список атрибутов приведён в официальной документации.
Сохранение логов в файл
Чтобы сохранять лог-сообщения в файл, нужно передать соответствующие параметры в метод basicConfig()
, указав имя файла с расширением .log и режим записи:
logging.basicConfig(filename='main.log', filemode='w')
Значения параметра filemode
:
w
— содержимое файла перезаписывается при каждом запуске программы;x
— создать файл и записывать логи в него; если файл с таким именем уже существует — будет ошибка;a
— дописывать новые логи в конец указанного файла.
Ротация логов
Логи со временем растут, занимают всё больше места и, в результате, могут занять всё дисковое пространство. Чтобы контролировать объём логов, можно выставить ограничение на размер этих файлов и на их количество.
Когда размер первого файла достигнет установленного предела — будет создан следующий файл, а когда количество файлов дойдёт до заданного количества — начнёт перезаписываться самый первый файл. И так по кругу: это называется ротацией логов.
Логер — это такая коробка или корзина, в которую Python скидывает лог-сообщения. Логер обрабатывает эти сообщения тем способом, который для него установлен.
import logging
from logging.handlers import RotatingFileHandler
# здесь мы задаем глобальную конфигурацию для всех логеров
logging.basicConfig(
level=logging.DEBUG,
filename='program.log',
format='%(asctime)s, %(levelname)s, %(message)s, %(name)s'
)
# а тут настраиваем логгер для текущего файла .py
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
handler = RotatingFileHandler('my_logger.log', maxBytes=50000000, backupCount=5)
logger.addHandler(handler)
Логеров может быть несколько: обычно пишут отдельный логер для каждого пакета. Имя логерам традиционно дают по имени __name__
пакета, для которого он создан. Когда в проекте больше одного пакета — такая структура логирования упрощает работу.
Далее в коде создан handler и добавлен в логер. Handler — это диспетчер логов, он берёт готовые логи, переданные в логер и обрабатывает их нужным образом.
В листинге применён RotatingFileHandler, он управляет ротацией логов: добавляет новые записи в файл, следит за объёмом и количеством лог-файлов. В параметрах RotatingFileHandler указывается максимальный размер одного лог-файла и предельное количество таких файлов. Также в нём указывается путь и имя файлов, так что при использовании RotatingFileHandler
не нужно создавать файл через basicConfig
.
Увидеть логи можно не только в файле, куда они сохранены: можно читать их в реальном времени при выполнении программы:
tail -f main.log
Логи из файла main.log будут выводиться в терминал, отображая самые последние события.
Логирование исключений
Исключения тоже можно логировать, но делать это нужно не всегда. Тут нет какого-то готового правила, разработчик обычно сам решает, какие исключения следует логировать, а какие нет.
Самый простой способ логирования исключений:
try:
42 / 0
except Exception as error:
logging.error(error, exc_info=True)
Без параметра exc_info
в лог запишется только текст исключения. Существует более компактная запись, с помощью метода logging.exception()
:
try:
42 / 0
except Exception:
logging.exception()
Перехватывать все исключения сразу — плохая практика. При таком подходе можно и не узнать о проблемах, потому что программа продолжит выполняться. Однако такой вариант возможен в некоторых случаях, например, когда вы пишите чат-бота и важно, чтобы он не падал, когда какой-то из запросов не выполнится.