My goal is to be able to access the extra
argument of each LogRecord
to format it properly, as a way to create a basic structured logger.
logger.info("my message", extra={"a": 1} # I will format this to my needs
Since the extra
argument is unpacked to record.__dict__
, I thought about patching the record at creation time to save it in specific attribute. I am able to do that with the following code:
class LoggerWithExtra(logging.Logger):
"""
Add the 'extra' argument to log record to be able to access it in the formatter.
By default, the 'extra' argument is stored in record.__dict__ and is polluted with other attributes.
"""
def makeRecord(self, *args, **kwargs):
record = super(LoggerWithExtra, self).makeRecord(*args, **kwargs)
record._extra = args[8] # the 'extra' argument
return record
logging.setLoggerClass(LoggerWithExtra)
I then format the records like this:
class MyFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
rec = {}
timestamp = time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(record.created))
rec["timestamp"] = timestamp + "%03dZ" % record.msecs
rec["severity"] = record.levelname
rec["message"] = record.getMessage()
extra = getattr(record, "_extra", None)
if extra is not None:
rec.update(**extra)
return json.dumps(rec)
However, seems like the best practice is to use logging.setLogRecordFactory(record_factory)
. I then tried this:
old_factory = logging.getLogRecordFactory()
def record_factory(*args, **kwargs):
record = old_factory(*args, **kwargs)
record._extra = args[8]
return record
logging.setLogRecordFactory(record_factory)
But record._extra = args[8]
seems to always be None
. What am I missing here?