日志记录手册 — Python 文档

来自菜鸟教程
Python/docs/2.7/howto/logging-cookbook
跳转至:导航、​搜索

记录食谱

作者
维奈沙吉普

此页面包含许多与日志记录相关的方法,这些方法在过去很有用。

在多个模块中使用日志记录

多次调用 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.warn('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.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')

通过网络发送和接收日志事件

假设您想通过网络发送日志事件,并在接收端处理它们。 一个简单的方法是将 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 = 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()

首先运行服务器,然后是客户端。 在客户端,控制台上没有打印任何内容; 在服务器端,您应该看到如下内容:

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 实例和一个包含上下文信息的类似 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() 方法是将上下文信息添加到日志输出的地方。 它传递了日志记录调用的消息和关键字参数,并将它们的(可能)修改版本传回以在对底层记录器的调用中使用。 此方法的默认实现不处理消息,而是在关键字参数中插入一个“额外”键,其值是传递给构造函数的类似 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 的值。

使用 dicts 以外的对象传递上下文信息

您不需要将实际的 dict 传递给 LoggerAdapter - 您可以传递一个实现 __getitem____iter__ 的类的实例,使其看起来像一个 dict到日志记录。 如果您想动态生成值(而 dict 中的值将是常量),这将很有用。


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

您还可以使用用户定义的 过滤器 将上下文信息添加到日志输出。 Filter 实例可以修改传递给它们的 LogRecords,包括添加额外的属性,然后可以使用合适的格式字符串输出,或者如果需要自定义 Formatter

例如,在 Web 应用程序中,正在处理的请求(或至少是其中有趣的部分)可以存储在线程本地 (threading.local) 变量中,然后从 [ X202X] 将来自请求的信息(例如远程 IP 地址和远程用户的用户名)添加到 LogRecord,使用 [ X382X] 上面的例子。 在这种情况下,可以使用相同的格式字符串来获得与上述类似的输出。 这是一个示例脚本:

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,并有一个单独的进程来实现从套接字读取的套接字服务器并记录到文件中。 (如果您愿意,可以在现有进程之一中指定一个线程来执行此功能。) 本节 更详细地记录了这种方法,并包括一个可用作起点的工作套接字接收器以便您适应自己的应用程序。

如果您使用的是包含 multiprocessing 模块的最新版本的 Python,您可以编写自己的处理程序,该处理程序使用该模块中的 Lock 类来序列化您的进程对文件的访问. 现有的 FileHandler 和子类目前没有使用 multiprocessing,尽管它们将来可能会这样做。 请注意,目前,multiprocessing 模块并未在所有平台上提供工作锁功能(参见 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 文档的 相关部分


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

RFC 5424 要求将 Unicode 消息作为具有以下结构的一组字节发送到系统日志守护程序:一个可选的纯 ASCII 组件,后跟一个 UTF-8 字节顺序标记 (BOM),后跟使用 UTF-8 编码的 Unicode。 (请参阅规范 相关部分。)

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

由于此行为被破坏,错误的 BOM 插入代码将从 Python 2.7.4 及更高版本中删除。 但是,它不会被替换,并且如果您想生成符合 RFC 5424 的消息,其中包括 BOM、它之前的可选纯 ASCII 序列和它之后的任意 Unicode(使用 UTF-8 编码),那么您需要执行下列的:

  1. Formatter 实例附加到您的 SysLogHandler 实例,使用格式字符串,例如:

    u'ASCII section\ufeffUnicode section'

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

  2. 用你喜欢的任何占位符替换 ASCII 部分,但要确保替换后出现在那里的数据始终是 ASCII(这样,它在 UTF-8 编码后将保持不变)。

  3. 用你喜欢的任何占位符替换 Unicode 部分; 如果替换后出现的数据包含 ASCII 范围之外的字符,那很好——它将使用 UTF-8 编码。

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


实现结构化日志

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

import json
import logging

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

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

_ = StructuredMessage   # 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(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 版本而有所不同。


使用 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() 的日志记录配置中指定通过调用此函数来创建日志记录处理程序:

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

实际上,处理程序创建函数可能位于项目中某处的实用程序模块中。 而不是配置中的行:

'()': owned_file_handler,

你可以使用例如:

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

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

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

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


使用 dictConfig() 配置过滤器

可以 使用 dictConfig() 配置过滤器,尽管乍一看如何去做可能并不明显(因此这个秘诀)。 由于 Filter 是标准库中唯一包含的过滤器类,并且不太可能满足许多要求(它仅作为基类存在),您通常需要定义自己的 Filter 子类具有覆盖的 filter() 方法。 为此,请在过滤器的配置字典中指定 () 键,指定将用于创建过滤器的可调用对象(类是最明显的,但您可以提供任何返回 [ X227X]过滤器实例)。 这是一个完整的例子:

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(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) 功能,即使它没有 Python 绑定,这也很容易做到。 大多数 TTS 系统都有一个可以运行的命令行程序,可以使用 subprocess 从处理程序调用它。 这里假设 TTS 命令行程序不会期望与用户交互或需要很长时间才能完成,并且记录消息的频率不会高到用消息淹没用户,并且可以接受一次说出一条消息而不是并发,下面的示例实现在处理下一条消息之前等待一条消息被说出,这可能会导致其他处理程序保持等待。 这是一个展示该方法的简短示例,它假定 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 系统,甚至可以通过从命令行运行的外部程序处理消息的其他系统。


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

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

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

示例脚本有一个简单的函数 foo,它只是循环遍历所有日志记录级别,写入 sys.stderr 以说明它将在哪个级别登录,然后在该级别实际记录一条消息等级。 您可以将参数传递给 foo,如果为真,将在 ERROR 和 CRITICAL 级别记录 - 否则,它仅在 DEBUG、INFO 和 WARNING 级别记录。

该脚本只是安排使用装饰器装饰 foo,该装饰器将执行所需的条件日志记录。 装饰器将记录器作为参数,并在调用被装饰函数的持续时间内附加内存处理程序。 装饰器还可以使用目标处理程序、应发生刷新的级别以及缓冲区的容量进行额外参数化。 这些默认为 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):
    ...

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

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

import logging
import time

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

然后您可以在代码中使用 UTCFormatter 而不是 Formatter。 如果你想通过配置来做到这一点,你可以使用 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,每个处理程序一个。


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

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

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

如果您指定级别值,则记录器的级别将设置为上下文管理器覆盖的 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 没有。 然后我们在下面的 with 块中临时将级别更改为 DEBUG,因此出现消息 #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.

在这种情况下,打印到 stdout 的消息 #5 没有出现,正如预期的那样。

当然,这里描述的方法可以推广,例如临时附加日志过滤器。 请注意,上述代码适用于 Python 2 和 Python 3。