diff --git a/README.md b/README.md index 0ed0d0a..30c022e 100644 --- a/README.md +++ b/README.md @@ -213,18 +213,13 @@ logging.error({"example": True}) # at=ERROR example=yes **Default Key/Value Pairs** -Instead of providing key/value pairs at each log call, you can override -the log record factory to provide defaults: +Instead of providing key/value pairs at each log call, you can setup +the formatter to provide defaults: ```py -_record_factory = logging.getLogRecordFactory() - -def record_factory(*args, **kwargs): - record = _record_factory(*args, **kwargs) - record.trace_id = 123 - return record +from logfmter import Logfmter -logging.setLogRecordFactory(record_factory) +formatter = Logfmter(default_extra={"trace_id": 123}) ``` This will cause all logs to have the `trace_id=123` pair regardless of including diff --git a/src/logfmter/formatter.py b/src/logfmter/formatter.py index f2e61d2..06c33f7 100644 --- a/src/logfmter/formatter.py +++ b/src/logfmter/formatter.py @@ -143,12 +143,18 @@ def __init__( self, keys: List[str] = ["at"], mapping: Dict[str, str] = {"at": "levelname"}, + default_extra: Optional[Dict[str, str]] = None, datefmt: Optional[str] = None, ): self.keys = [self.normalize_key(key) for key in keys] self.mapping = { self.normalize_key(key): value for key, value in mapping.items() } + self.default_extra = ( + {self.normalize_key(key): value for key, value in default_extra.items()} + if default_extra is not None + else {} + ) self.datefmt = datefmt def format(self, record: logging.LogRecord) -> str: @@ -163,6 +169,7 @@ def format(self, record: logging.LogRecord) -> str: else: params = {"msg": record.getMessage()} + params.update(self.default_extra) params.update(self.get_extra(record)) tokens = [] diff --git a/tests/test_formatter.py b/tests/test_formatter.py index c646473..cbfa46c 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -266,3 +266,34 @@ def test_extra_keys(record): assert ( Logfmter(keys=["at", "attr"]).format(record) == "at=INFO msg=alpha attr=value" ) + + +@pytest.mark.parametrize( + "default_extra,record,expected", + [ + # When a parameter is present in the default_extra it should be emitted + ( + {"extra1": "from_formatter"}, + {"levelname": "INFO", "msg": "test"}, + "at=INFO msg=test extra1=from_formatter", + ), + # When a parameter is present in the default_extra and also in the record + # the record value should take precedence + ( + {"extra1": "from_formatter"}, + {"levelname": "INFO", "extra1": "from_record", "msg": "test"}, + "at=INFO msg=test extra1=from_record", + ), + ], +) +def test_format_default_extra(default_extra, record, expected): + """ + If the `default_extra` parameter is specified in the formatter then all log + records must include such parameters. + """ + + # Generate a real `logging.LogRecord` from the provided dictionary. + record = logging.makeLogRecord(record) + formatter = Logfmter(keys=["at", "extra1"], default_extra=default_extra) + + assert formatter.format(record) == expected