Пособие по ведению логов
- Автор
-
Винай Саджип <vinay_sajip at red-dove dot com>
Эта страница содержит ряд рецептов по ведению логов, которые оказались полезными в прошлом.
Использование логов в нескольких модулях
Несколько вызовов logging.getLogger('someLogger') возвращают ссылку на тот же объект логгера. Это верно не только в рамках одного модуля, но и в разных модулях, пока это происходит в одном процессе интерпретатора Python. Это верно для ссылок на один и тот же объект; кроме того, код приложения может определить и настроить родительский логгер в одном модуле и создать (но не настроить) дочерний логгер в отдельном модуле, и все вызовы логгера дочернему объекту будут передаваться родителю. Вот главный модуль:
import logging
import auxiliary_module
# create logger with 'spam_application'
logger = logging.getLogger('spam_application')
logger.setLevel(logging.DEBUG)
# create file handler which logs even debug messages
fh = logging.FileHandler('spam.log')
fh.setLevel(logging.DEBUG)
# create console handler with a higher log level
ch = logging.StreamHandler()
ch.setLevel(logging.ERROR)
# create formatter and add it to the handlers
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# add the handlers to the logger
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
# create logger
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()
Ведение логов из нескольких потоков
Ведение логов из нескольких потоков не требует особых усилий. Следующий пример демонстрирует ведение логов из основного (начального) потока и другого потока:
import logging
import threading
import time
def worker(arg):
while not arg['stop']:
logging.debug('Hi from myfunc')
time.sleep(0.5)
def main():
logging.basicConfig(level=logging.DEBUG, format='%(relativeCreated)6d %(threadName)s %(message)s')
info = {'stop': False}
thread = threading.Thread(target=worker, args=(info,))
thread.start()
while True:
try:
logging.debug('Hello from main')
time.sleep(0.75)
except KeyboardInterrupt:
info['stop'] = True
break
thread.join()
if __name__ == '__main__':
main()
При запуске скрипта должно быть напечатано что-то вроде следующего:
0 Thread-1 Hi from myfunc 3 MainThread Hello from main 505 Thread-1 Hi from myfunc 755 MainThread Hello from main 1007 Thread-1 Hi from myfunc 1507 MainThread Hello from main 1508 Thread-1 Hi from myfunc 2010 Thread-1 Hi from myfunc 2258 MainThread Hello from main 2512 Thread-1 Hi from myfunc 3009 MainThread Hello from main 3013 Thread-1 Hi from myfunc 3515 Thread-1 Hi from myfunc 3761 MainThread Hello from main 4017 Thread-1 Hi from myfunc 4513 MainThread Hello from main 4518 Thread-1 Hi from myfunc
Это показывает вывод логов, перемешанный так, как можно было ожидать. Этот подход работает и для большего числа потоков, конечно.
Несколько обработчиков и форматеров
Логгеры — это обычные объекты Python. Метод addHandler() не имеет минимального или максимального лимита для количества добавляемых обработчиков. Иногда для приложения будет полезно регистрировать все сообщения всех уровней серьезности в текстовый файл, одновременно регистрируя ошибки или выше в консоль. Чтобы это настроить, просто настройте соответствующие обработчики. Вызовы ведения логов в прикладном коде останутся неизменными. Вот небольшая модификация предыдущего примера конфигурации на основе модулей:
import logging
logger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG)
# create file handler which logs even debug messages
fh = logging.FileHandler('spam.log')
fh.setLevel(logging.DEBUG)
# create console handler with a higher log level
ch = logging.StreamHandler()
ch.setLevel(logging.ERROR)
# create formatter and add it to the handlers
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
fh.setFormatter(formatter)
# add the handlers to logger
logger.addHandler(ch)
logger.addHandler(fh)
# 'application' code
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')
Обратите внимание, что код «приложения» не заботится о нескольких обработчиках. Все, что изменилось, — это добавление и настройка нового обработчика под именем fh.
Возможность создания новых обработчиков с фильтрами более высокого или более низкого уровня серьезности может быть очень полезной при написании и тестировании приложения. Вместо использования многих print операторов отладки, используйте logger.debug: В отличие от операторов print, которые вам придётся удалить или закомментировать позже, операторы logger.debug можно оставить нетронутыми в исходном коде и до поры до времени не выполнять. В этот момент единственное изменение, которое необходимо внести, — это изменение уровня серьезности логгера и/или обработчика на отладку.
Ведение логов в несколько мест назначения
Предположим, вы хотите вести логи в консоль и файл с разными форматами сообщений и в разных обстоятельствах. Предположим, вы хотите записывать сообщения с уровнями DEBUG и выше в файл, а сообщения с уровнями INFO и выше — в консоль. Также предположим, что файл должен содержать временные метки, но сообщения в консоли их не должны содержать. Вот как вы можете этого добиться:
import logging
# set up logging to file - see previous section for more details
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')
# define a Handler which writes INFO messages or higher to the sys.stderr
console = logging.StreamHandler()
console.setLevel(logging.INFO)
# set a format which is simpler for console use
formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
# tell the handler to use this format
console.setFormatter(formatter)
# add the handler to the root logger
logging.getLogger('').addHandler(console)
# Now, we can log to the root logger, or any other logger. First the root...
logging.info('Jackdaws love my big sphinx of quartz.')
# Now, define a couple of other loggers which might represent areas in your
# application:
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 появляется только в файле. Другие сообщения отправляются в оба места назначения.
В этом примере используются обработчики консоли и файлов, но вы можете использовать любое количество и комбинацию обработчиков по вашему выбору.
Пример конфигурационного сервера
Вот пример модуля, использующего конфигурационный сервер логов:
import logging
import logging.config
import time
import os
# read initial config file
logging.config.fileConfig('logging.conf')
# create and start listener on port 9999
t = logging.config.listen(9999)
t.start()
logger = logging.getLogger('simpleExample')
try:
# loop through logging calls to see the difference
# new configurations make, until Ctrl+C is pressed
while True:
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')
time.sleep(5)
except KeyboardInterrupt:
# cleanup
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')
Работа с обработчиками, блокирующими выполнение
Иногда вам нужно заставить обработчики логов выполнять свою работу без блокирования потока, из которого ведется регистрация. Это часто встречается в веб-приложениях, хотя, конечно, это происходит и в других сценариях.
Частой причиной медленной работы является SMTPHandler: отправка электронной почты может занять много времени по ряду причин, не зависящих от разработчика (например, из-за плохо работающей почтовой или сетевой инфраструктуры). Но практически любой обработчик, работающий через сеть, может блокировать выполнение: даже операция SocketHandler может производить запрос DNS на нижних уровнях, что слишком медленно (и этот запрос может быть глубоко в коде библиотеки сокетов, ниже уровня Python, и вне вашего контроля).
Одним из решений является использование двухэтапного подхода. На первом этапе к тем логгерам, к которым обращаются из критически важных для производительности потоков, подключается только QueueHandler. Они просто записывают данные в очередь, размер которой может быть достаточно большим или инициализирован без верхнего предела. Запись в очередь обычно принимается быстро, хотя вам, вероятно, придется поймать исключение queue.Full как меры предосторожности в вашем коде. Если вы разработчик библиотеки, у которого есть критически важные для производительности потоки в вашем коде, обязательно документируйте это (вместе с предложением подключать только QueueHandlers к вашим логгерам) для пользы других разработчиков, которые будут использовать ваш код.
Вторая часть решения — QueueListener, которая разработана как аналог QueueHandler. QueueListener очень прост: ему передается очередь и некоторые обработчики, и он запускает внутренний поток, который прослушивает очередь для LogRecords, отправленных из QueueHandlers (или любого другого источника LogRecords, в общем случае). Записи LogRecords удаляются из очереди и передаются обработчикам для обработки.
Преимущество наличия отдельного класса QueueListener заключается в том, что вы можете использовать один экземпляр для обслуживания нескольких QueueHandlers. Это более эффективно с точки зрения ресурсов, чем, скажем, иметь потоковые версии существующих классов обработчиков, которые бы потребляли по одному потоку на каждый обработчик без особой пользы.
Следующий пример использования этих двух классов (импорты опущены):
que = queue.Queue(-1) # no limit on size
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()
# The log output will display the thread which generated
# the event (the main thread) rather than the internal
# thread which monitors the internal queue. This is what
# you want to happen.
root.warning('Look out!')
listener.stop()
который при запуске выведет:
MainThread: Look out!
Изменено в версии 3.5: До Python 3.5 QueueListener всегда передавал каждое сообщение, полученное из очереди, каждому обработчику, с которым он был инициализирован. (Это происходило потому, что предполагалось, что фильтрация по уровням происходит с другой стороны, где заполняется очередь.) Начиная с версии 3.5, это поведение можно изменить, передав ключевой аргумент respect_handler_level=True в конструктор слушателя. При этом слушатель сравнивает уровень каждого сообщения с уровнем обработчика и передает сообщение обработчику только в том случае, если это уместно.
Отправка и получение событий логов через сеть
Предположим, вы хотите отправлять события логов через сеть и обрабатывать их на стороне получателя. Простой способ сделать это — подключить экземпляр SocketHandler к корневому логгеру на стороне отправителя:
import logging, logging.handlers
rootLogger = logging.getLogger('')
rootLogger.setLevel(logging.DEBUG)
socketHandler = logging.handlers.SocketHandler('localhost',
logging.handlers.DEFAULT_TCP_LOGGING_PORT)
# don't bother with a formatter, since a socket handler sends the event as
# an unformatted pickle
rootLogger.addHandler(socketHandler)
# Now, we can log to the root logger, or any other logger. First the root...
logging.info('Jackdaws love my big sphinx of quartz.')
# Now, define a couple of other loggers which might represent areas in your
# application:
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):
"""Handler for a streaming logging request.
This basically logs the record using whatever logging policy is
configured locally.
"""
def handle(self):
"""
Handle multiple requests - each expected to be a 4-byte length,
followed by the LogRecord in pickle format. Logs the record
according to whatever policy is configured locally.
"""
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 a name is specified, we use the named logger rather than the one
# implied by the record.
if self.server.logname is not None:
name = self.server.logname
else:
name = record.name
logger = logging.getLogger(name)
# N.B. EVERY record gets logged. This is because Logger.handle
# is normally called AFTER logger-level filtering. If you want
# to do filtering, do it at the client end to save wasting
# cycles and network bandwidth!
logger.handle(record)
class LogRecordSocketReceiver(socketserver.ThreadingTCPServer):
"""
Simple TCP socket-based logging receiver suitable for testing.
"""
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() и реализовав там свою альтернативу, а также адаптировав приведенный выше скрипт для использования вашей альтернативной сериализации.
Добавление контекстной информации в выходные данные регистрации
Иногда вам нужно, чтобы выходные данные регистрации содержали контекстную информацию помимо параметров, переданных вызову регистрации. Например, в сетевом приложении может потребоваться регистрировать информацию, специфичную для клиента, в журнале (например, имя пользователя удалённого клиента или его IP-адрес). Хотя вы можете использовать параметр extra для достижения этого, это не всегда удобно. Хотя может показаться заманчивым создавать Logger экземпляры для каждого соединения, это не очень хорошая идея, поскольку эти экземпляры не подлежат сборке мусора. Хотя на практике это не проблема, когда количество Logger экземпляров зависит от уровня детализации, который вы хотите использовать для регистрации приложения, это может быть трудно контролировать, если число Logger экземпляров станет фактически неограниченным.
Использование LoggerAdapters для передачи контекстной информации
Легкий способ передать контекстную информацию для вывода вместе с информацией об событии регистрации — использовать класс LoggerAdapter. Этот класс предназначен для работы как Logger, так что вы можете вызывать debug(), info(), warning(), error(), exception(), critical() и log(). Эти методы имеют такие же сигнатуры, как и их аналоги в Logger, поэтому вы можете использовать два типа экземпляров взаимозаменяемо.
При создании экземпляра LoggerAdapter вы передаёте ему экземпляр Logger и объект типа словаря, содержащий вашу контекстную информацию. Когда вы вызываете один из методов регистрации на экземпляре LoggerAdapter, он делегирует вызов базовому экземпляру Logger, переданному в его конструктор, и организует передачу контекстной информации в делегированный вызов. Вот фрагмент из кода LoggerAdapter:
def debug(self, msg, /, *args, **kwargs):
"""
Delegate a debug call to the underlying logger, after adding
contextual information from this adapter instance.
"""
msg, kwargs = self.process(msg, kwargs)
self.logger.debug(msg, *args, **kwargs)
Метод process() класса LoggerAdapter — это место, где контекстная информация добавляется в выходные данные регистрации. Ему передаётся сообщение и ключевые аргументы вызова регистрации, и он возвращает (возможно, изменённые) версии этих данных для использования в вызове базового логгера. По умолчанию этот метод оставляет сообщение неизменным, но вставляет ключ «extra» в аргументы ключевых слов, значение которого — объект типа словаря, переданный в конструктор. Разумеется, если вы передали ключевой аргумент «extra» в вызов адаптера, он будет незаметно перезаписан.
Преимущество использования «extra» заключается в том, что значения в объекте типа словаря объединяются в __dict__ экземпляра LogRecord, позволяя использовать настраиваемые строки со своими экземплярами Formatter, которые знают о ключах объекта типа словаря. Если вам нужен другой метод, например, если вы хотите добавить контекстную информацию в начало или конец строки сообщения, вам просто нужно создать подкласс LoggerAdapter и переопределить process(), чтобы сделать необходимое. Вот простой пример:
class CustomAdapter(logging.LoggerAdapter):
"""
This example adapter expects the passed in dict-like object to have a
'connid' key, whose value in brackets is prepended to the log message.
"""
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 в начале сообщений журнала.
Использование объектов, отличных от словарей, для передачи контекстной информации
Вам не обязательно передавать фактический словарь в LoggerAdapter — вы можете передать экземпляр класса, который реализует __getitem__ и __iter__, чтобы он выглядел как словарь для регистрации. Это будет полезно, если вы хотите динамически генерировать значения (в то время как значения в словаре будут постоянными).
Использование фильтров для передачи контекстной информации
Вы также можете добавить контекстную информацию в выходные данные журнала с помощью пользовательского Filter. Экземпляры Filter могут изменять объект LogRecords , переданный им, в том числе добавлять дополнительные атрибуты, которые затем можно вывести с помощью подходящей строки форматирования, или, при необходимости, пользовательского Formatter.
Например, в веб-приложении запрос, обрабатываемый (или по крайней мере, интересные его части), можно хранить в переменной threadlocal (threading.local), а затем получить доступ к ней из Filter , чтобы добавить, скажем, информацию из запроса — например, удалённый IP-адрес и имя пользователя удалённого пользователя — в LogRecord, используя имена атрибутов ‘ip’ и ‘user’ как в примере LoggerAdapter выше. В этом случае можно использовать ту же строку форматирования для получения аналогичных выходных данных, как показано выше. Вот пример скрипта:
import logging
from random import choice
class ContextFilter(logging.Filter):
"""
This is a filter which injects contextual information into the log.
Rather than use actual contextual information, we just use random
data in this demo.
"""
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
Регистрация в одном файле из нескольких процессов
Хотя регистрация является потокобезопасной и регистрация в одном файле из нескольких потоков в одном процессе поддерживается, регистрация в одном файле из нескольких процессов не поддерживается, поскольку нет стандартного способа сериализовать доступ к одному файлу через несколько процессов в Python. Если вам нужно регистрировать в одном файле из нескольких процессов, один из способов — заставить все процессы регистрировать в SocketHandler, а затем иметь отдельный процесс, реализующий сокет-сервер, который считывает из сокета и записывает в файл. (Если вы предпочитаете, вы можете выделить один поток в одном из существующих процессов для выполнения этой функции.) Этот раздел подробно описывает этот подход и включает в себя рабочий сокет-приёмник, который можно использовать как отправную точку для адаптации в ваших собственных приложениях.
Вы также можете написать свой собственный обработчик, который использует класс Lock из модуля multiprocessing для сериализации доступа к файлу из ваших процессов. Существующие FileHandler и подклассы в настоящее время не используют multiprocessing, хотя в будущем это может быть реализовано. Обратите внимание, что в настоящее время модуль multiprocessing не предоставляет работоспособную функциональность блокировок на всех платформах (см. https://bugs.python.org/issue3770).
В качестве альтернативы вы можете использовать Queue и QueueHandler, чтобы отправлять все события регистрации в один из процессов в вашем приложении с несколькими процессами. Следующий пример скрипта демонстрирует, как это можно сделать; в примере отдельный процесс-слушатель прослушивает события, отправленные другими процессами, и регистрирует их в соответствии с собственной конфигурацией регистрации. Хотя пример демонстрирует только один способ сделать это (например, вы можете использовать поток-слушатель вместо отдельного процесса-слушателя — реализация будет аналогичной), он позволяет иметь совершенно разные конфигурации регистрации для слушателя и других процессов в вашем приложении и может быть использован как основа для кода, соответствующего вашим собственным конкретным требованиям:
# You'll need these imports in your own code
import logging
import logging.handlers
import multiprocessing
# Next two import lines for this demo only
from random import choice, random
import time
#
# Because you'll want to define the logging configurations for listener and workers, the
# listener and worker process functions take a configurer parameter which is a callable
# for configuring logging for that process. These functions are also passed the queue,
# which they use for communication.
#
# In practice, you can configure the listener however you want, but note that in this
# simple example, the listener does not apply level or filter logic to received records.
# In practice, you would probably want to do this logic in the worker processes, to avoid
# sending events which would be filtered out between processes.
#
# The size of the rotated files is made small so you can see the results easily.
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)
# This is the listener process top-level loop: wait for logging events
# (LogRecords)on the queue and handle them, quit when you get a None for a
# LogRecord.
def listener_process(queue, configurer):
configurer()
while True:
try:
record = queue.get()
if record is None: # We send this as a sentinel to tell the listener to quit.
break
logger = logging.getLogger(record.name)
logger.handle(record) # No level or filter logic applied - just do it!
except Exception:
import sys, traceback
print('Whoops! Problem:', file=sys.stderr)
traceback.print_exc(file=sys.stderr)
# Arrays used for random selections in this demo
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',
]
# The worker configuration is done at the start of the worker process run.
# Note that on Windows you can't rely on fork semantics, so each process
# will run the logging configuration code when it starts.
def worker_configurer(queue):
h = logging.handlers.QueueHandler(queue) # Just the one handler needed
root = logging.getLogger()
root.addHandler(h)
# send all messages, for demo; no other level or filter logic applied.
root.setLevel(logging.DEBUG)
# This is the worker process top-level loop, which just logs ten events with
# random intervening delays before terminating.
# The print messages are just so you know it's doing something!
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)
# Here's where the demo gets orchestrated. Create the queue, create and start
# the listener, create ten workers and start them, wait for them to finish,
# then send a None to the queue to tell the listener to finish.
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()
# At this point, the main process could do some useful work of its own
# Once it's done that, it can wait for the workers to terminate...
for wp in workers:
wp.join()
# And now tell the logging thread to finish up, too
q.put(None)
lp.join()
Этот вариант показывает, как вы можете, например, применять конфигурацию для отдельных логгеров — например, логгер foo имеет специальный обработчик, который сохраняет все события в подсистеме foo в файле mplog-foo.log. Это будет использоваться механизмом регистрации в основном процессе (хотя события регистрации генерируются в рабочих процессах) для направления сообщений в соответствующие места назначения.
Использование concurrent.futures.ProcessPoolExecutor
Если вы хотите использовать concurrent.futures.ProcessPoolExecutor для запуска ваших рабочих процессов, вам нужно создать очередь немного по-другому. Вместо
queue = multiprocessing.Queue(-1)
вы должны использовать
queue = multiprocessing.Manager().Queue(-1) # also works with the examples above
и затем вы можете заменить создание рабочего процесса из этого:
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()
на это (не забывая сначала импортировать concurrent.futures):
with concurrent.futures.ProcessPoolExecutor(max_workers=10) as executor:
for i in range(10):
executor.submit(worker_process, queue, worker_configurer)
Использование вращения файлов
Иногда вам нужно, чтобы файл журнала увеличивался до определённого размера, а затем открывался новый файл и регистрировался в нём. Вы можете захотеть сохранить определённое количество этих файлов и, когда будет создано столько файлов, повернуть файлы таким образом, чтобы как количество файлов, так и размер файлов оставались ограниченными. Для этой модели использования пакет регистрации предоставляет RotatingFileHandler:
import glob
import logging
import logging.handlers
LOG_FILENAME = 'logging_rotatingfile_example.out'
# Set up a specific logger with our desired output level
my_logger = logging.getLogger('MyLogger')
my_logger.setLevel(logging.DEBUG)
# Add the log message handler to the logger
handler = logging.handlers.RotatingFileHandler(
LOG_FILENAME, maxBytes=20, backupCount=5)
my_logger.addHandler(handler)
# Log some messages
for i in range(20):
my_logger.debug('i = %d' % i)
# See what files are created
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 на подходящее значение.
Использование альтернативных стилей форматирования
Когда ведение журнала было добавлено в стандартную библиотеку 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, потому что внутренне пакет ведения журнала использует %-форматирование для слияния строки форматирования и переменных аргументов. Это изменение невозможно без потери обратной совместимости, поскольку все вызовы ведения журнала в существующем коде будут использовать %-строки форматирования.
Однако есть способ использовать форматирование {} и $- для построения индивидуальных сообщений журнала. Помните, что для сообщения вы можете использовать произвольный объект в качестве строки форматирования сообщения, и пакет ведения журнала вызовет 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)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:
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().__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()
Вышеприведенный скрипт должен вывести сообщение Hello, world! при выполнении с Python 3.2 или более поздней версией.
Настройка LogRecord
Каждое событие ведения журнала представлено экземпляром LogRecord. Когда событие регистрируется и не отфильтровывается уровнем логгера, создаётся экземпляр LogRecord, заполненный информацией о событии, а затем передаётся обработчикам этого логгера (и его предкам, вплоть до логгера, где дальнейшее распространение по иерархии отключено). До Python 3.2 это создание происходило только в двух местах:
-
Logger.makeRecord(), который вызывается в обычном процессе регистрации события. Это вызывалоLogRecordнапрямую для создания экземпляра. -
makeLogRecord(), который вызывается со словарем, содержащим атрибуты, которые нужно добавить в LogRecord. Это обычно вызывается, когда соответствующий словарь получен по сети (например, в виде pickle черезSocketHandler, или в формате JSON через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
Вы можете использовать подкласс QueueHandler для отправки сообщений в другие типы очередей, например, в сокет ZeroMQ «publish». В примере ниже сокет создаётся отдельно и передаётся в обработчик (как его «очередь»):
import zmq # using pyzmq, the Python binding for ZeroMQ
import json # for serializing records portably
ctx = zmq.Context()
sock = zmq.Socket(ctx, zmq.PUB) # or zmq.PUSH, or other suitable value
sock.bind('tcp://*:5556') # or wherever
class ZeroMQSocketHandler(QueueHandler):
def enqueue(self, record):
self.queue.send_json(record.__dict__)
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)
super().__init__(socket)
def enqueue(self, record):
self.queue.send_json(record.__dict__)
def close(self):
self.queue.close()
Наследование от QueueListener — пример с ZeroMQ
Вы также можете унаследовать QueueListener для получения сообщений из других типов очередей, например, из сокета ZeroMQ «subscribe». Вот пример:
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_string(zmq.SUBSCRIBE, '') # subscribe to everything
socket.connect(uri)
super().__init__(socket, *handlers, **kwargs)
def dequeue(self):
msg = self.queue.recv_json()
return logging.makeLogRecord(msg)
См. также
-
Modulelogging -
Справочник по API модуля ведения журнала.
-
Modulelogging.config -
API конфигурации для модуля ведения журнала.
-
Modulelogging.handlers -
Полезные обработчики, включённые в модуль ведения журнала.
Пример конфигурации на основе словаря
Ниже приведен пример словаря конфигурации ведения журнала — он взят из документации проекта 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.
Использование rotator и namer для настройки обработки вращения логов
Пример того, как можно определить 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. Этот фрагмент используется только для иллюстративных целей.
Более сложный пример с многопроцессорностью
Следующий рабочий пример демонстрирует, как ведение журнала может использоваться с многопроцессорностью с использованием конфигурационных файлов. Конфигурации довольно просты, но служат для иллюстрации того, как могут реализовываться более сложные конфигурации в реальном многопроцессорном сценарии.
В примере основной процесс запускает процесс-слушатель и несколько рабочих процессов. У каждого — основного процесса, слушателя и рабочих процессов — есть три отдельные конфигурации (все рабочие процессы используют одну и ту же конфигурацию). Мы можем увидеть ведение журнала в основном процессе, как рабочие процессы записывают в 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:
"""
A simple handler for logging events. It runs in the listener process and
dispatches events to loggers based on the name in the received record,
which then get dispatched, by the logging system, to the handlers
configured for those loggers.
"""
def handle(self, record):
if record.name == "root":
logger = logging.getLogger()
else:
logger = logging.getLogger(record.name)
if logger.isEnabledFor(record.levelno):
# The process name is transformed just to show that it's the listener
# doing the logging to files and console
record.processName = '%s (for %s)' % (current_process().name, record.processName)
logger.handle(record)
def listener_process(q, stop_event, config):
"""
This could be done in the main process, but is just done in a separate
process for illustrative purposes.
This initialises logging according to the specified configuration,
starts the listener and waits for the main process to signal completion
via the event. The listener is then stopped, and the process exits.
"""
logging.config.dictConfig(config)
listener = logging.handlers.QueueListener(q, MyHandler())
listener.start()
if os.name == 'posix':
# On POSIX, the setup logger will have been configured in the
# parent process, but should have been disabled following the
# dictConfig call.
# On Windows, since fork isn't used, the setup logger won't
# exist in the child, so it would be created and the message
# would appear - hence the "if posix" clause.
logger = logging.getLogger('setup')
logger.critical('Should not appear, because of disabled logger ...')
stop_event.wait()
listener.stop()
def worker_process(config):
"""
A number of these are spawned for the purpose of illustration. In
practice, they could be a heterogeneous bunch of processes rather than
ones which are identical to each other.
This initialises logging according to the specified configuration,
and logs a hundred messages with random levels to randomly selected
loggers.
A small sleep is added to allow other processes a chance to run. This
is not strictly needed, but it mixes the output from the different
processes a bit more than if it's left out.
"""
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':
# On POSIX, the setup logger will have been configured in the
# parent process, but should have been disabled following the
# dictConfig call.
# On Windows, since fork isn't used, the setup logger won't
# exist in the child, so it would be created and the message
# would appear - hence the "if posix" clause.
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()
# The main process gets a simple configuration which prints to the console.
config_initial = {
'version': 1,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO'
}
},
'root': {
'handlers': ['console'],
'level': 'DEBUG'
}
}
# The worker process configuration is just a QueueHandler attached to the
# root logger, which allows all messages to be sent to the queue.
# We disable existing loggers to disable the "setup" logger used in the
# parent process. This is needed on POSIX because the logger will
# be there in the child following a fork().
config_worker = {
'version': 1,
'disable_existing_loggers': True,
'handlers': {
'queue': {
'class': 'logging.handlers.QueueHandler',
'queue': q
}
},
'root': {
'handlers': ['queue'],
'level': 'DEBUG'
}
}
# The listener process configuration shows that the full flexibility of
# logging configuration is available to dispatch events to handlers however
# you want.
# We disable existing loggers to disable the "setup" logger used in the
# parent process. This is needed on POSIX because the logger will
# be there in the child following a 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',
'formatter': 'simple',
'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',
'formatter': 'detailed',
'level': 'ERROR'
}
},
'loggers': {
'foo': {
'handlers': ['foofile']
}
},
'root': {
'handlers': ['console', 'file', 'errors'],
'level': 'DEBUG'
}
}
# Log some initial events, just to show that logging in the parent works
# normally.
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')
# We now hang around for the workers to finish their work.
for wp in workers:
wp.join()
# Workers all done, listening can now stop.
# Logging in the parent still works normally.
logger.info('Telling listener to stop ...')
stop_event.set()
lp.join()
logger.info('All done.')
if __name__ == '__main__':
main()
Вставка BOM в сообщения, отправляемые в SysLogHandler
RFC 5424 требует, чтобы сообщение Unicode отправлялось в демоне syslog в виде набора байтов со следующей структурой: необязательная чисто ASCII-компонента, за которой следует маркер порядка байтов UTF-8 (BOM), за которым следует кодировка 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.
Отформатированное сообщение будет закодировано с помощью кодировки UTF-8 SysLogHandler. Если вы следуете вышеуказанным правилам, вы должны иметь возможность создавать сообщения, совместимые с RFC 5424. Если нет, ведение журнала может не жаловаться, но ваши сообщения не будут соответствовать RFC 5424, и ваш демон syslog может пожаловаться.
Реализация структурированного ведения журнала
Хотя большинство сообщений ведения журнала предназначены для чтения людьми и, следовательно, нелегко разбираются машиной, могут быть обстоятельства, когда вы хотите выводить сообщения в структурированном формате, который может быть проанализирован программой (без необходимости сложных регулярных выражений для анализа сообщения журнала). Это легко достижимо с помощью пакета ведения журнала. Существует несколько способов достижения этого, но следующее — простой подход, который использует JSON для сериализации события в формате, разбираемом машиной:
import json
import logging
class StructuredMessage:
def __init__(self, message, /, **kwargs):
self.message = message
self.kwargs = kwargs
def __str__(self):
return '%s >>> %s' % (self.message, json.dumps(self.kwargs))
_ = StructuredMessage # optional, to improve readability
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
# This next bit is to ensure the script runs unchanged on 2.x and 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().default(o)
class StructuredMessage:
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 # optional, to improve readability
def main():
logging.basicConfig(level=logging.INFO, format='%(message)s')
logging.info(_('message 1', set_value={1, 2, 3}, snowman='\u2603'))
if __name__ == '__main__':
main()
При запуске вышеуказанного сценария выводится:
message 1 >>> {"snowman": "\u2603", "set_value": [1, 2, 3]}
Обратите внимание, что порядок элементов может отличаться в зависимости от используемой версии Python.
Настройка обработчиков с 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':{
# The values below are popped from this dictionary and
# used to create the handler, set the handler's level and
# its formatter.
'()': owned_file_handler,
'level':'DEBUG',
'formatter': 'default',
# The values below are passed to the handler creator callable
# as keyword arguments.
'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':{
# The values below are popped from this dictionary and
# used to create the handler, set the handler's level and
# its formatter.
'()': owned_file_handler,
'level':'DEBUG',
'formatter': 'default',
# The values below are passed to the handler creator callable
# as keyword arguments.
'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 — например, один из обработчиков вращения файлов или другой тип обработчика вообще.
Использование определенных стилей форматирования в вашем приложении
В Python 3.2, у Formatter появился ключевой параметр style, который, хотя и по умолчанию % для обеспечения обратной совместимости, позволял указать { или $ для поддержки подходов форматирования, поддерживаемых str.format() и string.Template. Обратите внимание, что это управляет форматированием сообщений логирования для конечного вывода в журналы и полностью ортогонально тому, как строится отдельное сообщение логирования.
Вызовы логирования (debug(), info() и т.д.) принимают только позиционные параметры для самого сообщения логирования, а ключевые параметры используются только для определения параметров обработки вызова логирования (например, ключевой параметр exc_info для указания, что информация об отладке должна быть записана в журнал, или ключевой параметр extra для указания дополнительной контекстной информации, которая должна быть добавлена в журнал). Поэтому вы не можете напрямую использовать вызовы логирования с синтаксисом str.format() или string.Template, потому что внутренне пакет логирования использует %-форматирование для объединения строки форматирования и аргументов переменных. Изменение этого при сохранении обратной совместимости невозможно, так как все вызовы логирования, существующие в существующем коде, будут использовать %-строки форматирования.
Были предложения о связывании стилей форматирования со специфическими логгерами, но этот подход также сталкивается с проблемами обратной совместимости, поскольку любой существующий код может использовать имя данного логгера и использовать %-форматирование.
Для того, чтобы логирование работало согласованно между сторонними библиотеками и вашим кодом, решения о форматировании должны приниматься на уровне отдельного вызова логирования. Это открывает несколько способов адаптации альтернативных стилей форматирования.
Использование фабрик LogRecord
В Python 3.2, наряду с изменениями в Formatter, упомянутыми выше, пакет логирования получил возможность позволить пользователям устанавливать свои подклассы LogRecord с использованием функции setLogRecordFactory(). Вы можете использовать это для установки собственного подкласса LogRecord, который сделает правильное форматирование, переопределяя метод getMessage(). В реализации базового класса этого метода происходит %-форматирование msg % args, и вы можете заменить его альтернативным форматированием; однако, следует позаботиться о поддержке всех стилей форматирования и позволении %-форматирования по умолчанию, чтобы гарантировать взаимодействие с другим кодом. Также следует позаботиться об вызове str(self.msg), точно так же, как это делает базовая реализация.
Для получения дополнительной информации обратитесь к справочной документации по setLogRecordFactory() и LogRecord.
Использование пользовательских объектов сообщений
Существует еще один, возможно, более простой способ использовать форматирование {} и $ для создания отдельных сообщений журнала. Вы, возможно, помните (из Использование произвольных объектов в качестве сообщений), что при ведении журнала можно использовать произвольный объект в качестве строки форматирования сообщения, и что пакет логирования вызовет 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)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()
Вы можете настроить фильтры с помощью 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в приведенном выше примере. - Помимо фильтров, этот метод также можно использовать для настройки пользовательских обработчиков и форматировщиков. См. Пользовательские объекты для получения дополнительной информации о том, как логирование поддерживает использование пользовательских объектов в своей конфигурации, и см. другой рецепт кулинарной книги Настройка обработчиков с помощью dictConfig() выше.
Настройка форматирования исключений
Возможно, вам понадобятся настраиваемые форматирование исключений — для примера, предположим, что вы хотите ровно одну строку на событие журнала, даже когда присутствует информация об исключении. Вы можете сделать это с помощью пользовательского класса форматировщика, как показано в следующем примере:
import logging
class OneLineExceptionFormatter(logging.Formatter):
def formatException(self, exc_info):
"""
Format an exception so that it prints on a single line.
"""
result = super().formatException(exc_info)
return repr(result) # or format into one line however you want to
def format(self, record):
s = super().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 может быть полезен для более специализированных потребностей.
Сообщения об ошибках в формате речи
Возможны ситуации, когда желательно, чтобы сообщения об ошибках отображались в формате речи, а не в видимой форме. Это легко сделать, если в вашей системе доступна функция преобразования текста в речь (TTS), даже если у неё нет привязки к Python. Большинство систем TTS имеют программу командной строки, которую можно запустить, и к ней можно обратиться из обработчика, используя subprocess. Предполагается, что программы TTS командной строки не ожидают взаимодействия с пользователем или не требуют длительного времени для завершения, а частота сообщений об ошибках не будет настолько высокой, чтобы завалить пользователя сообщениями, и приемлемо, чтобы сообщения воспроизводились по одному, а не одновременно. Приведённая ниже реализация ожидает, пока одно сообщение не будет произнесено, прежде чем будет обработано следующее, и это может привести к тому, что другие обработчики будут ждать. Ниже представлен короткий пример, демонстрирующий подход, предполагающий, что пакет TTS espeak доступен:
import logging
import subprocess
import sys
class TTSHandler(logging.Handler):
def emit(self, record):
msg = self.format(record)
# Speak slowly in a female English voice
cmd = ['espeak', '-s150', '-ven+f3', msg]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
# wait for the program to finish
p.communicate()
def configure_logging():
h = TTSHandler()
root = logging.getLogger()
root.addHandler(h)
# the default formatter just returns the message
root.setLevel(logging.DEBUG)
def main():
logging.info('Hello')
logging.debug('Goodbye')
if __name__ == '__main__':
configure_logging()
sys.exit(main())
При запуске этого скрипта должно быть произнесено «Привет» и затем «До свидания» женским голосом.
Конечно, вышеуказанный подход можно адаптировать к другим системам TTS и даже к другим системам в целом, которые могут обрабатывать сообщения через внешние программы, запущенные из командной строки.
Буферизация сообщений об ошибках и их условный вывод
Могут возникнуть ситуации, когда вам нужно записывать сообщения об ошибках во временной области и выводить их только в том случае, если выполняется определённое условие. Например, вы можете начать записывать события отладки в функции, и если функция завершится без ошибок, вы не хотите засорять журнал собранной информацией об отладке, но если произошла ошибка, вы хотите, чтобы вся информация об отладке также была выведена, наряду с ошибкой.
Ниже приведен пример, демонстрирующий, как можно сделать это с помощью декоратора для ваших функций, где вы хотите, чтобы поведение журналирования было таким. Он использует logging.handlers.MemoryHandler, который позволяет буферизовать события об ошибках, пока не возникнет какое-либо условие, после чего буферизованные события flushed - передаются другому обработчику (обработчику target) для обработки. По умолчанию, MemoryHandler очищается, когда его буфер заполняется или появляется событие, уровень которого не ниже заданного порога. Вы можете использовать этот рецепт с более специализированным подклассом MemoryHandler, если хотите настроить поведение очистки.
В примере скрипта есть простая функция foo, которая просто циклически перебирает все уровни ведения журнала, записывая в sys.stderr для указания уровня, который она собирается залогировать, а затем фактически залогировывает сообщение на этом уровне. Вы можете передать параметр в foo, который, если он имеет значение True, будет регистрировать события на уровнях ERROR и CRITICAL, а в противном случае — только на уровнях DEBUG, INFO и WARNING.
Скрипт просто организует привязку декоратора к foo с декоратором, который выполнит требуемое условное ведение журнала. Декоратор принимает регистратор в качестве параметра и прикрепляет обработчик памяти на время вызова декорированной функции. Декоратор дополнительно может иметь параметры: целевой обработчик, уровень, на котором должна произойти очистка, и емкость буфера (число записей, которые буферизуются). По умолчанию используются StreamHandler, который записывает в sys.stderr, logging.ERROR и 100 соответственно.
Вот скрипт:
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) с помощью конфигурации
Иногда нужно форматировать время с использованием 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, по одному для каждого обработчика.
Использование контекстного менеджера для избирательного ведения журнала
Бывают случаи, когда полезно временно изменить конфигурацию ведения журнала и вернуть её обратно после выполнения чего-либо. Для этого контекстный менеджер — наиболее очевидный способ сохранения и восстановления контекста ведения журнала. Вот простой пример такого контекстного менеджера, который позволяет вам необязательно изменить уровень ведения журнала и добавить обработчик ведения журнала только в области действия контекстного менеджера:
import logging
import sys
class LoggingContext:
def __init__(self, logger, level=None, handler=None, close=True):
self.logger = logger
self.level = level
self.handler = handler
self.close = close
def __enter__(self):
if self.level is not None:
self.old_level = self.logger.level
self.logger.setLevel(self.level)
if self.handler:
self.logger.addHandler(self.handler)
def __exit__(self, et, ev, tb):
if self.level is not None:
self.logger.setLevel(self.old_level)
if self.handler:
self.logger.removeHandler(self.handler)
if self.handler and self.close:
self.handler.close()
# implicit return of None => don't swallow exceptions
Если вы укажете значение уровня, уровень регистратора устанавливается на это значение в области действия блока with, охватываемого контекстным менеджером. Если вы укажете обработчик, он добавляется в регистратор при входе в блок и удаляется при выходе из блока. Вы также можете попросить менеджера закрыть обработчик при выходе из блока — вы можете сделать это, если вам больше не нужен обработчик.
Чтобы продемонстрировать, как это работает, мы можем добавить следующий блок кода к вышеуказанному:
if __name__ == '__main__':
logger = logging.getLogger('foo')
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.INFO)
logger.info('1. This should appear just once on stderr.')
logger.debug('2. This should not appear.')
with LoggingContext(logger, level=logging.DEBUG):
logger.debug('3. This should appear once on stderr.')
logger.debug('4. This should not appear.')
h = logging.StreamHandler(sys.stdout)
with LoggingContext(logger, level=logging.DEBUG, handler=h, close=True):
logger.debug('5. This should appear twice - once on stderr and once on stdout.')
logger.info('6. This should appear just once on stderr.')
logger.debug('7. This should not appear.')
Изначально уровень регистратора установлен на INFO, поэтому сообщение №1 появляется, а сообщение №2 нет. Затем мы временно изменяем уровень на DEBUG в следующем with блоке, и поэтому сообщение №3 появляется. После выхода из блока уровень регистратора восстанавливается до INFO, и поэтому сообщение №4 не появляется. В следующем with блоке мы снова устанавливаем уровень на DEBUG, но также добавляем обработчик, записывающий в sys.stdout. Таким образом, сообщение №5 появляется дважды на консоли (один раз через stderr и один раз через stdout). После завершения инструкции with, состояние возвращается к таковому, каким оно было до этого, поэтому сообщение №6 появляется (как сообщение №1), в то время как сообщение №7 не появляется (как сообщение №2).
Если мы запустим получившийся скрипт, результат будет следующим:
$ python logctx.py 1. This should appear just once on stderr. 3. This should appear once on stderr. 5. This should appear twice - once on stderr and once on stdout. 5. This should appear twice - once on stderr and once on stdout. 6. This should appear just once on stderr.
Если мы запустим его снова, но перенаправим stderr в /dev/null, мы увидим следующее, что является единственным сообщением, записанным в stdout:
$ python logctx.py 2>/dev/null 5. This should appear twice - once on stderr and once on stdout.
Еще раз, но перенаправляя stdout в /dev/null, мы получим:
$ python logctx.py >/dev/null 1. This should appear just once on stderr. 3. This should appear once on stderr. 5. This should appear twice - once on stderr and once on stdout. 6. This should appear just once on stderr.
В этом случае сообщение №5, напечатанное в stdout не появляется, как ожидалось.
Конечно, описанный здесь подход может быть обобщён, например, для временного присоединения фильтров ведения журнала. Обратите внимание, что приведенный выше код работает как в Python 2, так и в Python 3.
Шаблон приложения командной строки
Вот пример, который показывает, как вы можете:
- Использовать уровень ведения журнала, основанный на аргументах командной строки
- Перенаправлять на несколько подкоманд в отдельных файлах, все они ведут журнал на одном уровне последовательно
- Использовать простую минимальную конфигурацию
Предположим, у нас есть приложение командной строки, задача которого — остановить, запустить или перезапустить некоторые сервисы. Для иллюстрации это можно организовать как файл app.py, который является основным скриптом приложения, с отдельными командами, реализованными в start.py, stop.py и restart.py. Предположим также, что мы хотим управлять объёмом информации в приложении через аргумент командной строки, по умолчанию logging.INFO. Вот один из способов, как можно написать app.py:
import argparse
import importlib
import logging
import os
import sys
def main(args=None):
scriptname = os.path.basename(__file__)
parser = argparse.ArgumentParser(scriptname)
levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
parser.add_argument('--log-level', default='INFO', choices=levels)
subparsers = parser.add_subparsers(dest='command',
help='Available commands:')
start_cmd = subparsers.add_parser('start', help='Start a service')
start_cmd.add_argument('name', metavar='NAME',
help='Name of service to start')
stop_cmd = subparsers.add_parser('stop',
help='Stop one or more services')
stop_cmd.add_argument('names', metavar='NAME', nargs='+',
help='Name of service to stop')
restart_cmd = subparsers.add_parser('restart',
help='Restart one or more services')
restart_cmd.add_argument('names', metavar='NAME', nargs='+',
help='Name of service to restart')
options = parser.parse_args()
# the code to dispatch commands could all be in this file. For the purposes
# of illustration only, we implement each command in a separate module.
try:
mod = importlib.import_module(options.command)
cmd = getattr(mod, 'command')
except (ImportError, AttributeError):
print('Unable to find the code for command \'%s\'' % options.command)
return 1
# Could get fancy here and load configuration from file or dictionary
logging.basicConfig(level=options.log_level,
format='%(levelname)s %(name)s %(message)s')
cmd(options)
if __name__ == '__main__':
sys.exit(main())
И start, stop и restart команды могут быть реализованы в отдельных модулях, например, для запуска:
# start.py
import logging
logger = logging.getLogger(__name__)
def command(options):
logger.debug('About to start %s', options.name)
# actually do the command processing here ...
logger.info('Started the \'%s\' service.', options.name)
и аналогично для остановки:
# stop.py
import logging
logger = logging.getLogger(__name__)
def command(options):
n = len(options.names)
if n == 1:
plural = ''
services = '\'%s\'' % options.names[0]
else:
plural = 's'
services = ', '.join('\'%s\'' % name for name in options.names)
i = services.rfind(', ')
services = services[:i] + ' and ' + services[i + 2:]
logger.debug('About to stop %s', services)
# actually do the command processing here ...
logger.info('Stopped the %s service%s.', services, plural)
и аналогично для перезапуска:
# restart.py
import logging
logger = logging.getLogger(__name__)
def command(options):
n = len(options.names)
if n == 1:
plural = ''
services = '\'%s\'' % options.names[0]
else:
plural = 's'
services = ', '.join('\'%s\'' % name for name in options.names)
i = services.rfind(', ')
services = services[:i] + ' and ' + services[i + 2:]
logger.debug('About to restart %s', services)
# actually do the command processing here ...
logger.info('Restarted the %s service%s.', services, plural)
Если мы запустим это приложение с уровнем журнала по умолчанию, мы получим вывод, подобный этому:
$ python app.py start foo INFO start Started the 'foo' service. $ python app.py stop foo bar INFO stop Stopped the 'foo' and 'bar' services. $ python app.py restart foo bar baz INFO restart Restarted the 'foo', 'bar' and 'baz' services.
Первое слово — уровень ведения журнала, а второе слово — имя модуля или пакета, где было залогировано событие.
Если мы изменим уровень ведения журнала, мы можем изменить информацию, отправленную в журнал. Например, если мы хотим больше информации:
$ python app.py --log-level DEBUG start foo DEBUG start About to start foo INFO start Started the 'foo' service. $ python app.py --log-level DEBUG stop foo bar DEBUG stop About to stop 'foo' and 'bar' INFO stop Stopped the 'foo' and 'bar' services. $ python app.py --log-level DEBUG restart foo bar baz DEBUG restart About to restart 'foo', 'bar' and 'baz' INFO restart Restarted the 'foo', 'bar' and 'baz' services.
А если мы хотим меньше:
$ python app.py --log-level WARNING start foo $ python app.py --log-level WARNING stop foo bar $ python app.py --log-level WARNING restart foo bar baz
В этом случае команды ничего не выводят на консоль, так как ничего на уровне WARNING или выше не регистрируется ими.
Qt GUI для ведения журнала
Периодически возникает вопрос о том, как вести журнал в приложении с графическим интерфейсом. Фреймворк Qt — это популярный кроссплатформенный фреймворк для графического интерфейса с привязками к Python, использующий библиотеки PySide2 или PyQt5.
Следующий пример показывает, как вести журнал в Qt GUI. Это вводит простой класс QtHandler, который принимает вызываемый объект, который должен быть слотом в главном потоке, выполняющим обновления графического интерфейса. Также создаётся рабочий поток, чтобы показать, как вы можете вести журнал в графическом интерфейсе как из самого интерфейса (через кнопку для ручного ведения журнала), так и из рабочего потока, выполняющего работу в фоновом режиме (здесь просто регистрируются сообщения на случайных уровнях с случайными короткими задержками между ними).
Рабочий поток реализован с использованием класса Qt QThread вместо модуля threading, так как есть ситуации, где необходимо использовать QThread, что обеспечивает лучшую интеграцию с другими компонентами Qt.
Код должен работать с недавними выпусками как PySide2, так и PyQt5. Вы должны иметь возможность адаптировать подход к более ранним версиям Qt. Подробную информацию см. в комментариях к фрагменту кода.
import datetime
import logging
import random
import sys
import time
# Deal with minor differences between PySide2 and PyQt5
try:
from PySide2 import QtCore, QtGui, QtWidgets
Signal = QtCore.Signal
Slot = QtCore.Slot
except ImportError:
from PyQt5 import QtCore, QtGui, QtWidgets
Signal = QtCore.pyqtSignal
Slot = QtCore.pyqtSlot
logger = logging.getLogger(__name__)
#
# Signals need to be contained in a QObject or subclass in order to be correctly
# initialized.
#
class Signaller(QtCore.QObject):
signal = Signal(str, logging.LogRecord)
#
# Output to a Qt GUI is only supposed to happen on the main thread. So, this
# handler is designed to take a slot function which is set up to run in the main
# thread. In this example, the function takes a string argument which is a
# formatted log message, and the log record which generated it. The formatted
# string is just a convenience - you could format a string for output any way
# you like in the slot function itself.
#
# You specify the slot function to do whatever GUI updates you want. The handler
# doesn't know or care about specific UI elements.
#
class QtHandler(logging.Handler):
def __init__(self, slotfunc, *args, **kwargs):
super().__init__(*args, **kwargs)
self.signaller = Signaller()
self.signaller.signal.connect(slotfunc)
def emit(self, record):
s = self.format(record)
self.signaller.signal.emit(s, record)
#
# This example uses QThreads, which means that the threads at the Python level
# are named something like "Dummy-1". The function below gets the Qt name of the
# current thread.
#
def ctname():
return QtCore.QThread.currentThread().objectName()
#
# Used to generate random levels for logging.
#
LEVELS = (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR,
logging.CRITICAL)
#
# This worker class represents work that is done in a thread separate to the
# main thread. The way the thread is kicked off to do work is via a button press
# that connects to a slot in the worker.
#
# Because the default threadName value in the LogRecord isn't much use, we add
# a qThreadName which contains the QThread name as computed above, and pass that
# value in an "extra" dictionary which is used to update the LogRecord with the
# QThread name.
#
# This example worker just outputs messages sequentially, interspersed with
# random delays of the order of a few seconds.
#
class Worker(QtCore.QObject):
@Slot()
def start(self):
extra = {'qThreadName': ctname() }
logger.debug('Started work', extra=extra)
i = 1
# Let the thread run until interrupted. This allows reasonably clean
# thread termination.
while not QtCore.QThread.currentThread().isInterruptionRequested():
delay = 0.5 + random.random() * 2
time.sleep(delay)
level = random.choice(LEVELS)
logger.log(level, 'Message after delay of %3.1f: %d', delay, i, extra=extra)
i += 1
#
# Implement a simple UI for this cookbook example. This contains:
#
# * A read-only text edit window which holds formatted log messages
# * A button to start work and log stuff in a separate thread
# * A button to log something from the main thread
# * A button to clear the log window
#
class Window(QtWidgets.QWidget):
COLORS = {
logging.DEBUG: 'black',
logging.INFO: 'blue',
logging.WARNING: 'orange',
logging.ERROR: 'red',
logging.CRITICAL: 'purple',
}
def __init__(self, app):
super().__init__()
self.app = app
self.textedit = te = QtWidgets.QPlainTextEdit(self)
# Set whatever the default monospace font is for the platform
f = QtGui.QFont('nosuchfont')
f.setStyleHint(f.Monospace)
te.setFont(f)
te.setReadOnly(True)
PB = QtWidgets.QPushButton
self.work_button = PB('Start background work', self)
self.log_button = PB('Log a message at a random level', self)
self.clear_button = PB('Clear log window', self)
self.handler = h = QtHandler(self.update_status)
# Remember to use qThreadName rather than threadName in the format string.
fs = '%(asctime)s %(qThreadName)-12s %(levelname)-8s %(message)s'
formatter = logging.Formatter(fs)
h.setFormatter(formatter)
logger.addHandler(h)
# Set up to terminate the QThread when we exit
app.aboutToQuit.connect(self.force_quit)
# Lay out all the widgets
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(te)
layout.addWidget(self.work_button)
layout.addWidget(self.log_button)
layout.addWidget(self.clear_button)
self.setFixedSize(900, 400)
# Connect the non-worker slots and signals
self.log_button.clicked.connect(self.manual_update)
self.clear_button.clicked.connect(self.clear_display)
# Start a new worker thread and connect the slots for the worker
self.start_thread()
self.work_button.clicked.connect(self.worker.start)
# Once started, the button should be disabled
self.work_button.clicked.connect(lambda : self.work_button.setEnabled(False))
def start_thread(self):
self.worker = Worker()
self.worker_thread = QtCore.QThread()
self.worker.setObjectName('Worker')
self.worker_thread.setObjectName('WorkerThread') # for qThreadName
self.worker.moveToThread(self.worker_thread)
# This will start an event loop in the worker thread
self.worker_thread.start()
def kill_thread(self):
# Just tell the worker to stop, then tell it to quit and wait for that
# to happen
self.worker_thread.requestInterruption()
if self.worker_thread.isRunning():
self.worker_thread.quit()
self.worker_thread.wait()
else:
print('worker has already exited.')
def force_quit(self):
# For use when the window is closed
if self.worker_thread.isRunning():
self.kill_thread()
# The functions below update the UI and run in the main thread because
# that's where the slots are set up
@Slot(str, logging.LogRecord)
def update_status(self, status, record):
color = self.COLORS.get(record.levelno, 'black')
s = '<pre><font color="%s">%s</font></pre>' % (color, status)
self.textedit.appendHtml(s)
@Slot()
def manual_update(self):
# This function uses the formatted message passed in, but also uses
# information from the record to format the message in an appropriate
# color according to its severity (level).
level = random.choice(LEVELS)
extra = {'qThreadName': ctname() }
logger.log(level, 'Manually logged!', extra=extra)
@Slot()
def clear_display(self):
self.textedit.clear()
def main():
QtCore.QThread.currentThread().setObjectName('MainThread')
logging.getLogger().setLevel(logging.DEBUG)
app = QtWidgets.QApplication(sys.argv)
example = Window(app)
example.show()
sys.exit(app.exec_())
if __name__=='__main__':
main()
© 2001–2022 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.8/howto/logging-cookbook.html