loguru icon indicating copy to clipboard operation
loguru copied to clipboard

Custom formatter fails if message contains JSON string

Open malcolmdimeglio opened this issue 9 months ago • 3 comments

JSON string parsing error

Problem

When creating a custom logger and providing the new logger with a custom formatter function, Loguru throws a KeyError if the string it's trying to format contains a JSON formatted string.

Environment

Python 3.13.2 loguru==0.7.3

How to reproduce

#! /usr/bin/env python3

import sys
from loguru import logger


def console_format(record: dict) -> str:
    level = record["level"].name.lower()
    timestamp = record["time"].strftime("%Y-%m-%d %H:%M:%S.%f %:z")

    message = record["message"]

    log_message = f"[{timestamp}] [{level}] {message}\n"

    return log_message


if __name__ == "__main__":
    logger.remove()

    logger.add(sys.stderr,
               format=console_format,
               level="DEBUG",
               colorize=False,
               catch=False)

    logger.info("This is an info message")

    # 1. Try with variable substituations
    name = "Alice"
    age = 30
    logger.info(f"Hello {name}, you are {age} years old")

    # 2. Try with json string as variable
    json_str = '{"name": "Bob", "age": 25}'
    logger.info(f"Hello {json_str} world!")

    # 3. Try with dumped json object
    json_obj = {"name": "Bob", "age": 25}
    logger.info(f"Hello {json.dumps(json_obj)} world!")

Error Trace

[2025-03-17 07:19:46.976159 -07:00] [info] This is an info message [2025-03-17 07:19:46.976263 -07:00] [info] Hello Alice, you are 30 years old Traceback (most recent call last): File "./test.py", line 39, in logger.info(f"Hello {json_str} world!") File ".venv/lib/python3.13/site-packages/loguru/_logger.py", line 2078, in info __self._log("INFO", False, __self._options, __message, args, kwargs) File ".venv/lib/python3.13/site-packages/loguru/_logger.py", line 2066, in _log handler.emit(log_record, level_id, from_decorator, raw, colored_message) File ".venv/lib/python3.13/site-packages/loguru/_handler.py", line 161, in emit formatted = precomputed_format.format_map(formatter_record) KeyError: '"name"'

Workaround

The only workaround I found to make this work is by changing the following:

# message = record["message"]
message = record["message"].replace("{", "{{").replace("}", "}}")

Output after the workaround

[2025-03-17 08:10:04.773069 -07:00] [info] This is an info message [2025-03-17 08:10:04.773193 -07:00] [info] Hello Alice, you are 30 years old [2025-03-17 08:10:04.773231 -07:00] [info] Hello {"name": "Bob", "age": 25} world! [2025-03-17 08:10:04.773267 -07:00] [info] Hello {"name": "John", "age": 99} world!

Observation

  • This behaviour/limitation is not documented anywhere (AFAIK).
  • The workaround is messy

Maybe it could be a good idea to:

  1. Document this limitation
  2. Update loguru for a more intuitive use where one can simply log a message with an f-string without worrying of what's happening under the hood of the logger. (ideally keeping message = record["message"] even if there is a json string in there

malcolmdimeglio avatar Mar 17 '25 15:03 malcolmdimeglio

Ran into the same issue! Seems like the same as https://github.com/Delgan/loguru/issues/1008 ?

karlicoss avatar Mar 30 '25 21:03 karlicoss

Ran into more problem with different characters. Apparently it is also not a fan of '<' and '>' if it is in the dumped json string.

malcolmdimeglio avatar May 22 '25 14:05 malcolmdimeglio

@karlicoss I just found this in the documentation: Why logging a message with f-string sometimes raises an exception?.

The solution?

  • "You must be careful not to inadvertently introduce curly braces into the message. Instead of using an f-string"
  • "You can possibly disable formatting by doubling the curly braces"

I guess what I need to test is not using an f-string to dump my JSON into? Or tweaking the logger to add double braces every time it sees one... The latter option is adding a lot of unneeded processing, specially when logging JSON strings is a big part of your logging logic

Sadly not being able to use one of the most commonly used way to build a string to use the logger looks more like a design flaw/bug than an understandable limitation. I might have to look for a more stable logger.

malcolmdimeglio avatar May 22 '25 14:05 malcolmdimeglio

same here

shifenhutu avatar Jun 10 '25 14:06 shifenhutu

Hi.

The problem is that the format function must return the template that will be used by Loguru to format the message, not the already formatted message. This may seem a little counter-intuitive, but this is a technical requirement, in particular to allow Loguru to integrate colors or not.

Therefore, you can solve your problem by updating your formatting function as such:

def console_format(record: dict) -> str:
    level = record["level"].name.lower()
    timestamp = record["time"].strftime("%Y-%m-%d %H:%M:%S.%f %:z")
    message = record["message"]

    record["extra"]["log_message"] = f"[{timestamp}] [{level}] {message}\n"

    return "{extra[log_message]}"

The documentations states the following:

If fine-grained control is needed, the format can also be a function which takes the record as parameter and return the format template string.

However, I agree that it does not sufficiently emphasize the importance of returning a template rather than formatting the message itself. I will make sure to provide more details on how to use this argument, add an example, and include troubleshooting help.

Delgan avatar Jul 27 '25 14:07 Delgan

I just updated the documentation to provide more guidance about usage of a dynamic format function.

See notably this new section of the troubleshooting page: Why am I facing errors when I use a custom formatting function?.

Delgan avatar Sep 13 '25 11:09 Delgan

I got this error because I was using record['message'].
I fixed it by changing into {{message}}.

My solution:

# inner function to format the path    
def format_with_path(record):
    path = os.path.relpath(record["file"].path, os.getcwd())
    return f"<cyan>{path}:{record['line']}</cyan> <magenta>{record['function']}()</magenta> | <lvl>{{message}}</lvl>\n"

# Format the logger with new settings
logger.add(sys.stdout, colorize=True, backtrace=True, diagnose=True, 
    level=level, format=format_with_path
)

sglbl avatar Nov 06 '25 14:11 sglbl

@sglbl Unfortunately, your format_with_path() is still error-prone due to the f-string.

In your example, {record['function']} is evaluated and formatted before being passed to Loguru's handler. Then, the handler will treat the string as if it was a format containing the placeholders (while they were already replaced by the f-string). This will raise an error if you try to use the logger at the root of a module (i.e. not within any function). Indeed, in such a case, {record['function']} is transformed to "<module>". When the handler will prepare the logging format, it'll interpret it as a clolor markup (like "<cyan>") and complain that there is no "module" color existing.

You should avoid returning an already formatted message, like so:

def format_with_path(record):
    record["extra"]["path"] = os.path.relpath(record["file"].path, os.getcwd())
    return "<cyan>{extra[path]}:{line}</cyan> <magenta>{function}()</magenta> | <lvl>{message}</lvl>\n{exception}"

Delgan avatar Nov 08 '25 18:11 Delgan