Перейти к содержимому

Как сохранить лог в переменную 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()
  • 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.

Модуль logging. Как при логировании указывать всю цепочку вызовов функции

Хочется написать декоратор, который указывает всю цепочку вызовов. Вот, опираясь на статьи 1 и 2, написал такое:

import logging import logging.config def log(func): """ Логируем какая функция вызывается. """ def wrap_log(*args, **kwargs): global logger saved_logger = logger # name = func.__name__ logger = logging.getLogger(f".") logger.info(f"Вызов с args=, kwargs=") result = func(*args, **kwargs) logger.debug(f"Результат: ") logger = saved_logger return func return wrap_log @log def double_function(a): """ Умножаем полученный параметр. """ return a*2 @log def add_himself(a): return a+a if __name__ == "__main__": logging.config.fileConfig('logging.config') logger = logging.getLogger("Sheduler") value = double_function(2) add_himself(2) add_himself("a") double_function("a") 

И в принципе оно работает, но смущают меня глобальные переменные. Наверняка есть более "Пайтон"-решение.
Файл logging.config такой:

[loggers] keys=root,Sheduler [handlers] keys=timedRotatingFileHandler, consoleHandler [formatters] keys=myFormatter [logger_root] level=CRITICAL handlers=consoleHandler [logger_Sheduler] level=DEBUG handlers=timedRotatingFileHandler qualname=Sheduler [handler_consoleHandler] level=INFO formatter=myFormatter args=(sys.stdout,) [handler_timedRotatingFileHandler] formatter=myFormatter args=("sheduler.log",) kwargs= [formatter_myFormatter] format=%(asctime)s - %(name)s - %(levelname)s - %(message)s datefmt= 

Логирование в Python

В переводе с английского 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() 

Перехватывать все исключения сразу — плохая практика. При таком подходе можно и не узнать о проблемах, потому что программа продолжит выполняться. Однако такой вариант возможен в некоторых случаях, например, когда вы пишите чат-бота и важно, чтобы он не падал, когда какой-то из запросов не выполнится.

Похожие записи:
  1. Библиотека python-telegram-bot
  2. Логирование в Python. Задание
  3. Бот в Telegram
  4. Дополнительные возможности ORM

Выбираем логирование в 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

И в консоли и в файле 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)

Вау! Лог выглядит круто. Даже показывает значение переменных:

Loguru обработка исключений

Заключение

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *