日志消息使用Python Logging出现两次


100

我正在使用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)

由于某种原因,所有消息都出现两次。我注释掉其中一个流处理程序,还是一样。有点奇怪,不确定为什么会这样...大声笑。假设我错过了一些显而易见的事情。

干杯,维克多


1
您确定configure_logging()不会被调用两次(例如,也从构造函数调用过)吗?仅创建Boy()的一个实例吗?
Jacek Konieczny

1
使用self.logger.handlers = [ch]替代方法可以解决此问题,尽管最好是确保不要通过例如if not self.logger在开始时使用两次来运行此代码。
Ninjakannon

Answers:


133

您正在调用configure_logging两次(也许使用的__init__方法Boy):getLogger将返回相同的对象,但addHandler不检查是否已将类似的处理程序添加到记录器。

尝试跟踪对该方法的调用并消除其中之一。或设置一个标志logging_initializedFalse__init__方法中初始化为,如果is为,Boy则更configure_logging改为不执行任何logging_initialized操作True,并True在初始化记录器后将其设置为。

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

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

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

1
是的,你是对的-愚蠢的我。我在init以及其他地方都将其称为。大声笑。谢谢=)。
victorhooi 2011年

谢谢。您的解决方案今天救了我。
ForeverLearner 2013年

1
就我而言,它们出现了6次。我怀疑这是因为我在6个oop类中声明了相同类型的记录器
answerSeeker

5
我想在此分享我的经验:对于我开发的Flask应用程序,日志消息出现的次数超过两次。我要说的是,它们正在日志文件中递增,原因是这样的事实,当加载应用程序和模块时,所logger使用的变量不是从我的一个类中实例化的logger变量,而是Python3缓存中存在的变量,并且该处理程序是由我配置的AppScheduler每60秒添加一次的。因此,这if not logger.handlers是避免此类现象的非常聪明的方法。谢谢您的解决,同志:)!
ivanleoncz

2
我在Flask应用中看到了这个问题。此解决方案解决了在主flask应用程序中生成的日志消息的问题,但是我的应用程序在库模块中运行,并且该库中的消息仍被多次记录。我不知道该如何解决。
Cas

24

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

来自文档

注意:如果将处理程序附加到记录器及其一个或多个祖先,则它可能会多次发出相同的记录。通常,您不需要将一个处理程序附加到一个以上的记录器上-如果您将它附加到记录器层次结构中最高的适当记录器上,则它将看到所有后代记录器记录的所有事件,前提是它们的传播设置保留为True。一种常见的情况是仅将处理程序附加到根记录器,并让传播来处理其余部分。

因此,如果您希望在“测试”上使用自定义处理程序,并且不希望其消息也发送到根处理程序,则答案很简单:关闭其传播标志:

logger.propagate = False


1
那是最好的答案。它不符合发布者的目的(编码中的逻辑错误),但是在大多数情况下,情况应该如此。
Artem,

太棒了 这是重复的实际原因(在大多数情况下)。
杜哈特先生

这也是我的代码的问题。非常感谢。
苛刻的

有史以来最好的答案。谢谢!
Foivos Ts

8

每次您从外部调用时都会添加该处理程序。完成工作后,尝试删除处理程序:

self.logger.removeHandler(ch)

1
logger.handlers.pop() 在python 2.7中使用过,可以达到目的
radtek

6

我是python新手,但这似乎对我有用(Python 2.7)

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

4

就我而言,我要设定 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')

这是我的问题。谢谢
q0987

好答案;添加logger.propagate = False的解决方案是在登录Flask的app.logger实例时防止在由Waitress托管的Flask应用程序中重复登录。
bluebinary

1

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


我认为这就是我正在发生的事情。您如何防止自动创建控制台记录器?
罗伯特

@Robert是关于确保在第一个日志记录调用之前用所需的记录器初始化的。测试框架可以掩盖这一点,但是应该有一种方法可以做到这一点。另外,如果您要进行多处理,则每个过程都必须执行相同的操作。
JimB

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电话时,问题消失了。


0

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

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

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

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)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.