禁用主日志时为多个子日志文件设置Python日志

禁用主日志时为多个子日志文件设置Python日志,python,logging,Python,Logging,我有一些相关但独立的Python脚本,它们都使用了两个使用日志记录的内部模块 第一个脚本使用根记录器工作正常,并从两个模块捕获日志语句。但是,对于第二个脚本,我希望有一个主日志,但当它在服务器列表上迭代时,将日志发送到每台机器的日志文件,同时挂起对主日志文件和控制台的日志记录。我现在有一个黑客解决方案,我将在下面展示 import logging DEFAULT_LOG_FORMAT = "%(asctime)s [%(levelname)s]: %(message)s" DEFAULT_LO

我有一些相关但独立的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")

因此,使用根记录器的主脚本(无论是scriptA.py还是scriptB.py)将获得来自这两个模块的事件,并将其传播并记录到main.log。我尝试过的其他一些解决方案是在所有现有处理程序上使用一个过滤器,该过滤器将忽略从“moduleA”和“moduleB”到“moduleB”的所有内容

我的下一个想法是为各个服务器创建一个新的命名记录器,其中server_file_处理程序是它们的唯一处理程序,并将其添加为两个模块记录器的处理程序,并在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")
来自两个模块的日志记录只会传播到主日志记录程序,而不会传播到两个服务器日志


这基本上是一个他们期望的树,我需要的图的问题。以前有人做过类似的事情吗,最适合的方式是什么?

main.log
的处理程序保留在适当的位置可能会稍微整洁一些,但只需将其级别更改为足够高的值,以防止其输出任何内容(例如
logging.CRITICAL+1
)在服务器中的
服务器之前
循环,然后恢复它。

这是一个有趣的问题。我的第一反应是使用
logger.getChild
,但是默认的实现并不能满足您的需要。假设您可以动态地将处理程序添加到单个记录器中,它仍然无法完成您想要的操作,因为您必须向主文件处理程序和服务器处理程序添加过滤器,以过滤不应进入服务器日志的消息,反之亦然

这就是说,好消息是,为每个孩子创建一个处理程序的自定义记录器实际上非常简单,只需修改
getChild
等简单的子类即可完成

下面的大变化只是
handlerphildlogger
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()
使用记录器命名和传播 如果您的模块使用名为
org.company.moduleX
的记录器,那么您可以将文件处理程序添加到名为
org.company
的记录器,并使用

将其实现为一个上下文管理器,以使其更好

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的记录器仍将到达根记录器。

您能在两个脚本中显示记录器定义吗?@stderr:这已经是个问题了。他们只是使用logger=logging.getLogger(moduleName)并从主脚本获取其余的日志配置。不管怎样,希望我的回答能有所帮助。@stderr:basic配置来自主脚本中的logging.basicConfig(level=DEFAULT\u LOG\u level),它会影响日志树中的所有内容。在实际脚本中,我有一个--debug命令行参数,如果设置了,它会将实际日志级别从INFO更改为debug。今天早上,我会给你的解决方案一个机会,看看它是如何进行的。是的,这可能与我最后要做的事情相似。我的问题是我不想同时调用两个主脚本org.company,但由于我不想在输出中显示记录器名称,所以这不重要。如果我将它们命名为org.company.scriptA和org.company.scriptB,那么它们就不会捕获org.company.ModuleX,除非我可以将其命名为org.company.*.ModuleX。您仍然可以调用主脚本的记录器org.company.scriptA或B。但是,您可以将主日志处理程序添加到根日志记录器(例如,使用logging.basicConfig)并在需要时封锁org.company。为了更好地说明这一点,我对上述答案进行了微调。
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.")