Содержание страницы
Сборник рецептов по логированию¶Logging Cookbook
| Автор: | Vinay Sajip <vinay_sajip at red-dove dot com> |
|---|
На этой странице представлен ряд рецептов, связанных с логированием, которые оказались полезными на практике.
Использование логирования в нескольких модулях¶Using logging in multiple modules
Многократные вызовы logging.getLogger('someLogger') возвращают ссылку на один и тот же объект логгера. Это верно не только в пределах одного модуля, но и между модулями, пока они находятся в одном процессе интерпретатора Python. Это верно для ссылок на один и тот же объект; кроме того, код приложения может определить и настроить родительский логгер в одном модуле и создать (но не настраивать) дочерний логгер в отдельном модуле, и все вызовы логгера к дочернему будут передаваться родительскому. Вот главный модуль:
import logging
import auxiliary_module
# создать логгер с именем 'spam_application'
logger = logging.getLogger('spam_application')
logger.setLevel(logging.DEBUG)
# создать файловый обработчик, записывающий даже отладочные сообщения
fh = logging.FileHandler('spam.log')
fh.setLevel(logging.DEBUG)
# создать консольный обработчик с более высоким уровнем логирования
ch = logging.StreamHandler()
ch.setLevel(logging.ERROR)
# создать форматтер и добавить его к обработчикам
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# добавить обработчики к логгеру
logger.addHandler(fh)
logger.addHandler(ch)
logger.info('creating an instance of auxiliary_module.Auxiliary')
a = auxiliary_module.Auxiliary()
logger.info('created an instance of auxiliary_module.Auxiliary')
logger.info('calling auxiliary_module.Auxiliary.do_something')
a.do_something()
logger.info('finished auxiliary_module.Auxiliary.do_something')
logger.info('calling auxiliary_module.some_function()')
auxiliary_module.some_function()
logger.info('done with auxiliary_module.some_function()')
А вот вспомогательный модуль:
import logging
# создать логгер
module_logger = logging.getLogger('spam_application.auxiliary')
class Auxiliary:
def __init__(self):
self.logger = logging.getLogger('spam_application.auxiliary.Auxiliary')
self.logger.info('creating an instance of Auxiliary')
def do_something(self):
self.logger.info('doing something')
a = 1 + 1
self.logger.info('done doing something')
def some_function():
module_logger.info('received a call to "some_function"')
Вывод выглядит так:
2005-03-23 23:47:11,663 - spam_application - INFO -
creating an instance of auxiliary_module.Auxiliary
2005-03-23 23:47:11,665 - spam_application.auxiliary.Auxiliary - INFO -
creating an instance of Auxiliary
2005-03-23 23:47:11,665 - spam_application - INFO -
created an instance of auxiliary_module.Auxiliary
2005-03-23 23:47:11,668 - spam_application - INFO -
calling auxiliary_module.Auxiliary.do_something
2005-03-23 23:47:11,668 - spam_application.auxiliary.Auxiliary - INFO -
doing something
2005-03-23 23:47:11,669 - spam_application.auxiliary.Auxiliary - INFO -
done doing something
2005-03-23 23:47:11,670 - spam_application - INFO -
finished auxiliary_module.Auxiliary.do_something
2005-03-23 23:47:11,671 - spam_application - INFO -
calling auxiliary_module.some_function()
2005-03-23 23:47:11,672 - spam_application.auxiliary - INFO -
received a call to 'some_function'
2005-03-23 23:47:11,673 - spam_application - INFO -
done with auxiliary_module.some_function()
Несколько обработчиков и форматировщиков¶Multiple handlers and formatters
Логгеры – это обычные объекты Python. Метод addHandler() не имеет минимального или максимального ограничения на количество добавляемых обработчиков. Иногда приложению бывает полезно записывать все сообщения всех уровней в текстовый файл, одновременно выводя ошибки и выше на консоль. Чтобы настроить это, просто сконфигурируйте соответствующие обработчики. Вызовы логирования в коде приложения останутся без изменений. Вот небольшая модификация предыдущего простого примера конфигурации на основе модуля:
import logging
logger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG)
# создать файловый обработчик, записывающий даже отладочные сообщения
fh = logging.FileHandler('spam.log')
fh.setLevel(logging.DEBUG)
# создать консольный обработчик с более высоким уровнем логирования
ch = logging.StreamHandler()
ch.setLevel(logging.ERROR)
# создать форматтер и добавить его к обработчикам
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
fh.setFormatter(formatter)
# добавить обработчики в логгер
logger.addHandler(ch)
logger.addHandler(fh)
# код 'приложения'
logger.debug('debug message')
logger.info('info message')
logger.warn('warn message')
logger.error('error message')
logger.critical('critical message')
Обратите внимание, что «прикладной» код не заботится о нескольких обработчиках. Единственное, что изменилось – добавление и настройка нового обработчика с именем fh.
Возможность создавать новые обработчики с фильтрами более высокого или низкого уровня серьезности может быть очень полезна при написании и тестировании приложения. Вместо использования множества операторов print для отладки используйте logger.debug: в отличие от операторов print, которые вам придется удалить или закомментировать позже, операторы logger.debug могут оставаться в исходном коде и оставаться неактивными до тех пор, пока они снова не понадобятся. В этом случае единственное, что нужно сделать, – изменить уровень серьезности логгера и/или обработчика на debug.
Журналирование в несколько мест назначения¶Logging to multiple destinations
Допустим, вы хотите вести журнал в консоль и файл с разными форматами сообщений и в разных ситуациях. Предположим, вы хотите записывать сообщения уровня DEBUG и выше в файл, а сообщения уровня INFO и выше – в консоль. Также предположим, что файл должен содержать временные метки, а консольные сообщения – нет. Вот как это можно сделать:
import logging
# настроить логирование в файл – подробнее см. предыдущий раздел
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
datefmt='%m-%d %H:%M',
filename='/temp/myapp.log',
filemode='w')
# Определяет обработчик, который записывает сообщения уровня INFO и выше в sys.stderr.
console = logging.StreamHandler()
console.setLevel(logging.INFO)
# Задает формат, упрощенный для вывода в консоль.
formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
# Указывает обработчику использовать этот формат.
console.setFormatter(formatter)
# Добавляет обработчик в корневой логгер.
logging.getLogger('').addHandler(console)
# Теперь можно выполнять запись в корневой логгер или любой другой. Сначала корневой...
logging.info('Jackdaws love my big sphinx of quartz.')
# Теперь определите несколько других логгеров, которые могут представлять разные части приложения:
# приложение:
logger1 = logging.getLogger('myapp.area1')
logger2 = logging.getLogger('myapp.area2')
logger1.debug('Quick zephyrs blow, vexing daft Jim.')
logger1.info('How quickly daft jumping zebras vex.')
logger2.warning('Jail zesty vixen who grabbed pay from quack.')
logger2.error('The five boxing wizards jump quickly.')
При запуске на консоли вы увидите
root : INFO Jackdaws love my big sphinx of quartz.
myapp.area1 : INFO How quickly daft jumping zebras vex.
myapp.area2 : WARNING Jail zesty vixen who grabbed pay from quack.
myapp.area2 : ERROR The five boxing wizards jump quickly.
а в файле увидите примерно такое
10-22 22:19 root INFO Jackdaws love my big sphinx of quartz.
10-22 22:19 myapp.area1 DEBUG Quick zephyrs blow, vexing daft Jim.
10-22 22:19 myapp.area1 INFO How quickly daft jumping zebras vex.
10-22 22:19 myapp.area2 WARNING Jail zesty vixen who grabbed pay from quack.
10-22 22:19 myapp.area2 ERROR The five boxing wizards jump quickly.
Как видите, сообщение DEBUG отображается только в файле. Остальные сообщения отправляются в оба места назначения.
В этом примере используются консольный и файловый обработчики, но вы можете использовать любое количество и любое сочетание обработчиков по своему выбору.
Пример сервера конфигурации¶Configuration server example
Вот пример модуля, использующего сервер конфигурации журналирования:
import logging
import logging.config
import time
import os
# Прочитать начальный конфигурационный файл.
logging.config.fileConfig('logging.conf')
# Создать и запустить слушатель на порту 9999.
t = logging.config.listen(9999)
t.start()
logger = logging.getLogger('simpleExample')
try:
# Прокрутить вызовы логирования, чтобы увидеть разницу.
# Создавать новые конфигурации до нажатия Ctrl+C.
while True:
logger.debug('debug message')
logger.info('info message')
logger.warn('warn message')
logger.error('error message')
logger.critical('critical message')
time.sleep(5)
except KeyboardInterrupt:
# очистка
logging.config.stopListening()
t.join()
А вот скрипт, который принимает имя файла и отправляет этот файл на сервер, предварив его двоично-закодированной длиной, в качестве новой конфигурации журналирования:
#!/usr/bin/env python
import socket, sys, struct
with open(sys.argv[1], 'rb') as f:
data_to_send = f.read()
HOST = 'localhost'
PORT = 9999
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print('connecting...')
s.connect((HOST, PORT))
print('sending config...')
s.send(struct.pack('>L', len(data_to_send)))
s.send(data_to_send)
s.close()
print('complete')
Работа с блокирующими обработчиками¶Dealing with handlers that block
Иногда требуется, чтобы обработчики логирования выполняли свою работу, не блокируя поток, из которого ведется логирование. Это часто встречается в веб-приложениях, хотя, конечно, бывает и в других сценариях.
Часто виновником медленной работы является SMTPHandler: отправка писем может занимать много времени по ряду причин, не зависящих от разработчика (например, плохая производительность почтовой или сетевой инфраструктуры). Но почти любой сетевой обработчик может блокироваться: даже операция SocketHandler может выполнять DNS-запрос внутри, который слишком медленный (и этот запрос может находиться глубоко в коде библиотеки сокетов, ниже уровня Python, и вне вашего контроля).
Одно из решений – использовать двухчастный подход. На первом этапе прикрепите только QueueHandler к тем логгерам, к которым обращаются из критичных по производительности потоков. Они просто пишут в свою очередь, которая может быть настроена на достаточно большую емкость или инициализирована без верхнего ограничения размера. Запись в очередь обычно принимается быстро, хотя вам, вероятно, потребуется перехватывать исключение queue.Full в качестве меры предосторожности в коде. Если вы разработчик библиотеки, и в вашем коде есть критичные по производительности потоки, обязательно задокументируйте это (вместе с рекомендацией прикреплять только QueueHandlers к вашим логгерам) на благо других разработчиков, которые будут использовать ваш код.
Вторая часть решения – QueueListener, который был разработан как аналог QueueHandler. QueueListener очень прост: ему передаются очередь и несколько обработчиков, и он запускает внутренний поток, который слушает свою очередь на предмет LogRecords, отправленных от QueueHandlers (или любого другого источника LogRecords, если на то пошло). LogRecords извлекаются из очереди и передаются обработчикам для обработки.
Преимущество отдельного класса QueueListener в том, что можно использовать один и тот же экземпляр для обслуживания нескольких QueueHandlers. Это более эффективно по ресурсам, чем, скажем, создание многопоточных версий существующих классов обработчиков, которые потребляют по одному потоку на обработчик без особой пользы.
Ниже приведён пример использования этих двух классов (импорты опущены):
que = queue.Queue(-1) # Без ограничения размера.
queue_handler = QueueHandler(que)
handler = logging.StreamHandler()
listener = QueueListener(que, handler)
root = logging.getLogger()
root.addHandler(queue_handler)
formatter = logging.Formatter('%(threadName)s: %(message)s')
handler.setFormatter(formatter)
listener.start()
# Вывод лога будет отображать поток, создавший событие (главный поток), а не внутренний поток,
# отслеживающий внутреннюю очередь.
# Именно это и должно происходить.
# то, что должно произойти.
root.warning('Look out!')
listener.stop()
который при запуске выведет:
MainThread: Look out!
Отправка и получение событий логирования по сети¶Sending and receiving logging events across a network
Допустим, вы хотите отправлять события логирования по сети и обрабатывать их на принимающей стороне. Простой способ сделать это – прикрепить экземпляр SocketHandler к корневому логгеру на отправляющей стороне:
import logging, logging.handlers
rootLogger = logging.getLogger('')
rootLogger.setLevel(logging.DEBUG)
socketHandler = logging.handlers.SocketHandler('localhost',
logging.handlers.DEFAULT_TCP_LOGGING_PORT)
# Не нужно использовать форматтер, так как сокет-обработчик отправляет событие в виде неформатированного пикла.
# неформатированный pickle
rootLogger.addHandler(socketHandler)
# Теперь можно выполнять запись в корневой логгер или любой другой. Сначала корневой...
logging.info('Jackdaws love my big sphinx of quartz.')
# Теперь определите несколько других логгеров, которые могут представлять разные части приложения:
# приложение:
logger1 = logging.getLogger('myapp.area1')
logger2 = logging.getLogger('myapp.area2')
logger1.debug('Quick zephyrs blow, vexing daft Jim.')
logger1.info('How quickly daft jumping zebras vex.')
logger2.warning('Jail zesty vixen who grabbed pay from quack.')
logger2.error('The five boxing wizards jump quickly.')
На принимающей стороне можно настроить приемник с помощью модуля socketserver. Вот базовый рабочий пример:
import pickle
import logging
import logging.handlers
import socketserver
import struct
class LogRecordStreamHandler(socketserver.StreamRequestHandler):
"""Обработчик для потокового запроса логирования.
Это по сути записывает запись, используя ту политику логирования, которая настроена локально.
настроено локально.
"""
def handle(self):
"""
Обрабатывает несколько запросов – каждый ожидается в формате: 4-байтовая длина, затем запись журнала в формате пикл.
Записывает запись в соответствии с политикой, настроенной локально.
согласно локально настроенной политике.
"""
while True:
chunk = self.connection.recv(4)
if len(chunk) < 4:
break
slen = struct.unpack('>L', chunk)[0]
chunk = self.connection.recv(slen)
while len(chunk) < slen:
chunk = chunk + self.connection.recv(slen - len(chunk))
obj = self.unPickle(chunk)
record = logging.makeLogRecord(obj)
self.handleLogRecord(record)
def unPickle(self, data):
return pickle.loads(data)
def handleLogRecord(self, record):
# Если указано имя, используется именованный логгер, а не тот, что
# подразумевается записью.
if self.server.logname is not None:
name = self.server.logname
else:
name = record.name
logger = logging.getLogger(name)
# Примечание: КАЖДАЯ запись попадает в журнал. Это связано с тем, что Logger.handle
# обычно вызывается ПОСЛЕ фильтрации на уровне регистратора. Если требуется
# выполнять фильтрацию, делайте это на стороне клиента, чтобы не тратить
# циклы процессора и сетевую пропускную способность!
logger.handle(record)
class LogRecordSocketReceiver(socketserver.ThreadingTCPServer):
"""
Простой приёмник журнала на основе TCP-сокетов, подходящий для тестирования.
"""
allow_reuse_address = True
def __init__(self, host='localhost',
port=logging.handlers.DEFAULT_TCP_LOGGING_PORT,
handler=LogRecordStreamHandler):
socketserver.ThreadingTCPServer.__init__(self, (host, port), handler)
self.abort = 0
self.timeout = 1
self.logname = None
def serve_until_stopped(self):
import select
abort = 0
while not abort:
rd, wr, ex = select.select([self.socket.fileno()],
[], [],
self.timeout)
if rd:
self.handle_request()
abort = self.abort
def main():
logging.basicConfig(
format='%(relativeCreated)5d %(name)-15s %(levelname)-8s %(message)s')
tcpserver = LogRecordSocketReceiver()
print('About to start TCP server...')
tcpserver.serve_until_stopped()
if __name__ == '__main__':
main()
Сначала запустите сервер, затем клиент. На стороне клиента в консоль ничего не выводится; на стороне сервера вы должны увидеть что-то вроде:
About to start TCP server...
59 root INFO Jackdaws love my big sphinx of quartz.
59 myapp.area1 DEBUG Quick zephyrs blow, vexing daft Jim.
69 myapp.area1 INFO How quickly daft jumping zebras vex.
69 myapp.area2 WARNING Jail zesty vixen who grabbed pay from quack.
69 myapp.area2 ERROR The five boxing wizards jump quickly.
Обратите внимание, что в некоторых сценариях существуют проблемы безопасности с pickle. Если они вас затрагивают, вы можете использовать альтернативную схему сериализации, переопределив метод makePickle() и реализовав свою альтернативу там, а также адаптировав приведенный выше скрипт для использования вашей альтернативной сериализации.
Добавление контекстной информации в вывод журнала¶Adding contextual information to your logging output
Иногда требуется, чтобы вывод логирования содержал контекстную информацию в дополнение к параметрам, переданным в вызов логирования. Например, в сетевом приложении может быть желательно регистрировать специфичную для клиента информацию в журнале (например, имя пользователя удаленного клиента или IP-адрес). Хотя вы можете использовать параметр extra для этого, не всегда удобно передавать информацию таким образом. Хотя может возникнуть соблазн создавать экземпляры Logger для каждого соединения, это не очень хорошая идея, потому что эти экземпляры не собираются сборщиком мусора. Хотя на практике это не проблема, когда количество экземпляров Logger зависит от уровня детализации, которую вы хотите использовать при логировании приложения, это может быть трудно управляемым, если количество экземпляров Logger становится фактически неограниченным.
Использование LoggerAdapter для добавления контекстной информации¶Using LoggerAdapters to impart contextual information
Простой способ передавать контекстную информацию для вывода вместе с информацией о событии логирования – использовать класс LoggerAdapter. Этот класс разработан так, чтобы выглядеть как Logger, так что вы можете вызывать debug(), info(), warning(), error(), exception(), critical() и log(). Эти методы имеют те же сигнатуры, что и их аналоги в Logger, поэтому вы можете использовать экземпляры обоих типов взаимозаменяемо.
Когда вы создаете экземпляр LoggerAdapter, вы передаете ему экземпляр Logger и объект, подобный словарю, который содержит вашу контекстную информацию. Когда вы вызываете один из методов логирования на экземпляре LoggerAdapter, он делегирует вызов базовому экземпляру Logger, переданному его конструктору, и обеспечивает передачу контекстной информации в делегированном вызове. Вот фрагмент из кода LoggerAdapter:
def debug(self, msg, *args, **kwargs):
"""
Передаёт вызов отладки нижележащему регистратору после добавления
контекстной информации из данного экземпляра адаптера.
"""
msg, kwargs = self.process(msg, kwargs)
self.logger.debug(msg, *args, **kwargs)
Метод process() класса LoggerAdapter – это то место, где контекстная информация добавляется к выводу логирования. Ему передаются сообщение и именованные аргументы вызова логирования, и он возвращает (потенциально) измененные версии этих данных для использования в вызове базового логгера. Реализация по умолчанию этого метода оставляет сообщение без изменений, но вставляет ключ 'extra' в именованные аргументы, значением которого является объект, подобный словарю, переданный конструктору. Конечно, если вы передали аргумент 'extra' в вызове адаптера, он будет молча перезаписан.
Преимущество использования 'extra' в том, что значения из dict-подобного объекта сливаются в __dict__ экземпляра LogRecord, что позволяет использовать настраиваемые строки с экземплярами Formatter, которые знают о ключах dict-подобного объекта. Если вам нужен другой метод, например, если вы хотите добавить контекстную информацию в начало или конец строки сообщения, вам просто нужно создать подкласс LoggerAdapter и переопределить process() для выполнения нужных действий. Вот простой пример:
class CustomAdapter(logging.LoggerAdapter):
"""
Данный пример адаптера ожидает, что переданный объект, подобный словарю, содержит
ключ 'connid', значение которого в квадратных скобках добавляется к началу сообщения журнала.
"""
def process(self, msg, kwargs):
return '[%s] %s' % (self.extra['connid'], msg), kwargs
который можно использовать так:
logger = logging.getLogger(__name__)
adapter = CustomAdapter(logger, {'connid': some_conn_id})
Тогда любые события, которые вы регистрируете через адаптер, будут иметь значение some_conn_id, добавленное в начало сообщений журнала.
Использование объектов, отличных от словарей, для передачи контекстной информации¶Using objects other than dicts to pass contextual information
Вам не нужно передавать фактический словарь в LoggerAdapter – вы можете передать экземпляр класса, который реализует __getitem__ и __iter__, так чтобы он выглядел как словарь для логирования. Это может быть полезно, если вы хотите генерировать значения динамически (тогда как значения в словаре были бы постоянными).
Использование фильтров для добавления контекстной информации¶Using Filters to impart contextual information
Вы также можете добавить контекстную информацию в вывод журнала с помощью пользовательского Filter. Экземпляры Filter могут изменять переданные им LogRecords, включая добавление дополнительных атрибутов, которые затем можно выводить с помощью подходящей строки формата или, при необходимости, пользовательского Formatter.
Например, в веб-приложении обрабатываемый запрос (или, по крайней мере, его интересные части) может храниться в переменной threadlocal (threading.local), а затем извлекаться из Filter для добавления, скажем, информации из запроса – например, удаленного IP-адреса и имени пользователя – в LogRecord, используя имена атрибутов 'ip' и 'user', как в примере с LoggerAdapter выше. В этом случае можно использовать ту же строку формата для получения аналогичного вывода, показанного выше. Вот пример скрипта:
import logging
from random import choice
class ContextFilter(logging.Filter):
"""
Это фильтр, который внедряет контекстную информацию в журнал.
Вместо использования реальной контекстной информации в данном примере используются случайные
данные.
"""
USERS = ['jim', 'fred', 'sheila']
IPS = ['123.231.231.123', '127.0.0.1', '192.168.0.1']
def filter(self, record):
record.ip = choice(ContextFilter.IPS)
record.user = choice(ContextFilter.USERS)
return True
if __name__ == '__main__':
levels = (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL)
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)-15s %(name)-5s %(levelname)-8s IP: %(ip)-15s User: %(user)-8s %(message)s')
a1 = logging.getLogger('a.b.c')
a2 = logging.getLogger('d.e.f')
f = ContextFilter()
a1.addFilter(f)
a2.addFilter(f)
a1.debug('A debug message')
a1.info('An info message with %s', 'some parameters')
for x in range(10):
lvl = choice(levels)
lvlname = logging.getLevelName(lvl)
a2.log(lvl, 'A message at %s level with %d %s', lvlname, 2, 'parameters')
который при запуске выдаёт примерно следующее:
2010-09-06 22:38:15,292 a.b.c DEBUG IP: 123.231.231.123 User: fred A debug message
2010-09-06 22:38:15,300 a.b.c INFO IP: 192.168.0.1 User: sheila An info message with some parameters
2010-09-06 22:38:15,300 d.e.f CRITICAL IP: 127.0.0.1 User: sheila A message at CRITICAL level with 2 parameters
2010-09-06 22:38:15,300 d.e.f ERROR IP: 127.0.0.1 User: jim A message at ERROR level with 2 parameters
2010-09-06 22:38:15,300 d.e.f DEBUG IP: 127.0.0.1 User: sheila A message at DEBUG level with 2 parameters
2010-09-06 22:38:15,300 d.e.f ERROR IP: 123.231.231.123 User: fred A message at ERROR level with 2 parameters
2010-09-06 22:38:15,300 d.e.f CRITICAL IP: 192.168.0.1 User: jim A message at CRITICAL level with 2 parameters
2010-09-06 22:38:15,300 d.e.f CRITICAL IP: 127.0.0.1 User: sheila A message at CRITICAL level with 2 parameters
2010-09-06 22:38:15,300 d.e.f DEBUG IP: 192.168.0.1 User: jim A message at DEBUG level with 2 parameters
2010-09-06 22:38:15,301 d.e.f ERROR IP: 127.0.0.1 User: sheila A message at ERROR level with 2 parameters
2010-09-06 22:38:15,301 d.e.f DEBUG IP: 123.231.231.123 User: fred A message at DEBUG level with 2 parameters
2010-09-06 22:38:15,301 d.e.f INFO IP: 123.231.231.123 User: fred A message at INFO level with 2 parameters
Ведение журнала в один файл из нескольких процессов¶Logging to a single file from multiple processes
Хотя логирование потокобезопасно и запись в один файл из нескольких потоков в одном процессе поддерживается, запись в один файл из нескольких процессов не поддерживается, потому что в Python нет стандартного способа сериализовать доступ к одному файлу из нескольких процессов. Если вам нужно вести журнал в один файл из нескольких процессов, один из способов – заставить все процессы записывать в SocketHandler, а отдельный процесс будет реализовывать сокет-сервер, который читает из сокета и пишет в файл. (Если хотите, можно выделить один поток в одном из существующих процессов для выполнения этой функции.) В этом разделе этот подход документирован более подробно и включает рабочий приемник сокетов, который можно использовать как отправную точку для адаптации в ваших собственных приложениях.
Если вы используете последнюю версию Python, которая включает модуль multiprocessing, вы можете написать свой собственный обработчик, который использует класс Lock из этого модуля для сериализации доступа к файлу из ваших процессов. Существующие FileHandler и его подклассы в настоящее время не используют multiprocessing, хотя в будущем они могут это сделать. Обратите внимание, что в настоящее время модуль multiprocessing не предоставляет работающей функциональности блокировок на всех платформах (см. https://bugs.python.org/issue3770).
В качестве альтернативы вы можете использовать Queue и QueueHandler для отправки всех событий логирования одному из процессов вашего многопроцессного приложения. Следующий пример скрипта демонстрирует, как это можно сделать; в примере отдельный процесс-слушатель прослушивает события, отправленные другими процессами, и регистрирует их в соответствии со своей собственной конфигурацией логирования. Хотя пример демонстрирует только один способ (например, вы можете захотеть использовать поток-слушатель вместо отдельного процесса-слушателя – реализация была бы аналогичной), он допускает полностью различные конфигурации логирования для слушателя и других процессов вашего приложения и может использоваться как основа для кода, удовлетворяющего вашим конкретным требованиям:
# Вам понадобятся эти импорты в вашем коде.
import logging
import logging.handlers
import multiprocessing
# Следующие две строки импорта только для этой демонстрации.
from random import choice, random
import time
#
# Поскольку нужно задать настройки логирования для слушателя и рабочих процессов, функции
# слушателя и рабочего процесса принимают параметр configurer – вызываемый объект для
# настройки логирования этого процесса. Эти функции также получают очередь, которую
# используют для взаимодействия.
#
# На практике слушателя можно настроить как угодно, но заметьте: в этом простом
# примере слушатель не применяет логику уровней или фильтров к полученным записям.
# На практике эту логику, вероятно, стоит вынести в рабочие процессы, чтобы не
# пересылать между процессами события, которые всё равно будут отфильтрованы.
#
# Размер файлов при ротации намеренно сделан небольшим, чтобы результат был хорошо виден.
def listener_configurer():
root = logging.getLogger()
h = logging.handlers.RotatingFileHandler('mptest.log', 'a', 300, 10)
f = logging.Formatter('%(asctime)s %(processName)-10s %(name)s %(levelname)-8s %(message)s')
h.setFormatter(f)
root.addHandler(h)
# Это главный цикл процесса-слушателя: ждём события логирования
# (LogRecords) в очереди и обрабатываем их; завершаем работу, когда получаем None вместо
# LogRecord.
def listener_process(queue, configurer):
configurer()
while True:
try:
record = queue.get()
if record is None: # Отправляем это как сигнал завершения, чтобы слушатель остановился.
break
logger = logging.getLogger(record.name)
logger.handle(record) # Никакой логики уровней и фильтров – просто делаем дело!
except Exception:
import sys, traceback
print('Whoops! Problem:', file=sys.stderr)
traceback.print_exc(file=sys.stderr)
# Массивы для случайного выбора в этой демонстрации.
LEVELS = [logging.DEBUG, logging.INFO, logging.WARNING,
logging.ERROR, logging.CRITICAL]
LOGGERS = ['a.b.c', 'd.e.f']
MESSAGES = [
'Random message #1',
'Random message #2',
'Random message #3',
]
# Настройка рабочего процесса выполняется в начале его работы.
# Обратите внимание: в Windows нельзя полагаться на семантику fork, поэтому каждый процесс
# будет выполнять код настройки логирования при запуске.
def worker_configurer(queue):
h = logging.handlers.QueueHandler(queue) # Нужен только один обработчик.
root = logging.getLogger()
root.addHandler(h)
root.setLevel(logging.DEBUG) # Отправляем все сообщения для демонстрации; никакой другой логики уровней или фильтров.
# Это главный цикл рабочего процесса, который просто логирует десять событий с
# случайные задержки перед завершением.
# Сообщения print нужны лишь для того, чтобы было видно, что программа работает.
def worker_process(queue, configurer):
configurer(queue)
name = multiprocessing.current_process().name
print('Worker started: %s' % name)
for i in range(10):
time.sleep(random())
logger = logging.getLogger(choice(LOGGERS))
level = choice(LEVELS)
message = choice(MESSAGES)
logger.log(level, message)
print('Worker finished: %s' % name)
# Здесь организуется демонстрация. Создать очередь, создать и запустить
# слушатель, создать десять рабочих процессов и запустить их, дождаться их завершения,
# затем отправить None в очередь, чтобы сообщить слушателю о завершении.
def main():
queue = multiprocessing.Queue(-1)
listener = multiprocessing.Process(target=listener_process,
args=(queue, listener_configurer))
listener.start()
workers = []
for i in range(10):
worker = multiprocessing.Process(target=worker_process,
args=(queue, worker_configurer))
workers.append(worker)
worker.start()
for w in workers:
w.join()
queue.put_nowait(None)
listener.join()
if __name__ == '__main__':
main()
Вариант приведённого выше сценария оставляет ведение журнала в главном процессе, в отдельном потоке:
import logging
import logging.config
import logging.handlers
from multiprocessing import Process, Queue
import random
import threading
import time
def logger_thread(q):
while True:
record = q.get()
if record is None:
break
logger = logging.getLogger(record.name)
logger.handle(record)
def worker_process(q):
qh = logging.handlers.QueueHandler(q)
root = logging.getLogger()
root.setLevel(logging.DEBUG)
root.addHandler(qh)
levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR,
logging.CRITICAL]
loggers = ['foo', 'foo.bar', 'foo.bar.baz',
'spam', 'spam.ham', 'spam.ham.eggs']
for i in range(100):
lvl = random.choice(levels)
logger = logging.getLogger(random.choice(loggers))
logger.log(lvl, 'Message no. %d', i)
if __name__ == '__main__':
q = Queue()
d = {
'version': 1,
'formatters': {
'detailed': {
'class': 'logging.Formatter',
'format': '%(asctime)s %(name)-15s %(levelname)-8s %(processName)-10s %(message)s'
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
},
'file': {
'class': 'logging.FileHandler',
'filename': 'mplog.log',
'mode': 'w',
'formatter': 'detailed',
},
'foofile': {
'class': 'logging.FileHandler',
'filename': 'mplog-foo.log',
'mode': 'w',
'formatter': 'detailed',
},
'errors': {
'class': 'logging.FileHandler',
'filename': 'mplog-errors.log',
'mode': 'w',
'level': 'ERROR',
'formatter': 'detailed',
},
},
'loggers': {
'foo': {
'handlers': ['foofile']
}
},
'root': {
'level': 'DEBUG',
'handlers': ['console', 'file', 'errors']
},
}
workers = []
for i in range(5):
wp = Process(target=worker_process, name='worker %d' % (i + 1), args=(q,))
workers.append(wp)
wp.start()
logging.config.dictConfig(d)
lp = threading.Thread(target=logger_thread, args=(q,))
lp.start()
# На этом этапе главный процесс может заняться своей полезной работой
# Когда он с этим закончит, он может дождаться завершения рабочих процессов...
for wp in workers:
wp.join()
# А теперь указать потоку журналирования завершиться
q.put(None)
lp.join()
Этот вариант показывает, как можно, например, применить конфигурацию для конкретных логгеров – например, логгер foo имеет специальный обработчик, который сохраняет все события подсистемы foo в файл mplog-foo.log. Это будет использоваться механизмом логирования в главном процессе (даже если события логирования генерируются в рабочих процессах) для направления сообщений в соответствующие места назначения.
Использование ротации файлов¶Using file rotation
Иногда требуется, чтобы файл журнала увеличивался до определенного размера, затем открывался новый файл и запись велась в него. Возможно, вы захотите сохранять определенное количество таких файлов, и когда будет создано это количество файлов, выполнять ротацию файлов, чтобы количество файлов и их размер оставались ограниченными. Для этого сценария использования в пакете logging предусмотрен RotatingFileHandler:
import glob
import logging
import logging.handlers
LOG_FILENAME = 'logging_rotatingfile_example.out'
# Настроить конкретный регистратор с нужным уровнем вывода
my_logger = logging.getLogger('MyLogger')
my_logger.setLevel(logging.DEBUG)
# Добавить обработчик сообщений журнала в регистратор
handler = logging.handlers.RotatingFileHandler(
LOG_FILENAME, maxBytes=20, backupCount=5)
my_logger.addHandler(handler)
# Записать несколько сообщений
for i in range(20):
my_logger.debug('i = %d' % i)
# Посмотреть, какие файлы созданы
logfiles = glob.glob('%s*' % LOG_FILENAME)
for filename in logfiles:
print(filename)
В результате должно получиться 6 отдельных файлов, каждый из которых содержит часть истории журнала приложения:
logging_rotatingfile_example.out
logging_rotatingfile_example.out.1
logging_rotatingfile_example.out.2
logging_rotatingfile_example.out.3
logging_rotatingfile_example.out.4
logging_rotatingfile_example.out.5
Самый текущий файл всегда называется logging_rotatingfile_example.out, и каждый раз, когда он достигает предельного размера, он переименовывается с суффиксом .1. Каждый из существующих резервных файлов переименовывается для увеличения суффикса (.1 становится .2 и т.д.), а файл .6 удаляется.
Разумеется, в этом примере размер файла журнала установлен слишком маленьким для наглядности. В реальности нужно установить maxBytes в подходящее значение.
Использование альтернативных стилей форматирования¶Use of alternative formatting styles
Когда логирование было добавлено в стандартную библиотеку Python, единственным способом форматирования сообщений с переменным содержимым был метод %-форматирования. С тех пор в Python появились два новых подхода к форматированию: string.Template (добавлено в Python 2.4) и str.format() (добавлено в Python 2.6).
Логирование (начиная с версии 3.2) обеспечивает улучшенную поддержку этих двух дополнительных стилей форматирования. Класс Formatter был расширен, чтобы принимать дополнительный необязательный именованный параметр с именем style. По умолчанию он равен '%', но другими возможными значениями являются '{' и '$', которые соответствуют двум другим стилям форматирования. Обратная совместимость поддерживается по умолчанию (как и ожидается), но при явном указании параметра style вы получаете возможность задавать строки формата, которые работают с str.format() или string.Template. Вот пример сеанса консоли, показывающий возможности:
>>> import logging
>>> root = logging.getLogger()
>>> root.setLevel(logging.DEBUG)
>>> handler = logging.StreamHandler()
>>> bf = logging.Formatter('{asctime} {name} {levelname:8s} {message}',
... style='{')
>>> handler.setFormatter(bf)
>>> root.addHandler(handler)
>>> logger = logging.getLogger('foo.bar')
>>> logger.debug('This is a DEBUG message')
2010-10-28 15:11:55,341 foo.bar DEBUG This is a DEBUG message
>>> logger.critical('This is a CRITICAL message')
2010-10-28 15:12:11,526 foo.bar CRITICAL This is a CRITICAL message
>>> df = logging.Formatter('$asctime $name ${levelname} $message',
... style='$')
>>> handler.setFormatter(df)
>>> logger.debug('This is a DEBUG message')
2010-10-28 15:13:06,924 foo.bar DEBUG This is a DEBUG message
>>> logger.critical('This is a CRITICAL message')
2010-10-28 15:13:11,494 foo.bar CRITICAL This is a CRITICAL message
>>>
Обратите внимание: форматирование сообщений журнала для конечного вывода в файлы совершенно не зависит от того, как конструируется отдельное сообщение. Оно по-прежнему может использовать %-форматирование, как показано ниже:
>>> logger.error('This is an%s %s %s', 'other,', 'ERROR,', 'message')
2010-10-28 15:19:29,833 foo.bar ERROR This is another, ERROR, message
>>>
Вызовы логирования (logger.debug(), logger.info() и т.д.) принимают только позиционные параметры для самого сообщения логирования, а именованные параметры используются только для определения параметров обработки самого вызова логирования (например, именованный параметр exc_info для указания, что следует регистрировать трассировку стека, или именованный параметр extra для указания дополнительной контекстной информации, добавляемой в журнал). Поэтому вы не можете напрямую делать вызовы логирования, используя синтаксис str.format() или string.Template, потому что внутри пакет logging использует %-форматирование для объединения строки формата и переменных аргументов. Это невозможно изменить, сохраняя обратную совместимость, поскольку все существующие вызовы логирования в существующем коде будут использовать %-строки формата.
Однако есть способ использовать форматирование {}- и $- для построения отдельных сообщений журнала. Напомним, что в качестве строки формата сообщения можно использовать произвольный объект, и пакет logging вызовет str() для этого объекта, чтобы получить фактическую строку формата. Рассмотрим следующие два класса:
class BraceMessage:
def __init__(self, fmt, *args, **kwargs):
self.fmt = fmt
self.args = args
self.kwargs = kwargs
def __str__(self):
return self.fmt.format(*self.args, **self.kwargs)
class DollarMessage:
def __init__(self, fmt, **kwargs):
self.fmt = fmt
self.kwargs = kwargs
def __str__(self):
from string import Template
return Template(self.fmt).substitute(**self.kwargs)
Любой из них можно использовать вместо строки формата, чтобы разрешить {}- или $-форматирование для построения фактической части «message», которая появляется в форматированном выводе журнала вместо «%(message)s» или «{message}» или «$message». Использовать имена классов всякий раз, когда нужно что-то записать, немного неудобно, но вполне приемлемо, если использовать псевдоним, например __ (двойное подчеркивание – не путать с _, одиночным подчеркиванием, используемым как синоним/псевдоним для gettext.gettext() или его собратьев).
Вышеуказанные классы не входят в состав Python, но их достаточно легко скопировать и вставить в свой код. Использовать их можно следующим образом (при условии, что они объявлены в модуле с именем wherever):
>>> from wherever import BraceMessage as __
>>> print(__('Message with {0} {name}', 2, name='placeholders'))
Message with 2 placeholders
>>> class Point: pass
...
>>> p = Point()
>>> p.x = 0.5
>>> p.y = 0.5
>>> print(__('Message with coordinates: ({point.x:.2f}, {point.y:.2f})',
... point=p))
Message with coordinates: (0.50, 0.50)
>>> from wherever import DollarMessage as __
>>> print(__('Message with $num $what', num=2, what='placeholders'))
Message with 2 placeholders
>>>
Хотя в приведённых выше примерах используется print() для демонстрации работы форматирования, в реальном логировании, конечно, применяется logger.debug() или аналогичный метод.
Следует отметить, что этот подход не даёт значительного снижения производительности с этим подходом: фактическое форматирование происходит не тогда, когда вы делаете вызов регистрации, а когда (и если) регистрируемое сообщение действительно должно быть выведено в журнал с помощью обработчика. Поэтому единственное, что может сбить с толку, – это то, что скобки ставятся вокруг строки формата и аргументов, а не только вокруг строки формата. Это потому, что нотация __ – это просто синтаксический сахар для вызова конструктора одного из классов XXXMessage.
При желании можно использовать LoggerAdapter для достижения похожего эффекта, как в следующем примере:
import logging
class Message(object):
def __init__(self, fmt, args):
self.fmt = fmt
self.args = args
def __str__(self):
return self.fmt.format(*self.args)
class StyleAdapter(logging.LoggerAdapter):
def __init__(self, logger, extra=None):
super(StyleAdapter, self).__init__(logger, extra or {})
def log(self, level, msg, *args, **kwargs):
if self.isEnabledFor(level):
msg, kwargs = self.process(msg, kwargs)
self.logger._log(level, Message(msg, args), (), **kwargs)
logger = StyleAdapter(logging.getLogger(__name__))
def main():
logger.debug('Hello, {}', 'world!')
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
main()
При запуске на Python 3.2 или новее указанный скрипт должен записать в лог сообщение Hello, world!.
Настройка LogRecord¶Customizing LogRecord
Каждое событие логирования представлено экземпляром LogRecord. Когда событие логируется и не отфильтровывается по уровню регистратора, создаётся LogRecord, заполняется информацией о событии и передаётся обработчикам этого регистратора (и его предкам, вплоть до регистратора, у которого дальнейшее распространение вверх по иерархии отключено). До Python 3.2 создание выполнялось только в двух местах:
- Logger.makeRecord() – вызывается в обычном процессе записи события. Он напрямую обращался к LogRecord для создания экземпляра.
- makeLogRecord(), which is called with a dictionary containing attributes to be added to the LogRecord. This is typically invoked when a suitable dictionary has been received over the network (e.g. in pickle form via a SocketHandler, or in JSON form via an HTTPHandler).
Обычно это означало, что если требуется что-то особенное с LogRecord, приходилось делать одно из следующего.
- Создать собственный подкласс Logger, переопределяющий Logger.makeRecord(), и установить его с помощью setLoggerClass() до того, как будут созданы какие-либо нужные регистраторы.
- Добавить Filter в регистратор или обработчик, который выполняет необходимые специальные манипуляции при вызове его метода filter().
Первый подход был бы несколько громоздким, если, скажем, несколько разных библиотек хотели делать разные вещи. Каждая пыталась бы установить собственный подкласс Logger, и победила бы та, которая сделала это последней.
Второй подход во многих случаях работает достаточно хорошо, но не позволяет, например, использовать специализированный подкласс LogRecord. Разработчики библиотек могут установить подходящий фильтр для своих регистраторов, но им придётся помнить об этом каждый раз, когда они создают новый регистратор (что они делают, просто добавляя новые пакеты или модули и выполняя
logger = logging.getLogger(__name__)
на уровне модуля). Вероятно, это лишняя забота. Разработчики могли бы также добавить фильтр в NullHandler, прикреплённый к их корневому регистратору, но он не будет вызываться, если разработчик приложения присоединит обработчик к нижележащему библиотечному регистратору – тогда вывод этого обработчика не отразит намерений разработчика библиотеки.
В Python 3.2 и новее создание LogRecord осуществляется через фабрику, которую можно указать. Фабрика – это просто вызываемый объект, который можно установить с помощью setLogRecordFactory() и запросить через getLogRecordFactory(). Фабрика вызывается с той же сигнатурой, что и конструктор LogRecord, поскольку LogRecord является настройкой по умолчанию для фабрики.
Этот подход позволяет пользовательской фабрике контролировать все аспекты создания LogRecord. Например, можно вернуть подкласс или просто добавить несколько дополнительных атрибутов к записи после ее создания, используя шаблон, подобный следующему:
old_factory = logging.getLogRecordFactory()
def record_factory(*args, **kwargs):
record = old_factory(*args, **kwargs)
record.custom_attribute = 0xdecafbad
return record
logging.setLogRecordFactory(record_factory)
Такой подход позволяет разным библиотекам объединять фабрики в цепочку, и пока они не перезаписывают атрибуты друг друга или случайно не переопределяют стандартные атрибуты, неожиданностей быть не должно. Однако следует помнить, что каждое звено в цепочке добавляет дополнительные накладные расходы во время выполнения всех операций логирования, и этот метод следует использовать только тогда, когда применение Filter не даёт желаемого результата.
Создание подкласса QueueHandler – пример с ZeroMQ¶Subclassing QueueHandler - a ZeroMQ example
Можно использовать подкласс QueueHandler для отправки сообщений в очереди других типов, например, в сокет «публикации» ZeroMQ. В примере ниже сокет создаётся отдельно и передаётся обработчику (в качестве его «очереди»):
import zmq # с использованием pyzmq, привязки Python к ZeroMQ
import json # для переносимой сериализации записей
ctx = zmq.Context()
sock = zmq.Socket(ctx, zmq.PUB) # или zmq.PUSH, или другое подходящее значение
sock.bind('tcp://*:5556') # или где угодно
class ZeroMQSocketHandler(QueueHandler):
def enqueue(self, record):
data = json.dumps(record.__dict__)
self.queue.send(data)
handler = ZeroMQSocketHandler(sock)
Конечно, есть и другие способы организации этого, например, передача данных, необходимых обработчику для создания сокета:
class ZeroMQSocketHandler(QueueHandler):
def __init__(self, uri, socktype=zmq.PUB, ctx=None):
self.ctx = ctx or zmq.Context()
socket = zmq.Socket(self.ctx, socktype)
socket.bind(uri)
QueueHandler.__init__(self, socket)
def enqueue(self, record):
data = json.dumps(record.__dict__)
self.queue.send(data)
def close(self):
self.queue.close()
Создание подкласса QueueListener – пример с ZeroMQ¶Subclassing QueueListener - a ZeroMQ example
Также можно создать подкласс QueueListener для получения сообщений из очередей других типов, например, из сокета «подписки» ZeroMQ. Вот пример:
class ZeroMQSocketListener(QueueListener):
def __init__(self, uri, *handlers, **kwargs):
self.ctx = kwargs.get('ctx') or zmq.Context()
socket = zmq.Socket(self.ctx, zmq.SUB)
socket.setsockopt(zmq.SUBSCRIBE, '') # подписаться на всё
socket.connect(uri)
def dequeue(self):
msg = self.queue.recv()
return logging.makeLogRecord(json.loads(msg))
См. также
- Модуль logging
- Справочник API для модуля logging.
- Модуль logging.config
- API конфигурации для модуля logging.
- Модуль logging.handlers
- Полезные обработчики, входящие в состав модуля logging.
Пример конфигурации на основе словаря¶An example dictionary-based configuration
Ниже приведён пример словаря конфигурации логирования – он взят из документации проекта Django. Этот словарь передаётся в dictConfig() для применения конфигурации:
LOGGING = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
},
'simple': {
'format': '%(levelname)s %(message)s'
},
},
'filters': {
'special': {
'()': 'project.logging.SpecialFilter',
'foo': 'bar',
}
},
'handlers': {
'null': {
'level':'DEBUG',
'class':'django.utils.log.NullHandler',
},
'console':{
'level':'DEBUG',
'class':'logging.StreamHandler',
'formatter': 'simple'
},
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
'filters': ['special']
}
},
'loggers': {
'django': {
'handlers':['null'],
'propagate': True,
'level':'INFO',
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
},
'myproject.custom': {
'handlers': ['console', 'mail_admins'],
'level': 'INFO',
'filters': ['special']
}
}
}
Для получения дополнительной информации об этой конфигурации можно обратиться к соответствующему разделу документации Django.
Использование ротатора и средства именования для настройки обработки ротации журналов¶Using a rotator and namer to customize log rotation processing
Пример того, как можно определить namer и rotator, приведен в следующем фрагменте кода, который демонстрирует сжатие журнала с использованием zlib:
def namer(name):
return name + ".gz"
def rotator(source, dest):
with open(source, "rb") as sf:
data = sf.read()
compressed = zlib.compress(data, 9)
with open(dest, "wb") as df:
df.write(compressed)
os.remove(source)
rh = logging.handlers.RotatingFileHandler(...)
rh.rotator = rotator
rh.namer = namer
Это не «настоящие» .gz-файлы, поскольку они представляют собой просто сжатые данные без «контейнера», который присутствует в обычном gzip-файле. Данный фрагмент приведен только для иллюстрации.
Более сложный пример с многопроцессностью¶A more elaborate multiprocessing example
Следующий рабочий пример показывает, как можно использовать логирование с многопроцессностью с помощью файлов конфигурации. Конфигурации довольно просты, но служат иллюстрацией того, как можно реализовать более сложные в реальном сценарии многопроцессной обработки.
В примере главный процесс порождает процесс-слушатель и несколько рабочих процессов. Каждый из главного процесса, слушателя и рабочих имеет три отдельные конфигурации (все рабочие используют одну и ту же конфигурацию). Видно ведение журнала в главном процессе, как рабочие пишут в QueueHandler, а слушатель реализует QueueListener и более сложную конфигурацию ведения журнала, и организует отправку событий, полученных через очередь, обработчикам, указанным в конфигурации. Обратите внимание, что эти конфигурации чисто иллюстративные, но этот пример можно адаптировать под свой сценарий.
Вот скрипт – строки документации и комментарии, надеюсь, поясняют, как он работает:
import logging
import logging.config
import logging.handlers
from multiprocessing import Process, Queue, Event, current_process
import os
import random
import time
class MyHandler:
"""
Простой обработчик для событий логирования. Он выполняется в процессе-слушателе и
распределяет события по логгерам на основе имени в полученной записи,
которые затем передаются системой логирования обработчикам,
настроенным для этих логгеров.
"""
def handle(self, record):
logger = logging.getLogger(record.name)
# Имя процесса преобразуется только для того, чтобы показать, что это слушатель
# выполняет логирование в файлы и консоль.
record.processName = '%s (for %s)' % (current_process().name, record.processName)
logger.handle(record)
def listener_process(q, stop_event, config):
"""
Это можно было бы сделать в главном процессе, но сделано в отдельном
процессе для наглядности.
Это инициализирует логирование согласно указанной конфигурации
запускает слушатель и ожидает сигнала от главного процесса о завершении
через событие. Затем слушатель останавливается, и процесс завершается.
"""
logging.config.dictConfig(config)
listener = logging.handlers.QueueListener(q, MyHandler())
listener.start()
if os.name == 'posix':
# В POSIX логгер setup уже был настроен в
# родительском процессе, но должен был быть отключён после вызова
# dictConfig.
# В Windows, поскольку fork не используется, логгер setup не будет
# существовать в дочернем процессе, поэтому он будет создан, и сообщение
# появится – отсюда и конструкция "if posix".
logger = logging.getLogger('setup')
logger.critical('Should not appear, because of disabled logger ...')
stop_event.wait()
listener.stop()
def worker_process(config):
"""
Некоторое количество таких процессов порождается для иллюстрации. На
практике это могла бы быть разнородная группа процессов, а не
идентичные друг другу.
Это инициализирует логирование согласно указанной конфигурации
и записывает сотню сообщений со случайными уровнями в случайно выбранные
логгеры.
Добавлена небольшая задержка, чтобы дать другим процессам шанс выполниться. Это
не является строго необходимым, но позволяет немного перемешать вывод от разных
процессов больше, чем если бы её не было.
"""
logging.config.dictConfig(config)
levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR,
logging.CRITICAL]
loggers = ['foo', 'foo.bar', 'foo.bar.baz',
'spam', 'spam.ham', 'spam.ham.eggs']
if os.name == 'posix':
# В POSIX логгер setup уже был настроен в
# родительском процессе, но должен был быть отключён после вызова
# dictConfig.
# В Windows, поскольку fork не используется, логгер setup не будет
# существовать в дочернем процессе, поэтому он будет создан, и сообщение
# появится – отсюда и конструкция "if posix".
logger = logging.getLogger('setup')
logger.critical('Should not appear, because of disabled logger ...')
for i in range(100):
lvl = random.choice(levels)
logger = logging.getLogger(random.choice(loggers))
logger.log(lvl, 'Message no. %d', i)
time.sleep(0.01)
def main():
q = Queue()
# Главный процесс получает простую конфигурацию, которая выводит информацию в консоль.
config_initial = {
'version': 1,
'formatters': {
'detailed': {
'class': 'logging.Formatter',
'format': '%(asctime)s %(name)-15s %(levelname)-8s %(processName)-10s %(message)s'
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
},
},
'root': {
'level': 'DEBUG',
'handlers': ['console']
},
}
# Конфигурация рабочего процесса представляет собой QueueHandler, прикреплённый к
# корневому логгеру, что позволяет отправлять все сообщения в очередь.
# Мы отключаем существующие логгеры, чтобы отключить логгер "setup", используемый в
# родительском процессе. Это необходимо в POSIX, потому что логгер будет
# присутствовать в дочернем процессе после вызова fork().
config_worker = {
'version': 1,
'disable_existing_loggers': True,
'handlers': {
'queue': {
'class': 'logging.handlers.QueueHandler',
'queue': q,
},
},
'root': {
'level': 'DEBUG',
'handlers': ['queue']
},
}
# Конфигурация процесса-слушателя показывает, что вся гибкость
# конфигурации логирования доступна для отправки событий обработчикам так, как
# требуется.
# Мы отключаем существующие логгеры, чтобы отключить логгер "setup", используемый в
# родительском процессе. Это необходимо в POSIX, потому что логгер будет
# присутствовать в дочернем процессе после вызова fork().
config_listener = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'detailed': {
'class': 'logging.Formatter',
'format': '%(asctime)s %(name)-15s %(levelname)-8s %(processName)-10s %(message)s'
},
'simple': {
'class': 'logging.Formatter',
'format': '%(name)-15s %(levelname)-8s %(processName)-10s %(message)s'
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
'formatter': 'simple',
},
'file': {
'class': 'logging.FileHandler',
'filename': 'mplog.log',
'mode': 'w',
'formatter': 'detailed',
},
'foofile': {
'class': 'logging.FileHandler',
'filename': 'mplog-foo.log',
'mode': 'w',
'formatter': 'detailed',
},
'errors': {
'class': 'logging.FileHandler',
'filename': 'mplog-errors.log',
'mode': 'w',
'level': 'ERROR',
'formatter': 'detailed',
},
},
'loggers': {
'foo': {
'handlers': ['foofile']
}
},
'root': {
'level': 'DEBUG',
'handlers': ['console', 'file', 'errors']
},
}
# Записываем несколько начальных событий, просто чтобы показать, что логирование в родительском процессе работает
# нормально.
logging.config.dictConfig(config_initial)
logger = logging.getLogger('setup')
logger.info('About to create workers ...')
workers = []
for i in range(5):
wp = Process(target=worker_process, name='worker %d' % (i + 1),
args=(config_worker,))
workers.append(wp)
wp.start()
logger.info('Started worker: %s', wp.name)
logger.info('About to create listener ...')
stop_event = Event()
lp = Process(target=listener_process, name='listener',
args=(q, stop_event, config_listener))
lp.start()
logger.info('Started listener')
# Теперь ожидаем завершения работы рабочих процессов.
for wp in workers:
wp.join()
# Все рабочие процессы завершены, теперь можно остановить прослушивание.
# Логирование в родительском процессе всё ещё работает нормально.
logger.info('Telling listener to stop ...')
stop_event.set()
lp.join()
logger.info('All done.')
if __name__ == '__main__':
main()
Вставка BOM в сообщения, отправляемые в SysLogHandler¶Inserting a BOM into messages sent to a SysLogHandler
RFC 5424 требует, чтобы Unicode-сообщение отправлялось демону syslog в виде набора байтов следующей структуры: необязательный компонент, состоящий только из ASCII, затем метка порядка байтов (BOM) UTF-8, затем Unicode, закодированный в UTF-8. (См. соответствующий раздел спецификации.)
В Python 3.1 в SysLogHandler был добавлен код для вставки метки BOM в сообщение, но, к сожалению, он был реализован неправильно: BOM появлялся в начале сообщения, не позволяя никакому чисто ASCII-компоненту находиться перед ним.
Поскольку такое поведение некорректно, код, неправильно вставляющий BOM, удаляется из Python 3.2.4 и более поздних версий. Однако он не заменяется, и если требуется создавать сообщения, совместимые с RFC 5424, которые содержат BOM, необязательную последовательность символов только из ASCII перед ним и произвольный Unicode после него, закодированные в UTF-8, то необходимо сделать следующее:
Прикрепите экземпляр Formatter к своему экземпляру SysLogHandler со строкой формата, например:
'ASCII section\ufeffUnicode section'
Кодовая точка Unicode U+FEFF при кодировании UTF-8 будет закодирована как метка BOM UTF-8 – строка байтов b'\xef\xbb\xbf'.
ASCII-часть следует заменить на любые подходящие заполнители, но необходимо следить, чтобы данные, которые появляются там после подстановки, всегда были в ASCII (таким образом, они останутся без изменений после кодирования UTF-8).
Unicode-часть следует заменить на любые заполнители; если данные после подстановки содержат символы за пределами диапазона ASCII, это нормально – они будут закодированы в UTF-8.
Отформатированное сообщение будет закодировано SysLogHandler в кодировке UTF-8. Если соблюдать приведённые выше правила, можно создавать сообщения, совместимые с RFC 5424. В противном случае логирование может не жаловаться, но сообщения не будут соответствовать RFC 5424, и ваш демон syslog может начать жаловаться.
Реализация структурированного ведения журнала¶Implementing structured logging
Хотя большинство сообщений лога предназначены для чтения человеком и поэтому нелегко разбираются машиной, могут возникнуть ситуации, когда нужно выводить сообщения в структурированном формате, который может быть проанализирован программой (без необходимости сложных регулярных выражений для разбора сообщения лога). Это легко достигается с помощью пакета logging. Есть несколько способов это сделать, но ниже приведён простой подход, использующий JSON для сериализации события в машиночитаемом виде:
import json
import logging
class StructuredMessage(object):
def __init__(self, message, **kwargs):
self.message = message
self.kwargs = kwargs
def __str__(self):
return '%s >>> %s' % (self.message, json.dumps(self.kwargs))
_ = StructuredMessage # необязательно, для улучшения читаемости
logging.basicConfig(level=logging.INFO, format='%(message)s')
logging.info(_('message 1', foo='bar', bar='baz', num=123, fnum=123.456))
Если запустить приведённый выше скрипт, он выведет:
message 1 >>> {"fnum": 123.456, "num": 123, "bar": "baz", "foo": "bar"}
Обратите внимание, что порядок элементов может отличаться в зависимости от используемой версии Python.
Если требуется более специализированная обработка, можно использовать пользовательский кодировщик JSON, как в следующем полном примере:
from __future__ import unicode_literals
import json
import logging
# Следующий фрагмент предназначен для того, чтобы скрипт работал без изменений в версиях 2.x и 3.x.
try:
unicode
except NameError:
unicode = str
class Encoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, set):
return tuple(o)
elif isinstance(o, unicode):
return o.encode('unicode_escape').decode('ascii')
return super(Encoder, self).default(o)
class StructuredMessage(object):
def __init__(self, message, **kwargs):
self.message = message
self.kwargs = kwargs
def __str__(self):
s = Encoder().encode(self.kwargs)
return '%s >>> %s' % (self.message, s)
_ = StructuredMessage # необязательно, для улучшения читаемости
def main():
logging.basicConfig(level=logging.INFO, format='%(message)s')
logging.info(_('message 1', set_value=set([1, 2, 3]), snowman='\u2603'))
if __name__ == '__main__':
main()
При запуске приведённого выше скрипта он выведет:
message 1 >>> {"snowman": "\u2603", "set_value": [1, 2, 3]}
Обратите внимание, что порядок элементов может отличаться в зависимости от используемой версии Python.
Настройка обработчиков с помощью dictConfig()¶Customizing handlers with dictConfig()
Иногда требуется настроить обработчики логирования особым образом, и при использовании dictConfig() это можно сделать без создания подклассов. Например, можно задать владельца файла журнала. В POSIX это легко делается с помощью shutil.chown(), но файловые обработчики в стандартной библиотеке не имеют встроенной поддержки. Создание обработчика можно настроить с помощью обычной функции, например:
def owned_file_handler(filename, mode='a', encoding=None, owner=None):
if owner:
if not os.path.exists(filename):
open(filename, 'a').close()
shutil.chown(filename, *owner)
return logging.FileHandler(filename, mode, encoding)
Затем в конфигурации логирования, передаваемой в dictConfig(), можно указать, что обработчик должен быть создан вызовом этой функции:
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'default': {
'format': '%(asctime)s %(levelname)s %(name)s %(message)s'
},
},
'handlers': {
'file':{
# Значения ниже извлекаются из этого словаря и
# используются для создания обработчика, установки его уровня и
# его форматтера.
'()': owned_file_handler,
'level':'DEBUG',
'formatter': 'default',
# Значения ниже передаются вызываемому объекту-создателю обработчика
# в качестве именованных аргументов.
'owner': ['pulse', 'pulse'],
'filename': 'chowntest.log',
'mode': 'w',
'encoding': 'utf-8',
},
},
'root': {
'handlers': ['file'],
'level': 'DEBUG',
},
}
В этом примере я устанавливаю владельца через пользователя и группу pulse – только для иллюстрации. Собирая всё вместе в рабочий скрипт chowntest.py:
import logging, logging.config, os, shutil
def owned_file_handler(filename, mode='a', encoding=None, owner=None):
if owner:
if not os.path.exists(filename):
open(filename, 'a').close()
shutil.chown(filename, *owner)
return logging.FileHandler(filename, mode, encoding)
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'default': {
'format': '%(asctime)s %(levelname)s %(name)s %(message)s'
},
},
'handlers': {
'file':{
# Значения ниже извлекаются из этого словаря и
# используются для создания обработчика, установки его уровня и
# его форматтера.
'()': owned_file_handler,
'level':'DEBUG',
'formatter': 'default',
# Значения ниже передаются вызываемому объекту-создателю обработчика
# в качестве именованных аргументов.
'owner': ['pulse', 'pulse'],
'filename': 'chowntest.log',
'mode': 'w',
'encoding': 'utf-8',
},
},
'root': {
'handlers': ['file'],
'level': 'DEBUG',
},
}
logging.config.dictConfig(LOGGING)
logger = logging.getLogger('mylogger')
logger.debug('A debug message')
Чтобы запустить это, возможно, потребуется выполнить от имени root:
$ sudo python3.3 chowntest.py
$ cat chowntest.log
2013-11-05 09:34:51,128 DEBUG mylogger A debug message
$ ls -l chowntest.log
-rw-r--r-- 1 pulse pulse 55 2013-11-05 09:34 chowntest.log
Обратите внимание, что в этом примере используется Python 3.3, так как именно в нём появилась функция shutil.chown(). Этот подход должен работать с любой версией Python, которая поддерживает dictConfig() – а именно Python 2.7, 3.2 и выше. В версиях до 3.3 вам потребуется реализовать смену владельца, например, с помощью os.chown().
На практике функция создания обработчика может находиться в служебном модуле где-нибудь в вашем проекте. Вместо строки в конфигурации:
'()': owned_file_handler,
можно использовать, например:
'()': 'ext://project.util.owned_file_handler',
где project.util можно заменить на фактическое имя пакета, в котором находится функция. В приведённом выше рабочем скрипте должно сработать использование 'ext://__main__.owned_file_handler'. Здесь фактическая вызываемая сущность разрешается функцией dictConfig() из спецификации ext://.
Надеемся, этот пример также показывает, как можно реализовать другие типы изменений файлов (например, установку определённых битов разрешений POSIX) тем же способом, с помощью os.chmod().
Конечно, этот подход можно расширить и на другие типы обработчиков, отличные от FileHandler – например, на один из ротационных файловых обработчиков или на совершенно другой тип обработчика.
Использование определённых стилей форматирования во всём приложении¶Using particular formatting styles throughout your application
В Python 3.2 у Formatter появился именованный параметр style, который по умолчанию равен % для обратной совместимости, но позволяет указать { или $ для поддержки подходов форматирования, используемых в str.format() и string.Template. Обратите внимание, что это управляет форматированием сообщений журнала для финального вывода в журнал и совершенно независимо от того, как конструируется отдельное сообщение для записи.
Вызовы журналирования (debug(), info() и т.д.) принимают только позиционные параметры для самого сообщения журнала; именованные параметры используются только для определения параметров обработки вызова (например, именованный параметр exc_info указывает, что нужно записывать информацию о трассировке, или extra для добавления дополнительной контекстной информации в журнал). Поэтому нельзя напрямую использовать синтаксис str.format() или string.Template в вызовах журналирования, потому что внутри пакет logging использует %-форматирование для объединения строки формата и переменных аргументов. Изменить это, сохранив обратную совместимость, невозможно, так как все существующие вызовы в коде используют %-формат.
Высказывались предложения связывать стили форматирования с конкретными регистраторами, но этот подход также сталкивается с проблемами обратной совместимости, поскольку любой существующий код может использовать данное имя регистратора и %-форматирование.
Чтобы логирование работало совместимо между любыми сторонними библиотеками и вашим кодом, решения о форматировании необходимо принимать на уровне отдельного вызова логирования. Это открывает несколько способов, которыми можно реализовать альтернативные стили форматирования.
Использование фабрик LogRecord¶Using LogRecord factories
В Python 3.2, вместе с изменениями Formatter, упомянутыми выше, пакет logging получил возможность позволять пользователям задавать собственные подклассы LogRecord с помощью функции setLogRecordFactory(). Это можно использовать для установки собственного подкласса LogRecord, который делает правильные вещи, переопределяя метод getMessage(). В реализации базового класса этого метода происходит форматирование msg % args, и здесь можно подставить своё альтернативное форматирование; однако нужно быть осторожным, чтобы поддерживать все стили форматирования и оставить %-форматирование по умолчанию, чтобы обеспечить совместимость с другим кодом. Также следует не забыть вызвать str(self.msg), как это делает базовая реализация.
Обратитесь к справочной документации по setLogRecordFactory() и LogRecord для получения дополнительной информации.
Использование пользовательских объектов сообщений¶Using custom message objects
Существует ещё один, возможно, более простой способ использовать {}- и $-форматирование для построения отдельных сообщений журнала. Возможно, вы помните (из раздела Использование произвольных объектов в качестве сообщений), что при журналировании можно использовать произвольный объект в качестве строки формата сообщения, и пакет logging вызовет str() для этого объекта, чтобы получить фактическую строку формата. Рассмотрим следующие два класса:
class BraceMessage(object):
def __init__(self, fmt, *args, **kwargs):
self.fmt = fmt
self.args = args
self.kwargs = kwargs
def __str__(self):
return self.fmt.format(*self.args, **self.kwargs)
class DollarMessage(object):
def __init__(self, fmt, **kwargs):
self.fmt = fmt
self.kwargs = kwargs
def __str__(self):
from string import Template
return Template(self.fmt).substitute(**self.kwargs)
Любой из них можно использовать вместо строки формата, чтобы с помощью {}- или $-форматирования строить фактическую часть «message», которая появляется в отформатированном выводе журнала вместо «%(message)s», «{message}» или «$message». Если использовать имена классов каждый раз, когда нужно что-то записать, кажется громоздким, можно сделать это более удобным, используя псевдоним, например M или _ для сообщения (или, возможно, __, если вы используете _ для локализации).
Примеры этого подхода приведены ниже. Сначала форматирование с помощью str.format():
>>> __ = BraceMessage
>>> print(__('Message with {0} {1}', 2, 'placeholders'))
Message with 2 placeholders
>>> class Point: pass
...
>>> p = Point()
>>> p.x = 0.5
>>> p.y = 0.5
>>> print(__('Message with coordinates: ({point.x:.2f}, {point.y:.2f})', point=p))
Message with coordinates: (0.50, 0.50)
Затем форматирование с помощью string.Template:
>>> __ = DollarMessage
>>> print(__('Message with $num $what', num=2, what='placeholders'))
Message with 2 placeholders
>>>
Стоит отметить, что при таком подходе нет значительного снижения производительности: фактическое форматирование происходит не в момент вызова журналирования, а когда (и если) сообщение должно быть выведено в журнал обработчиком. Единственная необычная деталь, которая может сбить с толку, – скобки окружают строку формата и аргументы, а не только строку формата. Это связано с тем, что нотация __ – это просто синтаксический сахар для вызова конструктора одного из классов XXXMessage, показанных выше.
Настройка фильтров с помощью dictConfig()¶Configuring filters with dictConfig()
Фильтры можно настраивать с помощью dictConfig(), хотя с первого взгляда может быть неочевидно, как это сделать (поэтому и приведён этот рецепт). Поскольку Filter – единственный класс фильтра, входящий в стандартную библиотеку, и вряд ли он удовлетворит все потребности (он существует только как базовый класс), обычно потребуется определить собственный подкласс Filter с переопределённым методом filter(). Для этого укажите ключ () в словаре конфигурации для фильтра, задав вызываемый объект, который будет использоваться для создания фильтра (класс – самый очевидный вариант, но можно указать любой вызываемый объект, возвращающий экземпляр Filter). Вот полный пример:
import logging
import logging.config
import sys
class MyFilter(logging.Filter):
def __init__(self, param=None):
self.param = param
def filter(self, record):
if self.param is None:
allow = True
else:
allow = self.param not in record.msg
if allow:
record.msg = 'changed: ' + record.msg
return allow
LOGGING = {
'version': 1,
'filters': {
'myfilter': {
'()': MyFilter,
'param': 'noshow',
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'filters': ['myfilter']
}
},
'root': {
'level': 'DEBUG',
'handlers': ['console']
},
}
if __name__ == '__main__':
logging.config.dictConfig(LOGGING)
logging.debug('hello')
logging.debug('hello - noshow')
Этот пример показывает, как можно передавать конфигурационные данные вызываемому объекту, который создает экземпляр, в виде именованных параметров. При запуске приведенный выше скрипт выведет:
changed: hello
что показывает, что фильтр работает в соответствии с настройками.
Несколько дополнительных моментов, на которые стоит обратить внимание:
- Если нет возможности сослаться на вызываемый объект непосредственно в конфигурации (например, он находится в другом модуле и его нельзя импортировать напрямую в месте, где находится словарь конфигурации), можно использовать форму ext://..., как описано в разделе Доступ к внешним объектам. Например, в приведённом выше примере можно было использовать текст 'ext://__main__.MyFilter' вместо MyFilter.
- Помимо фильтров, этот метод также можно использовать для настройки пользовательских обработчиков и форматтеров. Смотрите раздел Пользовательские объекты для получения дополнительной информации о том, как logging поддерживает использование пользовательских объектов в своей конфигурации, а также см. другой рецепт из этой книги рецептов Настройка обработчиков с помощью dictConfig() выше.
Настраиваемое форматирование исключений¶Customized exception formatting
Могут возникнуть ситуации, когда вы захотите настроить форматирование исключений – для примера, предположим, вы хотите ровно одну строку на каждое событие лога, даже если присутствует информация об исключении. Вы можете сделать это с помощью пользовательского класса форматтера, как показано в следующем примере:
import logging
class OneLineExceptionFormatter(logging.Formatter):
def formatException(self, exc_info):
"""
Форматировать исключение так, чтобы оно выводилось одной строкой.
"""
result = super(OneLineExceptionFormatter, self).formatException(exc_info)
return repr(result) # или форматировать одной строкой по своему усмотрению
def format(self, record):
s = super(OneLineExceptionFormatter, self).format(record)
if record.exc_text:
s = s.replace('\n', '') + '|'
return s
def configure_logging():
fh = logging.FileHandler('output.txt', 'w')
f = OneLineExceptionFormatter('%(asctime)s|%(levelname)s|%(message)s|',
'%d/%m/%Y %H:%M:%S')
fh.setFormatter(f)
root = logging.getLogger()
root.setLevel(logging.DEBUG)
root.addHandler(fh)
def main():
configure_logging()
logging.info('Sample message')
try:
x = 1 / 0
except ZeroDivisionError as e:
logging.exception('ZeroDivisionError: %s', e)
if __name__ == '__main__':
main()
При запуске это создает файл ровно с двумя строками:
28/01/2015 07:21:23|INFO|Sample message|
28/01/2015 07:21:23|ERROR|ZeroDivisionError: integer division or modulo by zero|'Traceback (most recent call last):\n File "logtest7.py", line 30, in main\n x = 1 / 0\nZeroDivisionError: integer division or modulo by zero'|
Хотя описанный выше подход упрощён, он показывает, как можно форматировать информацию об исключениях по своему усмотрению. Модуль traceback может быть полезен для более специализированных нужд.
Озвучивание сообщений лога¶Speaking logging messages
Могут возникнуть ситуации, когда сообщения журнала желательно воспроизводить в звуковом, а не в видимом формате. Это легко сделать, если в вашей системе доступна функция преобразования текста в речь (TTS), даже если у неё нет привязки к Python. У большинства TTS-систем есть программа командной строки, которую можно запустить, и её можно вызвать из обработчика с помощью подпроцесса. Предполагается, что программы командной строки TTS не ожидают взаимодействия с пользователем и не требуют много времени для выполнения, а частота сообщений журнала не настолько высока, чтобы заваливать пользователя сообщениями, и что приемлемо воспроизводить сообщения по одному, а не одновременно. Приведённый ниже пример реализации ожидает завершения озвучивания одного сообщения, прежде чем обработать следующее, что может задерживать другие обработчики. Вот краткий пример, демонстрирующий подход, в котором предполагается, что доступен TTS-пакет espeak:
import logging
import subprocess
import sys
class TTSHandler(logging.Handler):
def emit(self, record):
msg = self.format(record)
# Говорить медленно женским английским голосом.
cmd = ['espeak', '-s150', '-ven+f3', msg]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
# дождаться завершения программы
p.communicate()
def configure_logging():
h = TTSHandler()
root = logging.getLogger()
root.addHandler(h)
# стандартный форматтер просто возвращает сообщение
root.setLevel(logging.DEBUG)
def main():
logging.info('Hello')
logging.debug('Goodbye')
if __name__ == '__main__':
configure_logging()
sys.exit(main())
При запуске этот скрипт должен сказать «Hello», а затем «Goodbye» женским голосом.
Описанный выше подход, конечно, можно адаптировать для других TTS-систем и даже для других систем, которые могут обрабатывать сообщения через внешние программы, запускаемые из командной строки.
Буферизация сообщений лога и условный вывод¶Buffering logging messages and outputting them conditionally
Могут возникнуть ситуации, когда вы хотите записывать сообщения во временную область и выводить их только при наступлении определенного условия. Например, вы можете начать запись отладочных событий в функции, и если функция завершится без ошибок, вы не захотите засорять лог собранной отладочной информацией, но если возникнет ошибка, вы захотите вывести всю отладочную информацию вместе с ошибкой.
Вот пример, показывающий, как это можно сделать с помощью декоратора для функций, в которых требуется такое поведение журналирования. В нём используется logging.handlers.MemoryHandler, который позволяет буферизовать события журнала до наступления некоторого условия, после чего буферизованные события сбрасываются (передаются другому обработчику – целевому – для обработки). По умолчанию MemoryHandler сбрасывает буфер, когда он заполняется или когда встречается событие с уровнем, равным или превышающим указанный порог. Этот рецепт можно использовать с более специализированным подклассом MemoryHandler, если требуется собственное поведение сброса.
The example script has a simple function, foo, which just cycles through all the logging levels, writing to sys.stderr to say what level it’s about to log at, and then actually logging a message that that level. You can pass a parameter to foo which, if true, will log at ERROR and CRITICAL levels - otherwise, it only logs at DEBUG, INFO and WARNING levels.
The script just arranges to decorate foo with a decorator which will do the conditional logging that’s required. The decorator takes a logger as a parameter and attaches a memory handler for the duration of the call to the decorated function. The decorator can be additionally parameterised using a target handler, a level at which flushing should occur, and a capacity for the buffer. These default to a StreamHandler which writes to sys.stderr, logging.ERROR and 100 respectively.
Вот скрипт:
import logging
from logging.handlers import MemoryHandler
import sys
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
def log_if_errors(logger, target_handler=None, flush_level=None, capacity=None):
if target_handler is None:
target_handler = logging.StreamHandler()
if flush_level is None:
flush_level = logging.ERROR
if capacity is None:
capacity = 100
handler = MemoryHandler(capacity, flushLevel=flush_level, target=target_handler)
def decorator(fn):
def wrapper(*args, **kwargs):
logger.addHandler(handler)
try:
return fn(*args, **kwargs)
except Exception:
logger.exception('call failed')
raise
finally:
super(MemoryHandler, handler).flush()
logger.removeHandler(handler)
return wrapper
return decorator
def write_line(s):
sys.stderr.write('%s\n' % s)
def foo(fail=False):
write_line('about to log at DEBUG ...')
logger.debug('Actually logged at DEBUG')
write_line('about to log at INFO ...')
logger.info('Actually logged at INFO')
write_line('about to log at WARNING ...')
logger.warning('Actually logged at WARNING')
if fail:
write_line('about to log at ERROR ...')
logger.error('Actually logged at ERROR')
write_line('about to log at CRITICAL ...')
logger.critical('Actually logged at CRITICAL')
return fail
decorated_foo = log_if_errors(logger)(foo)
if __name__ == '__main__':
logger.setLevel(logging.DEBUG)
write_line('Calling undecorated foo with False')
assert not foo(False)
write_line('Calling undecorated foo with True')
assert foo(True)
write_line('Calling decorated foo with False')
assert not decorated_foo(False)
write_line('Calling decorated foo with True')
assert decorated_foo(True)
При запуске этого скрипта отобразится следующий вывод:
Calling undecorated foo with False
about to log at DEBUG ...
about to log at INFO ...
about to log at WARNING ...
Calling undecorated foo with True
about to log at DEBUG ...
about to log at INFO ...
about to log at WARNING ...
about to log at ERROR ...
about to log at CRITICAL ...
Calling decorated foo with False
about to log at DEBUG ...
about to log at INFO ...
about to log at WARNING ...
Calling decorated foo with True
about to log at DEBUG ...
about to log at INFO ...
about to log at WARNING ...
about to log at ERROR ...
Actually logged at DEBUG
Actually logged at INFO
Actually logged at WARNING
Actually logged at ERROR
about to log at CRITICAL ...
Actually logged at CRITICAL
Как видно, фактический вывод журнала происходит только при регистрации события, уровень которого ERROR или выше, но в этом случае также регистрируются все предыдущие события с более низкими уровнями.
Конечно, можно использовать обычные средства декорирования:
@log_if_errors(logger)
def foo(fail=False):
...
Форматирование времени с использованием UTC (GMT) через конфигурацию¶Formatting times using UTC (GMT) via configuration
Иногда требуется форматировать время по UTC, что можно сделать с помощью класса такого как UTCFormatter, показанного ниже:
import logging
import time
class UTCFormatter(logging.Formatter):
converter = time.gmtime
И после этого можно использовать UTCFormatter в своём коде вместо Formatter. Если это нужно сделать через конфигурацию, можно воспользоваться API dictConfig() с подходом, проиллюстрированным следующим полным примером:
import logging
import logging.config
import time
class UTCFormatter(logging.Formatter):
converter = time.gmtime
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'utc': {
'()': UTCFormatter,
'format': '%(asctime)s %(message)s',
},
'local': {
'format': '%(asctime)s %(message)s',
}
},
'handlers': {
'console1': {
'class': 'logging.StreamHandler',
'formatter': 'utc',
},
'console2': {
'class': 'logging.StreamHandler',
'formatter': 'local',
},
},
'root': {
'handlers': ['console1', 'console2'],
}
}
if __name__ == '__main__':
logging.config.dictConfig(LOGGING)
logging.warning('The local time is %s', time.asctime())
При запуске этого скрипта он должен вывести примерно следующее:
2015-10-17 12:53:29,501 The local time is Sat Oct 17 13:53:29 2015
2015-10-17 13:53:29,501 The local time is Sat Oct 17 13:53:29 2015
показывая, как время форматируется как в местном времени, так и в UTC, по одному для каждого обработчика.