Python:日志记录模块 - 全局

77
我在思考如何实现一个全局记录器,可以使用你自己的设置在任何地方:
我目前有一个自定义的记录器类:
class customLogger(logging.Logger):
   ...

这个类在一个单独的文件中,其中包含一些格式化程序和其他东西。

记录器本身运行良好。

我在我的主Python文件中导入此模块并像这样创建一个对象:

self.log = logModule.customLogger(arguments)

但显然,我无法从代码的其他部分访问此对象。 我是否使用了错误的方法? 有更好的方法来做这个吗?


1
我不确定我理解你试图克服的“日志记录”限制是什么。 - Savino Sguera
5个回答

154

使用logging.getLogger(name)来创建一个带名称的全局日志记录器。

main.py

import log
logger = log.setup_custom_logger('root')
logger.debug('main message')

import submodule

log.py

import logging

def setup_custom_logger(name):
    formatter = logging.Formatter(fmt='%(asctime)s - %(levelname)s - %(module)s - %(message)s')

    handler = logging.StreamHandler()
    handler.setFormatter(formatter)

    logger = logging.getLogger(name)
    logger.setLevel(logging.DEBUG)
    logger.addHandler(handler)
    return logger

submodule.py

import logging

logger = logging.getLogger('root')
logger.debug('submodule message')

输出

2011-10-01 20:08:40,049 - DEBUG - main - main message
2011-10-01 20:08:40,050 - DEBUG - submodule - submodule message

7
每个子模块中定义的日志记录器不是全局参数, 有没有方法可以使其成为全局参数?我想要的是,当我在 submodule.py 中定义 logger = logging.getLogger('root') 时,它将与在 submodule2.py 中定义的 logger = logging.getLogger('root') 相同。 - Shahar Hamuzim Rajuan
如果您在导入模块后配置了“全局”记录器,该怎么办?(例如,在使用多进程模块分叉进程时) - frankster
5
由于你需要在submodule.py中重新定义记录器,所以全局定义并不是这样。同样,你可以在每个子模块中从配置文件中重新初始化记录器(实际上需要更少的按键)。 - gented
1
在调用 setup_custom_logger() 时,建议使用 __name__ 而不是硬编码字符串。 - Nom1fan

93

由于我还没有找到一个令人满意的答案,因此我想详细阐述一下回答问题的答案,以便提供关于 Python 标准库中附带的 logging 库的工作原理和意图的一些见解。

与原贴的方法相反,该库明确地分离了日志记录器的接口和日志记录器本身的配置。

处理程序的配置是使用您的库的应用程序开发人员的特权。

这意味着您不应该创建自定义日志记录器类,并通过在该类中添加任何配置来配置记录器。

logging 库引入了四个组件:记录器处理程序过滤器格式化程序

  • 记录器公开应用程序代码直接使用的接口。
  • 处理程序将由记录器创建的日志记录发送到适当的目的地。
  • 过滤器提供了一个更精细的机制来确定要输出哪些日志记录。
  • 格式化程序指定最终输出中日志记录的布局。

一个常见的项目结构如下:

Project/
|-- .../
|   |-- ...
|
|-- project/
|   |-- package/
|   |   |-- __init__.py
|   |   |-- module.py
|   |   
|   |-- __init__.py
|   |-- project.py
|
|-- ...
|-- ...

在你的代码中(比如在 module.py 中),你可以引用模块的日志记录器实例,将事件按照特定级别进行记录。

命名日志记录器时一个好的约定是,在使用日志记录的每个模块中定义一个模块级别的日志记录器,名称如下:

logger = logging.getLogger(__name__)

特殊变量__name__指的是你的模块名称,根据应用程序代码结构的不同,它可能看起来像project.package.module

module.py(和任何其他类)本质上可以看起来像这样:

The special variable __name__ refers to your module's name and looks something like project.package.module depending on your application's code structure.

module.py (and any other class) could essentially look like this:

import logging
...
log = logging.getLogger(__name__)

class ModuleClass:
    def do_something(self):
        log.debug('do_something() has been called!')
每个模块中的记录器都会将任何事件传播到父记录器,然后父记录器将信息传递给其附加的处理程序! 类似于Python软件包/模块结构,父记录器是通过使用“点分模块名称”确定命名空间的。这就是为什么使用特殊的__name__变量(在上面的示例中,name与字符串"project.package.module"匹配)初始化记录器是有意义的。
有两种全局配置记录器的选项:
1. 在project.py中实例化一个记录器,并将其名称设置为__package__,在此示例中它等于"project",因此是所有子模块的记录器的父记录器。只需要向记录器添加适当的处理程序和格式化程序即可。 2. 使用最高级别的包名称在执行脚本(如main.py)中设置带有处理程序和格式化程序的记录器。
开发使用日志记录的库时,您应该注意记录库使用日志记录的方式-例如,所使用的记录器名称。
例如,执行脚本可能最终看起来像这样:
import logging
from project import App

def setup_logger():
    # create logger
    logger = logging.getLogger('project')
    logger.setLevel(logging.DEBUG)

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

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

    # add formatter to ch
    ch.setFormatter(formatter)

    # add ch to logger
    logger.addHandler(ch)

if __name__ == '__main__' and __package__ is None:
     setup_logger()
     app = App()
     app.do_some_funny_stuff()
方法调用log.setLevel(...)指定了记录器将要处理的最低严重性日志消息,但不一定会输出!这仅意味着如果消息的严重级别高于(或等于)设置的级别,则该消息将传递给处理程序。但是处理程序负责处理日志消息(例如打印或存储它)。
因此,logging库提供了一个结构化和模块化的方法,只需要根据个人需求进行利用。 记录文档

2
感谢您提供详细的答案。对于Python 3,这是链接:https://docs.python.org/3/howto/logging.html#logging-advanced-tutorial - Sylvain
太棒了。这应该是被接受的答案。 - Prajwal
我觉得那不是回答问题。问题是如何使用自定义记录器类,logging 支持可注册到 logging 管理器并由 logging.get_logger(name) 分配的自定义记录器类。 - Marcel

13
在您的日志模块中创建一个customLogger实例,并将其用作单例 - 只使用导入的实例,而不是类。

12

Python的logging模块已经足够作为全局记录器,你可能只是在寻找这个:

main.py

import logging
logging.basicConfig(level = logging.DEBUG,format = '[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s')
将上述代码放入你的执行脚本中,然后你就可以在项目的任何地方使用相同的配置来使用这个日志记录器: module.py
import logging
logger = logging.getLogger(__name__)
logger.info('hello world!')

对于更复杂的配置,您可以使用带有记录功能的配置文件 logging.conf

logging.config.fileConfig("logging.conf")

3
我一直在寻找的答案...感谢Funny LI #dadjokes - Nils

3

您只需要传递一个字符串,其中包含第一个句点之前的公共子字符串。由句点(“。”)分隔的字符串部分可以用于不同的类/模块/文件等。就像这样(特别是logger = logging.getLogger(loggerName)部分):

def getLogger(name, logdir=LOGDIR_DEFAULT, level=logging.DEBUG, logformat=FORMAT):
    base = os.path.basename(__file__)
    loggerName = "%s.%s" % (base, name)
    logFileName = os.path.join(logdir, "%s.log" % loggerName)
    logger = logging.getLogger(loggerName)
    logger.setLevel(level)
    i = 0
    while os.path.exists(logFileName) and not os.access(logFileName, os.R_OK | os.W_OK):
        i += 1
        logFileName = "%s.%s.log" % (logFileName.replace(".log", ""), str(i).zfill((len(str(i)) + 1)))
    try:
        #fh = logging.FileHandler(logFileName)
        fh = RotatingFileHandler(filename=logFileName, mode="a", maxBytes=1310720, backupCount=50)
    except IOError, exc:
        errOut = "Unable to create/open log file \"%s\"." % logFileName
        if exc.errno is 13: # Permission denied exception
            errOut = "ERROR ** Permission Denied ** - %s" % errOut
        elif exc.errno is 2: # No such directory
            errOut = "ERROR ** No such directory \"%s\"** - %s" % (os.path.split(logFileName)[0], errOut)
        elif exc.errno is 24: # Too many open files
            errOut = "ERROR ** Too many open files ** - Check open file descriptors in /proc/<PID>/fd/ (PID: %s)" % os.getpid()
        else:
            errOut = "Unhandled Exception ** %s ** - %s" % (str(exc), errOut)
        raise LogException(errOut)
    else:
        formatter = logging.Formatter(logformat)
        fh.setLevel(level)
        fh.setFormatter(formatter)
        logger.addHandler(fh)
    return logger

class MainThread:
    def __init__(self, cfgdefaults, configdir, pidfile, logdir, test=False):
        self.logdir = logdir
        logLevel = logging.DEBUG
        logPrefix = "MainThread_TEST" if self.test else "MainThread"
        try:
            self.logger = getLogger(logPrefix, self.logdir, logLevel, FORMAT)
        except LogException, exc:
            sys.stderr.write("%s\n" % exc)
            sys.stderr.flush()
            os._exit(0)
        else:
            self.logger.debug("-------------------- MainThread created.  Starting __init__() --------------------")

    def run(self):
        self.logger.debug("Initializing ReportThreads..")
        for (group, cfg) in self.config.items():
            self.logger.debug(" ------------------------------ GROUP '%s' CONFIG ------------------------------     " % group)
            for k2, v2 in cfg.items():
                self.logger.debug("%s <==> %s: %s" % (group, k2, v2))
            try:
                rt = ReportThread(self, group, cfg, self.logdir, self.test)
            except LogException, exc:
                sys.stderr.write("%s\n" % exc)
                sys.stderr.flush()
                self.logger.exception("Exception when creating ReportThread (%s)" % group)
                logging.shutdown()
                os._exit(1)
            else:
                self.threads.append(rt)
        self.logger.debug("Threads initialized.. \"%s\"" % ", ".join([t.name for t in self.threads]))
        for t in self.threads:
            t.Start()
        if not self.test:
            self.loop()


class ReportThread:
    def __init__(self, mainThread, name, config, logdir, test):
        self.mainThread = mainThread
        self.name = name
        logLevel = logging.DEBUG
        self.logger = getLogger("MainThread%s.ReportThread_%s" % ("_TEST" if self.test else "", self.name), logdir, logLevel, FORMAT)
        self.logger.info("init database...")
        self.initDB()
        # etc....

if __name__ == "__main__":
    # .....
    MainThread(cfgdefaults=options.cfgdefaults, configdir=options.configdir, pidfile=options.pidfile, logdir=options.logdir, test=options.test)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接