DevGang
Авторизоваться

Модуль Logging в Python

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

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

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

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

Модуль Logging

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

Добавить запись в вашу программу на Python так же просто, как это:

import logging

С импортированным модулем регистрации вы можете использовать то, что называется «регистратором», для регистрации сообщений, которые вы хотите видеть. По умолчанию существует 5 стандартных уровней, указывающих на серьезность событий. У каждого есть соответствующий метод, который можно использовать для регистрации событий на этом уровне. Определенные уровни в порядке возрастания серьезности следующие:

  • DEBUG
  • INFO
  • WARNING
  • ERROR
  • CRITICAL

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

import logging

logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')

Вывод вышеупомянутой программы будет выглядеть так:

WARNING:root:This is a warning message
ERROR:root:This is an error message
CRITICAL:root:This is a critical message

Выходные данные показывают уровень серьезности перед каждым сообщением вместе с корнем, который является именем, которое модуль журналирования дает своему регистратору по умолчанию. (Регистраторы подробно обсуждаются в последующих разделах.) Этот формат, показывающий уровень, имя и сообщение, разделенные двоеточием (:), является форматом вывода по умолчанию, который можно настроить для включения таких вещей, как отметка времени, номер строки и другие детали.

Обратите внимание, что сообщения debug() и info() не были зарегистрированы. Это связано с тем, что по умолчанию модуль ведения журнала регистрирует сообщения с уровнем серьезности WARNING или выше. Вы можете изменить это, сконфигурировав модуль регистрации для регистрации событий всех уровней, если хотите. Вы также можете определить свои собственные уровни серьезности, изменив конфигурации, но, как правило, это не рекомендуется, так как это может привести к путанице с журналами некоторых сторонних библиотек, которые вы можете использовать.

Базовая конфигурация

Вы можете использовать метод basicConfig(**kwargs) для настройки ведения журнала:

«Вы заметите, что модуль ведения журнала нарушает руководство по стилю PEP8 и использует соглашения об именах camelCase. Это потому, что он был взят из Log4j, утилиты ведения журналов в Java. Это известная проблема в пакете, но к тому времени, когда было решено добавить ее в стандартную библиотеку, она уже была принята пользователями, и ее изменение в соответствии с требованиями PEP8 вызовет проблемы обратной совместимости ». (Источник)

Некоторые из часто используемых параметров для basicConfig():

  • level - Корневой регистратор будет установлен на указанный уровень серьезности.
  • filename - Файл в который будут записываться логи.
  • filemode - Если указано имя файла, файл открывается в этом режиме. По умолчанию это 'a', что означает добавление.
  • format - Определяет формат выведения сообщений.

Используя параметр level, вы можете установить, какой уровень сообщений журнала вы хотите записать. Это может быть сделано путем передачи одной из констант, доступных в классе, и это позволило бы регистрировать все вызовы регистрации на уровне или выше этого уровня. Вот пример:

import logging

logging.basicConfig(level=logging.DEBUG)
logging.debug('This will get logged')
DEBUG:root:This will get logged

Все события на уровне DEBUG или выше теперь будут регистрироваться.

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

import logging

logging.basicConfig(
    filename='app.log', 
    filemode='w', 
    format='%(name)s - %(levelname)s - %(message)s'
)
logging.warning('This will get logged to a file')
root - ERROR - This will get logged to a file

Сообщение будет выглядеть так, но будет записано в файл с именем app.log вместо консоли. Для режима файла задано значение w, что означает, что файл журнала открывается в «режиме записи» каждый раз, когда вызывается basicConfig(), и при каждом запуске программы файл перезаписывается. 

Еще больше настроек можно найти здесь.

Следует отметить, что вызов basicConfig() для настройки корневого регистратора работает, только если корневой регистратор не был настроен ранее. По сути, эта функция может быть вызвана только один раз.

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

Форматирование вывода

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

import logging

logging.basicConfig(format='%(process)d-%(levelname)s-%(message)s')
logging.warning('This is a Warning')
18472-WARNING-This is a Warning

Формат может принимать строку с атрибутами LogRecord в любом порядке. Весь список доступных атрибутов можно найти здесь.

Вот еще один пример, где вы можете добавить информацию о дате и времени:

import logging

logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO)
logging.info('Admin logged in')
2018-07-11 20:12:06,288 - Admin logged in

%(asctime)s добавляет время создания LogRecord. Формат можно изменить с помощью атрибута datefmt, который использует тот же язык форматирования, что и функции форматирования в модуле datetime, например time.strftime():

import logging

logging.basicConfig(
    format='%(asctime)s - %(message)s', 
    datefmt='%d-%b-%y %H:%M:%S'
)
logging.warning('Admin logged out')
12-Jul-18 20:53:19 - Admin logged out

Вы можете найти руководство здесь.

Регистрация переменных данных

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

import logging

name = 'John'

logging.error('%s raised an error', name)
ERROR:root:John raised an error

Аргументы, передаваемые методу, будут включены в сообщение в качестве переменных данных.

Хотя вы можете использовать любой стиль форматирования, f-strings, представленные в Python 3.6, являются отличным способом форматирования строк, так как они могут помочь сделать форматирование коротким и легким для чтения:

import logging

name = 'John'

logging.error(f'{name} raised an error')
ERROR:root:John raised an error

Захват стека

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

import logging

a = 5
b = 0

try:
  c = a / b
except Exception as e:
  logging.error("Exception occurred", exc_info=True)
ERROR:root:Exception occurred
Traceback (most recent call last):
  File "exceptions.py", line 6, in 
    c = a / b
ZeroDivisionError: division by zero
[Finished in 0.2s]

Если для exc_info не задано значение True, выходные данные вышеприведенной программы не сообщат нам ничего об исключении, которое в реальном сценарии может быть не таким простым, как ZeroDivisionError. Представьте, что вы пытаетесь отладить ошибку в сложной кодовой базе с помощью журнала, который показывает только это:

ERROR:root:Exception occurred

Вот быстрый совет: если вы входите в систему из обработчика исключений, используйте метод logging.exception(), который регистрирует сообщение с уровнем ERROR и добавляет в сообщение информацию об исключении. Проще говоря, вызов logging.exception() похож на вызов logging.error(exc_info = True). Но поскольку этот метод всегда выводит информацию об исключении, его следует вызывать только из обработчика исключений. Взгляните на этот пример:

import logging

a = 5
b = 0
try:
  c = a / b
except Exception as e:
  logging.exception("Exception occurred")
ERROR:root:Exception occurred
Traceback (most recent call last):
  File "exceptions.py", line 6, in 
    c = a / b
ZeroDivisionError: division by zero
[Finished in 0.2s]

Использование logging.exception() покажет сообщение на уровне ERROR. Если вы не хотите этого, вы можете вызвать любой из других методов ведения журнала от debug() до critical() и передать параметр exc_info как True.

Классы и функции

До сих пор мы видели регистратор по умолчанию с именем root, который используется модулем журналирования всякий раз, когда его функции вызываются прямо так: logging.debug(). Вы можете (и должны) определить свой собственный регистратор, создав объект класса Logger, особенно если ваше приложение имеет несколько модулей. Давайте посмотрим на некоторые классы и функции в модуле.

Наиболее часто используемые классы, определенные в модуле регистрации:

import logging

logger = logging.getLogger('example_logger')
logger.warning('This is a warning')
This is a warning

Это создает пользовательский регистратор с именем example_logger, но в отличие от корневого регистратора, имя настраиваемого регистратора не является частью выходного формата по умолчанию и должен быть добавлен в конфигурацию. Конфигурирование его в формате для отображения имени регистратора даст вывод, подобный этому:

WARNING:example_logger:This is a warning

Опять же, в отличие от корневого регистратора, пользовательский регистратор нельзя настроить с помощью basicConfig(). Вы должны настроить его с помощью обработчиков и форматеров:

«Рекомендуется использовать средства ведения журнала уровня модуля, передавая __name__ в качестве параметра имени функции getLogger() для создания объекта logging, поскольку имя самого средства ведения журнала сообщит нам, откуда регистрируются события. __name__ - это специальная встроенная переменная в Python, которая выводит имя текущего модуля.» (Источник)

Использование обработчиков

Обработчики пригодятся, когда вы хотите настроить свои собственные регистраторы и отправлять журналы в несколько мест. Обработчики отправляют сообщения журнала в сконфигурированные места назначения, такие как стандартный поток вывода или файл или по HTTP, или на вашу электронную почту через SMTP.

У созданного вами регистратора может быть несколько обработчиков, что означает, что вы можете настроить его на сохранение в файл журнала, а также на отправку по электронной почте.

Подобно регистраторам, вы также можете установить уровень серьезности в обработчиках. Это полезно, если вы хотите установить несколько обработчиков для одного и того же регистратора, но хотите разные уровни серьезности для каждого из них. Например, вы можете захотеть, чтобы журналы с уровнем WARNING и выше регистрировались на консоли, но все с уровнем ERROR и выше также должно быть сохранено в файл. Вот программа, которая делает это:

# logging_example.py

import logging

# Create a custom logger
logger = logging.getLogger(__name__)

# Create handlers
c_handler = logging.StreamHandler()
f_handler = logging.FileHandler('file.log')
c_handler.setLevel(logging.WARNING)
f_handler.setLevel(logging.ERROR)

# Create formatters and add it to handlers
c_format = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
f_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
c_handler.setFormatter(c_format)
f_handler.setFormatter(f_format)

# Add handlers to the logger
logger.addHandler(c_handler)
logger.addHandler(f_handler)

logger.warning('This is a warning')
logger.error('This is an error')
__main__ - WARNING - This is a warning
__main__ - ERROR - This is an error

Здесь logger.warning() создает LogRecord, который содержит всю информацию о событии, и передает ее всем имеющимся обработчикам: c_handler и f_handler.

c_handler является StreamHandler с уровнем WARNING и берет информацию из LogRecord для генерации вывода в указанном формате и выводит его на консоль. f_handler - это FileHandler с уровнем ERROR, и он игнорирует этот LogRecord, так как его уровень - WARNING.

Когда вызывается logger.error(), c_handler ведет себя точно так же, как и раньше, а f_handler получает LogRecord на уровне ERROR, поэтому он продолжает генерировать вывод точно так же, как c_handler, но вместо вывода его на консоль он записывает его указанный файл в этом формате:

2018-08-03 16:12:21,723 - __main__ - ERROR - This is an error

Имя регистратора, соответствующее переменной __name__, записывается как __main__, то есть имя, которое Python назначает модулю, с которого начинается выполнение. Если этот файл импортируется каким-либо другим модулем, то переменная __name__ будет соответствовать его имени например logging_example. Вот как это будет выглядеть:

# run.py

import logging_example
logging_example - WARNING - This is a warning
logging_example - ERROR - This is an error

Другие методы настройки

Вы можете настроить ведение журнала, как показано выше, используя функции модуля и класса или создав файл конфигурации или словарь и загрузив его, используя fileConfig() или dictConfig() соответственно. Это полезно, если вы хотите изменить конфигурацию ведения журнала в работающем приложении.

Вот пример конфигурации файла:

[loggers]
keys=root,sampleLogger

[handlers]
keys=consoleHandler

[formatters]
keys=sampleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_sampleLogger]
level=DEBUG
handlers=consoleHandler
qualname=sampleLogger
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=sampleFormatter
args=(sys.stdout,)

[formatter_sampleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s

В приведенном выше файле есть два регистратора, один обработчик и один форматер. После того как их имена определены, они настраиваются путем добавления слов logger, handler и formatter перед их именами, разделенными подчеркиванием.

Чтобы загрузить этот файл конфигурации, вы должны использовать fileConfig():

import logging
import logging.config

logging.config.fileConfig(fname='file.conf', disable_existing_loggers=False)

# Get the logger specified in the file
logger = logging.getLogger(__name__)

logger.debug('This is a debug message')
2018-07-13 13:57:45,467 - __main__ - DEBUG - This is a debug message

Путь к файлу конфигурации передается в качестве параметра методу fileConfig(), а параметр disable_existing_loggers используется для сохранения или отключения регистраторов, присутствующих при вызове функции. По умолчанию установлено значение True.

Вот та же конфигурация в формате YAML:

version: 1
formatters:
  simple:
    format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
  console:
    class: logging.StreamHandler
    level: DEBUG
    formatter: simple
    stream: ext://sys.stdout
loggers:
  sampleLogger:
    level: DEBUG
    handlers: [console]
    propagate: no
root:
  level: DEBUG
  handlers: [console]

Вот пример, который показывает, как загрузить конфигурацию из файла yaml:

import logging
import logging.config
import yaml

with open('config.yaml', 'r') as f:
    config = yaml.safe_load(f.read())
    logging.config.dictConfig(config)

logger = logging.getLogger(__name__)

logger.debug('This is a debug message')
2018-07-13 14:05:03,766 - __main__ - DEBUG - This is a debug message

Сохраняйте спокойствие и читайте логи

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

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

Перевод статьи: Logging in Python

#Python
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться