Logging Cookbook

  • Author

    • Vinay Sajip <vinay_sajip at red-dove dot com>

该页面包含许多与日志记录有关的食谱,这些食谱在过去被发现有用。

使用登录多个模块

多次调用logging.getLogger('someLogger')会返回对同一 Logger 对象的引用。只要在同一 Python 解释器进程中,不仅在同一模块内,而且在各个模块之间都是如此。对于相同对象的引用是正确的。此外,应用程序代码可以在一个模块中定义和配置父 Logger,并在单独的模块中创建(但不配置)子 Logger,并且所有对子 Logger 的调用都将传递给父 Logger。这是一个主要模块:

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

这显示了日志输出如预期的那样散布。当然,这种方法适用于比此处显示的线程更多的线程。

多个处理程序和格式化程序

Logger 是普通的 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.warn('warn message')
logger.error('error message')
logger.critical('critical message')

请注意,“应用程序”代码并不关心多个处理程序。所做的更改只是添加和配置了名为* fh *的新处理程序。

使用较高或较低严重性过滤器创建新的处理程序的能力在编写和测试应用程序时非常有用。而不是使用许多print语句进行调试,而使用logger.debug:与 print 语句不同,在稍后您将其删除或 Comments 掉之后,logger.debug 语句可以在源代码中保持不变并保持休眠状态,直到再次需要它们为止。那时,唯一需要进行的更改是修改 Logger 和/或处理程序的严重性级别以进行调试。

登录到多个目的地

假设您要使用不同的消息格式和在不同的情况下登录控制台和文件。假设您要记录 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.warn('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')

pass网络发送和接收日志记录事件

假设您要pass网络发送日志记录事件,并在接收端进行处理。一种简单的方法是在发送端将SocketHandler实例附加到根 Logger:

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 = 1

    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()

首先运行服务器,然后运行 Client 端。在 Client 端,控制台上未打印任何内容。在服务器端,您应该看到类似以下内容:

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()方法并在那里实现替代方案,并改编上述脚本以使用替代序列化方案。

将上下文信息添加到您的日志记录输出中

有时您希望日志输出除了传递给日志调用的参数外,还包含上下文信息。例如,在联网应用中,可能希望在日志中记录特定于 Client 端的信息(例如,远程 Client 端的用户名或 IP 地址)。尽管可以使用* extra *参数来实现此目的,但是以这种方式传递信息并不总是很方便。尽管可能很想在每个连接的基础上创建Logger个实例,但这不是一个好主意,因为这些实例不会被垃圾回收。尽管实际上这不是问题,但当Logger实例的数量取决于您要在记录应用程序时使用的粒度级别时,如果Logger实例的数量实际上变得无界,则可能很难 Management。

使用 LoggerAdapters 传递上下文信息

传递上下文信息与日志事件信息一起输出的一种简单方法是使用LoggerAdapter类。此类设计为看起来像Logger,因此您可以调用debug()info()warning()error()exception()critical()log()。这些方法与Logger中的方法具有相同的签名,因此您可以互换使用两种类型的实例。

创建LoggerAdapter的实例时,您将为其传递Logger实例和一个包含上下文信息的类似 dict 的对象。当您在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)

LoggerAdapterprocess()方法是将上下文信息添加到日志输出的位置。它传递了日志记录调用的消息和关键字参数,并将它们的(可能)修改后的版本传递回用于基础 Logger 的调用中。该方法的默认实现不考虑消息,而是在关键字参数中插入“额外”键,其值是传递给构造函数的类似 dict 的对象。当然,如果您在对适配器的调用中传递了“ extra”关键字参数,则它将被静默覆盖。

使用“额外”的优点是,将类似 dict 的对象中的值合并到LogRecord实例的__dict_中,从而使您可以将自定义字符串与Formatter实例一起使用,这些字符串了解类似 dict 的对象的键。如果您需要其他方法,例如如果要在消息字符串的前面或后面添加上下文信息,则只需要子类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的值。

使用除字典以外的对象传递上下文信息

您不需要将实际的 dict 传递给LoggerAdapter-您可以传递实现__getitem____iter__的类的实例,这样它看起来像是记录日志的 dict。如果要动态生成值(而 dict 中的值将是恒定的),这将很有用。

使用过滤器传递上下文信息

您还可以使用用户定义的Filter将上下文信息添加到日志输出中。允许Filter实例修改传递给它们的LogRecords,包括添加其他属性,然后可以使用适当的格式字符串或需要的自定义Formatter输出这些属性。

例如,在 Web 应用程序中,可以将正在处理的请求(或至少是其中有趣的部分)存储在 threadlocal(threading.local)变量中,然后从Filter访问以添加例如来自请求的信息- ,即LogRecord的远程 IP 地址和远程用户的用户名-,使用属性名'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,并有一个单独的进程,该进程实现一个套接字服务器,该服务器从套接字读取并记录到文件。 (如果愿意,可以在现有进程之一中指定一个线程来执行此Function.)This section详细记录了此方法,并包括一个有效的套接字接收器,可以用作您适应自己的起点。应用程序。

如果您正在使用包含multiprocessing模块的最新版本的 Python,则可以编写自己的处理程序,该处理程序使用此模块中的Lock类来序列化对进程的文件访问。现有的FileHandler和子类目前不使用multiprocessing,尽管将来可能会使用。请注意,目前multiprocessing模块并非在所有平台上都提供工作锁定Function(请参见https://bugs.python.org/issue3770)。

使用文件旋转

有时您想让日志文件增长到一定大小,然后打开一个新文件并记录到该文件。您可能需要保留一定数量的这些文件,并且在创建了那么多文件之后,旋转文件,以使文件数和文件大小都保持有界。对于此使用模式,日志记录包提供了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 *设置为适当的值。

示例基于字典的配置

以下是日志记录配置字典的示例-取自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 文档的relevant section

将 BOM 插入发送到 SysLogHandler 的消息中

RFC 5424要求将 Unicode 消息作为一组字节发送到 syslog 守护程序,该字节具有以下结构:可选的纯 ASCII 组件,后跟 UTF-8 字节 Sequences 标记(BOM),然后是使用 UTF- 8. (请参见规范的相关部分。)

在 Python 2.6 和 2.7 中,代码已添加到SysLogHandler中,以将 BOM 表插入消息中,但是不幸的是,它的实现不正确,因为 BOM 表出现在消息的开头,因此不允许任何纯 ASCII 组件出现在消息之前。

由于此行为已被破坏,因此错误的 BOM 插入代码已从 Python 2.7.4 及更高版本中删除。但是,它并没有被替换,并且如果您想生成兼容 RFC 5424 的消息,其中包括 BOM,使用 UTF-8 编码的 BOM 表,可选的纯 ASCII 序列以及其后的任意 Unicode,那么您需要以下:

u'ASCII section\ufeffUnicode section'

使用 UTF-8 编码的 Unicode 代码点u'\ufeff'将被编码为 UTF-8 BOM –字节串'\xef\xbb\xbf'

  • 用任何喜欢的占位符替换 ASCII 节,但要确保替换后出现在其中的数据始终是 ASCII(这样,在 UTF-8 编码后,数据将保持不变)。

  • 用您喜欢的任何占位符替换 Unicode 部分;如果替换后出现的数据中包含的字符超出了 ASCII 范围,则可以-使用 UTF-8 进行编码。

如果格式化的消息是 Unicode,则它将*由SysLogHandler使用 UTF-8 编码进行编码。如果遵循上述规则,则应该能够生成符合 RFC 5424 的消息。如果您不这样做,则日志记录可能不会抱怨,但是您的消息将不符合 RFC 5424,并且您的 syslog 守护程序可能会抱怨。

实施结构化日志记录

尽管大多数日志记录消息都是供人阅读的,因此不容易pass机器解析,但是在某些情况下,您可能希望以结构化格式输出消息,该结构化格式能够被程序解析(而无需复杂的正则表达式)解析日志消息)。使用日志记录包可以轻松实现。有多种方法可以实现此目的,但是以下是一种简单的方法,该方法使用 JSON 以机器可解析的方式序列化事件:

import json
import logging

class StructuredMessage(object):
    def __init__(self, message, **kwargs):
        self.message = message
        self.kwargs = kwargs

    def __str__(self):
        return '%s >>> %s' % (self.message, json.dumps(self.kwargs))

_ = StructuredMessage   # 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 版本,项目的 Sequences 可能会有所不同。

如果需要更专业的处理,则可以使用自定义 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(Encoder, self).default(o)

class StructuredMessage(object):
    def __init__(self, message, **kwargs):
        self.message = message
        self.kwargs = kwargs

    def __str__(self):
        s = Encoder().encode(self.kwargs)
        return '%s >>> %s' % (self.message, s)

_ = StructuredMessage   # optional, to improve readability

def main():
    logging.basicConfig(level=logging.INFO, format='%(message)s')
    logging.info(_('message 1', set_value=set([1, 2, 3]), snowman='\u2603'))

if __name__ == '__main__':
    main()

运行上面的脚本时,它会打印:

message 1 >>> {"snowman": "\u2603", "set_value": [1, 2, 3]}

请注意,根据所使用的 Python 版本,项目的 Sequences 可能会有所不同。

使用 dictConfig()自定义处理程序

有时您想以特定方式自定义日志处理程序,如果您使用dictConfig(),则无需子类就可以做到这一点。例如,考虑您可能要设置日志文件的所有权。在 POSIX 上,可以使用os.chown()轻松完成此操作,但是 stdlib 中的文件处理程序不提供内置支持。您可以使用简单的函数来自定义处理程序的创建,例如:

def owned_file_handler(filename, mode='a', encoding=None, owner=None):
    if owner:
        import os, pwd, grp
        # convert user and group names to uid and gid
        uid = pwd.getpwnam(owner[0]).pw_uid
        gid = grp.getgrnam(owner[1]).gr_gid
        owner = (uid, gid)
        if not os.path.exists(filename):
            open(filename, 'a').close()
        os.chown(filename, *owner)
    return logging.FileHandler(filename, mode, encoding)

然后,您可以在传递给dictConfig()的日志记录配置中指定pass调用此函数来创建日志记录处理程序:

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()出现在此处。这种方法应适用于任何支持dictConfig()的 Python 版本-即 python 2.7、3.2 或更高版本。在 3.3 之前的版本中,您需要使用来实现实际的所有权更改。 os.chown()

实际上,创建处理程序的Function可能位于项目中某个地方的 Util 模块中。代替配置中的行:

'()': owned_file_handler,

您可以使用例如:

'()': 'ext://project.util.owned_file_handler',

其中project.util可以替换为函数所在包的实际名称。在上面的工作脚本中,应使用'ext://__main__.owned_file_handler'。在这里,实际的可调用项由ext://规范中的dictConfig()解析。

希望本示例还指出了如何实现其他类型的文件更改的方式-例如使用os.chmod()以相同的方式设置特定的 POSIX 权限位。

当然,该方法还可以扩展到除FileHandler之外的其他类型的处理程序-例如,旋转文件处理程序之一,或完全不同的处理程序类型。

使用 dictConfig()配置过滤器

您可以*使用_ 配置过滤器,尽管乍看之下操作方法可能并不明显(因此该方法)。由于Filter是标准库中唯一包含的过滤器类,并且不太可能满足许多要求(仅作为 Base Class 存在),因此通常需要使用覆盖的filter()方法定义自己的Filter子类。为此,请在过滤器的配置字典中指定()键,并指定将用于创建过滤器的 callable(一个类是最明显的,但是您可以提供任何返回Filter实例的 callable)。这是一个完整的示例:

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

  • 除过滤器外,此技术还可用于配置自定义处理程序和格式化程序。有关日志如何支持在配置中使用用户定义的对象的更多信息,请参见User-defined objects,请参阅上面的其他食谱使用 dictConfig()自定义处理程序

自定义 exception 格式

有时您可能想要进行自定义的异常格式设置-出于参数的考虑,假设即使存在异常信息,每个记录的事件也只需要一行。您可以使用自定义格式器类来执行此操作,如以下示例所示:

import logging

class OneLineExceptionFormatter(logging.Formatter):
    def formatException(self, exc_info):
        """
        Format an exception so that it prints on a single line.
        """
        result = super(OneLineExceptionFormatter, self).formatException(exc_info)
        return repr(result) # or format into one line however you want to

    def format(self, record):
        s = super(OneLineExceptionFormatter, self).format(record)
        if record.exc_text:
            s = s.replace('\n', '') + '|'
        return s

def configure_logging():
    fh = logging.FileHandler('output.txt', 'w')
    f = OneLineExceptionFormatter('%(asctime)s|%(levelname)s|%(message)s|',
                                  '%d/%m/%Y %H:%M:%S')
    fh.setFormatter(f)
    root = logging.getLogger()
    root.setLevel(logging.DEBUG)
    root.addHandler(fh)

def main():
    configure_logging()
    logging.info('Sample message')
    try:
        x = 1 / 0
    except ZeroDivisionError as e:
        logging.exception('ZeroDivisionError: %s', e)

if __name__ == '__main__':
    main()

运行时,这将产生一个包含两行的文件:

28/01/2015 07:21:23|INFO|Sample message|
28/01/2015 07:21:23|ERROR|ZeroDivisionError: integer division or modulo by zero|'Traceback (most recent call last):\n  File "logtest7.py", line 30, in main\n    x = 1 / 0\nZeroDivisionError: integer division or modulo by zero'|

尽管上面的处理很简单,但是它为如何根据自己的喜好格式化异常信息指明了方向。 traceback模块可能有助于满足更多特殊需求。

记录日志信息

在某些情况下,希望以可听而不是可见的格式呈现日志消息。如果您的系统中具有文本转语音(TTS)Function,即使它没有 Python 绑定,也很容易做到。大多数 TTS 系统都有您可以运行的命令行程序,可以使用subprocess从处理程序中调用该程序。这里假设 TTS 命令行程序不会与用户交互或花费很长时间才能完成,并且记录消息的频率不会很高,不会使用户充满消息,并且拥有一次而不是同时说出一条消息,下面的示例实现在处理另一条消息之前先 await 一条消息,这可能会使其他处理程序保持 await 状态。这是一个显示此方法的简短示例,它假定espeak TTS 包可用:

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 系统,甚至可以与其他可以pass命令行运行的外部程序处理消息的系统一起使用。

缓冲日志消息并有条件地输出

在某些情况下,您可能希望将消息记录在临时区域中,并且仅在发生特定情况时才输出消息。例如,您可能要开始在一个函数中记录调试事件,并且如果该函数完成而没有错误,则您不希望使用收集的调试信息使日志混乱,但是,如果有错误,则需要所有调试要输出的信息以及错误。

这是一个示例,显示了如何使用装饰器为希望日志记录以这种方式运行的函数执行此操作。它使用logging.handlers.MemoryHandler,它允许缓冲记录的事件,直到发生某种情况为止,此时将缓冲的事件flushed-传递给另一个处理程序(target处理程序)进行处理。默认情况下,MemoryHandler在其缓冲区被填满或看到级别大于或等于指定阈值的事件时刷新。如果要自定义冲洗行为,可以将此配方与MemoryHandler的更专门的子类一起使用。

该示例脚本具有一个简单的函数foo,该函数仅循环浏览所有日志记录级别,写入sys.stderr表示要登录的级别,然后实际在该级别记录消息。您可以将参数传递给foo,如果为 true,它将以 ERROR 和 CRITICAL 级别记录-否则,它仅以 DEBUG,INFO 和 WARNING 级别记录。

该脚本只是安排用装饰器装饰foo,它将执行所需的条件日志记录。装饰器将 Logger 作为参数,并在对装饰函数的调用期间附加一个内存处理程序。装饰器还可以使用目标处理程序,应该进行刷新的级别以及缓冲区的容量进行参数化。这些默认为StreamHandler,分别写入sys.stderrlogging.ERROR100

这是脚本:

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):
    ...

pass配置使用 UTC(GMT)格式化时间

有时您想使用 UTC 格式化时间,可以使用类似 UTCFormatter 的类来完成,如下所示:

import logging
import time

class UTCFormatter(logging.Formatter):
    converter = time.gmtime

然后您可以在代码中使用UTCFormatter而不是Formatter。如果要pass配置完成此操作,则可以将dictConfig() API 与以下完整示例所示的方法结合使用:

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 格式,每个处理程序一个。

使用上下文 Management 器进行选择性日志记录

有时候,临时更改日志记录配置并在执行某些操作后将其还原会很有用。为此,上下文 Management 器是保存和还原日志上下文的最明显的方法。这是一个这样的上下文 Management 器的简单示例,它允许您有选择地更改日志记录级别并仅在上下文 Management 器的范围内添加日志记录处理程序:

import logging
import sys

class LoggingContext(object):
    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

如果指定级别值,那么 Logger 的级别在上下文 Management 器覆盖的 with 块的范围内设置为该值。如果指定处理程序,则在进入该块时将其添加到 Logger,并在退出该块时将其删除。您也可以要求 Manager 在代码块退出时为您关闭处理程序-如果您不再需要该处理程序,则可以执行此操作。

为了说明它是如何工作的,我们可以在上面添加以下代码块:

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.')

我们最初将 Logger 的级别设置为INFO,因此出现消息#1,而没有消息#2.然后,在下面的with块中将级别临时更改为DEBUG,因此出现消息#3.块退出后,Logger 的级别恢复为INFO,因此消息#4 不出现。在下一个with块中,我们再次将级别设置为DEBUG,但还添加了写入sys.stdout的处理程序。因此,消息#5 在控制台上出现两次(一次passstderr一次,一次passstdout)。 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.

在这种情况下,按预期不会出现打印到stdout的消息#5.

当然,这里描述的方法可以通用化,例如临时附加日志记录过滤器。请注意,以上代码可在 Python 2 和 Python 3 中使用。