HOWTO по логированию

Автор:Винай Саджип

Общий учебник по логированию

Логирование — это средство отслеживания событий, которые происходят при запуске некоторого программного обеспечения. Разработчик программы добавляет в свой код вызовы логирования, чтобы указать, что произошли определенные события. Событие определяется описательным сообщением, опционально содержащее изменяющиеся данные (т. е. данные, потенциально могут различаться для каждого наступившего события). У событий также есть важность, которое разработчик придаёт событию; важность также можно назвать уровнем или серьёзностью.

Когда использовать логирование

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

Задача, которую вы хотите выполнить Лучший инструмент для задачи
Отображение вывода в консоли для обычного использования сценария командной строки или программы print()
Сообщать о событиях, которые происходят во время нормальной работы программы (например, для мониторинга состояния или расследования неисправностей) logging.info() (или logging.debug() для очень подробного вывода для диагностических целей)
Сообщить предупреждение о событии во время выполнения

warnings.warn() в коде библиотеки, если проблему можно избежать, и клиентское приложение должно быть изменено, чтобы исключить предупреждение.

logging.warning() если программа клиента никак не реагирует на ситуацию, но событие всё равно следует зафиксировать

Сообщить об ошибке, связанной с событием во время выполнения Вызвать исключение
Сообщить о подавлении ошибки, не вызывая исключение (например, обработчик ошибок в длительном серверном процессе) logging.error(), logging.exception() или logging.critical() в зависимости от ошибки и области применения

Функции logging перечислены в соответствии с уровнем или серьёзностью отслеживаемых событий. Стандартные уровни и их применимость, описаны ниже (в порядке возрастания серьёзности):

Уровень Когда используется
DEBUG Подробная информация, как правило, интересна только для диагностики проблем.
INFO Подтверждение, что всё работает, как ожидалось.
WARNING Указание на то, что произошло что-то неожиданное или указание на проблему в ближайшем будущем (например, «недостаточно места на диске»). Программное обеспечение всё ещё работает как ожидалось.
ERROR Появление более серьёзной проблемы, когда программное обеспечение не смогло выполнить какую-либо функцию.
CRITICAL Серьезная ошибка, указывающая на то, что программа не может дальше продолжать работу.

Уровень по умолчанию — WARNING, что означает, что будут отслеживаться только события этого уровня и выше, если пакет logging не настроен на иное.

Отслеживаемые события можно обрабатывать по-разному. Самый простой способ обработки отслеживаемых событий — выводить их на консоль. Другой распространенный способ — записать их в файл на диске.

Простой пример

Очень простой пример:

import logging
logging.warning('Watch out!')  # вывести сообщение в консоль
logging.info('I told you so')  # не будет ничего печатать

Если вы введёте эти строки в сценарий и запустите его, вы увидите:

WARNING:root:Watch out!

распечатку в консоли. Сообщение INFO не появляется, поскольку уровень по умолчанию — WARNING. Распечатанное сообщение включает в себя указание уровня и описание события, представленного в вызове логирования, то есть «Watch out!». Пока не беспокойтесь о «корневой» части: она будет объяснена позже. Фактический вывод может быть достаточно гибко отформатирован, если вам это нужно; параметры форматирования также будут объяснены позже.

Запись в файл

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

import logging
logging.basicConfig(filename='example.log',level=logging.DEBUG)
logging.debug('This message should go to the log file')
logging.info('So should this')
logging.warning('And this, too')

А теперь, если мы откроем файл и посмотрим, что у нас есть, мы должны найти лог сообщения:

DEBUG:root:This message should go to the log file
INFO:root:So should this
WARNING:root:And this, too

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

Если вы хотите установить уровень журналирования с помощью параметра командной строки, например:

--log=INFO

и у вас есть значение параметра, переданного для --log в некоторой переменной loglevel, которое вы можете использовать:

getattr(logging, loglevel.upper())

чтобы получить значение, которое вы передадите basicConfig() через аргумент level. Возможно, вы захотите проверить на ошибку любое значение, введённое пользователем, например, как в следующем примере:

# предполагая, что loglevel привязан к строковому значению, полученному из
# аргумента командной строки. Преобразование в верхний регистр, чтобы позволить пользователю
# указать --log=DEBUG или --log=debug
numeric_level = getattr(logging, loglevel.upper(), None)
if not isinstance(numeric_level, int):
    raise ValueError('Invalid log level: %s' % loglevel)
logging.basicConfig(level=numeric_level, ...)

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

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

logging.basicConfig(filename='example.log', filemode='w', level=logging.DEBUG)

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

Логирование из нескольких модулей

Далее пример того, как организовать логирование в программе состоящей из нескольких модулей:

# myapp.py
import logging
import mylib

def main():
    logging.basicConfig(filename='myapp.log', level=logging.INFO)
    logging.info('Started')
    mylib.do_something()
    logging.info('Finished')

if __name__ == '__main__':
    main()
# mylib.py
import logging

def do_something():
    logging.info('Doing something')

Если вы запустите myapp.py, то увидите в myapp.log:

INFO:root:Started
INFO:root:Doing something
INFO:root:Finished

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

Логирование изменяющиеся данных

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

import logging
logging.warning('%s before you %s', 'Look', 'leap!')

отобразится:

WARNING:root:Look before you leap!

При объединении переменных с данными в сообщении с описанием события используется старый %-стиль форматирования строк. Это сделано для обратной совместимости: в пакете logging появились новые параметры форматирования str.format() и string.Template. Новые параметры форматирования поддерживаются, но их изучение выходит за рамки данного руководства: дополнительную информацию см. в Использование определенных стилей форматирования во всём приложении.

Изменение формата отображаемых сообщений

Чтобы изменить формат, используемый для отображения сообщений, вам необходимо определить параметр format:

import logging
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
logging.debug('This message should appear on the console')
logging.info('So should this')
logging.warning('And this, too')

который будет печатать:

DEBUG:This message should appear on the console
INFO:So should this
WARNING:And this, too

Обратите внимание, что «root», который появлялся в предыдущих примерах, исчез. Для получения полного набора элементов, которые могут отображаться в форматных строках, вы можете обратиться к документации для Атрибуты LogRecord, но для простого использования вам просто нужно имя уровня (серьёзность), сообщение (описание события, включая переменные данные) и, возможно, время, когда событие произошло. Про это рассказано в следующем разделе.

Отображение даты/времени в сообщениях

Чтобы отобразить дату и время события, поместите «%(asctime)s» в форматную строку:

import logging
logging.basicConfig(format='%(asctime)s %(message)s')
logging.warning('is when this event was logged.')

будет напечатано что-то вроде этого:

2010-12-12 11:41:42,612 is when this event was logged.

Формат по умолчанию для отображения даты/времени (приведённый выше) подобен ISO8601 или RFC 3339. Если вам нужен больший контроль над форматированием даты и времени, укажите datefmt аргумент basicConfig, как в этом примере:

import logging
logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
logging.warning('is when this event was logged.')

который будет отображать что-то вроде этого:

12/12/2010 11:46:36 AM is when this event was logged.

Формат аргумента datefmt такой же, как поддерживаемый time.strftime().

Следующие шаги

На этом базовый учебник завершён. Этого должно быть достаточно, чтобы вы начали работать с logging. Пакет logging предлагает гораздо больше, но чтобы извлечь из него максимум пользы, вам нужно потратить немного больше времени на чтение следующих разделов. Если вы готовы к этому, возьмите свой любимый напиток и продолжайте.

Если ваши потребности в журналирования просты, используйте приведенные выше примеры, чтобы включить логирование в свои собственные сценарии, а если вы столкнетесь с проблемами или чего-то не понимаете, задайте вопрос в группе comp.lang.python Usenet (доступно по адресу), и вскоре вы получите помощь.

Всё ещё здесь? Вы можете продолжить чтение следующих разделов, которые представляют собой немного более продвинутый/углубленный учебник, чем вышеприведённый базовый. После этого вы можете взглянуть на Поваренная книга логирования.

Продвинутый учебник по журналированию

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

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

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

Логирование выполняется путём вызова методов экземпляров класса Logger (в дальнейшем называемого логгером). У каждого экземпляра есть имя, и они концептуально упорядочены в иерархии пространства имён с использованием точек (крапинок) в качестве разделителей. Например, логгер с именем «scan» является родительским для логгеров «scan.text», «scan.html» и «scan.pdf». Имена логгеров могут быть любыми, как вы хотите, и указывать на область приложения, в которой происходит логируемое сообщение.

Хорошее соглашение, которое следует использовать при именовании логгеров заключается в использовании логгера на уровне модуля в каждом использующем logging модуле и имеет следующие названия:

logger = logging.getLogger(__name__)

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

Корень иерархии логгеров называется корневым логгером. Это логгер, используемый функциями debug(), info(), warning(), error() и critical(), которые просто вызывают одноименный метод корневого логгера. Функции и методы имеют одинаковые сигнатуры. Имя корневого логгера печатается как «root» в выходных данных журналов.

Конечно, можно записывать сообщения в разные места назначения. В пакет включена поддержка записи лог сообщений в файлы, HTTP локации по протоколам GET/POST, электронную почту через SMTP, общие сокеты, очереди или механизмы ведения журналов для ОС, такие, как syslog или журнал событий Windows NT. Назначения обслуживаются классами обработчиками. Вы можете создать свой собственный класс назначения журнала, если у вас есть особые требования, которые не выполняются ни одним из встроенных классов обработчиков.

По умолчанию для журналируемых сообщений не задано место назначения. Вы можете указать место назначения (например, консоль или файл) с помощью basicConfig(), как в примерах учебника. Если вы вызовете функции debug(), info(), warning(), error() и critical(), они проверит, не задан ли пункт назначения; и если он не установлен, они установят место назначения консоли (sys.stderr) и формат по умолчанию для отображаемого сообщения перед делегированием корневому логгеру для выполнения фактического вывода сообщения.

Формат по умолчанию, установленный basicConfig() для сообщений:

severity:logger name:message

Вы можете изменить это, передав форматную строку в basicConfig() с ключевым аргументом format. Для всех вариантов построения строки формата см. Formatter объекты.

Поток логирования

Поток информации о журналируемых событиях в логгерах и обработчиках проиллюстрирован на следующей диаграмме.

../_images/logging_flow.png

Логгеры

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

Наиболее широко используемые методы для объектов логгера делятся на две категории: конфигурация и отправка сообщений.

Наиболее распространенные методы настройки:

  • Logger.setLevel() устанавливает лог сообщение с самым низким уровнем серьёзности, которое будет обрабатывать логгер, где DEBUG — это самый низкий встроенный уровень серьёзности, а CRITICAL — самый высокий встроенный уровень серьёзности. Например, если уровень серьёзности — INFO, логгер будет обрабатывать только сообщения INFO, WARNING, ERROR и CRITICAL и игнорировать сообщения DEBUG.
  • Logger.addHandler() и Logger.removeHandler() добавляют и удаляют объекты обработчика из объекта логгера. Обработчики более подробно описаны в Обработчиках.
  • Logger.addFilter() и Logger.removeFilter() добавляют и удаляют объекты фильтра из объекта логгера. Фильтры более подробно описаны в Filter объекты.

Вам не нужно всегда вызывать эти методы для каждого созданного вами логгера. См. последние два абзаца в этом разделе.

После настройки объекта логгера следующие методы создают лог сообщения:

  • Logger.debug(), Logger.info(), Logger.warning(), Logger.error() и Logger.critical() создают лог записи с сообщением и уровнем, который соответствует их именам методов. Сообщение фактически представляет собой форматную строку, которая может содержать стандартный синтаксис подстановки строк %s, %d, %f и т. д. Остальные их аргументы — это соответствующий полям подстановки в сообщении список объектов. Что касается **kwargs, методы журналирования заботятся только о ключевом exc_info и используют его, чтобы определить, нужно ли журналировать информацию об исключении.
  • Logger.exception() создает лог сообщение, подобное Logger.error(). Разница в том, что Logger.exception() выгружает трассировку стека вместе с ней. Вызывайте этот метод только из обработчика исключений.
  • Logger.log() принимает уровень журналирования в качестве явного аргумента. Это немного более подробный способ записи сообщений в журнал, чем использование перечисленных выше удобных методов уровня журнала, но это то, как вести лог на настраиваемых уровнях журналирования.

getLogger() возвращает ссылку на экземпляр логгера с указанным именем, если оно указано, или root, если нет. Имена представляют собой иерархические структуры, разделенные точками. Несколько вызовов getLogger() с одним и тем же именем вернут ссылку на один и тот же объект логгера. Логгеры, расположенные ниже в иерархическом списке, являются потомками логгеров выше в списке. Например, для логгера с именем foo, логгеры с именами foo.bar, foo.bar.baz и foo.bam являющиеся потомками foo.

У логгеров есть понятие эффективного уровня. Если уровень явно не установлен в логгере, вместо него используется уровень его родительского элемента. Если у родителя нет явного установленного уровня, проверяется его родительский элемент и так далее, — ищутся все предки, пока не будет найден явно установленный уровень. У корневого логгера всегда есть явный установленный уровень (по умолчанию WARNING). При принятии решения о том, обрабатывать ли событие, используется эффективный уровень логгера для определения того, передается ли событие обработчикам логгера.

Дочерние логгеры распространяют сообщения до обработчиков, связанных с их предками логгеров. Из-за этого нет необходимости определять и настраивать обработчики для всех логгеров, используемых приложением. Достаточно настроить обработчики для логгера верхнего уровня и при необходимости создать дочерние логгеры. (Однако вы можете отключить распространение, установив для атрибута распространения логгера значение False.)

Обработчики

Объекты Handler отвечают за отправку соответствующих лог сообщений (в зависимости от серьёзности лог сообщений) в указанное место назначения обработчика. Объекты Logger могут добавлять к себе ноль или более объектов-обработчиков с помощью метода addHandler(). В качестве примера сценария приложение может захотеть отправить все лог сообщения в файл журнала, а все лог сообщения об ошибках или выше в стандартный вывод, а также все критические сообщения на адрес электронной почты. Для этого сценария требуются три отдельных обработчика, каждый из которых отвечает за отправку сообщений определенной степени серьёзности в определенное место.

Стандартная библиотека включает довольно много типов обработчиков (см. Полезные обработчики); в примерах в руководствах используются в основном StreamHandler и FileHandler.

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

  • Метод setLevel(), как и в объектах логгера, определяет самый низкий уровень серьёзности, который будет отправлен в соответствующее место назначения. Почему существует два метода setLevel()? Уровень, установленный в логгере, определяет, какой уровень серьёзности сообщений он будет передавать своим обработчикам. Уровень, установленный в каждом обработчике, определяет, какие сообщения он будет отправлять.
  • setFormatter() выбирает объект Formatter для использования этим обработчиком.
  • addFilter() и removeFilter() соответственно настраивают и отменяют конфигурацию объектов фильтра на обработчиках.

Код приложения не должен напрямую создавать и использовать экземпляры Handler. Класс Handler является базовым определяющим интерфейс классом, который должны иметь все обработчики, и устанавливает поведение по умолчанию, используемое (или переопределяемое) дочерними классами по желанию.

Форматтеры

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

logging.Formatter.__init__(fmt=None, datefmt=None, style='%')

Если строка формата сообщения отсутствует, по умолчанию используется необработанное сообщение. Если строка формата даты отсутствует, по умолчанию используется формат даты:

%Y-%m-%d %H:%M:%S

с добавленными миллисекундами в конце. style может быть одним из «%», «{» или «$». Если один из них не указан, будет использоваться «%».

Если style — «%», в строке формата сообщения используется подстановка строки в стиле %(<dictionary key>)s; возможные ключи задокументированы в Атрибуты LogRecord. Если стиль — «{», предполагается, что строка формата сообщения совместима с str.format() (с использованием ключевых аргументов), а если стиль «$», то строка формата сообщения должна соответствовать тому, что ожидает string.Template.substitute().

Изменено в версии 3.2: Добавлен параметр style.

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

'%(asctime)s - %(levelname)s - %(message)s'

Форматтеры используют настраиваемую пользователем функцию для преобразования времени создания записи в кортеж. По умолчанию используется time.localtime(); чтобы изменить это для экземпляра форматтера, установите атрибут converter экземпляра на функцию с той же сигнатурой, что и time.localtime() или time.gmtime(). Чтобы изменить его для всех форматтеров, например, если вы хотите, чтобы всё время логирования отображалось в GMT, установите атрибут converter в классе Formatter (на time.gmtime для отображения GMT).

Настройка журналирования

Программисты могут настроить логирование тремя способами:

  1. Создание логгеров, обработчиков и форматтеров с явным использованием кода Python, который вызывает перечисленные выше методы конфигурации.
  2. Создание конфигурационного файла логгера и его чтение с помощью функции fileConfig().
  3. Создание словаря информации о конфигурации и передача его функции dictConfig().

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

import logging

# создать логгер
logger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG)

# создать обработчик консоли и установить уровень для отладки
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

# создать форматтер
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# добавить форматтер в ch
ch.setFormatter(formatter)

# добавить ch в логгер
logger.addHandler(ch)

# код 'приложения'
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')

Запуск этого модуля из командной строки возвращает следующий результат:

$ python simple_logging_module.py
2005-03-19 15:10:26,618 - simple_example - DEBUG - debug message
2005-03-19 15:10:26,620 - simple_example - INFO - info message
2005-03-19 15:10:26,695 - simple_example - WARNING - warn message
2005-03-19 15:10:26,697 - simple_example - ERROR - error message
2005-03-19 15:10:26,773 - simple_example - CRITICAL - critical message

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

import logging
import logging.config

logging.config.fileConfig('logging.conf')

# создать логгер
logger = logging.getLogger('simpleExample')

# код 'приложения'
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')

Файл logging.conf:

[loggers]
keys=root,simpleExample

[handlers]
keys=consoleHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0

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

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

Результат почти идентичен выходным данным примера, не основанного на файле конфигурации:

$ python simple_logging_config.py
2005-03-19 15:38:55,977 - simpleExample - DEBUG - debug message
2005-03-19 15:38:55,979 - simpleExample - INFO - info message
2005-03-19 15:38:56,054 - simpleExample - WARNING - warn message
2005-03-19 15:38:56,055 - simpleExample - ERROR - error message
2005-03-19 15:38:56,130 - simpleExample - CRITICAL - critical message

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

Предупреждение

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

Переданный в dictConfig() словарь, также может указывать логическое значение с ключом disable_existing_loggers, которое, если не указано явно в словаре, также по умолчанию интерпретируется как True. Это приводит к описанному выше поведению при отключении логгера, которое может быть не тем, что вам нужно — и в этом случае укажите ключ явно со значением False.

Обратите внимание, что указанные в файлах конфигурации имена классов, должны быть либо относительными к модулю logging, либо абсолютными значениями, которые могут быть разрешены с помощью обычных механизмов импорта. Таким образом, вы можете использовать либо WatchedFileHandler (относительно модуля logging), либо mypackage.mymodule.MyHandler (для класса, определенного в пакете mypackage и модуле mymodule, где mypackage доступен в пути импорта Python).

В Python 3.2 были представлены новые средства настройки журналирования, использующие словари для хранения информации о конфигурации. Это обеспечивает расширенный набор функций подхода на основе файла конфигурации, описанного выше, и является рекомендуемым методом настройки для новых приложений и развертываний. Поскольку словарь Python используется для хранения информации о конфигурации, и поскольку вы можете заполнять этот словарь различными способами, у вас есть больше возможностей для конфигурации. Например, вы можете использовать файл конфигурации в формате JSON или, если у вас есть доступ к функциям обработки YAML, файл в формате YAML, чтобы заполнить словарь конфигурации. Или, конечно, вы можете создать словарь в коде Python, получить его в пикл форме через сокет или использовать любой подход, который имеет смысл для вашего приложения.

Вот пример той же конфигурации, что и выше, в формате 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:
  simpleExample:
    level: DEBUG
    handlers: [console]
    propagate: no
root:
  level: DEBUG
  handlers: [console]

Для получения дополнительной информации о ведении журнала с использованием словаря см. Функции конфигурации.

Что произойдет, если конфигурация не указана

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

Для версий Python до 3.2 поведение выглядит следующим образом:

  • Если logging.raiseExceptionsFalse (рабочий режим), событие автоматически отбрасывается.
  • Если logging.raiseExceptionsTrue (режим разработки), сообщение «Не удалось найти обработчики для логгера X.Y.Z» печатается один раз.

В Python 3.2 и более поздних версиях поведение выглядит следующим образом:

  • Событие выводится с использованием «обработчика последней инстанции», хранящегося в logging.lastResort. Данный внутренний обработчик не связан с каким-либо логгером и действует как StreamHandler, который записывает сообщение с описанием события в текущее значение sys.stderr (соблюдая любые действующие перенаправления). Печатается сообщение с описанием события без форматирования. Уровень обработчика установлен на WARNING, поэтому будут выводиться все события с этой и большей серьёзностью.

Чтобы получить поведение до версии 3.2, для logging.lastResort можно задать значение None.

Настройка журналирования для библиотеки

При разработке библиотеки, использующей логирование, вы должны позаботиться о документировании того, как библиотека использует логирование — например, имена используемых логгеров. Некоторое внимание также необходимо уделить конфигурации её журналирования. Если используемое приложение не использует логирование, а код библиотеки выполняет вызовы журналирования, то (как описано в предыдущем разделе) события серьёзности WARNING и выше будут распечатаны в sys.stderr. Это считается лучшим поведением по умолчанию.

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

В пакет logging включён ничего не делающий обработчик: NullHandler (начиная с Python 3.1). Экземпляр этого обработчика может быть добавлен в логгер верхнего уровня пространства имён журналирования, используемое библиотекой (если вы хотите предотвратить вывод регистрируемых событий библиотеки в sys.stderr при отсутствии конфигурации журналирования). Если все журналирование библиотекой foo выполняется с использованием логгеров с именами, соответствующими «foo.x», «foo.x.y» и т. д., тогда код:

import logging
logging.getLogger('foo').addHandler(logging.NullHandler())

должен приводить к желаемому эффекту. Если организация производит несколько библиотек, то указанное имя логгера может быть «orgname.foo», а не просто «foo».

Примечание

Настоятельно рекомендуется не добавлять никаких обработчиков кроме NullHandler к логгерам вашей библиотеки. Это связано с тем, что настройка обработчиков является прерогативой разработчика приложения, использующего вашу библиотеку. Разработчик приложения знает свою целевую аудиторию и какие обработчики наиболее подходят для их приложения: если вы добавите обработчики «под капотом», вы вполне можете помешать их способности выполнять модульные тесты и доставлять журналы, соответствующие их требованиям.

Уровни журналирования

Числовые значения уровней журналирования приведены в следующей таблице. Они в первую очередь интересны, если вы хотите определить свои собственные уровни и хотите, чтобы они имели определенные значения относительно предопределенных уровней. Если вы определяете уровень с тем же числовым значением, он перезаписывает предопределенное значение; предопределённое имя теряется.

Уровень Цифровое значение
CRITICAL 50
ERROR 40
WARNING 30
INFO 20
DEBUG 10
NOTSET 0

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

Сообщения журнала кодируются как экземпляры класса LogRecord. Когда логгер решает фактически зарегистрировать событие, из сообщения логирования создаётся экземпляр LogRecord.

Сообщения журнала подвергаются механизму диспетчеризации с использованием обработчиков, которые являются экземплярами подклассов класса Handler. Обработчики несут ответственность за то, чтобы зарегистрированное сообщение (в форме LogRecord) попало в определенное место (или набор местоположений), которое полезно для целевой аудитории этого сообщения (например, конечных пользователей, сотрудников службы поддержки, системных администраторов, разработчиков). Обработчикам передаются экземплярам LogRecord, предназначенные для определенных мест назначения. С каждым логгером может быть связано ноль, один или несколько обработчиков (с помощью метода addHandler() из Logger). В дополнение к любым обработчикам, непосредственно связанным с логгером, все обработчики, связанные со всеми предками логгера, вызываются для отправки сообщения (если флаг propagate для логгера не установлен в ложное значение, после чего передача обработчикам-предкам прекращается).

Как и в случае с логгерами, обработчики могут иметь связанные с ними уровни. Уровень обработчика действует как фильтр так же, как и уровень логгера. Если обработчик решает фактически отправить событие, для отправки сообщения по назначению используется метод emit(). Большинство определяемых пользователем подклассов Handler должны переопределить emit().

Пользовательские уровни

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

Полезные обработчики

В дополнение к базовому классу Handler предоставляется множество полезных подклассов:

  1. Экземпляры StreamHandler отправляют сообщения в потоки (файлоподобные объекты).
  2. Экземпляры FileHandler отправляют сообщения в файлы на диске.
  3. BaseRotatingHandler — это базовый класс для обработчиков, которые ротируют файлы журналов в определенный момент. Он не предназначен для непосредственного создания экземпляра. Вместо этого используйте RotatingFileHandler или TimedRotatingFileHandler.
  4. Экземпляры RotatingFileHandler отправляют сообщения в файлы на диске с поддержкой максимального размера файлов журнала и ротации файлов журнала.
  5. Экземпляры TimedRotatingFileHandler отправляют сообщения в файлы на диске, чередуя файл журнала через определенные интервалы времени.
  6. Экземпляры SocketHandler отправляют сообщения в сокеты TCP/IP. Начиная с версии 3.4 также поддерживаются доменные сокеты Unix.
  7. Экземпляры DatagramHandler отправляют сообщения в сокеты UDP. Начиная с версии 3.4 также поддерживаются доменные сокеты Unix.
  8. Экземпляры SMTPHandler отправляют сообщения на указанный адрес электронной почты.
  9. Экземпляры SysLogHandler отправляют сообщения демону syslog Unix, возможно, на удаленном компьютере.
  10. Экземпляры NTEventLogHandler отправляют сообщения в журнал событий Windows NT/2000/XP.
  11. Экземпляры MemoryHandler отправляют сообщения в буфер в памяти, который очищается всякий раз, когда выполняются определенные критерии.
  12. Экземпляры HTTPHandler отправляют сообщения на HTTP- сервер, используя семантику GET или POST.
  13. Экземпляры WatchedFileHandler следят за файлом, в который они входят. Если файл изменяется, он закрывается и открывается повторно с использованием имени файла. Этот обработчик полезен только в Unix-подобных системах; Windows не поддерживает используемый основной механизм.
  14. Экземпляры QueueHandler отправляют сообщения в очередь, например, реализованные в модулях queue или multiprocessing.
  15. Экземпляры NullHandler ничего не делают с сообщениями об ошибках. Они используются разработчиками библиотек, которые хотят использовать логирование, но хотят избежать отображения сообщения «Обработчики не найдены для логгера XXX», если пользователь библиотеки не настроил логирование. См. Настройка журналирования для библиотеки для получения дополнительной информации.

Добавлено в версии 3.1: Класс NullHandler.

Добавлено в версии 3.2: Класс QueueHandler.

Классы NullHandler, StreamHandler и FileHandler определены в основном пакете logging. Другие обработчики определены в подмодуле logging.handlers. (Существует также другой подмодуль, logging.config, для функций конфигурации.)

Логируемые сообщения форматируются для представления через экземпляры класса Formatter. Они инициализируются строкой формата, подходящей для использования с оператором % и словарём.

Для форматирования нескольких сообщений в пакете можно использовать экземпляры BufferingFormatter. В дополнение к строке формата (которая применяется к каждому сообщению в пакете) предусмотрен заголовок и строка форматного трейлера.

Если фильтрации на основе уровня логгера и/или уровня обработчика недостаточно, экземпляры Filter могут быть добавлены к экземплярам Logger и Handler (с помощью их метода addFilter()). Прежде чем принять решение о дальнейшей обработке сообщения, и логгеры, и обработчики обращаются ко всем своим фильтрам для получения разрешения. Если какой-либо фильтр возвращает ложное значение, сообщение дальше не обрабатывается.

Базовая функциональность Filter позволяет фильтровать по определенному имени логгера. Если функция используется, сообщения, отправленные указанному логгеру и его дочерним элементам, разрешаются через фильтр, а все остальные сообщения удаляются.

Вызываемые исключения во время логирования

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

Исключения SystemExit и KeyboardInterrupt никогда не принимаются. Другие исключения, которые вызываются во время метода emit() подкласса Handler, передаются его методу handleError().

Реализация по умолчанию handleError() в Handler проверяет, установлена ли переменная уровня модуля raiseExceptions. Если установлено, трассировка печатается до sys.stderr. Если не установлен, исключение будет проглочено.

Примечание

Значение по умолчанию raiseExceptionsTrue. Это связано с тем, что во время разработки вы обычно хотите получать уведомления о любых вызываемых исключениях. Рекомендуется установить raiseExceptions на False для производственного использования.

Использование произвольных объектов в качестве сообщений

В предыдущих разделах и примерах предполагалось, что сообщение, передаваемое при логировании события, является строкой. Однако это не единственная возможность. Вы можете передать произвольный объект как сообщение, и его метод __str__() будет вызываться, когда системе логирования потребуется преобразовать его в строковое представление. Фактически, если нужно, вы можете вообще избежать вычисления строкового представления, например, SocketHandler генерирует событие, обрабатывая его и отправляя по сети.

Оптимизация

Форматирование аргументов сообщения откладывается до тех пор, пока этого нельзя будет избежать. Однако вычисление аргументов, передаваемых методу логирования, также может быть дорогостоящим, и вы можете избежать этого, если логгер просто выбросит ваше событие. Чтобы решить, что делать, вы можете вызвать метод isEnabledFor(), который принимает аргумент уровня и возвращает true, если событие будет создано логгером для этого уровня вызова. Вы можете написать такой код:

if logger.isEnabledFor(logging.DEBUG):
    logger.debug('Message with %s, %s', expensive_func1(),
                                        expensive_func2())

так что, если порог логгера установлен выше DEBUG, вызовы expensive_func1() и expensive_func2() никогда не выполняются.

Примечание

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

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

То, что вы не хотите собирать Как избежать сбора
Информация о том, откуда вызывалось. Установить параметр logging._srcfile в None. Он старается не вызывать sys._getframe(), который может помочь ускорить код в среде подобной PyPy (что не может ускорить код, использующий sys._getframe()), когда PyPy поддерживает Python 3.x.
Потоковая информация. Установить logging.logThreads в 0.
Процессная информация. Установить logging.logProcesses в 0.

Также обратите внимание, что основной модуль logging включает только основные обработчики. Если вы не импортируете logging.handlers и logging.config, они не будут занимать память.

См.также

Модуль logging
Ссылка на API для модуля логирование.
Модуль logging.config
Конфигурационное API для логирования.
Модуль logging.handlers
Полезные обработчики, входящие в состав модуля логирования.

Поваренная книга по логированию