Ведение журнала HOWTO

Автор:

Винай Саджип <vinay_sajip at red-dove dot com>

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

Самоучитель по ведению журналов

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

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

Вы можете получить доступ к функциям протоколирования, создав логгер с помощью logger = getLogger(__name__), а затем вызвав методы debug(), info(), warning(), error() и critical() этого логгера. Чтобы определить, когда следует использовать протоколирование, и узнать, какие методы логгера следует использовать, смотрите таблицу ниже. В ней для каждой из множества общих задач указано, какой инструмент лучше всего использовать для этой задачи.

Задание, которое вы хотите выполнить

Лучший инструмент для выполнения задачи

Отображение вывода консоли при обычном использовании скрипта или программы командной строки

print()

Сообщать о событиях, происходящих во время нормальной работы программы (например, для мониторинга состояния или исследования неисправностей)

Метод info() регистратора (или debug() для очень подробного вывода в целях диагностики)

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

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

Метод warning() регистратора, если клиентское приложение ничего не может сделать с ситуацией, но событие все равно должно быть отмечено

Сообщить об ошибке, связанной с определенным событием времени выполнения

Поднять исключение

Сообщайте о подавлении ошибки, не поднимая исключения (например, обработчик ошибок в долго работающем серверном процессе)

Метод регистратора error(), exception() или critical() в зависимости от конкретной ошибки и области применения

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

Уровень

Когда используется

DEBUG

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

INFO

Подтверждение того, что все работает так, как ожидалось.

WARNING

Признак того, что произошло что-то непредвиденное, или указание на то, что в ближайшем будущем возникнет какая-то проблема (например, «мало места на диске»). Программное обеспечение по-прежнему работает, как и ожидалось.

ERROR

Из-за более серьезной проблемы программное обеспечение не смогло выполнить какую-то функцию.

CRITICAL

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

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

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

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

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

import logging
logging.warning('Watch out!')  # will print a message to the console
logging.info('I told you so')  # will not print anything

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

WARNING:root:Watch out!

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

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

Ведение журнала в файл

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

import logging
logger = logging.getLogger(__name__)
logging.basicConfig(filename='example.log', encoding='utf-8', level=logging.DEBUG)
logger.debug('This message should go to the log file')
logger.info('So should this')
logger.warning('And this, too')
logger.error('And non-ASCII stuff, too, like Øresund and Malmö')

Изменено в версии 3.9: Добавлен аргумент кодировка. В более ранних версиях Python или если он не указан, используется кодировка по умолчанию open(). Хотя в примере выше это не показано, теперь также можно передавать аргумент errors, который определяет, как будут обрабатываться ошибки кодировки. Доступные значения и значение по умолчанию см. в документации к open().

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

DEBUG:__main__:This message should go to the log file
INFO:__main__:So should this
WARNING:__main__:And this, too
ERROR:__main__:And non-ASCII stuff, too, like Øresund and Malmö

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

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

--log=INFO

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

getattr(logging, loglevel.upper())

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

# assuming loglevel is bound to the string value obtained from the
# command line argument. Convert to upper case to allow the user to
# specify --log=DEBUG or --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)

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

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

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

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

появится на экране:

WARNING:root:Look before you leap!

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

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

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

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, но для простого использования вам нужны только levelname (степень серьезности), message (описание события, включая данные переменных) и, возможно, отображение времени, когда произошло событие. Это описано в следующем разделе.

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

Чтобы отобразить дату и время события, поместите „%(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().

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

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

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

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

Учебник по расширенному протоколированию

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

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

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

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

  • Форматировщики определяют расположение записей журнала в конечном выводе.

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

Ведение журнала осуществляется путем вызова методов на экземплярах класса Logger (далее - loggers). Каждый экземпляр имеет имя, и концептуально они располагаются в иерархии пространств имен с использованием точек (периодов) в качестве разделителей. Например, логгер с именем „scan“ является родителем логгеров „scan.text“, „scan.html“ и „scan.pdf“. Имена регистраторов могут быть любыми и указывать на область приложения, в которой возникло регистрируемое сообщение.

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

logger = logging.getLogger(__name__)

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

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

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

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

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

severity:logger name:message

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

Поток регистрации

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

../_images/logging_flow.png

Логгеры

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

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

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

  • Logger.setLevel() задает наименьшую степень серьезности сообщения журнала, которое будет обрабатываться регистратором, где debug - это наименьший встроенный уровень серьезности, а critical - наибольший встроенный уровень серьезности. Например, если уровень серьезности равен INFO, логгер будет обрабатывать только сообщения INFO, WARNING, ERROR и CRITICAL и будет игнорировать сообщения DEBUG.

  • Logger.addHandler() и Logger.removeHandler() добавляют и удаляют объекты-обработчики из объекта логгера. Более подробно обработчики рассматриваются в Обработчики.

  • Logger.addFilter() и Logger.removeFilter() добавляют и удаляют объекты фильтров из объекта регистратора. Более подробно фильтры рассматриваются в Фильтр объектов.

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

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

  • 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 по умолчанию). При принятии решения о том, обрабатывать ли событие, используется эффективный уровень регистратора, чтобы определить, передавать ли событие обработчикам регистратора.

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

Обработчики

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

Стандартная библиотека включает в себя довольно много типов обработчиков (см. Полезные обработчики); в учебных пособиях в примерах используются в основном 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(). Чтобы изменить этот параметр для всех форматеров, например, если вы хотите, чтобы все время регистрации отображалось по Гринвичу, установите атрибут converter в классе Formatter (значение time.gmtime для отображения по Гринвичу).

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

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

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

  2. Создание файла конфигурации протоколирования и его чтение с помощью функции fileConfig().

  3. Создание словаря с информацией о конфигурации и передача его в функцию dictConfig().

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

import logging

# create logger
logger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG)

# create console handler and set level to debug
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

# create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# add formatter to ch
ch.setFormatter(formatter)

# add ch to logger
logger.addHandler(ch)

# 'application' code
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')

# create logger
logger = logging.getLogger('simpleExample')

# 'application' code
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

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

$ 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 из соображений обратной совместимости. Это может быть и не то, что вам нужно, поскольку это приведет к отключению всех не корневых регистраторов, существовавших до вызова fileConfig(), если только они (или их предки) не будут явно названы в конфигурации. За дополнительной информацией обратитесь к справочной документации и при желании укажите False для этого параметра.

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

Обратите внимание, что имена классов, на которые ссылаются в конфигурационных файлах, должны быть либо относительными к модулю регистрации, либо абсолютными значениями, которые могут быть разрешены с помощью обычных механизмов импорта. Таким образом, вы можете использовать либо WatchedFileHandler (относительно модуля протоколирования), либо 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]

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

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

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

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

Изменено в версии 3.2: Для версий Python, предшествующих 3.2, поведение будет следующим:

  • Если raiseExceptions равно False (режим производства), событие тихо отменяется.

  • Если raiseExceptions равен True (режим разработки), то один раз выводится сообщение „No handlers could be found for logger X.Y.Z“.

Чтобы получить поведение до версии 3.2, 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“.

Примечание

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

Примечание

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

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

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

Уровень

Числовое значение

CRITICAL

50

ERROR

40

WARNING

30

INFO

20

DEBUG

10

NOTSET

0

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

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

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

Как и в случае с регистраторами, обработчики могут иметь связанные с ними уровни. Уровень обработчика действует как фильтр так же, как и уровень регистратора. Если обработчик решает действительно отправить событие, то для отправки сообщения по назначению используется метод 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 отправляют сообщения демону Unix syslog, возможно, на удаленной машине.

  10. NTEventLogHandler экземпляры отправляют сообщения в журнал событий Windows NT/2000/XP.

  11. Экземпляры MemoryHandler отправляют сообщения в буфер в памяти, который очищается при выполнении определенных критериев.

  12. Экземпляры HTTPHandler отправляют сообщения на HTTP-сервер, используя семантику GET или POST.

  13. WatchedFileHandler экземпляры следят за файлом, в который они записываются. Если файл изменяется, он закрывается и открывается снова, используя имя файла. Этот обработчик полезен только в Unix-подобных системах; Windows не поддерживает используемый механизм.

  14. Экземпляры QueueHandler отправляют сообщения в очередь, например, реализованную в модулях queue или multiprocessing.

  15. Экземпляры NullHandler ничего не делают с сообщениями об ошибках. Они используются разработчиками библиотек, которые хотят использовать логирование, но хотят избежать сообщения „No handlers could be found for logger XXX“, которое может быть выведено, если пользователь библиотеки не настроил логирование. Дополнительную информацию см. в Настройка ведения журнала для библиотеки.

Added in version 3.1: Класс NullHandler.

Added in version 3.2: Класс QueueHandler.

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

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

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

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

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

Исключения, возникающие при ведении журнала

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

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

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

Примечание

По умолчанию для raiseExceptions используется значение True. Это связано с тем, что во время разработки вы обычно хотите получать уведомления о любых возникающих исключениях. Для использования в производстве рекомендуется установить значение 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()).

Информация о резьбе.

Установите logging.logThreads на False.

Идентификатор текущего процесса (os.getpid())

Установите logging.logProcesses на False.

Имя текущего процесса при использовании multiprocessing для управления несколькими процессами.

Установите logging.logMultiprocessing на False.

Текущее имя asyncio.Task при использовании asyncio.

Установите logging.logAsyncioTasks на False.

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

Другие ресурсы

См.также

Модуль logging

Ссылка на API для модуля протоколирования.

Модуль logging.config

API конфигурации для модуля протоколирования.

Модуль logging.handlers

Полезные обработчики, входящие в состав модуля протоколирования.

A logging cookbook