Skip to content

Commit 27347d9

Browse files
authored
feat: allow specifying format-able default values
1 parent 271b756 commit 27347d9

3 files changed

Lines changed: 100 additions & 16 deletions

File tree

README.md

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,34 @@ logging.basicConfig(handlers=[handler])
190190
logging.error("hello") # at=ERROR when=2022-04-20 msg=hello
191191
```
192192

193+
**defaults**
194+
195+
Instead of providing key/value pairs at each log call, you can provide defaults:
196+
197+
```py
198+
import logging
199+
from logfmter import Logfmter
200+
201+
formatter = Logfmter(
202+
keys=["at", "when", "trace_id"],
203+
mapping={"at": "levelname", "when": "asctime"},
204+
datefmt="%Y-%m-%d",
205+
defaults={"trace_id": "123"},
206+
)
207+
208+
handler = logging.StreamHandler()
209+
handler.setFormatter(formatter)
210+
211+
logging.basicConfig(handlers=[handler])
212+
213+
logging.error("hello") # at=ERROR when=2022-04-20 trace_id=123 msg=hello
214+
```
215+
216+
This will cause all logs to have the `trace_id=123` pair regardless of including
217+
`trace_id` in keys or manually adding `trace_id` to the `extra` parameter or the `msg` object.
218+
219+
> Note, the defaults object uses format strings as values. This allows for variables templating. See "Aliases" guide for more information.
220+
193221
## Extension
194222

195223
You can subclass the formatter to change its behavior.
@@ -222,24 +250,28 @@ logging.error({"example": True}) # at=ERROR example=yes
222250

223251
## Guides
224252

225-
**Default Key/Value Pairs**
253+
**Aliases**
226254

227-
Instead of providing key/value pairs at each log call, you can override
228-
the log record factory to provide defaults:
255+
Providing a format string as a default's key/value allows the realization of aliases:
229256

230257
```py
231-
_record_factory = logging.getLogRecordFactory()
258+
import logging
259+
from logfmter import Logfmter
232260

233-
def record_factory(*args, **kwargs):
234-
record = _record_factory(*args, **kwargs)
235-
record.trace_id = 123
236-
return record
261+
formatter = Logfmter(
262+
keys=["at", "when", "func"],
263+
mapping={"at": "levelname", "when": "asctime"},
264+
datefmt="%Y-%m-%d",
265+
defaults={"func": "{module}.{funcName}:{lineno}"},
266+
)
237267

238-
logging.setLogRecordFactory(record_factory)
239-
```
268+
handler = logging.StreamHandler()
269+
handler.setFormatter(formatter)
240270

241-
This will cause all logs to have the `trace_id=123` pair regardless of including
242-
`trace_id` in keys or manually adding `trace_id` to the `extra` parameter or the `msg` object.
271+
logging.basicConfig(handlers=[handler])
272+
273+
logging.error("hello") # at=ERROR when=2022-04-20 func="mymodule.__main__:12" msg=hello
274+
```
243275

244276
## Gotchas
245277

src/logfmter/formatter.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@
3737
)
3838

3939

40+
class _DefaultFormatter(logging.Formatter):
41+
def format(self, record):
42+
exc_info = record.exc_info
43+
exc_text = record.exc_text
44+
stack_info = record.stack_info
45+
record.exc_info = None
46+
record.exc_text = None
47+
record.stack_info = None
48+
try:
49+
return super().format(record)
50+
finally:
51+
record.exc_info = exc_info
52+
record.exc_text = exc_text
53+
record.stack_info = stack_info
54+
55+
4056
class Logfmter(logging.Formatter):
4157
@classmethod
4258
def format_string(cls, value: str) -> str:
@@ -168,12 +184,17 @@ def __init__(
168184
keys: List[str] = ["at"],
169185
mapping: Dict[str, str] = {"at": "levelname"},
170186
datefmt: Optional[str] = None,
187+
defaults: Optional[Dict[str, str]] = None,
171188
):
172189
self.keys = [self.normalize_key(key) for key in keys]
173190
self.mapping = {
174191
self.normalize_key(key): value for key, value in mapping.items()
175192
}
176193
self.datefmt = datefmt
194+
self.defaults = {
195+
key: _DefaultFormatter(value, style="{")
196+
for key, value in (defaults or {}).items()
197+
}
177198

178199
def format(self, record: logging.LogRecord) -> str:
179200
# If the 'asctime' attribute will be used, then generate it.
@@ -208,12 +229,13 @@ def format(self, record: logging.LogRecord) -> str:
208229
if attribute in params:
209230
continue
210231

211-
# If the attribute doesn't exist on the log record, then skip it.
212-
if not hasattr(record, attribute):
232+
if hasattr(record, attribute):
233+
value = getattr(record, attribute)
234+
elif attribute in self.defaults:
235+
value = self.defaults[attribute].format(record)
236+
else:
213237
continue
214238

215-
value = getattr(record, attribute)
216-
217239
tokens.append("{}={}".format(key, self.format_value(value)))
218240

219241
formatted_params = self.format_params(params)

tests/test_formatter.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,3 +316,33 @@ def test_external_tools_compatibility(value):
316316
assert result.returncode == 0, formatted
317317
assert len(result.stdout.splitlines()) == 1, formatted
318318
assert result.stdout.splitlines()[0] == formatted
319+
320+
321+
@pytest.mark.parametrize(
322+
"record",
323+
[
324+
{
325+
"msg": "alpha",
326+
"levelname": "INFO",
327+
"funcName": "test_defaults",
328+
"module": "test_formatter",
329+
"lineno": "324",
330+
},
331+
{
332+
"msg": {"msg": "alpha"},
333+
"levelname": "INFO",
334+
"funcName": "test_defaults",
335+
"module": "test_formatter",
336+
"lineno": "324",
337+
},
338+
],
339+
)
340+
def test_defaults(record):
341+
record = logging.makeLogRecord(record)
342+
343+
assert (
344+
Logfmter(
345+
keys=["at", "func"], defaults={"func": "{module}.{funcName}:{lineno}"}
346+
).format(record)
347+
== "at=INFO func=test_formatter.test_defaults:324 msg=alpha"
348+
)

0 commit comments

Comments
 (0)