使用Python Logging记录消息两次

时间:2011-07-18 06:24:53

标签: python logging

我正在使用Python日志记录,出于某种原因,我的所有消息都出现了两次。

我有一个配置日志记录的模块:

# BUG: It's outputting logging messages twice - not sure why - it's not the propagate setting.
def configure_logging(self, logging_file):
    self.logger = logging.getLogger("my_logger")
    self.logger.setLevel(logging.DEBUG)
    self.logger.propagate = 0
    # Format for our loglines
    formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
    # Setup console logging
    ch = logging.StreamHandler()
    ch.setLevel(logging.DEBUG)
    ch.setFormatter(formatter)
    self.logger.addHandler(ch)
    # Setup file logging as well
    fh = logging.FileHandler(LOG_FILENAME)
    fh.setLevel(logging.DEBUG)
    fh.setFormatter(formatter)
    self.logger.addHandler(fh)

稍后,我调用此方法来配置日志记录:

if __name__ == '__main__':
    tom = Boy()
    tom.configure_logging(LOG_FILENAME)
    tom.buy_ham()

然后说,buy_ham模块,我打电话给:

self.logger.info('Successfully able to write to %s' % path)

由于某种原因,所有消息都出现了两次。我注释掉了一个流处理程序,仍然是一样的。有点怪异,不知道为什么会发生这种情况......哈哈。假设我错过了一些明显的东西。

干杯, 维克多

9 个答案:

答案 0 :(得分:93)

您正在拨打configure_logging两次(可能使用__init__ Boy方法):getLogger将返回相同的对象,但addHandler不会检查如果已经将类似的处理程序添加到记录器中。

尝试跟踪对该方法的调用并删除其中一个。或者在logging_initialized False方法中将__init__初始化为Boy,并在configure_logging为{logging_initialized时更改True不做任何操作1}},并在初始化记录器后将其设置为True

如果您的程序创建了多个Boy实例,则必须使用添加处理程序的全局configure_logging函数更改执行操作的方式,并且Boy.configure_logging方法仅初始化self.logger属性。

解决此问题的另一种方法是检查记录器的处理程序属性:

logger = logging.getLogger('my_logger')
if not logger.handlers:
    # create the handlers and call logger.addHandler(logging_handler)

答案 1 :(得分:7)

如果您看到此问题并且您没有添加处理程序两次,请参阅abarnert的回答here

来自docs

  

注意:如果您将处理程序附加到记录器及其中的一个或多个   祖先,它可能会多次发出相同的记录。一般来说,你   不应该将处理程序附加到多个记录器 - 如果你   只需将它附加到最适合的记录器中   记录器层次结构,然后它将看到所有后代记录的所有事件   记录器,只要它们的传播设置保持设置为True。一个   常见的情况是仅将处理程序附加到根记录程序,并将其附加到   让传播照顾其余部分。

因此,如果你想在“test”上使用自定义处理程序,并且你不希望它的消息也转到根处理程序,答案很简单:关闭它的传播标志:

logger.propagate = False

答案 2 :(得分:6)

每次从外面打电话时都会添加处理程序。完成工作后尝试删除处理程序:

self.logger.removeHandler(ch)

答案 3 :(得分:5)

我是一个蟒蛇新手,但这似乎对我有用(Python 2.7)

while logger.handlers:
     logger.handlers.pop()

答案 4 :(得分:1)

如果您(意外地)将某些内容输出到记录器然后进行配置,似乎为时已晚。例如,在我的代码中,我有

logging.warning("look out)"

...
ch = logging.StreamHandler(sys.stdout)
root = logging.getLogger()
root.addHandler(ch)

root.info("hello")

我会得到类似的信息(忽略格式选项)

look out
hello
hello

,所有内容都两次写入标准输出。我相信这是因为第一次调用logging.warning会自动创建一个新的处理程序,然后我显式添加了另一个处理程序。当我删除意外的第一个logging.warning呼叫时,问题消失了。

答案 5 :(得分:0)

如果未安装root处理程序,则对logging.debug()的调用将调用logging.basicConfig()。对于我来说,这是在无法控制测试用例触发顺序的测试框架中发生的。我的初始化代码正在安装第二个。默认使用我不需要的logging.BASIC_FORMAT。

答案 6 :(得分:0)

就我而言,我将设置logger.propagate = False以防止重复打印。

在下面的代码中,如果您删除logger.propagate = False,则会看到两次打印。

import logging
from typing import Optional

_logger: Optional[logging.Logger] = None

def get_logger() -> logging.Logger:
    global _logger
    if _logger is None:
        raise RuntimeError('get_logger call made before logger was setup!')
    return _logger

def set_logger(name:str, level=logging.DEBUG) -> None:
    global _logger
    if _logger is not None:
        raise RuntimeError('_logger is already setup!')
    _logger = logging.getLogger(name)
    _logger.handlers.clear()
    _logger.setLevel(level)
    ch = logging.StreamHandler()
    ch.setLevel(level)
    # warnings.filterwarnings("ignore", "(Possibly )?corrupt EXIF data", UserWarning)
    ch.setFormatter(_get_formatter())
    _logger.addHandler(ch)
    _logger.propagate = False # otherwise root logger prints things again


def _get_formatter() -> logging.Formatter:
    return logging.Formatter(
        '[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s')

答案 7 :(得分:0)

我遇到了奇怪的情况,控制台日志增加了一倍,但文件日志却没有。经过一番挖掘,我弄清楚了。

请注意,第三方软件包可以注册记录器。这是要提防的事情(在某些情况下是无法避免的)。在许多情况下,第三方代码会检查是否存在任何现有的 root 记录程序处理程序;如果没有,他们会注册一个新的控制台处理程序。

我的解决方案是在根目录下注册控制台记录器:

rootLogger = logging.getLogger()  # note no text passed in--that's how we grab the root logger
if not rootLogger.handlers:
        ch = logging.StreamHandler()
        ch.setLevel(logging.INFO)
        ch.setFormatter(logging.Formatter('%(process)s|%(levelname)s] %(message)s'))
        rootLogger.addHandler(ch)

答案 8 :(得分:0)

在多个过程中,我一直在同一个问题上挣扎。 (有关代码,请参见docs,我几乎是逐字遵循的。)即,源自任何子进程的所有日志消息都重复了。

我的错误是打电话给worker_configurer()

def worker_configurer(logging_queue):
    queue_handler = logging.handlers.QueueHandler(logging_queue)
    root = logging.getLogger()
    root.addHandler(queue_handler)
    root.setLevel(level)

既在子进程中也在主进程中(因为我也希望主进程也记录日志)。导致麻烦的原因(在我的Linux机器上)是在Linux上,子进程是通过派生开始的,因此从主进程继承了现有的日志处理程序。也就是说,在Linux上QueueHandler被注册了两次。

现在,阻止QueueHandlerworker_configurer()函数中两次注册并不像看起来那么简单:

  • 诸如根记录器root之类的记录器对象具有handlers属性,但未记录。

  • 以我的经验,即使any([handler is queue_handler for handler in root.handlers])似乎包含相同的any([handler == queue_handler for handler in root.handlers]),在分叉后测试root.handlers(身份)还是QueueHandler(相等)是否失败。 (显然,由于queue_handler in root.handlers运算符checks for both identity and equality in the case of lists的存在,前两个表达式可以用in缩写。)

  • root logger通过pytest之类的软件包进行了修改,因此root.handlersroot.hasHandlers()一开始并不是很可靠。 (毕竟它们是全局状态。)

因此,干净的解决方案是用生成代替分支,以从一开始就防止此类多处理错误(当然,前提是您可以忍受更多的内存占用)。或者使用logging包的替代方案,该替代方案不依赖于全局状态,而是要求您进行适当的依赖注入,但是我在偏离方向……:)

话虽如此,我最终还是做了一次琐碎的检查:

def worker_configurer(logging_queue):
    queue_handler = logging.handlers.QueueHandler(logging_queue)
    root = logging.getLogger()

    for handler in root.handlers:
        if isinstance(handler, logging.handlers.QueueHandler):
            return

    root.addHandler(queue_handler)
    root.setLevel(level)

很明显,当我决定在其他地方注册第二个队列处理程序时,这将带来讨厌的副作用。