Программы
Настройка журналирования (логирования) в 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.

Изображение Python 3.11. Что нового?