Программы
Настройка журналирования (логирования) в Python с примерами

Настройка журналирования (логирования) в Python с примерами

Во время работы программы часто нужно сохранять некоторые важные записи о процессе выполнения команды. В Python есть довольно мощный модуль для работы с логами - давайте разберёмся с тем, как его использовать.

Для начала подключим модуль для работы с логами, создадим в глобальной переменной объект логгера log и напишем пару сообщений:

import logging

log = logging.getLogger(__name__)


def main():
    log.debug('Отладочное сообщение')
    log.info('Информационное сообщение')
    log.warning('Сообщение с предостережением')
    log.error('Сообщение об ошибке')
    log.fatal('Фатальная ошибка, срочно остановить работу')


if __name__ == '__main__':
    main()

Замечу, что модуль logging написан потоко-безопасно, поэтому логгер вполне можно хранить как глобальную переменную. Также при создании логгера logging.getLogger в качестве параметра используется название логгера, чтобы позже можно было различать, откуда пришло сообщение. Использую специальную переменную __name__, чтобы названием логгера было название модуля, где инициализирован логгер. Лучше иметь отдельный логгер хотя бы на каждый модуль.

Также вы могли заметить, что сообщения мы отправляем 5-ю разными способами. Так мы отправляем 5 сообщений, имеющих разный уровень важности:

  • debug - отладочная информация, которая используется во время разработки / поиска ошибок.
  • info - информационная информация о статусе исполнения программы.
  • warning - информация о потенциально опасных или нежелательных операциях (попытка получения несанкционированного доступа, невозможность отправки почты пользователю и т.д.).
  • error - об ошибках, произошедших во время работы программы (некритичная ошибка базы данных, падение соединения до используемого сервиса).
  • fatal - критичные ошибки, которые не позволяют приложению более нормально работать.

В случае кода выше на выход получаем:

Сообщение с предостережением
Сообщение об ошибке
Фатальная ошибка, срочно остановить работу

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

Во время разработки или отладки полезно включить логи отладки. Поэтому добавим небольшую настройку лога:

import logging

logging.basicConfig(level=logging.DEBUG)  # <- Добавляем вывод info и debug
log = logging.getLogger(__name__)

Помимо этого логи можно сделать информативнее (или установив более удобный формат), добавив время сообщения, уровень важности сообщения, место возникновения вплоть до строки:

import logging

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s - [%(levelname)s] - %(name)s - (%(filename)s).%(funcName)s(%(lineno)d) - %(message)s",
)
log = logging.getLogger(__name__)

В итоге получим те же сообщения, но в следующем формате:

2021-06-21 16:10:55,866 - [DEBUG] - __main__ - (main.py).main(11) - Отладочное сообщение
2021-06-21 16:10:55,867 - [INFO] - __main__ - (main.py).main(12) - Информационное сообщение
2021-06-21 16:10:55,867 - [WARNING] - __main__ - (main.py).main(13) - Сообщение с предостережением
2021-06-21 16:10:55,867 - [ERROR] - __main__ - (main.py).main(14) - Сообщение об ошибке
2021-06-21 16:10:55,867 - [CRITICAL] - __main__ - (main.py).main(15) - Фатальная ошибка, срочно остановить работу

Сообщения логов могут содержать переменные. Для этого сначала задаём формат в виде printf-строки, после чего аргументами передаём данные для printf-форматирования:

def main():
    log.warning(
        'Пользователь #%d не имеет прав доступа на "%s"',
        1234,
        'Чтение файлов пользователей',
    )

В нашем случае будут подставлены идентификатор пользователя и название права доступа:

2021-06-21 16:20:35,872 - [WARNING] - __main__ - (main.py).main(14) - Пользователь #1234 не имеет прав доступа на "Чтение файлов пользователей"

В случае, если нам нужно определять часть параметров логирования где-то в другом месте, например, если мы хотим в каждом сообщении указывать ip и идентификатор пользователя, по запросу которого выведено сообщение, то можно использовать:

  • threading.local - глобальные переменные, однако для каждого потока (thread) они локальны.
  • contextvars.ContextVar - глобальные переменные, локальные для каждого асинхронного контекста при программировании с asyncio.

К примеру, у нас используется aiohttp приложение, в middleware мы получаем ip и идентификатор пользователя. Чтобы не протаскивать эти данные всюду, не забывать с ними вызывать логгер, мы сложим их в глобальную переменную ContextVar, а в нашем специальном логгере будем использовать эту глобальную переменную:

from contextvars import ContextVar
import logging

SOME_LOG_EXTRA = ContextVar('SOME_LOG_EXTRA', default={})
SOME_LOG_EXTRA.set({'ip': '127.0.0.1', 'user_id': '1234'})

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s - [%(levelname)s] - %(name)s - (%(filename)s).%(funcName)s(%(lineno)d) - %(message)s",
)

class CustomAdapter(logging.LoggerAdapter):
    def process(self, msg, kwargs):
        return '%s %s' % (self.extra.get(), msg), kwargs


log = CustomAdapter(logging.getLogger(__name__), SOME_LOG_EXTRA)


def main():
    log.warning(
        'Пользователь не имеет прав доступа на "%s"',
        'Чтение файлов пользователей',
    )

if __name__ == '__main__':
    main()

Мы добавили свой кастомный адаптер для логгера, чтобы он добавлял к каждому сообщению информацию из нашей глобальной переменной. В результате, в дополнении к сообщению мы видим и полезную информацию о пользователе:

2021-06-21 16:35:30,030 - [WARNING] - __main__ - (main.py).main(22) - {'ip': '127.0.0.1', 'user_id': '1234'} Пользователь не имеет прав доступа на "Чтение файлов пользователей"

До этого все сообщения выводились на стандартный поток вывода (stdout). Однако, часто нужно настроить вывод в какой-то файл или вообще отправлять в сеть. Тогда нам нужно настроить обработчик вывода нашего логгера. В модуле logging есть множество обработчиков - найти их можно по слову Handler. В случае, если нам нужен вывод в файл - используем файловый обработчик - FileHandler:

import logging

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s - [%(levelname)s] - %(name)s - (%(filename)s).%(funcName)s(%(lineno)d) - %(message)s",
    handlers=[logging.FileHandler('./file.log', encoding='utf-8')]
)

Также часто нам нужно настраивать не один логгер, а сразу несколько - помните, что лучше иметь как минимум по одному логгеру на Python модуль. Для этого подойдёт настройка в виде dictConfig, ведь basicConfig для этих целей уже не хватает.

В общем виде dictConfig выглядит так:

import logging.config

logging.config.dictConfig({
    'version': 1,
    'disable_existing_loggers': True,
    'formatters': {
        'standard': {
            'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
        },
    },
    'handlers': {
        'default': {
            'level': 'INFO',
            'formatter': 'standard',
            'class': 'logging.FileHandler',
            'filename': './file.log',
            'mode': 'a',
            'encoding': 'utf-8',
        },
    },
    'loggers': {
        '': {  # root logger
            'handlers': ['default'],
            'level': 'DEBUG',
            'propagate': True,
        },
        'some.group.*': {
            'level': 'WARNING',
        },
        'some.module': {
            'level': 'ERROR',
        }
    },
})

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

Далее описываются логгеры по названиям (либо же, если вы использовали __name__ - по модулям). Также можно указывать сразу группу логгеров, указывая их по маске some.group.*.

В описании логгеров будут использоваться handlers, описанные выше. Таким образом, описав обработчик раз, его можно будет использовать в нескольких логгерах.

Аналогично с форматированием - описываем в formatters.

Что же, для большинства небольших проектов этих знаний о системе логирования в python более чем достаточно. Также советую посмотеть и другие заметки, рассказывающие о Python.

Также может быть вам интересно:

Шаблон разработки ПО — Model View Controller (MVC)

MVC - один из самых распространённых архитектурных шаблонов разработки. Часто используется в различных фреймворках. В том числе и в Django.

Читать »

Добавляем поддержку медиа-файлов в Django проект

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

Читать »
Фото Python: Встроенные типы данных (list, set, dict, etc)

Python: Встроенные типы данных (list, set, dict, etc)

В Python есть множество встроенных типов данных. Их использование значительно упрощает жизнь и ускоряет разработку программных продуктов.

Фото Python: типы данных, переменные, логическое ветвление и циклы

Python: типы данных, переменные, логическое ветвление и циклы

Первая часть заметок о Python. О базовых типах, переменных, ветвлении и циклах.

Фото Как сделать свою middleware в Django (с примерами)

Как сделать свою middleware в Django (с примерами)

Middleware или "промежуточное программное обеспечение" - элегантный способ установить общие правила обработки запросов и ответов приложения. Давайте напишем парочку middleware, чтобы понять, как они работают.

Фото Как настроить отправку почты из Django

Как настроить отправку почты из Django

Письма об ошибках, отчёты на почту, восстановление паролей - всё это полезно при работе с сайтом. Django предоставляет удобный способ это сделать с минимумом настроек!

Фото Добавляем постраничную пагинацию на Django сайт

Добавляем постраничную пагинацию на Django сайт

На сайтах часто встречаются многостраничные объекты: список товаров, список заметок и т.д. Поэтому важно уметь добавить навигацию по страницам на Django-проекте.

Фото Новый оператор match-case в Python

Новый оператор match-case в Python

В новой версии Python (3.10) появится новый оператор. Новый оператор сопоставления по шаблону (match-case).

Фото Нет слов, одни... однострочники

Нет слов, одни... однострочники

На днях вышел пост со списком полезных однострочников для JavaScript программистов. Памятуя Perl-овую молодость, заглянул туда.

Фото Добавляем переменные в контекст Django шаблонов (свой контекст-процессор)

Добавляем переменные в контекст Django шаблонов (свой контекст-процессор)

В Django вы можете передавать данные в шаблоны посредством контекстов. Контекст передаётся из контроллера (view в терминах Django), однако, если одни и те же данные нужны в разных местах, лучше сделать свой контекст-процессор.