Как изменить формат logger.info(. ) в str или передать в переменную(python)?
Здраствуйте. Я создал плюс минус работающий logger для приложения fastapi. Даные выводяться в консоль через logger.info(. ) и logger.error(. ). Хотел бы сохранить эти ошибки в бд. А когда пытаюсь передать loger.info(. ) в переменую выдает: ошибка типа. Тип None.
Как передать это в переменную для записи в бд? спасибо
P.C не судите строго
- Вопрос задан более года назад
- 82 просмотра
Комментировать
Решения вопроса 1

Никак, логгеры не возвращают сообщения об ошибках.
Пиши класс-потомок logger.Handler который будет сохранять сообщения в БД, и назначай экземпляр этого хандлера нужным логам.
Подробности в доках на модуль logging, но если кратко — там разделение обязанностей. Есть Formatter, который занимается превращением записи в строку, есть Handler, который пихает полученную строку в файл или ещё куда, есть Filter, который реализует умную фильтрацию записей, ну и т.д.
Ответ написан более года назад
Нравится 2 1 комментарий
Robert @Lerts Автор вопроса
понял, спасибо
Ответы на вопрос 1
Вот пример как можно реализовать хранение логов в бд оракл.
# built-in modules import datetime import functools import logging import pathlib import time # third-party modules import cx_Oracle as cxO # project modules import utilities_config as cutil class DBHandler(logging.Handler): """ Класс-обработчик логирования в базу данных (оракл) """ _query_create = f""" create table ( logger_name varchar2(100 char), logging_started date, idx number, record varchar2(1000 char))""" _query_insert = f""" insert into values (:1, :2, :3, :4)""" def __init__(self, logger_name, logging_started, chunk_size=100): super().__init__() self.logger_name = logger_name self.logging_started = logging_started self.chunk_size = chunk_size self.count = 0 self.chunk = list() self._create() def emit(self, record): """ Обработка каждого лог-сообщения """ self.count += 1 self.chunk.append((self.logger_name, self.logging_started, self.count, self.format(record))) if len(self.chunk) > 0 and len(self.chunk) % self.chunk_size == 0: self.flush() def flush(self): """ Запись накопившихся лог-сообщений """ try: self._insert() except cxO.DatabaseError as exc: logging.getLogger(self.logger_name).error(f"can't flush logs to oracle: ") self.chunk.clear() def _create(self): """ Подключение к базе и создание таблицы """ with cxO.connect(**cutil.LOGFIG_ORACLE_CONNECTION) as connection: cursor = connection.cursor() try: cursor.execute(self._query_create) except cxO.DatabaseError as exc: # пропускаем ошибку # ORA-00955: name is already used by an existing object # так как в оракле нет синтаксиса 'CREATE TABLE IF NOT EXISTS . ' if exc.args[0].code != 955: # все остальные ошибки просто пробрасываем выше raise exc def _insert(self): """ Подключение к базе и вставка строк """ with cxO.connect(**cutil.LOGFIG_ORACLE_CONNECTION) as connection: cursor = connection.cursor() cursor.executemany(self._query_insert, self.chunk) connection.commit() def get_logger(logger_name, *, log_to_file, log_to_oracle): """ Создание логгера с заданным именем """ # общий для всех шаблон формата лог-сообщений fmt = ('[%(asctime)s] [%(levelname)s] %(message)s', '%Y.%m.%d %H:%M:%S') # создаем логгер logging_started = datetime.datetime.now() logger = logging.getLogger(logger_name) logger.setLevel(logging.DEBUG) # [1/3] - логирование в консоль - обработчик и формат лог-сообщений ch = logging.StreamHandler() ch.setFormatter(logging.Formatter(*fmt)) logger.addHandler(ch) # [2/3] - логирование в файл - обработчик и формат лог-сообщений if log_to_file: # папка с логами log_dir = pathlib.Path(cutil.LOGFIG_STORAGE_LOCATION['log_dir']) log_dir.mkdir(exist_ok=True) # логгер fh = logging.FileHandler( pathlib.Path( log_dir, f"_log_.log"), # вот здесь кодировка имеет значение, иначе будет системная кодировка (windows-1251) encoding='utf-8') fh.setFormatter(logging.Formatter(*fmt)) logger.addHandler(fh) # [3/3] - логирование в оракл - обработчик и формат лог-сообщений # оборачиваем в try/except, так как логирование в оракл опционально if log_to_oracle: try: oh = DBHandler(logger_name, logging_started) except cxO.DatabaseError as exc: logger.error(f"can't create logger to oracle: ") else: oh.setFormatter(logging.Formatter(*fmt)) logger.addHandler(oh) return logger
Ответ написан более года назад
Комментировать
Нравится Комментировать
Ваш ответ на вопрос
Войдите, чтобы написать ответ

- Python
- +3 ещё
Как наладить отправку сообщений от бота каждый день на aiogram?
- 1 подписчик
- 44 минуты назад
- 14 просмотров
Настройка журналирования (логирования) в 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()
- debug — отладочная информация, которая используется во время разработки / поиска ошибок.
- info — информационная информация о статусе исполнения программы.
- warning — информация о потенциально опасных или нежелательных операциях (попытка получения несанкционированного доступа, невозможность отправки почты пользователю и т.д.).
- error — об ошибках, произошедших во время работы программы (некритичная ошибка базы данных, падение соединения до используемого сервиса).
- fatal — критичные ошибки, которые не позволяют приложению более нормально работать.
В случае кода выше на выход получаем:
Сообщение с предостережением Сообщение об ошибке Фатальная ошибка, срочно остановить работу
Это не все сообщения, ибо по умолчанию установлен уровень уведомлений, игнорирующий debug и info , дабы не засорять логи отладочной информацией.
Во время разработки или отладки полезно включить логи отладки. Поэтому добавим небольшую настройку лога:
import logging logging.basicConfig(level=logging.DEBUG) #
Помимо этого логи можно сделать информативнее (или установив более удобный формат), добавив время сообщения, уровень важности сообщения, место возникновения вплоть до строки:
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() 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) - Пользователь не имеет прав доступа на "Чтение файлов пользователей"
До этого все сообщения выводились на стандартный поток вывода (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: руководство разработчика
Сталкивались ли вы с трудностями при отладке Python-кода? Если это так — то изучение того, как наладить логирование (журналирование, logging) в Python, способно помочь вам упростить задачи, решаемые при отладке.

Если вы — новичок, то вы, наверняка, привыкли пользоваться командой print() , выводя с её помощью определённые значения в ходе работы программы, проверяя, работает ли код так, как от него ожидается. Использование print() вполне может оправдать себя при отладке маленьких Python-программ. Но, когда вы перейдёте к более крупным и сложным проектам, вам понадобится постоянный журнал, содержащий больше информации о поведении вашего кода, помогающий вам планомерно отлаживать и отслеживать ошибки.
Из этого учебного руководства вы узнаете о том, как настроить логирование в Python, используя встроенный модуль logging . Вы изучите основы логирования, особенности вывода в журналы значений переменных и исключений, разберётесь с настройкой собственных логгеров, с форматировщиками вывода и со многим другим.
Вы, кроме того, узнаете о том, как Sentry Python SDK способен помочь вам в мониторинге приложений и в упрощении рабочих процессов, связанных с отладкой кода. Платформа Sentry обладает нативной интеграцией со встроенным Python-модулем logging , и, кроме того, предоставляет подробную информацию об ошибках приложения и о проблемах с производительностью, которые в нём возникают.
Начало работы с Python-модулем logging
В Python имеется встроенный модуль logging , применяемый для решения задач логирования. Им мы будем пользоваться в этом руководстве. Первый шаг к профессиональному логированию вы можете выполнить прямо сейчас, импортировав этот модуль в своё рабочее окружение.
import logging
Встроенный модуль логирования Python даёт нам простой в использовании функционал и предусматривает пять уровней логирования. Чем выше уровень — тем серьёзнее неприятность, о которой сообщает соответствующая запись. Самый низкий уровень логирования — это debug (10) , а самый высокий — это critical (50) .
Дадим краткие характеристики уровней логирования:
- Debug (10) : самый низкий уровень логирования, предназначенный для отладочных сообщений, для вывода диагностической информации о приложении.
- Info (20) : этот уровень предназначен для вывода данных о фрагментах кода, работающих так, как ожидается.
- Warning (30) : этот уровень логирования предусматривает вывод предупреждений, он применяется для записи сведений о событиях, на которые программист обычно обращает внимание. Такие события вполне могут привести к проблемам при работе приложения. Если явно не задать уровень логирования — по умолчанию используется именно warning .
- Error (40) : этот уровень логирования предусматривает вывод сведений об ошибках — о том, что часть приложения работает не так как ожидается, о том, что программа не смогла правильно выполниться.
- Critical (50) : этот уровень используется для вывода сведений об очень серьёзных ошибках, наличие которых угрожает нормальному функционированию всего приложения. Если не исправить такую ошибку — это может привести к тому, что приложение прекратит работу.
В следующем фрагменте кода показано использование вышеперечисленных уровней логирования при выводе нескольких сообщений. Здесь используется синтаксическая конструкция logging.() .
logging.debug("A DEBUG Message") logging.info("An INFO") logging.warning("A WARNING") logging.error("An ERROR") logging.critical("A message of CRITICAL severity")
Ниже приведён результат выполнения этого кода. Как видите, сообщения, выведенные с уровнями логирования warning , error и critical , попадают в консоль. А сообщения с уровнями debug и info — не попадают.
WARNING:root:A WARNING ERROR:root:An ERROR CRITICAL:root:A message of CRITICAL severity
Это так из-за того, что в консоль выводятся лишь сообщения с уровнями от warning и выше. Но это можно изменить, настроив логгер и указав ему, что в консоль надо выводить сообщения, начиная с некоего, заданного вами, уровня логирования.
Подобный подход к логированию, когда данные выводятся в консоль, не особо лучше использования print() . На практике может понадобиться записывать логируемые сообщения в файл. Этот файл будет хранить данные и после того, как работа программы завершится. Такой файл можно использовать в качестве журнала отладки.
Обратите внимание на то, что в примере, который мы будем тут разбирать, весь код находится в файле main.py . Когда мы производим рефакторинг существующего кода или добавляем новые модули — мы сообщаем о том, в какой файл (имя которого построено по схеме .py ) попадает новый код. Это поможет вам воспроизвести у себя то, о чём тут идёт речь.
Логирование в файл
Для того чтобы настроить простую систему логирования в файл — можно воспользоваться конструктором basicConfig() . Вот как это выглядит:
logging.basicConfig(level=logging.INFO, filename="py_log.log",filemode="w") logging.debug("A DEBUG Message") logging.info("An INFO") logging.warning("A WARNING") logging.error("An ERROR") logging.critical("A message of CRITICAL severity")
Поговорим о логгере root , рассмотрим параметры basicConfig() :
- level : это — уровень, на котором нужно начинать логирование. Если он установлен в info — это значит, что все сообщения с уровнем debug игнорируются.
- filename : этот параметр указывает на объект обработчика файла. Тут можно указать имя файла, в который нужно осуществлять логирование.
- filemode : это — необязательный параметр, указывающий режим, в котором предполагается работать с файлом журнала, заданным параметром filename . Установка filemode в значение w (write, запись) приводит к тому, что логи перезаписываются при каждом запуске модуля. По умолчанию параметр filemode установлен в значение a (append, присоединение), то есть — в файл будут попадать записи из всех сеансов работы программы.
После выполнения модуля main можно будет увидеть, что в текущей рабочей директории был создан файл журнала, py_log.log .

Так как мы установили уровень логирования в значение info — в файл попадут записи с уровнем info и с более высокими уровнями.

Записи в лог-файле имеют формат :: . По умолчанию , имя логгера, установлено в root , так как мы пока не настраивали пользовательские логгеры.

Помимо базовой информации, выводимой в лог, может понадобится снабдить записи отметками времени, указывающими на момент вывода той или иной записи. Это упрощает анализ логов. Сделать это можно, воспользовавшись параметром конструктора format :
logging.basicConfig(level=logging.INFO, filename="py_log.log",filemode="w", format="%(asctime)s %(levelname)s %(message)s") logging.debug("A DEBUG Message") logging.info("An INFO") logging.warning("A WARNING") logging.error("An ERROR") logging.critical("A message of CRITICAL severity")

Существуют и многие другие атрибуты записи лога, которыми можно воспользоваться для того чтобы настроить внешний вид сообщений в лог-файле. Настраивая поведение логгера root — так, как это показано выше, проследите за тем, чтобы конструктор logging.basicConfig() вызывался бы лишь один раз. Обычно это делается в начале программы, до использования команд логирования. Последующие вызовы конструктора ничего не изменят — если только не установить параметр force в значение True .
Логирование значений переменных и исключений
Модифицируем файл main.py . Скажем — тут будут две переменных — x и y , и нам нужно вычислить значение выражения x/y . Мы знаем о том, что при y=0 мы столкнёмся с ошибкой ZeroDivisionError . Обработать эту ошибку можно в виде исключения с использованием блока try/except .
Далее — нужно залогировать исключение вместе с данными трассировки стека. Чтобы это сделать — можно воспользоваться logging.error(message, exc_info=True) . Запустите следующий код и посмотрите на то, как в файл попадают записи с уровнем логирования info , указывающие на то, что код работает так, как ожидается.
x = 3 y = 4 logging.info(f"The values of x and y are and .") try: x/y logging.info(f"x/y successful with result: .") except ZeroDivisionError as err: logging.error("ZeroDivisionError",exc_info=True)

Теперь установите значение y в 0 и снова запустите модуль.
Исследуя лог-файл py_log.log , вы увидите, что сведения об исключении были записаны в него вместе со стек-трейсом.
x = 4 y = 0 logging.info(f"The values of x and y are and .") try: x/y logging.info(f"x/y successful with result: .") except ZeroDivisionError as err: logging.error("ZeroDivisionError",exc_info=True)

Теперь модифицируем код так, чтобы в нём имелись бы списки значений x и y , для которых нужно вычислить коэффициенты x/y . Для логирования исключений ещё можно воспользоваться конструкцией logging.exception() .
x_vals = [2,3,6,4,10] y_vals = [5,7,12,0,1] for x_val,y_val in zip(x_vals,y_vals): x,y = x_val,y_val logging.info(f"The values of x and y are and .") try: x/y logging.info(f"x/y successful with result: .") except ZeroDivisionError as err: logging.exception("ZeroDivisionError")
Сразу после запуска этого кода можно будет увидеть, что в лог-файл попала информация и о событиях успешного вычисления коэффициента, и об ошибке, когда возникло исключение.

Настройка логирования с помощью пользовательских логгеров, обработчиков и форматировщиков
Отрефакторим код, который у нас уже есть. Объявим функцию test_division .
def test_division(x,y): try: x/y logger2.info(f"x/y successful with result: .") except ZeroDivisionError as err: logger2.exception("ZeroDivisionError")
Объявление этой функции находится в модуле test_div . В модуле main будут лишь вызовы функций. Настроим пользовательские логгеры в модулях main и test_div , проиллюстрировав это примерами кода.
Настройка пользовательского логгера для модуля test_div
import logging logger2 = logging.getLogger(__name__) logger2.setLevel(logging.INFO) # настройка обработчика и форматировщика для logger2 handler2 = logging.FileHandler(f".log", mode='w') formatter2 = logging.Formatter("%(name)s %(asctime)s %(levelname)s %(message)s") # добавление форматировщика к обработчику handler2.setFormatter(formatter2) # добавление обработчика к логгеру logger2.addHandler(handler2) logger2.info(f"Testing the custom logger for module . ") def test_division(x,y): try: x/y logger2.info(f"x/y successful with result: .") except ZeroDivisionError as err: logger2.exception("ZeroDivisionError")
Настройка пользовательского логгера для модуля main
import logging from test_div import test_division # получение пользовательского логгера и установка уровня логирования py_logger = logging.getLogger(__name__) py_logger.setLevel(logging.INFO) # настройка обработчика и форматировщика в соответствии с нашими нуждами py_handler = logging.FileHandler(f".log", mode='w') py_formatter = logging.Formatter("%(name)s %(asctime)s %(levelname)s %(message)s") # добавление форматировщика к обработчику py_handler.setFormatter(py_formatter) # добавление обработчика к логгеру py_logger.addHandler(py_handler) py_logger.info(f"Testing the custom logger for module . ") x_vals = [2,3,6,4,10] y_vals = [5,7,12,0,1] for x_val,y_val in zip(x_vals,y_vals): x,y = x_val, y_val # вызов test_division test_division(x,y) py_logger.info(f"Call test_division with args and ")
Разберёмся с тем, что происходит коде, где настраиваются пользовательские логгеры.
Сначала мы получаем логгер и задаём уровень логирования. Команда logging.getLogger(name) возвращает логгер с заданным именем в том случае, если он существует. В противном случае она создаёт логгер с заданным именем. На практике имя логгера устанавливают с использованием специальной переменной name , которая соответствует имени модуля. Мы назначаем объект логгера переменной. Затем мы, используя команду logging.setLevel(level) , устанавливаем нужный нам уровень логирования.
Далее мы настраиваем обработчик. Так как мы хотим записывать сведения о событиях в файл, мы пользуемся FileHandler . Конструкция logging.FileHandler(filename) возвращает объект обработчика файла. Помимо имени лог-файла, можно, что необязательно, задать режим работы с этим файлом. В данном примере режим ( mode ) установлен в значение write . Есть и другие обработчики, например — StreamHandler , HTTPHandler , SMTPHandler .
Затем мы создаём объект форматировщика, используя конструкцию logging.Formatter(format) . В этом примере мы помещаем имя логгера (строку) в начале форматной строки, а потом идёт то, чем мы уже пользовались ранее при оформлении сообщений.
Потом мы добавляем форматировщик к обработчику, пользуясь конструкцией вида .setFormatter() . А в итоге добавляем обработчик к объекту логгера, пользуясь конструкцией .addHandler() .
Теперь можно запустить модуль main и исследовать сгенерированные лог-файлы.


Рекомендации по организации логирования в Python
До сих пор мы говорили о том, как логировать значения переменных и исключения, как настраивать пользовательские логгеры. Теперь же предлагаю вашему вниманию рекомендации по логированию.
- Устанавливайте оптимальный уровень логирования. Логи полезны лишь тогда, когда их можно использовать для отслеживания важных ошибок, которые нужно исправлять. Подберите такой уровень логирования, который соответствует специфике конкретного приложения. Вывод в лог сообщений о слишком большом количестве событий может быть, с точки зрения отладки, не самой удачной стратегией. Дело в том, что при таком подходе возникнут сложности с фильтрацией логов при поиске ошибок, которым нужно немедленно уделить внимание.
- Конфигурируйте логгеры на уровне модуля. Когда вы работаете над приложением, состоящим из множества модулей — вам стоит задуматься о том, чтобы настроить свой логгер для каждого модуля. Установка имени логгера в name помогает идентифицировать модуль приложения, в котором имеются проблемы, нуждающиеся в решении.
- Включайте в состав сообщений логов отметку времени и обеспечьте единообразное форматирование сообщений. Всегда включайте в сообщения логов отметки времени, так как они полезны в деле поиска того момента, когда произошла ошибка. Единообразно форматируйте сообщения логов, придерживаясь одного и того же подхода в разных модулях.
- Применяйте ротацию лог-файлов ради упрощения отладки. При работе над большим приложением, в состав которого входит несколько модулей, вы, вполне вероятно, столкнётесь с тем, что размер ваших лог-файлов окажется очень большим. Очень длинные логи сложно просматривать в поисках ошибок. Поэтому стоит подумать о ротации лог-файлов. Сделать это можно, воспользовавшись обработчиком RotatingFileHandler , применив конструкцию, которая строится по следующей схеме: logging.handlers.RotatingFileHandler(filename, maxBytes, backupCount) . Когда размер текущего лог-файла достигнет размера maxBytes , следующие записи будут попадать в другие файлы, количество которых зависит от значения параметра backupCount . Если установить этот параметр в значение K — у вас будет K файлов журнала.
Сильные и слабые стороны логирования
Теперь, когда мы разобрались с основами логирования в Python, поговорим о сильных и слабых сторонах этого механизма.
Мы уже видели, как логирование позволяет поддерживать файлы журналов для различных модулей, из которых состоит приложение. Мы, кроме того, можем конфигурировать подсистему логирования и подстраивать её под свои нужды. Но эта система не лишена недостатков. Даже когда уровень логирования устанавливают в значение warning , или в любое значение, которое выше warning , размеры лог-файлов способны быстро увеличиваться. Происходит это в том случае, когда в один и тот же журнал пишут данные, полученные после нескольких сеансов работы с приложением. В результате использование лог-файлов для отладки программ превращается в нетривиальную задачу.
Кроме того, исследование логов ошибок — это сложно, особенно в том случае, если сообщения об ошибках не содержат достаточных сведений о контекстах, в которых происходят ошибки. Когда выполняют команду logging.error(message) , не устанавливая при этом exc_info в True , сложно обнаружить и исследовать первопричину ошибки в том случае, если сообщение об ошибке не слишком информативно.
В то время как логирование даёт диагностическую информацию, сообщает о том, что в приложении нужно исправить, инструменты для мониторинга приложений, вроде Sentry, могут предоставить более детальную информацию, которая способна помочь в диагностике приложения и в исправлении проблем с производительностью.
В следующем разделе мы поговорим о том, как интегрировать в Python-проект поддержку Sentry, что позволит упростить процесс отладки кода.
Интеграция Sentry в Python-проект
Установить Sentry Python SDK можно, воспользовавшись менеджером пакетов pip .
pip install sentry-sdk
После установки SDK для настройки мониторинга приложения нужно воспользоваться таким кодом:
sentry_sdk.init( dsn , traces_sample_rate=0.85, )
Как можно видеть — вам, для настройки мониторинга, понадобится ключ dsn . DSN расшифровывается как Data Source Name (имя источника данных). Найти этот ключ можно, перейдя в Your-Project > Settings > Client Keys (DSN) .
После того, как вы запустите Python-приложение, вы можете перейти на Sentry.io и открыть панель управления проекта. Там должны быть сведения о залогированных ошибках и о других проблемах приложения. В нашем примере можно видеть сообщение об исключении, соответствующем ошибке ZeroDivisionError .

Изучая подробности об ошибке, вы можете увидеть, что Sentry предоставляет подробную информацию о том, где именно произошла ошибка, а так же — об аргументах x и y , работа с которыми привела к появлению исключения.

Продолжая изучение логов, можно увидеть, помимо записей уровня error , записи уровня info . Налаживая мониторинг приложения с использованием Sentry, нужно учитывать, что эта платформа интегрирована с модулем logging . Вспомните — в нашем экспериментальном проекте уровень логирования был установлен в значение info . В результате Sentry записывает все события, уровень которых соответствует info и более высоким уровням, делая это в стиле «навигационной цепочки», что упрощает отслеживание ошибок.
Sentry позволяет фильтровать записи по уровням логирования, таким, как info и error . Это удобнее, чем просмотр больших лог-файлов в поиске потенциальных ошибок и сопутствующих сведений. Это позволяет назначать решению проблем приоритеты, зависящие от серьёзности этих проблем, и, кроме того, позволяет, используя навигационные цепочки, находить источники неполадок.

В данном примере мы рассматриваем ZeroDivisionError как исключение. В более крупных проектах, даже если мы не реализуем подобный механизм обработки исключений, Sentry автоматически предоставит диагностическую информацию о наличии необработанных исключений. С помощью Sentry, кроме того, можно анализировать проблемы с производительностью кода.

Код, использованный в данном руководстве, можно найти в этом GitHub-репозитории.
Итоги
Освоив это руководство, вы узнали о том, как настраивать логирование с использованием стандартного Python-модуля logging . Вы освоили основы настройки логгера root и пользовательских логгеров, ознакомились с рекомендациями по логированию. Вы, кроме того, узнали о том, как платформа Sentry может помочь вам в деле мониторинга ваших приложений, обеспечивая вас сведениями о проблемах с производительностью и о других ошибках, и используя при этом все возможности модуля logging .
Когда вы будете работать над своим следующим Python-проектом — не забудьте реализовать в нём механизмы логирования. И можете испытать бесплатную пробную версию Sentry.
О, а приходите к нам работать?
Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.
Выбираем логирование в Python: logging vs loguru
В этой заметке мы попробуем выбрать библиотеку для логирования в Python. Логи помогают зафиксировать и понять, что пошло не так в работе вашего микросервиса. Так же в логи часто пишут информационные сообщения. Например: параметры, метрики качества и ход обучения модели. Пример куска лога обучения модели:

logging vs loguru
Библиотека Logging является стандартным решением для логирования в Python. Logging часто критикуем за сложность конфигов, неудобство настроек разного уровня логирования и ротации файлов логов.
Loguru же наоборот, позиционирует себя как максимально простая библиотека для логирования в Python.
Попробуем разробрать плюсы и минусы каждой библиотеки и выбрать лучшее решение для своих задач.
Базовое применение
Сравним код для самого базового логирования. Будем записывать лог в файл.
import logging logging.basicConfig(filename='logs/logs.log', level=logging.DEBUG) logging.debug('Error') logging.info('Information message') logging.warning('Warning')
Разберем немножко код. basicConfig — создаёт базовую конфигурацию для нашего логирования, filename — путь к файлу лога, level — уровень логирования. При logging.DEBUG он будет пропускать все записи в лог.
from loguru import logger logger.add('logs/logs.log', level='DEBUG') logger.debug('Error') logger.info('Information message') logger.warning('Warning')
Думаю тут всё понятно и код выглядит весьма похоже. Но посмотрим на результат в файле и консоли.
DEBUG:root:Error INFO:root:Information message WARNING:root:Warning 2021-02-04 17:44:10.914 | DEBUG | main::14 - Error 2021-02-04 17:44:10.915 | INFO | main::15 - Information message 2021-02-04 17:44:10.915 | WARNING | main::16 - Warning

И в консоли и в файле Loguru выглядит поинформативней сразу по умолчанию.
Форматирование
Давайте теперь попробуем сделать форматирование в Logger. Для этого есть метод .setFormatter Будем выводить время события, тип и само событие как в Loguru.
import logging import sys logger = logging.getLogger() logger.setLevel(logging.DEBUG) fileHandler = logging.FileHandler('logs/logs.log') fileHandler.setFormatter(logging.Formatter(fmt='[%(asctime)s: %(levelname)s] %(message)s')) logger.addHandler(fileHandler) streamHandler = logging.StreamHandler(stream=sys.stdout) streamHandler.setFormatter(logging.Formatter(fmt='[%(asctime)s: %(levelname)s] %(message)s')) logger.addHandler(streamHandler) logging.debug('Error') logging.info('Information message') logging.warning('Warning')
Ох ты ж! Кода стало гораздо больше. Давайте разберем новые классы и методы. Для начала у нас есть класс Handler . FileHandler для записи в файл и StreamHandler для запись в консоль. Затем нужно с помощью метода addHandler передать их в наш logger. В документации вы найдете еще несколько Handler .
Теперь разберемся с классом Formatter . Из названия понятно, что этот класс отвечает за формат записи нашего лога. В нашем примере мы добавили помимо самого сообщения время записи и его тип. Теперь наш лог выглядит так:
[2021-02-04 21:28:28,283: DEBUG] Error [2021-02-04 21:28:28,283: INFO] Information message [2021-02-04 21:28:28,283: WARNING] Warning
В Loguru тоже есть форматирование. Делается это так: logger.add('logs/logs.log', level='DEBUG', format=" ")
Ротация / очистка / архивирование
Часто возникает необходимость ротировать лог — то есть архивировать, очищать или создать новый лог файл с заданной переодичностью. Например это нужно когда логи становятся слишком большими. Снова сравним код.
import logging import time from logging.handlers import RotatingFileHandler def create_rotating_log(path): logger = logging.getLogger("Rotating Log") logger.setLevel(logging.INFO) handler = RotatingFileHandler(path, maxBytes=20, backupCount=5) logger.addHandler(handler) for i in range(10): logger.info("This is test log line %s" % i) time.sleep(1.5) if name = : log_file = "test.log" create_rotating_log(log_file)
Тут используется RotatingFileHandler . maxBytes — максимальный размер файла, backupCount — сколько файлов хранить.
Посмотрим как это можно сделать в Loguru:
logger.add("file_1.log", rotation="500 MB") # Пишет в новый файл после достижения размера лога 500 MB logger.add("file_2.log", rotation="12:00") # В 12:00 создаёт новый файл logger.add("file_X.log", retention="10 days") # Очищает наш лог после 10 дней logger.add("file_Y.log", compression="zip") # Архивирует наши логи
Опять всё выглядит попроще и удобнее.
Обработка исключений
В Logging есть отдельный метод — exception . Это достаточно удобно и нужно применять его в блоке try except.
import logging def my_function(x, y, z): return x / (y * z) try: my_function(1, 2, 0) except ZeroDivisionError: logging.exception("message")
Лог будет выгледеть так:
ERROR:root:message Traceback (most recent call last): File "logs.py", line 5, in my_function(1, 2, 0) File "logs.py", line 3, in my_function return x / (y * z) ZeroDivisionError: division by zero
В Loguru нам будет нужно использовать декоратор @logger.catch :
from loguru import logger @logger.catch def my_function(x, y, z): return x / (y * z) my_function(1, 2, 0)
Вау! Лог выглядит круто. Даже показывает значение переменных:

Заключение
В этой заметке достаточно бегло рассмотрели две библиотеки для логирования в Python. Обе библиотеки имеют еще ряд фишек и возможностей. Рекомендую подробнее почитать в документации. Мне нравится Loguru и он отлично подходит для использования в пайплайнах машинного обучения или для тренировок нейронных сетей. В небольших микросервисах Loguru тоже отлично подходит. Единственный минус использования Loguru, который я нашел это еще одна лишняя зависимость в вашем проекте.