À l'heure actuelle, j'ai un module central dans un cadre qui génère plusieurs processus à l'aide du module Python 2.6 multiprocessing
module . Comme il utilise multiprocessing
, il existe un journal multitraitement au niveau du module, LOG = multiprocessing.get_logger()
. Per les docs , cet enregistreur dispose de verrous partagés afin que vous ne perdiez rien dans sys.stderr
(ou quelque traitement de fichier que ce soit) en ayant plusieurs processus écrivant simultanément.
Le problème que j'ai maintenant est que les autres modules du framework ne sont pas compatibles avec le multi-traitement. D'après ce que je vois, je dois faire en sorte que toutes les dépendances de ce module central utilisent une journalisation prenant en charge le multitraitement. C'est ennuyeux dans le cadre, sans parler de tous les clients du cadre. Existe-t-il des alternatives auxquelles je ne pense pas?
La seule façon de traiter ce problème de manière non intrusive est de:
select
à partir des descripteurs de fichiers des canaux, effectuez un tri par fusion sur les entrées de journal disponibles et redéfinissez-le en un journal centralisé. Répétez.)Je viens tout juste d’écrire un de mes propres gestionnaires de journaux qui transfère tout au processus parent via un canal. Je ne fais que le tester depuis dix minutes mais cela semble fonctionner assez bien.
(Note: Ceci est codé en dur en RotatingFileHandler
, ce qui est mon propre cas d'utilisation.)
Ceci utilise maintenant une file d'attente pour le traitement correct de la simultanéité et récupère également correctement des erreurs. Je l'utilise depuis plusieurs mois en production et la version actuelle ci-dessous fonctionne sans problème.
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)
Une autre alternative pourrait être les divers gestionnaires de journalisation non basés sur fichier dans le paquet logging
:
SocketHandler
DatagramHandler
SyslogHandler
(et d'autres)
De cette façon, vous pourriez facilement avoir un démon de journalisation dans lequel vous pourriez écrire en toute sécurité et gérer les résultats correctement. (Par exemple, un serveur de socket simple qui désélectionne le message et l’émet dans son propre gestionnaire de fichiers en rotation.)
La SyslogHandler
s'occuperait de cela pour vous aussi. Bien sûr, vous pouvez utiliser votre propre instance de syslog
, pas celle du système.
Le livre de recettes de journalisation Python contient deux exemples complets ici: https://docs.python.org/3/howto/logging-cookbook.html#logging-to-a-single-file-from-multiple-processes
Il utilise QueueHandler
, qui est nouveau dans python 3.2 mais facile à copier dans votre propre code (comme je l’ai moi-même dans python 2.7) depuis: https://Gist.github.com/vsajip/591589
Chaque processus met sa journalisation sur la Queue
, puis un thread ou un processus listener
(un exemple est fourni pour chacun) les sélectionne et les écrit dans un fichier - aucun risque de corruption ou de garbling.
Une variante des autres qui maintient la journalisation et le thread de file d'attente séparés.
"""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 __== '__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)
Vous trouverez ci-dessous une autre solution axée sur la simplicité pour quiconque (comme moi) qui arrive ici depuis Google. L'enregistrement devrait être facile! Seulement pour 3.2 ou plus.
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 __== '__main__':
main()
Toutes les solutions actuelles sont trop couplées à la configuration de journalisation à l'aide d'un gestionnaire. Ma solution présente l'architecture et les fonctionnalités suivantes:
multiprocessing.Queue
logging.Logger
(et les instances déjà définies) sont corrigés pour envoyer des enregistrements tous à la file d'attente.Vous trouverez le code avec un exemple d'utilisation et une sortie à l'adresse suivante: https://Gist.github.com/schlamar/7003737
Étant donné que nous pouvons représenter la journalisation multiprocessus comme plusieurs éditeurs et un seul abonné (auditeur), utiliser ZeroMQ pour implémenter la messagerie PUB-SUB est effectivement une option.
De plus, PyZMQ module, les liaisons Python pour ZMQ, implémente PUBHandler , objet de publication des messages de journalisation sur un socket zmq.PUB.
Il existe une solution sur le Web , pour la journalisation centralisée à partir d’une application distribuée utilisant PyZMQ et PUBHandler, qui peut être facilement adoptée pour travailler localement avec plusieurs processus de publication.
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()
J'aime aussi la réponse de zzzeek mais André a raison de dire qu'une file d'attente est nécessaire pour éviter les garbling. J'ai eu un peu de chance avec la pipe, mais j'ai vu des garbling, ce qui est quelque peu attendu. Sa mise en œuvre s'est avérée plus difficile que je ne le pensais, en particulier sous Windows, où des restrictions supplémentaires s'appliquent aux variables globales et autres (voir: Comment Python Multiprocessing est-il implémenté sous Windows? )
Mais j'ai finalement réussi à le faire fonctionner. Cet exemple n'est probablement pas parfait, les commentaires et suggestions sont donc les bienvenus. En outre, il ne prend pas en charge la configuration du formateur ou de tout autre élément que le consignateur racine. En gros, vous devez réinitialiser le consignateur dans chacun des processus de pool avec la file d'attente et configurer les autres attributs du consignateur.
Encore une fois, toute suggestion visant à améliorer le code est la bienvenue. Je ne connais certainement pas encore toutes les astuces de 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 __== '__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()
publiez simplement votre instance de l'enregistreur. De cette façon, les autres modules et clients peuvent utiliser votre API pour obtenir le consignateur sans avoir à import multiprocessing
.
J'ai aimé la réponse de zzzeek. Je voudrais simplement remplacer le canal par une file d'attente, car si plusieurs threads/processus utilisent la même extrémité du canal pour générer des messages de journal, ils seront tronqués.
Que diriez-vous de déléguer toute la journalisation à un autre processus qui lit toutes les entrées de journal d'une file d'attente?
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()
Partagez simplement LOG_QUEUE via l'un des mécanismes multiprocessus ou même l'héritage et tout se passera bien!
Voici mon simple hack/solution de contournement ... pas le plus complet, mais facilement modifiable et plus simple à lire et à comprendre, je pense que toutes les autres réponses que j'ai trouvées avant d'écrire ceci:
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 __== '__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)
Ci-dessous, une classe utilisable dans l'environnement Windows, requiert ActivePython . Vous pouvez également hériter pour d'autres gestionnaires de journalisation (StreamHandler, etc.).
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
Et voici un exemple qui illustre l'utilisation:
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 __== '__main__':
#multiprocessing stuff
pool = Pool(processes=10)
imap_result = pool.imap(f , range(30))
for i , _ in enumerate(imap_result):
pass
J'ai une solution similaire à ironhacker, sauf que j'utilise logging.exception dans certains de mes codes et que j'ai dû formater l'exception avant de la renvoyer dans la file d'attente, car les retraits ne sont pas conservables:
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
Si vous avez des blocages dans une combinaison de verrous, de threads et de fourches dans le module logging
, cela est rapporté dans rapport de bogue 6721 (voir aussi related SO question ).
Une petite solution de réparation a été publiée ici .
Cependant, cela ne fera que corriger les blocages potentiels dans logging
. Cela ne réglera pas le fait que les choses sont peut-être brouillées. Voir les autres réponses présentées ici.
Une des solutions consiste à écrire la journalisation de multitraitement dans un fichier connu et à enregistrer un gestionnaire atexit
afin de le relier à ces processus et de le relire sur stderr; Cependant, vous n'obtiendrez pas un flux en temps réel dans les messages de sortie sur stderr de cette façon.
Il y a ce super forfait
Package: https://pypi.python.org/pypi/multiprocessing-logging/
code: https://github.com/jruere/multiprocessing-logging
Installer:
pip install multiprocessing-logging
Puis ajouter:
import multiprocessing_logging
# This enables logs inside process
multiprocessing_logging.install_mp_handler()