Python在Windows中使用多进程记录日志

5

我有一个相当大的Python项目,目前在Linux上运行,但我正在尝试扩展到Windows。我已经将代码简化为一个完整的示例,可以运行以说明我的问题:我有两个类,Parent和Child。首先初始化Parent,创建一个记录器,并生成一个Child来执行工作:

import logging
import logging.config
import multiprocessing

class Parent( object ):
    def __init__(self, logconfig):
        logging.config.dictConfig(logconfig)
        self.logger = logging.getLogger(__name__)
    
    def spawnChild(self):
        self.logger.info('One')
        c = Child(self.logger)
        c.start()

class Child(multiprocessing.Process):
    def __init__(self, logger):
        multiprocessing.Process.__init__(self)
        self.logger = logger

    def run(self):
        self.logger.info('Two')

if __name__ == '__main__':
    p = Parent({
            'version':1, 
            "handlers": {
                "console": {
                    "class": "logging.StreamHandler",
                    "stream": "ext://sys.stdout"
                },
            },
            "root": {
                "level": "DEBUG",
                "handlers": [
                    "console",
                    ]
                }
            }
        )
    p.spawnChild()

在Linux上(具体来说是Ubuntu 12.04),我得到了以下(预期的)输出:
user@ubuntu:~$ python test.py 
One
Two

但是,在Windows上(具体来说,Windows 7),它会出现一个pickling错误:

C:\>python test.py
<snip>
pickle.PicklingError: Can't pickle <type 'thread.lock'>: it's not found as thread.lock

问题归结于Windows缺乏真正的fork,因此在线程之间传送对象时必须进行pickle。但是,记录器无法被pickled。我尝试使用 __getstate__ 和 __setstate__ 避免pickling,并通过名称在Child中引用:
def __getstate__(self):
    d = self.__dict__.copy()
    if 'logger' in d.keys():
        d['logger'] = d['logger'].name
    return d

def __setstate__(self, d):
    if 'logger' in d.keys():
        d['logger'] = logging.getLogger(d['logger'])
    self.__dict__.update(d)

这在Linux中仍然有效,现在Windows不会出现“PicklingError”错误。然而,我的输出只来自父进程:
C:\>python test.py
One

C:\>

看起来孩子无法使用记录器,尽管没有任何消息抱怨找不到处理程序'__main__'的记录器或任何其他错误消息。我已经四处寻找了解如何完全重构我的程序中的日志记录方式的方法,但这显然是最后的手段。我希望我只是错过了一些明显的东西,希望群众的智慧能指出给我。


if key in some_dict.keys() 正是执行该检查的错误方式。在Python2中,它需要O(n)时间。只需使用 if key in some_dict 。关于您的问题。子进程可能具有不同的标准输出,因此您看不到输出。尝试添加一个文件处理程序并检查文件中的输出是否正确。 - Bakuriu
感谢关于密钥的说明,那只是从另一个SO帖子中复制出来进行测试的。我的所有实际日志记录都是在文件中完成的,问题仍然存在。stdout只是更容易用于创建上述脚本的测试。使用 “multi_file_handler”:{ “class”:“logging.handlers.RotatingFileHandler”, “filename”:“output.log” }, 会导致相同的问题-在Linux上为“One \ nTwo”,在Windows上为“One”。 - user2093082
1
问题可能在于,在反序列化时通常不会调用__init__。这意味着子进程没有调用logging.config.dictConfig(...),因此可能正在使用默认配置。尝试更改__setstate__方法,以便它使用正确的设置调用dictConfig,看看是否有所改变。 - Bakuriu
那样做使得上面的测试代码能够工作,尽管我还没有在更大的项目中让它工作。这绝对是进步,我会继续努力工作,并在找到真正的解决方案后关闭它。 - user2093082
1个回答

3
在大多数情况下,Logger对象不可被picklable,因为它们在内部使用不可picklable的theading.Lock和/或file对象。您尝试的解决方法确实避免了pickling logger,但最终在子进程中创建了一个完全不同的Logger,它恰好与父进程中的Logger具有相同的名称;您所做的logging.config调用效果将会丢失。为了获得您想要的行为,您需要在子进程中重新创建记录器重新调用logging.config.dictConfig:
class Parent( object ):
    def __init__(self, logconfig):
        self.logconfig = logconfig
        logging.config.dictConfig(logconfig)
        self.logger = logging.getLogger(__name__)

    def spawnChild(self):
        self.logger.info('One')
        c = Child(self.logconfig)
        c.start()

class Child(multiprocessing.Process):
    def __init__(self, logconfig):
        multiprocessing.Process.__init__(self)
        self.logconfig = logconfig

    def run(self):
        # Recreate the logger in the child
        logging.config.dictConfig(self.logconfig)
        self.logger = logging.getLogger(__name__)

        self.logger.info('Two')

或者,如果您想继续使用__getstate__/__setstate__

class Parent( object ):
    def __init__(self, logconfig):
        logging.config.dictConfig(logconfig)
        self.logger = logging.getLogger(__name__)
        self.logconfig = logconfig

    def spawnChild(self):
        self.logger.info('One')
        c = Child(self.logger, self.logconfig)
        c.start()

class Child(multiprocessing.Process):
    def __init__(self, logger, logconfig):
        multiprocessing.Process.__init__(self)
        self.logger = logger
        self.logconfig = logconfig

    def run(self):
        self.logger.info('Two')

    def __getstate__(self):
        d = self.__dict__.copy()
        if 'logger' in d:
            d['logger'] = d['logger'].name
        return d

    def __setstate__(self, d):
        if 'logger' in d:
            logging.config.dictConfig(d['logconfig'])
            d['logger'] = logging.getLogger(d['logger'])
        self.__dict__.update(d)

这与@Bakuriu上面的评论类似,尽管他还指出unpickling不会调用__init__,这就是为什么我将logging.config.dictConfig(self.logconfig)放在__init__中失败的原因。它在我的实际项目中无法工作,但在我的测试代码中可以工作。如果我能从这里找到解决方案,我将继续调整并关闭此问题。 - user2093082
@user2093082 嗯,我在你的问题或评论中没有看到你说过将对 logging.config.dictConfig 的调用放在 Child__init__ 中。我有什么遗漏吗?还是你在这里没有明确提到就尝试了那个方法? - dano
我没有明确说过,但是在Bakuriu的评论后,我先进行了测试。 - user2093082
在这个答案和Bakuriu的评论之间,我得到了一切都能够正常工作的东西。 - user2093082

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