2

The following piece of code creates 3 or 4 log files. How should it be written so that only 1 file is created while keeping the format of the filename? Also, this seems to be a Windows only issue. When I run this on a RHEL 5.3 box even setting nproc to 1000, I only get 1 log file. [Python 3.4.1]

import datetime, logging, multiprocessing, os.path, time

logging.basicConfig(level=logging.DEBUG, format="%(asctime)-4s %(process)6s  %(message)s", datefmt="%m-%d %H:%M:%S",
    filename="test_%s.log"%(datetime.datetime.today().strftime("%Y%m%d-%H%M%S")))

def worker(n):
    logging.info("before")
    time.sleep(n)
    logging.info("after")

if __name__=='__main__':
    nproc = 40
    pool = multiprocessing.Pool(processes=nproc)
    pool.map(worker, (6,)*nproc)
ironv
  • 978
  • 10
  • 25

2 Answers2

1

Original Answer: Put the logging.basicConfig call in your if main statement.

New Answer: Add the logging call to the beginning of the worker function. You could put it into a function named setup_logging for future changes. My previous answer probably won't work because the workers won't have logging set up.

When multiprocessing runs a new target it has to start a whole new interpreter that then loads the module with the function to run. Where you have your logging call it will be run every time the module is loaded. I'm guessing that it only creating one file on linux is just luck.

I should note that the normal pattern for logging (at least that I use) is:

import logging

log = logging.getLogger(__name__)

# Code
def a_func():
    log.debug("This is my message")

if __name__ == "__main__":
    logging.basicConfig(...)
    a_func()

Edit: If it wasn't clear what I meant by luck, I meant that you are creating a filename with the current date/time in it so getting multiple processes started at the same time is just luck. If an operating system takes too long to start up X number of workers then you'll get different filenames. On linux it's possible it's doing something smart to load all the interpreters/modules at the same time and then call the function.

Your code: I should apologize for writing stackoverflow answers when tired. My previous solution doesn't actually solve the problem. This does:

import datetime, logging, multiprocessing, os.path, time

def worker(args):
    # it's not simple to pass more than one argument so we expand the tuple here
    n, log_filename = args
    logging.basicConfig(level=logging.DEBUG,
            format="%(asctime)-4s %(process)6s  %(message)s",
            datefmt="%m-%d %H:%M:%S",
            filename=log_filename)

    logging.info("before")
    time.sleep(n)
    logging.info("after")

if __name__=='__main__':
    filename="test_%s.log" % (datetime.datetime.today().strftime("%Y%m%d-%H%M%S"),)
    nproc = 40
    pool = multiprocessing.Pool(processes=nproc)
    pool.map(worker, ((6, filename),)*nproc)
djhoese
  • 3,567
  • 1
  • 27
  • 45
  • Thanks for your response. I tried this and I am getting an empty log file. Can you please make the change to my code and then post that. Thanks again. – ironv Jul 30 '14 at 05:43
  • @ironv, I updated my question with what your code should look like with the least amount of changes. – djhoese Jul 30 '14 at 13:24
  • thanks. (i) Can you please check the number of lines printed in the log file. There should be 80 lines. When I am running it, I am getting fewer lines. Run it a couple of times and you will see that the number of lines written is different. (ii) suppose I want to write to log from the main part as well, do I creating another logging.basicConfig call there? – ironv Jul 30 '14 at 13:40
  • I had only been testing this on mac. I just tested it on windows and I got the same results you did. I did a few ugly things to try to get it to output all the lines, but no luck (it always missed 1). I don't normally use python on windows so I'm not sure. Maybe create another question to ask about this. For your second point, yes you could add another basicConfig call to the main part. The `basicConfig` function is just a convenience function for adding handlers and formatters to the root logger. Check out the logging documentation for more information. Bottom line: I'm stumped. – djhoese Jul 30 '14 at 14:03
  • `logging` is thread-safe, but not process-safe. You're creating many handles to a file and writing to it without any sychronization. You don't see it on UNIX because on those platforms, the file handle will be inherited in the worker processes via `os.fork`. Windows doesn't support `os.fork`, so it needs to spawn a completely new process and re-import the `__main__` module of your script, which means it creates a completely new handle to your file. – dano Jul 30 '14 at 15:24
  • @dano, I thought on UNIX it was mainly because of the append mode of the open file. I didn't think it mattered if it was Windows or UNIX since if you have two processes appending to a file it should be atomic. But then again, I do recall Windows handling appending differently. – djhoese Jul 30 '14 at 16:03
  • @daveydave400 See [this question](http://stackoverflow.com/q/7842511/2073595) for why it's not safe to have two processes writing to a file concurrently on UNIX. – dano Jul 30 '14 at 16:09
  • Right, if you go over the buffer. If you can guarantee that you aren't going to go over the buffer (4K usually) then append is atomic: http://stackoverflow.com/questions/1154446/is-file-append-atomic-in-unix. Although there seems to be some confusion on whether or not it applies to regular files (I've always had it work for regular files). – djhoese Jul 30 '14 at 16:19
  • Looks like atomic appends are implemented in most unix/linux systems, but it isn't guaranteed by any standards from the answers I'm reading. – djhoese Jul 30 '14 at 16:25
0

There are a couple of things you need to do:

  1. Make sure you create the name of the file once, and share it across all processes. daveydave400 covered this in his answer.
  2. Make sure write operations across all processes are synchronized. If you don't do this, your worker processes will try to write to the log concurrently, which can result in one write stomping on another. Avoiding this was not covered in the other answer.

The logging module internally uses a threading.RLock to synchronize writes, but this is not process-safe: each process gets its own Rlock that knows nothing about the others. So, you need to create your own logging.Handler that uses a process-safe lock, and then share that lock with every process in your pool. We can do this by taking advantage of the initializer/initargs keyword arguments to multiprocessing.Pool; we just pass all the parameters we need to create identical loggers in all the worker processes to an initializer function, and then build a global logging.Logger object for each process.

Here's a working example:

import datetime, logging, multiprocessing, os.path, time

log = None
class ProcessFileHandler(logging.FileHandler):
    def __init__(self, *args, **kwargs):
        if 'lock' in kwargs:
            self._lock = kwargs['lock']
            del kwargs['lock']
        else:
            raise ValueError("No 'lock' keyword argument provided")
        super(ProcessFileHandler, self).__init__(*args, **kwargs)

    def createLock(self):
        return self._lock

def setup_logging(level, filename, format, datefmt, lock):
    global log  # Creates a global log variable in each process
    log = logging.getLogger()
    handler = ProcessFileHandler(filename, lock=lock)
    log.setLevel(level)
    fmt = logging.Formatter(fmt=format, datefmt=datefmt)
    handler.setFormatter(fmt)
    log.addHandler(handler)

def worker(n):
    log.info("before")
    time.sleep(n)
    log.info("after")

if __name__=='__main__':
    nproc = 40
    # initialize all the logging attributes we need
    format="%(asctime)-4s %(process)6s  %(message)s"
    datefmt="%m-%d %H:%M:%S"
    filename="test_%s.log"%(datetime.datetime.today().strftime("%Y%m%d-%H%M%S"))
    level=logging.DEBUG
    lock = multiprocessing.RLock()
    setup_logging(level, filename, format, datefmt, lock)  # Create one for this process

    pool = multiprocessing.Pool(processes=nproc, initializer=setup_logging, initargs=(level, filename, format, datefmt, lock))
    pool.map(worker, (6,)*nproc)
dano
  • 91,354
  • 19
  • 222
  • 219