import logging import re class OptionalFieldFormatter(logging.Formatter): """ Logging formatter that supports optional fields marked by `?`. Optional fields are denoted by placing a `?` after the field name inside the parentheses, e.g., `%(role?)s`. If the field is not provided in the log call's `extra` dict, it will use the default value from `defaults` or `None` if no default is specified. :param fmt: Format string with optional `%(name?)s` style fields. :type fmt: str or None :param datefmt: Date format string, passed to parent :class:`logging.Formatter`. :type datefmt: str or None :param style: Formatting style, must be '%'. Passed to parent. :type style: str :param defaults: Default values for optional fields when not provided. :type defaults: dict or None :example: >>> formatter = OptionalFieldFormatter( ... fmt="%(asctime)s %(levelname)s %(role?)s %(message)s", ... defaults={"role": ""-""} ... ) >>> handler = logging.StreamHandler() >>> handler.setFormatter(formatter) >>> logger = logging.getLogger(__name__) >>> logger.addHandler(handler) >>> >>> logger.chat("Hello there!", extra={"role": "USER"}) 2025-01-15 10:30:00 CHAT USER Hello there! >>> >>> logger.info("A logging message") 2025-01-15 10:30:01 INFO - A logging message .. note:: Only `%`-style formatting is supported. The `{` and `$` styles are not compatible with this formatter. .. seealso:: :class:`logging.Formatter` for base formatter documentation. """ # Match %(name?)s or %(name?)d etc. OPTIONAL_PATTERN = re.compile(r"%\((\w+)\?\)([sdifFeEgGxXocrba%])") def __init__(self, fmt=None, datefmt=None, style="%", defaults=None): self.defaults = defaults or {} self.optional_fields = set(self.OPTIONAL_PATTERN.findall(fmt or "")) # Convert %(name?)s to %(name)s for standard formatting normalized_fmt = self.OPTIONAL_PATTERN.sub(r"%(\1)\2", fmt or "") super().__init__(normalized_fmt, datefmt, style) def format(self, record): for field, _ in self.optional_fields: if not hasattr(record, field): default = self.defaults.get(field, None) setattr(record, field, default) return super().format(record)