在Python中使用多重处理时应如何记录?


239

现在,我在框架中有一个中央模块,该模块使用Python 2.6 multiprocessing模块产生多个进程。因为使用multiprocessing,所以存在模块级的多处理感知日志LOG = multiprocessing.get_logger()。根据文档,此记录器具有进程共享的锁,因此您不会sys.stderr通过让多个进程同时向其写入(或任何文件句柄)来乱码。

我现在遇到的问题是框架中的其他模块不支持多处理。以我的方式看,我需要使这个中央模块上的所有依赖项都使用支持多处理的日志记录。框架这很烦人,更不用说框架的所有客户了。有我没有想到的替代方法吗?


10
您链接到的文档,与您所说的完全相反,记录器没有进程共享锁,事情变得混乱,这也是我遇到的问题。
塞巴斯蒂安·布拉斯克

3
请参阅stdlib文档中的示例:从多个进程登录到单个文件。配方不需要其他模块即可进行多处理。
jfs 2012年

那么,用例是multiprocessing.get_logger()什么?似乎基于这些其他日志记录方式的日志记录功能multiprocessing价值不高。
蒂姆·卢德温斯基2014年

4
get_logger()multiprocessing模块本身使用的记录器。如果要调试multiprocessing问题,这将很有用。
jfs 2015年

Answers:


69

唯一地解决此问题的方法是:

  1. 生成每个工作进程,以使其日志转到不同的文件描述符(到磁盘或管道)。理想情况下,应为所有日志条目加上时间戳。
  2. 然后,您的控制器进程可以执行以下操作之一
    • 如果使用磁盘文件:在运行结束时合并日志文件,按时间戳排序
    • 如果使用管道(推荐):即时将所有管道中的日志条目合并到中央日志文件中。(例如,定期select从管道的文件描述符中,对可用的日志条目执行合并排序,然后刷新到集中式日志。重复。)

好的,那是我想到之前35秒钟(我想用atexit:-)。问题在于它不会为您提供实时读数。与多线程相比,这可能是多处理价格的一部分。
cdleary

@cdleary,使用管道方法将尽可能接近实时(尤其是如果在生成的进程中没有缓冲stderr的情况。)
vladr

1
顺便说一下,这里有一个很大的假设:不是Windows。您在Windows上吗?
vladr

22
为什么不只在主进程中使用multiprocessing.Queue和日志线程呢?似乎更简单。
布兰登·罗兹

1
@BrandonRhodes-就像我说的那样,不打扰multiprocessing.Queue如果有很多代码需要重用multiprocessing.Queue,并且/或者如果性能存在问题
vladr

122

我刚刚编写了自己的日志处理程序,该处理程序通过管道将所有内容馈送到父进程。我只测试了十分钟,但看起来效果很好。

注意:这是硬编码为RotatingFileHandler,这是我自己的用例。)


更新:@javier现在将这种方法保持为Pypi上可用的软件包-请参阅Pypi上的multiprocessing-logging,github在https://github.com/jruere/multiprocessing-logging


更新:实施!

现在,它使用队列来正确处理并发,并且还可以从错误中正确恢复。我现在已经在生产中使用它几个月了,下面的当前版本可以正常工作。

from logging.handlers import RotatingFileHandler
import multiprocessing, threading, logging, sys, traceback

class MultiProcessingLog(logging.Handler):
    def __init__(self, name, mode, maxsize, rotate):
        logging.Handler.__init__(self)

        self._handler = RotatingFileHandler(name, mode, maxsize, rotate)
        self.queue = multiprocessing.Queue(-1)

        t = threading.Thread(target=self.receive)
        t.daemon = True
        t.start()

    def setFormatter(self, fmt):
        logging.Handler.setFormatter(self, fmt)
        self._handler.setFormatter(fmt)

    def receive(self):
        while True:
            try:
                record = self.queue.get()
                self._handler.emit(record)
            except (KeyboardInterrupt, SystemExit):
                raise
            except EOFError:
                break
            except:
                traceback.print_exc(file=sys.stderr)

    def send(self, s):
        self.queue.put_nowait(s)

    def _format_record(self, record):
        # ensure that exc_info and args
        # have been stringified.  Removes any chance of
        # unpickleable things inside and possibly reduces
        # message size sent over the pipe
        if record.args:
            record.msg = record.msg % record.args
            record.args = None
        if record.exc_info:
            dummy = self.format(record)
            record.exc_info = None

        return record

    def emit(self, record):
        try:
            s = self._format_record(record)
            self.send(s)
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            self.handleError(record)

    def close(self):
        self._handler.close()
        logging.Handler.close(self)

4
上面的处理程序完成了父进程的所有文件写入操作,并且仅使用一个线程来接收从子进程传递的消息。如果您从生成的子进程中调用处理程序本身,则说明该处理程序使用不正确,并且会遇到与RotatingFileHandler相同的所有问题。我已经使用上面的代码多年没有问题了。
zzzeek 2012年

9
不幸的是,这种方法在Windows上不起作用。来自docs.python.org/library/multiprocessing.html 16.6.2.12“请注意,在Windows上,子进程将仅继承父进程记录器的级别-不会继承该记录器的任何其他自定义。” 子流程不会继承该处理程序,并且您不能显式传递它,因为它不是可腌制的。
Noah Yetter'3

2
值得注意的是,要multiprocessing.Queue使用in线程put()。因此,在创建所有子流程之前,请勿调用put(即使用MultiProcessingLog处理程序记录消息)。否则线程将在子进程中死亡。一种解决方案是Queue._after_fork()在每个子进程的开始处调用,或者改用multiprocessing.queues.SimpleQueue不涉及线程但正在阻塞的子进程。
Danqi Wang 2013年

5
您能否添加一个简单的示例来显示初始化以及假设的子进程的用法?我不太确定子进程如何在不实例化类的另一个实例的情况下访问队列。
JesseBuesking

11
@zzzeek,此解决方案很好,但是我找不到与此包或类似的包,所以我创建了一个名为的包multiprocessing-logging
哈维尔2015年

30

QueueHandler在Python 3.2+中是本机,并且正是这样做的。它很容易在以前的版本中复制。

Python文档有两个完整的示例:从多个进程登录到单个文件

对于使用Python <3.2的用户,只需QueueHandler从以下网址复制到您自己的代码中即可:https : //gist.github.com/vsajip/591589或导入logutils

每个进程(包括父进程)将其日志记录在上Queue,然后listener线程或进程(为每个进程提供一个示例)将其拾取并将它们全部写入文件中-不会有损坏或乱码的危险。


21

下面是另一种解决方案,其重点是为那些从Google到这里的人(如我)提供简单性。记录应该很容易!仅适用于3.2或更高版本。

import multiprocessing
import logging
from logging.handlers import QueueHandler, QueueListener
import time
import random


def f(i):
    time.sleep(random.uniform(.01, .05))
    logging.info('function called with {} in worker thread.'.format(i))
    time.sleep(random.uniform(.01, .05))
    return i


def worker_init(q):
    # all records from worker processes go to qh and then into q
    qh = QueueHandler(q)
    logger = logging.getLogger()
    logger.setLevel(logging.DEBUG)
    logger.addHandler(qh)


def logger_init():
    q = multiprocessing.Queue()
    # this is the handler for all log records
    handler = logging.StreamHandler()
    handler.setFormatter(logging.Formatter("%(levelname)s: %(asctime)s - %(process)s - %(message)s"))

    # ql gets records from the queue and sends them to the handler
    ql = QueueListener(q, handler)
    ql.start()

    logger = logging.getLogger()
    logger.setLevel(logging.DEBUG)
    # add the handler to the logger so records from this process are handled
    logger.addHandler(handler)

    return ql, q


def main():
    q_listener, q = logger_init()

    logging.info('hello from main thread')
    pool = multiprocessing.Pool(4, worker_init, [q])
    for result in pool.map(f, range(10)):
        pass
    pool.close()
    pool.join()
    q_listener.stop()

if __name__ == '__main__':
    main()

2
QueueHandlerQueueListener类可以关于Python 2.7在使用为好,可用的logutils包。
列夫·莱维茨基

5
主进程的记录器还应该使用QueueHandler。在您当前的代码中,主进程绕过队列,因此主进程和工作进程之间可能存在竞争条件。每个人都应该登录到队列(通过QueueHandler),并且只应允许QueueListener登录到StreamHandler。
Ismael EL ATIFI

另外,您不必在每个孩子中都初始化记录器。只需在父进程中初始化记录器,然后在每个子进程中获取记录器。
okwap

20

另一个选择可能是logging软件包中的各种非基于文件的日志处理程序

  • SocketHandler
  • DatagramHandler
  • SyslogHandler

(和别的)

这样,您可以轻松地将日志记录守护程序放置在可以安全写入并正确处理结果的位置。(例如,一个简单的套接字服务器,仅释放消息并将其发送到自己的旋转文件处理程序。)

SyslogHandler会照顾这也适用于你。当然,您可以使用自己的实例syslog,而不是系统实例。


13

其他变量的一种,使日志记录线程和队列线程分开。

"""sample code for logging in subprocesses using multiprocessing

* Little handler magic - The main process uses loggers and handlers as normal.
* Only a simple handler is needed in the subprocess that feeds the queue.
* Original logger name from subprocess is preserved when logged in main
  process.
* As in the other implementations, a thread reads the queue and calls the
  handlers. Except in this implementation, the thread is defined outside of a
  handler, which makes the logger definitions simpler.
* Works with multiple handlers.  If the logger in the main process defines
  multiple handlers, they will all be fed records generated by the
  subprocesses loggers.

tested with Python 2.5 and 2.6 on Linux and Windows

"""

import os
import sys
import time
import traceback
import multiprocessing, threading, logging, sys

DEFAULT_LEVEL = logging.DEBUG

formatter = logging.Formatter("%(levelname)s: %(asctime)s - %(name)s - %(process)s - %(message)s")

class SubProcessLogHandler(logging.Handler):
    """handler used by subprocesses

    It simply puts items on a Queue for the main process to log.

    """

    def __init__(self, queue):
        logging.Handler.__init__(self)
        self.queue = queue

    def emit(self, record):
        self.queue.put(record)

class LogQueueReader(threading.Thread):
    """thread to write subprocesses log records to main process log

    This thread reads the records written by subprocesses and writes them to
    the handlers defined in the main process's handlers.

    """

    def __init__(self, queue):
        threading.Thread.__init__(self)
        self.queue = queue
        self.daemon = True

    def run(self):
        """read from the queue and write to the log handlers

        The logging documentation says logging is thread safe, so there
        shouldn't be contention between normal logging (from the main
        process) and this thread.

        Note that we're using the name of the original logger.

        """
        # Thanks Mike for the error checking code.
        while True:
            try:
                record = self.queue.get()
                # get the logger for this record
                logger = logging.getLogger(record.name)
                logger.callHandlers(record)
            except (KeyboardInterrupt, SystemExit):
                raise
            except EOFError:
                break
            except:
                traceback.print_exc(file=sys.stderr)

class LoggingProcess(multiprocessing.Process):

    def __init__(self, queue):
        multiprocessing.Process.__init__(self)
        self.queue = queue

    def _setupLogger(self):
        # create the logger to use.
        logger = logging.getLogger('test.subprocess')
        # The only handler desired is the SubProcessLogHandler.  If any others
        # exist, remove them. In this case, on Unix and Linux the StreamHandler
        # will be inherited.

        for handler in logger.handlers:
            # just a check for my sanity
            assert not isinstance(handler, SubProcessLogHandler)
            logger.removeHandler(handler)
        # add the handler
        handler = SubProcessLogHandler(self.queue)
        handler.setFormatter(formatter)
        logger.addHandler(handler)

        # On Windows, the level will not be inherited.  Also, we could just
        # set the level to log everything here and filter it in the main
        # process handlers.  For now, just set it from the global default.
        logger.setLevel(DEFAULT_LEVEL)
        self.logger = logger

    def run(self):
        self._setupLogger()
        logger = self.logger
        # and here goes the logging
        p = multiprocessing.current_process()
        logger.info('hello from process %s with pid %s' % (p.name, p.pid))


if __name__ == '__main__':
    # queue used by the subprocess loggers
    queue = multiprocessing.Queue()
    # Just a normal logger
    logger = logging.getLogger('test')
    handler = logging.StreamHandler()
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    logger.setLevel(DEFAULT_LEVEL)
    logger.info('hello from the main process')
    # This thread will read from the subprocesses and write to the main log's
    # handlers.
    log_queue_reader = LogQueueReader(queue)
    log_queue_reader.start()
    # create the processes.
    for i in range(10):
        p = LoggingProcess(queue)
        p.start()
    # The way I read the multiprocessing warning about Queue, joining a
    # process before it has finished feeding the Queue can cause a deadlock.
    # Also, Queue.empty() is not realiable, so just make sure all processes
    # are finished.
    # active_children joins subprocesses when they're finished.
    while multiprocessing.active_children():
        time.sleep(.1)

我喜欢从队列记录中获取记录器名称的想法。这允许fileConfig()在MainProcess中使用常规,而在PoolWorkers中仅使用几乎未配置的记录器(仅使用setLevel(logging.NOTSET))。正如我在另一条评论中提到的那样,我正在使用Pool,因此必须从Manager中获取我的Queue(代理),而不是进行多进程处理,以便对其进行腌制。这使我可以将队列传递给字典内部的工作人员(其中大部分工作是使用argsparse对象派生的vars())。最后,我觉得这是缺少fork()并破坏@zzzeak解决方案的MS Windows最佳方法。
13年

@mlt我认为您也可以在初始化中放入一个多处理队列,而不使用管理器(请参阅stackoverflow.com/questions/25557686/…的答案-关于锁,但我认为它也适用于队列)
幻想

@fantabolous不能在MS Windows或缺少的任何其他平台上使用fork。这样,每个进程将具有其自己独立的无用队列。链接的Q / A中的第二种方法不适用于此类平台。这是获取非便携式代码的一种方法。
mlt

@mlt有趣。我正在使用Windows,对我来说似乎可以正常工作-在我上一次评论后不久,我建立了一个multiprocessing.Queue与主进程共享a的进程池,此后一直在使用。不会声称理解它为什么起作用。
疯狂

10

通过使用处理程序,所有当前解决方案都与日志记录配置耦合在一起。我的解决方案具有以下架构和功能:

  • 您可以使用所需的任何日志记录配置
  • 记录是在守护程序线程中完成的
  • 使用上下文管理器安全关闭守护程序
  • 与日志记录线程的通信由 multiprocessing.Queue
  • 在子流程中 logging.Logger(和已经定义的实例)被修补以将所有记录发送到队列
  • :在发送到队列之前,格式化回溯和消息,以防止出现腌制错误

带有用法示例和输出的代码可以在以下Gist中找到:https : //gist.github.com/schlamar/7003737


除非我缺少任何内容,否则实际上不是守护进程线程,因为您从未设置daemon_thread.daemonTrue。我需要这样做,以便在上下文管理器中发生异常时让我的Python程序正确退出。
blah238 '16

我还需要捕获,记录和吞入目标func在中抛出logged_call的异常,否则该异常将与其他已记录的输出混为一谈。这是我对此的修改后的版本:gist.github.com/blah238/8ab79c4fe9cdb254f5c37abfc5dc85bf
blah238 '16

8

由于我们可以将多进程日志记录表示为许多发布者和一个订户(侦听器),因此使用ZeroMQ来实现PUB-SUB消息传递确实是一种选择。

而且,PyZMQ模块(ZMQ的Python绑定)实现了PUBHandler,PUBHandler是用于通过zmq.PUB套接字发布日志消息的对象。

Web上有一个解决方案,可以使用PyZMQ和PUBHandler从分布式应用程序进行集中日志记录,可以轻松地将其用于本地多个发布过程。

formatters = {
    logging.DEBUG: logging.Formatter("[%(name)s] %(message)s"),
    logging.INFO: logging.Formatter("[%(name)s] %(message)s"),
    logging.WARN: logging.Formatter("[%(name)s] %(message)s"),
    logging.ERROR: logging.Formatter("[%(name)s] %(message)s"),
    logging.CRITICAL: logging.Formatter("[%(name)s] %(message)s")
}

# This one will be used by publishing processes
class PUBLogger:
    def __init__(self, host, port=config.PUBSUB_LOGGER_PORT):
        self._logger = logging.getLogger(__name__)
        self._logger.setLevel(logging.DEBUG)
        self.ctx = zmq.Context()
        self.pub = self.ctx.socket(zmq.PUB)
        self.pub.connect('tcp://{0}:{1}'.format(socket.gethostbyname(host), port))
        self._handler = PUBHandler(self.pub)
        self._handler.formatters = formatters
        self._logger.addHandler(self._handler)

    @property
    def logger(self):
        return self._logger

# This one will be used by listener process
class SUBLogger:
    def __init__(self, ip, output_dir="", port=config.PUBSUB_LOGGER_PORT):
        self.output_dir = output_dir
        self._logger = logging.getLogger()
        self._logger.setLevel(logging.DEBUG)

        self.ctx = zmq.Context()
        self._sub = self.ctx.socket(zmq.SUB)
        self._sub.bind('tcp://*:{1}'.format(ip, port))
        self._sub.setsockopt(zmq.SUBSCRIBE, "")

        handler = handlers.RotatingFileHandler(os.path.join(output_dir, "client_debug.log"), "w", 100 * 1024 * 1024, 10)
        handler.setLevel(logging.DEBUG)
        formatter = logging.Formatter("%(asctime)s;%(levelname)s - %(message)s")
        handler.setFormatter(formatter)
        self._logger.addHandler(handler)

  @property
  def sub(self):
      return self._sub

  @property
  def logger(self):
      return self._logger

#  And that's the way we actually run things:

# Listener process will forever listen on SUB socket for incoming messages
def run_sub_logger(ip, event):
    sub_logger = SUBLogger(ip)
    while not event.is_set():
        try:
            topic, message = sub_logger.sub.recv_multipart(flags=zmq.NOBLOCK)
            log_msg = getattr(logging, topic.lower())
            log_msg(message)
        except zmq.ZMQError as zmq_error:
            if zmq_error.errno == zmq.EAGAIN:
                pass


# Publisher processes loggers should be initialized as follows:

class Publisher:
    def __init__(self, stop_event, proc_id):
        self.stop_event = stop_event
        self.proc_id = proc_id
        self._logger = pub_logger.PUBLogger('127.0.0.1').logger

     def run(self):
         self._logger.info("{0} - Sending message".format(proc_id))

def run_worker(event, proc_id):
    worker = Publisher(event, proc_id)
    worker.run()

# Starting subscriber process so we won't loose publisher's messages
sub_logger_process = Process(target=run_sub_logger,
                                 args=('127.0.0.1'), stop_event,))
sub_logger_process.start()

#Starting publisher processes
for i in range(MAX_WORKERS_PER_CLIENT):
    processes.append(Process(target=run_worker,
                                 args=(stop_event, i,)))
for p in processes:
    p.start()

6

我也喜欢zzzeek的回答,但是Andre正确地要求需要一个队列来防止乱码。我的烟斗有些运气,但确实看到了一些意外的声。事实证明,实现它比我想象的要困难得多,尤其是由于它在Windows上运行,这对全局变量和内容有一些其他限制(请参阅:如何在Windows上实现Python多处理?

但是,我终于让它工作了。该示例可能并不完美,因此欢迎提出评论和建议。它还不支持设置格式化程序或除根记录程序以外的任何设置。基本上,您必须使用队列在每个池进程中重新初始化记录器,并在记录器上设置其他属性。

同样,欢迎提出任何有关如何使代码更好的建议。我当然还不知道所有的Python技巧:-)

import multiprocessing, logging, sys, re, os, StringIO, threading, time, Queue

class MultiProcessingLogHandler(logging.Handler):
    def __init__(self, handler, queue, child=False):
        logging.Handler.__init__(self)

        self._handler = handler
        self.queue = queue

        # we only want one of the loggers to be pulling from the queue.
        # If there is a way to do this without needing to be passed this
        # information, that would be great!
        if child == False:
            self.shutdown = False
            self.polltime = 1
            t = threading.Thread(target=self.receive)
            t.daemon = True
            t.start()

    def setFormatter(self, fmt):
        logging.Handler.setFormatter(self, fmt)
        self._handler.setFormatter(fmt)

    def receive(self):
        #print "receive on"
        while (self.shutdown == False) or (self.queue.empty() == False):
            # so we block for a short period of time so that we can
            # check for the shutdown cases.
            try:
                record = self.queue.get(True, self.polltime)
                self._handler.emit(record)
            except Queue.Empty, e:
                pass

    def send(self, s):
        # send just puts it in the queue for the server to retrieve
        self.queue.put(s)

    def _format_record(self, record):
        ei = record.exc_info
        if ei:
            dummy = self.format(record) # just to get traceback text into record.exc_text
            record.exc_info = None  # to avoid Unpickleable error

        return record

    def emit(self, record):
        try:
            s = self._format_record(record)
            self.send(s)
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            self.handleError(record)

    def close(self):
        time.sleep(self.polltime+1) # give some time for messages to enter the queue.
        self.shutdown = True
        time.sleep(self.polltime+1) # give some time for the server to time out and see the shutdown

    def __del__(self):
        self.close() # hopefully this aids in orderly shutdown when things are going poorly.

def f(x):
    # just a logging command...
    logging.critical('function number: ' + str(x))
    # to make some calls take longer than others, so the output is "jumbled" as real MP programs are.
    time.sleep(x % 3)

def initPool(queue, level):
    """
    This causes the logging module to be initialized with the necessary info
    in pool threads to work correctly.
    """
    logging.getLogger('').addHandler(MultiProcessingLogHandler(logging.StreamHandler(), queue, child=True))
    logging.getLogger('').setLevel(level)

if __name__ == '__main__':
    stream = StringIO.StringIO()
    logQueue = multiprocessing.Queue(100)
    handler= MultiProcessingLogHandler(logging.StreamHandler(stream), logQueue)
    logging.getLogger('').addHandler(handler)
    logging.getLogger('').setLevel(logging.DEBUG)

    logging.debug('starting main')

    # when bulding the pool on a Windows machine we also have to init the logger in all the instances with the queue and the level of logging.
    pool = multiprocessing.Pool(processes=10, initializer=initPool, initargs=[logQueue, logging.getLogger('').getEffectiveLevel()] ) # start worker processes
    pool.map(f, range(0,50))
    pool.close()

    logging.debug('done')
    logging.shutdown()
    print "stream output is:"
    print stream.getvalue()

1
我想知道是否if 'MainProcess' == multiprocessing.current_process().name:可以代替传递child
13年

如果其他人试图使用进程池而不是Windows上的单独进程对象,则值得一提的是,应该使用Manager将队列传递给子进程,因为它不能直接进行腌制。
13年

此实现对我来说效果很好。我修改了它以使用任意数量的处理程序。这样,您可以以非多处理方式配置根处理程序,然后在安全的地方创建队列,将根处理程序传递给该处理程序,将其删除,然后使其成为唯一的处理程序。
Jaxor24 2015年

3

只需将您的记录器实例发布到某个地方即可。这样,其他模块和客户端就可以使用您的API来获取记录器,而不必这样做import multiprocessing


1
问题在于,多处理记录器未命名,因此您将无法轻松地解密消息流。也许可以在创建后命名它们,这将使其看起来更加合理。
cdleary

好,为每个模块发布一个记录器,或者更好地,导出使用带有模块名称的记录器的不同闭包。关键是让其他模块使用您的API
Javier

绝对合理(我也可以+1!),但我会错过能够import logging; logging.basicConfig(level=logging.DEBUG); logging.debug('spam!')从任何地方正常运行的机会。
cdleary

3
当我使用Python时,我发现这是一个有趣的现象,我们习惯于以1或2条简单的行来完成我们想要的事情,以至于其他语言的简单和逻辑方法(例如,发布多处理记录器或包装它在访问器中)仍然感觉像是一种负担。:)
Kylotan

3

我喜欢zzzeek的回答。我只是将管道替换为队列,因为如果多个线程/进程使用相同的管道末端生成日志消息,则它们将变得乱码。


我在处理程序上遇到了一些问题,尽管并不是消息乱码,但整个过程都会停止。我将Pipe更改为Queue,因为这更合适。但是,我得到的错误并不能因此解决-最终我在try()方法中添加了try / except-极少数情况下,尝试记录异常会失败并最终被捕获。一旦添加了try / except,它就会运行数周而没有问题,而standarderr文件每周将捕获大约两个错误的异常。
zzzeek

2

如何将所有日志委托给另一个从队列读取所有日志条目的进程?

LOG_QUEUE = multiprocessing.JoinableQueue()

class CentralLogger(multiprocessing.Process):
    def __init__(self, queue):
        multiprocessing.Process.__init__(self)
        self.queue = queue
        self.log = logger.getLogger('some_config')
        self.log.info("Started Central Logging process")

    def run(self):
        while True:
            log_level, message = self.queue.get()
            if log_level is None:
                self.log.info("Shutting down Central Logging process")
                break
            else:
                self.log.log(log_level, message)

central_logger_process = CentralLogger(LOG_QUEUE)
central_logger_process.start()

只需通过任何多进程机制甚至继承共享LOG_QUEUE,一切都很好!


1

我有一个与Ironhacker相似的解决方案,除了我在某些代码中使用logging.exception之外,发现我需要格式化异常,然后再将其传递回Queue,因为无法进行回溯:

class QueueHandler(logging.Handler):
    def __init__(self, queue):
        logging.Handler.__init__(self)
        self.queue = queue
    def emit(self, record):
        if record.exc_info:
            # can't pass exc_info across processes so just format now
            record.exc_text = self.formatException(record.exc_info)
            record.exc_info = None
        self.queue.put(record)
    def formatException(self, ei):
        sio = cStringIO.StringIO()
        traceback.print_exception(ei[0], ei[1], ei[2], None, sio)
        s = sio.getvalue()
        sio.close()
        if s[-1] == "\n":
            s = s[:-1]
        return s

我在这里找到了一个完整的例子。
Aryeh Leib Taurog 2012年

1

下面是可以在Windows环境中使用的类,需要ActivePython。您还可以继承其他日志记录处理程序(StreamHandler等)。

class SyncronizedFileHandler(logging.FileHandler):
    MUTEX_NAME = 'logging_mutex'

    def __init__(self , *args , **kwargs):

        self.mutex = win32event.CreateMutex(None , False , self.MUTEX_NAME)
        return super(SyncronizedFileHandler , self ).__init__(*args , **kwargs)

    def emit(self, *args , **kwargs):
        try:
            win32event.WaitForSingleObject(self.mutex , win32event.INFINITE)
            ret = super(SyncronizedFileHandler , self ).emit(*args , **kwargs)
        finally:
            win32event.ReleaseMutex(self.mutex)
        return ret

这是一个演示用法的示例:

import logging
import random , time , os , sys , datetime
from string import letters
import win32api , win32event
from multiprocessing import Pool

def f(i):
    time.sleep(random.randint(0,10) * 0.1)
    ch = random.choice(letters)
    logging.info( ch * 30)


def init_logging():
    '''
    initilize the loggers
    '''
    formatter = logging.Formatter("%(levelname)s - %(process)d - %(asctime)s - %(filename)s - %(lineno)d - %(message)s")
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)

    file_handler = SyncronizedFileHandler(sys.argv[1])
    file_handler.setLevel(logging.INFO)
    file_handler.setFormatter(formatter)
    logger.addHandler(file_handler)

#must be called in the parent and in every worker process
init_logging() 

if __name__ == '__main__':
    #multiprocessing stuff
    pool = Pool(processes=10)
    imap_result = pool.imap(f , range(30))
    for i , _ in enumerate(imap_result):
        pass

可能使用multiprocessing.Lock()而不是Windows Mutex将使该解决方案具有可移植性。
xmedeko

1

这是我的简单技巧/解决方法...不是最全面的工具,而是我认为比我在编写此书之前发现的任何其他答案更容易修改和易于理解的方法:

import logging
import multiprocessing

class FakeLogger(object):
    def __init__(self, q):
        self.q = q
    def info(self, item):
        self.q.put('INFO - {}'.format(item))
    def debug(self, item):
        self.q.put('DEBUG - {}'.format(item))
    def critical(self, item):
        self.q.put('CRITICAL - {}'.format(item))
    def warning(self, item):
        self.q.put('WARNING - {}'.format(item))

def some_other_func_that_gets_logger_and_logs(num):
    # notice the name get's discarded
    # of course you can easily add this to your FakeLogger class
    local_logger = logging.getLogger('local')
    local_logger.info('Hey I am logging this: {} and working on it to make this {}!'.format(num, num*2))
    local_logger.debug('hmm, something may need debugging here')
    return num*2

def func_to_parallelize(data_chunk):
    # unpack our args
    the_num, logger_q = data_chunk
    # since we're now in a new process, let's monkeypatch the logging module
    logging.getLogger = lambda name=None: FakeLogger(logger_q)
    # now do the actual work that happens to log stuff too
    new_num = some_other_func_that_gets_logger_and_logs(the_num)
    return (the_num, new_num)

if __name__ == '__main__':
    multiprocessing.freeze_support()
    m = multiprocessing.Manager()
    logger_q = m.Queue()
    # we have to pass our data to be parallel-processed
    # we also need to pass the Queue object so we can retrieve the logs
    parallelable_data = [(1, logger_q), (2, logger_q)]
    # set up a pool of processes so we can take advantage of multiple CPU cores
    pool_size = multiprocessing.cpu_count() * 2
    pool = multiprocessing.Pool(processes=pool_size, maxtasksperchild=4)
    worker_output = pool.map(func_to_parallelize, parallelable_data)
    pool.close() # no more tasks
    pool.join()  # wrap up current tasks
    # get the contents of our FakeLogger object
    while not logger_q.empty():
        print logger_q.get()
    print 'worker output contained: {}'.format(worker_output)

1

有这个很棒的包裹

包: https //pypi.python.org/pypi/multiprocessing-logging/

码: https //github.com/jruere/multiprocessing-logging

安装:

pip install multiprocessing-logging

然后加:

import multiprocessing_logging

# This enables logs inside process
multiprocessing_logging.install_mp_handler()

3
该库从字面上是基于对当前SO帖子的另一条评论:stackoverflow.com/a/894284/1698058
克里斯·亨特

来源:stackoverflow.com/a/894284/1663382除了主页上的文档之外,我还赞赏该模块的示例用法。
Liquidgenius

0

一种选择是将多进程日志记录写入已知文件,并注册atexit处理程序以加入这些进程,并在stderr上将其读回。但是,您将无法以这种方式在stderr上实时获取输出消息。


是您提出以下相同,从您的评论这里的一个办法stackoverflow.com/questions/641420/...
iruvar

0

如果logging模块中的锁,线程和fork组合中发生死锁,则在错误报告6721报告该死锁(另请参见相关的SO问题)。

有贴小修正的解决方案在这里

但是,这只会解决中的任何潜在僵局logging。这不能解决可能出现乱码的问题。请参阅此处提供的其他答案。


0

提到的最简单的想法:

  • 获取文件名和当前进程的进程ID。
  • 设置一个[WatchedFileHandler][1]。这种处理器的原因进行了详细的讨论在这里,但总之,与其他日志记录处理程序相比,某些竞争条件会更糟。这是竞争条件最短的窗口。
    • 选择将日志保存到的路径,例如/ var / log /...。

0

对于可能需要此服务的人,我为multiprocessing_logging包编写了一个装饰器,该装饰器将当前进程名称添加到日志中,因此可以清楚地知道谁记录了什么。

它还会运行install_mp_handler(),因此在创建池之前运行它变得毫无用处。

这使我可以看到哪个工作人员创建了哪些日志消息。

这是带有示例的蓝图:

import sys
import logging
from functools import wraps
import multiprocessing
import multiprocessing_logging

# Setup basic console logger as 'logger'
logger = logging.getLogger()
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(logging.Formatter(u'%(asctime)s :: %(levelname)s :: %(message)s'))
logger.setLevel(logging.DEBUG)
logger.addHandler(console_handler)


# Create a decorator for functions that are called via multiprocessing pools
def logs_mp_process_names(fn):
    class MultiProcessLogFilter(logging.Filter):
        def filter(self, record):
            try:
                process_name = multiprocessing.current_process().name
            except BaseException:
                process_name = __name__
            record.msg = f'{process_name} :: {record.msg}'
            return True

    multiprocessing_logging.install_mp_handler()
    f = MultiProcessLogFilter()

    # Wraps is needed here so apply / apply_async know the function name
    @wraps(fn)
    def wrapper(*args, **kwargs):
        logger.removeFilter(f)
        logger.addFilter(f)
        return fn(*args, **kwargs)

    return wrapper


# Create a test function and decorate it
@logs_mp_process_names
def test(argument):
    logger.info(f'test function called via: {argument}')


# You can also redefine undecored functions
def undecorated_function():
    logger.info('I am not decorated')


@logs_mp_process_names
def redecorated(*args, **kwargs):
    return undecorated_function(*args, **kwargs)


# Enjoy
if __name__ == '__main__':
    with multiprocessing.Pool() as mp_pool:
        # Also works with apply_async
        mp_pool.apply(test, ('mp pool',))
        mp_pool.apply(redecorated)
        logger.info('some main logs')
        test('main program')

-5

对于数十年来遇到相同问题并在此站点上发现此问题的孩子们,我留下此答案。

简单与过度复杂。只需使用其他工具即可。Python很棒,但并非旨在执行某些操作。

以下logrotate守护程序的代码段对我有用,并且不会使事情复杂化。安排它每小时运行一次,

/var/log/mylogfile.log {
    size 1
    copytruncate
    create
    rotate 10
    missingok
    postrotate
        timeext=`date -d '1 hour ago' "+%Y-%m-%d_%H"`
        mv /var/log/mylogfile.log.1 /var/log/mylogfile-$timeext.log
    endscript
}

这是我的安装方式(符号链接不适用于logrotate):

sudo cp /directpath/config/logrotate/myconfigname /etc/logrotate.d/myconfigname
sudo cp /etc/cron.daily/logrotate /etc/cron.hourly/logrotate
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.