pythonloggingpath

How to change the path separator of the logging module?


I want to use the logging module in Python to log in a custom JSON format, and one of the variables I want to log is the pathname.

It seems that the logging module uses a backslash(\) to separate directories which causes the Invalid escape character error in JSON.

first question is: Is this separator OS-dependent? I am using Windows 11 for now but the code must deploy on Unix-based OS.

second question is: How may I change this separator? I think if I could change it to a single slash(/) or a double backslash(\\) it could fix the problem.

I tried the below example.

config.json:

{
    "version": 1,
    "disable_existing_loggers": true,
    "formatters": {
        "standard": {
            "format": "{\"timestamp\":%(created)f,\"time\":\"%(asctime)s\",\"path\":\"%(pathname)s\",\"file\":\"%(filename)s\",\"line\":%(lineno)d,\"level\":\"%(levelname)s\",\"text\":\"%(message)s\"}",
            "datefmt": "%Y/%m/%d-%H:%M:%S"
        }
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "standard",
            "level": "DEBUG",
            "stream": "ext://sys.stdout"
        }
    },
    "loggers": {
        "": {
            "handlers": [
                "console"
            ],
            "level": "DEBUG",
            "propagate": false
        }
    }
}

example.py:

import logging
import json
import logging.config

with open("./config.json", "r", encoding="UTF_8") as file:
    config = json.load(file)
logging.config.dictConfig(config)
logger = logging.getLogger(__name__)
logger.info("This is an info log.")

Output:

{"timestamp":1726603464.416349,"time":"2024/09/17-23:34:24","path":"d:\python\project\example.py","file":"example.py","line":9,"level":"INFO","text":"This is an info log."}

Should be:

{"timestamp":1726603464.416349,"time":"2024/09/17-23:34:24","path":""d:\\python\\project\\example.py","file":"example.py","line":9,"level":"INFO","text":"This is an info log."}

Or

{"timestamp":1726603464.416349,"time":"2024/09/17-23:34:24","path":"d:/python/project/example.py","file":"example.py","line":9,"level":"INFO","text":"This is an info log."}

Solution

  • thanks a ton to @Chepner for that correct hint.

    I went back to one of my previous searches about this which was not related to my problem but now can be a solution for.

    The previous search

    So there is an answer by @Bogdan Mircea who wrote a custom formatter class that gets a dictionary as the format setup and converts logs records to that format then dumps them into JSON, which I am thankful for it. That class just has a problem which is it can't parse nested dictionaries.

    I've enhanced the class to address the issue of parsing nested dictionaries. I'm now sharing the updated solution to benefit those who may encounter a similar challenge. I hope they find it useful.

    Here is the solution:

    config.json:

    {
        "version": 1,
        "disable_existing_loggers": true,
        "loggers": {
            "": {
                "handlers": [],
                "level": "DEBUG",
                "propagate": false
            }
        }
    }
    

    custom_json_formatter.py:

    import json
    import logging
    
    
    class CustomJOSNFormatter(logging.Formatter):
        def __init__(
            self,
            fmt_dict: dict = None,
            datefmt: str = "%Y-%m-%dT, %H:%M:%S",
            msec_format: str = "%s.%03d",
        ):
            self.fmt_dict = fmt_dict if fmt_dict is not None else {"message": "message"}
            self.default_datefmt = datefmt
            self.default_msec_format = msec_format
            self.datefmt = None
    
        def usesTime(self, fmt_dict) -> bool:
            result = False
            for _, fmt_val in fmt_dict.items():
                if result:
                    break
                if isinstance(fmt_val, dict):
                    result = self.usesTime(fmt_val)
                else:
                    result = "asctime" in fmt_val
            return result
    
        def formatMessage(self, record, fmt_dict) -> dict:
            result = {}
            for fmt_key, fmt_val in fmt_dict.items():
                if isinstance(fmt_val, dict):
                    result[fmt_key] = self.formatMessage(record, fmt_val)
                else:
                    result[fmt_key] = record.__dict__[fmt_val]
            return result
    
        def format(self, record) -> str:
            record.message = record.getMessage()
            if self.usesTime(self.fmt_dict):
                record.asctime = self.formatTime(record, self.datefmt)
            message_dict = self.formatMessage(record, self.fmt_dict)
            if record.exc_info:
                if not record.exc_text:
                    record.exc_text = self.formatException(record.exc_info)
            if record.exc_text:
                message_dict["exc_info"] = record.exc_text
            if record.stack_info:
                message_dict["stack_info"] = self.formatStack(record.stack_info)
            return json.dumps(message_dict, default=str)
    

    example.py:

    import json
    import logging
    import logging.config
    from custom_json_formatter import CustomJOSNFormatter
    
    format_dict = {
        "time": {
            "timestamp": "created",
            "date and time": "asctime",
        },
        "path": "pathname",
        "file": "filename",
        "line": "lineno",
        "logger": "name",
        "level": {"name": "levelname", "priority": "levelno"},
        "text": "message",
    }
    
    with open("./config.json", "r", encoding="UTF_8") as file:
        config = json.load(file)
    logging.config.dictConfig(config)
    logger = logging.getLogger(__name__)
    stream_handler = logging.StreamHandler()
    stream_handler.formatter = CustomJOSNFormatter(format_dict, datefmt="%Y/%m/%d-%H:%M:%S")
    logger.addHandler(stream_handler)
    logger.info("This is an info log.")
    

    And finally the output:

    {
        "time": {
            "timestamp": 1726618535.0693908,
            "date and time": "2024-09-18 03:45:35.069"
        },
        "path": "d:\\python\\project\\example.py",
        "file": "example.py",
        "line": 26,
        "logger": "__main__",
        "level": {
            "name": "INFO",
            "priority": 20
        },
        "text": "This is an info log."
    }