How to handle optional arguments in logging format strings?

Viewed 2938

I would like to use the following log format:

'format': '{"message": "%(message)s", "user": "%(user)s"}'

However, I would like to call it in two different ways:

log.info("hi", extra={"user": "asmith"})
log.info("hi")

The first log statement works because it provides the user argument, but the second one fails with a KeyError.

Is there any way to make a format string argument optional?

1 Answers

Optional format args replaced with None

Given a fixed logging format string, a custom Formatter class can be used to replace missing arguments with None.

import logging
import re

class CustomFormatter(logging.Formatter):

    def format(self, record: logging.LogRecord) -> str:
        arg_pattern = re.compile(r'%\((\w+)\)')
        arg_names = [x.group(1) for x in arg_pattern.finditer(self._fmt)]
        for field in arg_names:
            if field not in record.__dict__:
                record.__dict__[field] = None

        return super().format(record)


logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
formatter = CustomFormatter('{"message": "%(message)s", "user": "%(user)s"}')
handler.setFormatter(formatter)
logger.setLevel(logging.INFO)
logger.addHandler(handler)

logger.info('hi')
logger.info('hi', extra={"user": "asmith"})

Output

{"message": "hi", "user": "None"}
{"message": "hi", "user": "asmith"}

Dynamically add extra args to logging output

A custom Formatter can dynamically update the format string based on the dictionary passed to extra.

import logging

class ExtraFormatter(logging.Formatter):

    def format(self, record: logging.LogRecord) -> str:
        default_attrs = logging.LogRecord(None, None, None, None, None, None, None).__dict__.keys()
        extras = set(record.__dict__.keys()) - default_attrs

        log_items = ['"message": "%(message)s"']
        for attr in extras:
            log_items.append(f'"{attr}": "%({attr})s"')
        format_str = f'{{{", ".join(log_items)}}}'
        self._style._fmt = format_str

        return super().format(record)


logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
formatter = ExtraFormatter()
handler.setFormatter(formatter)
logger.setLevel(logging.INFO)
logger.addHandler(handler)

logger.info('hi')
logger.info('hi', extra={"user": "asmith", "number": "42"})

Output

{"message": "hi"}
{"message": "hi", "user": "asmith", "number": "42"}
Related