Python 日志设置:禁用主日志,创建多个子日志文件

4 投票
3 回答
3461 浏览
提问于 2025-04-18 16:00

我有几个相关但又独立的Python脚本,它们都使用了两个内部模块,这些模块里有日志记录的功能。

第一个脚本运行得很好,使用的是根日志记录器,可以捕捉到这两个模块的日志信息。不过在第二个脚本中,我想要一个主日志,但在遍历服务器列表时,希望把日志发送到每台机器的日志文件,同时暂停主日志文件和控制台的日志记录。目前我有一个比较笨的方法,下面会展示。

import logging

DEFAULT_LOG_FORMAT = "%(asctime)s [%(levelname)s]: %(message)s"
DEFAULT_LOG_LEVEL = logging.INFO

def get_log_file_handler(filename, level=None, log_format=None):
  file_handler = logging.FileHandler(filename=filename, encoding="utf-8", mode="w")
  file_handler.setLevel(level or DEFAULT_LOG_LEVEL)
  file_handler.setFormatter(logging.Formatter(log_format or DEFAULT_LOG_FORMAT))

  return file_handler

def process(server):
  server_file_handler = get_log_file_handler("%s.log" % server.name)
  root_logger = logging.getLogger()

  # This works, but is hacky
  main_handlers = list(root_logger.handlers) # copy list of root log handlers
  root_logger.handlers = [] # empty the list on the root logger

  root_logger.addHandler(server_file_handler)

  try:
    # do some stuff with the server
    logging.info("This should show up only in the server-specific log file.")
  finally:
    root_logger.removeHandler(server_file_handler)

    # Add handlers back in
    for handler in main_handlers:
      root_logger.addHandler(handler)

def main():
  logging.basicConfig(level=DEFAULT_LOG_LEVEL)

  logging.getLogger().addHandler(get_log_file_handler("main.log"))

  servers = [] # retrieved from another function, just here for iteration

  logging.info("This should show up in the console and main.log.")

  for server in servers:
    process(server)

  logging.info("This should show up in the console and main.log again.")


if __name__ == "__main__":
  main()

我想找一种不那么笨的方法来实现这个功能。我意识到直接调用logging.info()等方法会有问题,所以我已经把两个模块的代码改成了:

logger = logging.getLogger("moduleA")

还有

logger = logging.getLogger("moduleB")

这样一来,无论是scriptA.py还是scriptB.py这个主脚本,使用根日志记录器,就能把这两个模块的事件记录到main.log里。我尝试过的其他一些解决方案是对所有现有的处理器使用过滤器,忽略来自“moduleA”和“moduleB”的所有日志。

我接下来的想法是为每台服务器创建一个新的命名日志记录器,并且只使用server_file_handler作为它们的处理器,同时把这个处理器也添加到这两个模块的日志记录器中,最后在process()结束时移除这些处理器。这样我可以把根日志记录器的级别设置为WARNING,这样来自这两个模块的所有INFO/DEBUG信息就只会发送到特定服务器的日志记录器。

我不能完全使用层级日志命名,除非它支持某种通配符,因为那样的话我会得到:

logging.getLogger("org.company") # main logger for script
logging.getLogger("org.company.serverA") 
logging.getLogger("org.company.serverB")
logging.getLogger("org.company.moduleA")
logging.getLogger("org.company.moduleB")

来自这两个模块的日志只会传递到主日志记录器,而不会传递到两个服务器日志。

这基本上是一个他们期待树状结构,而我需要图形结构的问题。有没有人做过类似的事情,最Pythonic的做法是什么呢?

3 个回答

1

使用日志记录器命名和传播

如果你的模块使用了一个叫 org.company.moduleX 的日志记录器,那么你可以把你的文件处理器添加到一个叫 org.company 的日志记录器上,并通过 Logger.propogate 来阻止消息传播到根日志记录器的处理器。

这样做可以让它看起来更整洁,像一个上下文管理器一样使用。

import contextlib

log = logging.getLogger("org.company.scriptB")

@contextlib.contextmanager
def block_and_divert_logging(logger, new_handler):
    logger.propagate = False
    logger.addHandler(new_handler)
    try:
        yield
    finally:
        logger.propogate = True
        logger.removeHandler(new_handler)

def process(server):
    server_file_handler = get_log_file_handler("%s.log" % server.name)
    logger_block = logging.getLogger("org.company")

    with block_and_divert_logging(logger_block, server_file_handler):
        # do some stuff with the server
        log.info("This should show up only in the server-specific log file.")

这样一来,任何来自 org.company 或更低层级的日志记录器的消息就不会到达根日志记录器的处理器了。相反,它们会由你的文件处理器来处理。

不过,这也意味着那些没有像 org.company.something 这样命名的日志记录器,仍然会到达根日志记录器。

1

可能把 main.log 的处理器保留在那儿会更整洁一些,但只需要把它的级别调高到一个足够高的值,这样它就不会输出任何内容了(比如可以设置为 logging.CRITICAL + 1)。在 server in servers 循环之前这样做,然后在之后再把它恢复过来。

2

这是一个有趣的问题。我最初的想法是使用 logger.getChild,但默认的实现并不能满足你的需求。即使你能动态地给一个日志记录器添加处理器,仍然无法达到你的目的,因为你需要在主文件处理器和服务器处理器上都添加过滤器,以过滤掉那些不应该进入服务器日志的消息,反之亦然。

不过,好消息是,创建一个自定义的日志记录器,为每个子日志记录器生成一个处理器,其实是相当简单的。你只需要简单地修改 getChild 方法,其他的就不需要太多改动。

下面的主要变化是 HandlerPerChildLoggerLogger。这个 Logger 和普通的 Logger 不同,它需要两个参数,而不仅仅是一个 name 参数。

import logging

DEFAULT_LOG_FORMAT = "%(asctime)s [%(levelname)s]: %(message)s"
DEFAULT_LOG_LEVEL = logging.INFO

class HandlerPerChildLogger(logging.Logger):
    selector = "server"

    def __init__(self, name, handler_factory, level=logging.NOTSET):
        super(HandlerPerChildLogger, self).__init__(name, level=level)
        self.handler_factory = handler_factory

    def getChild(self, suffix):
        logger = super(HandlerPerChildLogger, self).getChild(suffix)
        if not logger.handlers:
            logger.addHandler(self.handler_factory(logger.name))
            logger.setLevel(DEFAULT_LOG_LEVEL)
        return logger

def file_handler_factory(name):
    handler = logging.FileHandler(filename="{}.log".format(name), encoding="utf-8", mode="a")
    formatter = logging.Formatter(DEFAULT_LOG_FORMAT)
    handler.setFormatter(formatter)
    return handler

logger = HandlerPerChildLogger("my.company", file_handler_factory)
logger.setLevel(DEFAULT_LOG_LEVEL)
ch = logging.StreamHandler()
fh = logging.FileHandler(filename="my.company.log", encoding="utf-8", mode="a")
ch.setLevel(DEFAULT_LOG_LEVEL)
fh.setLevel(DEFAULT_LOG_LEVEL)
formatter = logging.Formatter(DEFAULT_LOG_FORMAT)
ch.setFormatter(formatter)
fh.setFormatter(formatter)
logger.addHandler(ch)
logger.addHandler(fh)

def process(server):
    server_logger = logger.getChild(server)
    server_logger.info("This should show up only in the server-specific log file for %s", server)
    server_logger.info("another log message for %s", server)

def main():
    # servers list retrieved from another function, just here for iteration
    servers = ["server1", "server2", "server3"]

    logger.info("This should show up in the console and main.log.")

    for server in servers:
        process(server)

    logger.info("This should show up in the console and main.log again.")

if __name__ == "__main__":
    main()

撰写回答