0

I'd like to change the log's directory depending on the time (specifically the hour) when new log is created.

For example, let's say the message aaa is saved in a log file in directory d:\log\191105\09\log.log at 09:59 during running a program.

As the time passes, new log bbb is saved in a log file but the directory should be different, d:\log\191105\10.log at 10:00 without terminating the program.

My log manager code is following,

import logging
import datetime
import psutil
import os, os.path

class LogManager:
    today = datetime.datetime.now()
    process_name = psutil.Process().name()
    process_id = str(psutil.Process().pid)
    log_dir = "D:/LOG/" + today.strftime("%Y%m%d") + "/" + today.strftime("%H") + "/"
    log_filename = process_name + '_[' + process_id + ']_' + today.strftime("%Y-%m-%d_%H") + '.log'
    if not os.path.exists(log_dir):
        os.makedirs(log_dir)

    # date/directory config
    log = logging.getLogger('mypython')
    log.setLevel(logging.INFO)
    formatter = logging.Formatter('%(asctime)s||%(levelname)s||%(message)s')
    fileHandler = logging.FileHandler(log_dir+log_filename)
    fileHandler.setFormatter(formatter)

    log.addHandler(fileHandler)

    def update(self):
        self.today = datetime.datetime.now()
        old_log_dir = self.log_dir
        self.log_dir = "D:/LOG/" + self.today.strftime("%Y%m%d") + "/" + self.today.strftime("%H") + "/"

        if (old_log_dir == self.log_dir):
            return

        self.log_filename = self.process_name + '_[' + self.process_id + ']_' + self.today.strftime("%Y-%m-%d_%H") + '.log'
        if not os.path.exists(self.log_dir):
            os.makedirs(self.log_dir)

        self.log = logging.getLogger('mypython')

        # here, i wanna update my filehandler
        for hdlr in self.log.handlers[:]:
            if isinstance(hdlr, self.log.FileHander):
                self.log.removeHandler(hdlr)

        self.fileHandler = logging.FileHandler(self.log_dir+self.log_filename)
        self.fileHandler.setFormatter(self.formatter)

        self.log.addHandler(self.fileHandler)

    def i(self, ex):
        self.update()
        self.log.info(ex)

    def w(self, ex):
        self.update()
        self.log.warning(ex)

and I call these functions like

import LogManager as lm

logManager = lm.LogManager()
logManager.i('message')

It works well, but seems like it does not update its fileHandler after passing the hour.

I tried to find if the log has updateHandler method... but it doesn't .

What should I do??

MikeMajara
  • 922
  • 9
  • 23
cointreau
  • 864
  • 1
  • 10
  • 21
  • As a first impression, you are implementing lots of code that might already be available in existing libraries. Do you really need a LogManager? Can't you do the [same](https://stackoverflow.com/questions/6386698/how-to-write-to-a-file-using-the-logging-python-module), with less code, with the `logging` library? [This question](https://stackoverflow.com/questions/13839554/how-to-change-filehandle-with-python-logging-on-the-fly-with-different-classes-a) also addresses your issue, doesn't it fit your case? Apart from that, what is `i(self, ex)` and `w(self, ex)` -> against all python phylosophy. – MikeMajara Nov 05 '19 at 08:00
  • Does this answer your question? [How to change filehandle with Python logging on the fly with different classes and imports](https://stackoverflow.com/questions/13839554/how-to-change-filehandle-with-python-logging-on-the-fly-with-different-classes-a) – MikeMajara Nov 05 '19 at 08:01
  • @MikeMajara Thank you for your comment, I've already read that answer. That's why I tried removing and then adding the fileHandler, instead of updating. And, i is info and w is warning, I followed someone's code on the internet. Since I'm very new to python so I didn't know that it does not fit into python philosophy. I'm trying to learn. Thanks. :) – cointreau Nov 05 '19 at 08:04
  • Yeah, just saying, so you can learn. Advice would be to rename to `log_info(self, ex)` and so on. And so, you read that answer, but it does not work for you? In your code it seems you are not retrieving the root logger, as recommended by answer in that question, but instead 'mypython' logger. – MikeMajara Nov 05 '19 at 08:30
  • @MikeMajara Yes. I've editted my code(not in this question yet) and tried using root logger. However I also added ```if isinstance(hdlr,log.FileHander):``` according to the answer by Arun. it made an error message ```'RootLogger' object has no attribute 'FileHander'```. I think I need to try again without that code. But as Arun said it may generate the other errors by removing other handlers. :( – cointreau Nov 05 '19 at 09:03
  • With the link from MikeMajara, I finally succeeded what I've wanted. But I still have the concern that it might remove other handlers from other processes + thread safe problem. – cointreau Nov 06 '19 at 03:59
  • If you are using a library, thread safety responsibility lies on that library (separation of concerns). If you were doing something wrong it should (and probably will) warn you or throw Exceptions. What do you mean with handlers from other processes? Anyhow you could do a more sofisticated function which would only remove your FileHandler, to add another. I guess. Instead of all like shown in the snippet of linked question. check [here](https://docs.python.org/3/library/logging.html#logger-objects) and [here](https://docs.python.org/3/howto/logging-cookbook.html) for more examples. – MikeMajara Nov 06 '19 at 06:29
  • Sorry, just now I located the post by @Arun. So, yes, that is what I mean in my last comment. All that you can check by your self, e.g.: `filename` attribute in `FileHandler` object. About that you should worry. Anyhow, for python starters... this feels a bit too much. Is logging so important right now for you? Can you not redesign your logging policy to keep on? If you were to trust your money on this function, would you bet on it? If your not sure, rethink. Often programming problems have little to do with language specifics. Remember KISS. – MikeMajara Nov 06 '19 at 06:45

2 Answers2

1

You can do this much easier by using what is already in the logging library. Specifically there is a TimedRotatingFileHandler for which you only need to change how it names the files it creates. I've created a working minimal demo for you which changes the folder that logs get saved into on every second. Edit: change to rollover on every full hour instead of every second as per comment below

import os
import logging
from time import sleep, strftime
from logging.handlers import TimedRotatingFileHandler

def namer(default_name):
    head, tail = os.path.split(default_name)
    folder_name = 'logs'+strftime('%H%M%S') 
    folder = os.path.join(head, folder_name)
    try:
        os.mkdir(folder)
    except:
        # folder already exists
        pass
    return os.path.join(folder, tail)

logger = logging.getLogger()
handler = TimedRotatingFileHandler('base.log', when='H')
handler.namer = namer
# set the rollover time to the next full hour
handler.rolloverAt = datetime.now().replace(microsecond=0,second=0,minute=0).timestamp() + 60*60
logger.addHandler(handler)

logger.warning('test1')
sleep(2)
logger.warning('test2')
blues
  • 4,547
  • 3
  • 23
  • 39
  • Thank you. I looked at ```TimedRotatingFileHandler```. It seems like it has a logging based on time interval or the day. What I exactly need is logging every hour as a new file, not the time interval.. – cointreau Nov 06 '19 at 01:59
  • So you want to rollover on every full hour? That is just one more line of code. See the edit above. – blues Nov 06 '19 at 09:13
  • This assumes `namer` names the current log, which is incorrect. See my answer for a working alternative. – laker93 Aug 22 '23 at 12:49
0

The problem with @blues solution is that it assumes namer names the current log, which is incorrect and will lead to logging issues.

The intention of the BaseRotatingHandler logic as shown here: https://docs.python.org/3/howto/logging-cookbook.html#using-a-rotator-and-namer-to-customize-log-rotation-processing is to customise the logic of what happens to the rotated files. The rotation logic assumes the current log file name is fixed (hence why I have fixed the path to use the path: logs/today/my_app.log

Below is a full working example that keeps the current logs in a 'today' directory. It then rotates based on the 'when' parameter given to the TimedRotatingFileHandler. On rotation it creates a directory with the previous interval's date time stamp in format %Y%m%d%M. It then rotates everything in 'today' in to that folder.

On the configured rotation time it will then rotate the logs from the past interval in to a directory with name given by the timestamp of now - interval seconds.

For your specific example you would change when to be when='H' and the date_str time format in namer() to be:

date_str = f"{ts.strftime('%Y%m%d')}/{ts.strftime('%H')}"

import datetime
import logging
import time
import logging.handlers as handlers
from pathlib import Path
from typing import List


class DateFormattedPathRotator(handlers.TimedRotatingFileHandler):

    def rotate(self, source: str, dest: str) -> None:
        """
        :param source: The current name of the log file
        :param dest: The name the log file will be rotated to (given by the return of namer)
        :return:
        """
        # rotate existing logs to a folder for the last minute
        print(f"rotating from {source} to {dest}")
        rotation_source = Path(source)
        rotation_destination = Path(dest)
        rotation_destination.parent.mkdir(parents=True, exist_ok=True)
        rotation_source.replace(rotation_destination)

    def namer(self, name):
        """
        :param name: The current name of the log file
        :return: The destination name of the rotated log file
        """
        # the interval is set in the parent class depending on 'when'
        # we will rotate the logs produced within the last 'interval' seconds in to their own time formatted directory
        time_stamp = datetime.datetime.utcnow() - datetime.timedelta(seconds=self.interval)
        date_str = time_stamp.strftime("%Y%m%d%M")
        rotation_path = Path(f"logs/{date_str}")
        rotation_destination = rotation_path / Path(name).name
        return str(rotation_destination)


if __name__ == "__main__":
    log_path_str = f"logs/today/my_app.log"
    log_path = Path(log_path_str)
    log_path.parent.mkdir(parents=True, exist_ok=True)
    file_handler = DateFormattedPathRotator(
        log_path,
        when='m',
        interval=1,
    )
    log_handlers: List[logging.Handler] = [logging.StreamHandler(), file_handler]
    logging.basicConfig(
        level="INFO",
        format="[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s",
        handlers=log_handlers,
    )
    logger = logging.getLogger(__name__)
    i = 0
    while True:
        logger.info(f"hello world {i}")
        time.sleep(10)
        i += 1

Running the example as is gives the following output to console:

[2023-07-13 15:42:10,092] [INFO] [__main__]: hello world 0
[2023-07-13 15:42:20,102] [INFO] [__main__]: hello world 1
[2023-07-13 15:42:30,112] [INFO] [__main__]: hello world 2
[2023-07-13 15:42:40,123] [INFO] [__main__]: hello world 3
[2023-07-13 15:42:50,133] [INFO] [__main__]: hello world 4
[2023-07-13 15:43:00,143] [INFO] [__main__]: hello world 5
[2023-07-13 15:43:10,153] [INFO] [__main__]: hello world 6
rotating from /tmp/pycharm_project_89/logs/today/my_app.log to logs/2023071342/my_app.log.2023-07-13_15-42
[2023-07-13 15:43:20,164] [INFO] [__main__]: hello world 7
[2023-07-13 15:43:30,174] [INFO] [__main__]: hello world 8
[2023-07-13 15:43:40,184] [INFO] [__main__]: hello world 9
[2023-07-13 15:43:50,194] [INFO] [__main__]: hello world 10
[2023-07-13 15:44:00,204] [INFO] [__main__]: hello world 11
[2023-07-13 15:44:10,214] [INFO] [__main__]: hello world 12
rotating from /tmp/pycharm_project_89/logs/today/my_app.log to logs/2023071343/my_app.log.2023-07-13_15-43
[2023-07-13 15:44:20,225] [INFO] [__main__]: hello world 13
[2023-07-13 15:44:30,235] [INFO] [__main__]: hello world 14

and creates the following log structure:

tree /tmp/pycharm_project_89/logs/
/tmp/pycharm_project_89/logs/
├── 2023071342
│   └── my_app.log.2023-07-13_15-42
├── 2023071343
│   └── my_app.log.2023-07-13_15-43
└── today
    └── my_app.log

3 directories, 3 files

with log file content:

cat /tmp/pycharm_project_89/logs/2023071342/my_app.log.2023-07-13_15-42
[2023-07-13 15:42:10,092] [INFO] [__main__]: hello world 0
[2023-07-13 15:42:20,102] [INFO] [__main__]: hello world 1
[2023-07-13 15:42:30,112] [INFO] [__main__]: hello world 2
[2023-07-13 15:42:40,123] [INFO] [__main__]: hello world 3
[2023-07-13 15:42:50,133] [INFO] [__main__]: hello world 4
[2023-07-13 15:43:00,143] [INFO] [__main__]: hello world 5

cat /tmp/pycharm_project_89/logs/today/my_app.log
[2023-07-13 15:44:10,214] [INFO] [__main__]: hello world 12
[2023-07-13 15:44:20,225] [INFO] [__main__]: hello world 13
[2023-07-13 15:44:30,235] [INFO] [__main__]: hello world 14
laker93
  • 498
  • 4
  • 9