Skip to content

Commit a8eca47

Browse files
authored
[core] Support sequence of strings for Formatter fmt (#55)
Fixes: #16 ### Test Plan - Unit tests ### To Do - [ ] ? Add tests for this format when using dict config?
1 parent b53b930 commit a8eca47

File tree

4 files changed

+77
-23
lines changed

4 files changed

+77
-23
lines changed

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
- Allows using values like `ext://sys.stderr` in `fileConfig`/`dictConfig` value fields.
1212
- Support comma seperated lists for Formatter `fmt` (`style=","`) e.g. `"asctime,message,levelname"` [#15](https://github.com/nhairs/python-json-logger/issues/15)
1313
- Note that this style is specific to `python-json-logger` and thus care should be taken not to pass this format to other logging Formatter implementations.
14+
- Supports sequences of strings (e.g. lists and tuples) of field names for Formatter `fmt`. [#16](https://github.com/nhairs/python-json-logger/issues/16)
1415

1516
### Changed
1617
- Rename `pythonjsonlogger.core.LogRecord` and `log_record` arguments to avoid confusion / overlapping with `logging.LogRecord`. [#38](https://github.com/nhairs/python-json-logger/issues/38)

docs/quickstart.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,23 @@ logger.addHandler(logHandler)
4545
### Output fields
4646

4747
#### Required Fields
48-
You can control the logged fields by setting the `fmt` argument when creating the formatter. By default formatters will follow the same `style` of `fmt` as the `logging` module: `%`, `$`, and `{`. All [`LogRecord` attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes) can be output using their name.
48+
You can control the logged fields by setting the `fmt` argument when creating the formatter. A variety of different formats are supported including:
49+
50+
- Standard library formats: where `style` is one of `%`, `$`, or `{`. This allows using Python JSON Logger Formatters with your existing config.
51+
- Comma format: where `style` is `,` which simplifies the writing of formats where you can't use more complex formats.
52+
- A sequence of string: e.g. lists or tuples.
53+
54+
All [`LogRecord` attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes) can be output using their name.
4955

5056
```python
57+
# Standard library format
5158
formatter = JsonFormatter("{message}{asctime}{exc_info}", style="{")
59+
60+
# Comma format
61+
formatter = JsonFormatter("message,asctime,exc_info", style=",")
62+
63+
# Sequence of strings format
64+
formatter = JsonFormatter(["message", "asctime", "exc_info"])
5265
```
5366

5467
#### Message Fields

src/pythonjsonlogger/core.py

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ class BaseJsonFormatter(logging.Formatter):
128128
# pylint: disable=too-many-arguments,super-init-not-called
129129
def __init__(
130130
self,
131-
fmt: Optional[str] = None,
131+
fmt: Optional[Union[str, Sequence[str]]] = None,
132132
datefmt: Optional[str] = None,
133133
style: str = "%",
134134
validate: bool = True,
@@ -145,11 +145,11 @@ def __init__(
145145
) -> None:
146146
"""
147147
Args:
148-
fmt: string representing fields to log
148+
fmt: String format or `Sequence` of field names of fields to log.
149149
datefmt: format to use when formatting `asctime` field
150-
style: how to extract log fields from `fmt`
150+
style: how to extract log fields from `fmt`. Ignored if `fmt` is a `Sequence[str]`.
151151
validate: validate `fmt` against style, if implementing a custom `style` you
152-
must set this to `False`.
152+
must set this to `False`. Ignored if `fmt` is a `Sequence[str]`.
153153
defaults: a dictionary containing default fields that are added before all other fields and
154154
may be overridden. The supplied fields are still subject to `rename_fields`.
155155
prefix: an optional string prefix added at the beginning of
@@ -181,25 +181,34 @@ def __init__(
181181
- `fmt` now supports comma seperated lists (`style=","`). Note that this style is specific
182182
to `python-json-logger` and thus care should be taken to not to pass this format to other
183183
logging Formatter implementations.
184+
- `fmt` now supports sequences of strings (e.g. lists and tuples) of field names.
184185
"""
185186
## logging.Formatter compatibility
186187
## ---------------------------------------------------------------------
187188
# Note: validate added in python 3.8, defaults added in 3.10
188-
if style in logging._STYLES:
189-
_style = logging._STYLES[style][0](fmt) # type: ignore[operator]
190-
if validate:
191-
_style.validate()
192-
self._style = _style
193-
self._fmt = _style._fmt
194-
195-
elif style == "," or not validate:
196-
self._style = style
197-
self._fmt = fmt
198-
199-
# TODO: Validate comma format
200-
201-
else:
202-
raise ValueError("Style must be one of: '%{$,'")
189+
if fmt is None or isinstance(fmt, str):
190+
if style in logging._STYLES:
191+
_style = logging._STYLES[style][0](fmt) # type: ignore[operator]
192+
if validate:
193+
_style.validate()
194+
self._style = _style
195+
self._fmt = _style._fmt
196+
197+
elif style == "," or not validate:
198+
self._style = style
199+
self._fmt = fmt
200+
# TODO: Validate comma format
201+
202+
else:
203+
raise ValueError("Style must be one of: '%{$,'")
204+
205+
self._required_fields = self.parse()
206+
207+
# Note: we do this check second as string is still a Sequence[str]
208+
elif isinstance(fmt, Sequence):
209+
self._style = "__sequence__"
210+
self._fmt = str(fmt)
211+
self._required_fields = list(fmt)
203212

204213
self.datefmt = datefmt
205214

@@ -221,7 +230,6 @@ def __init__(
221230
self.reserved_attrs = set(reserved_attrs if reserved_attrs is not None else RESERVED_ATTRS)
222231
self.timestamp = timestamp
223232

224-
self._required_fields = self.parse()
225233
self._skip_fields = set(self._required_fields)
226234
self._skip_fields.update(self.reserved_attrs)
227235
self.defaults = defaults if defaults is not None else {}
@@ -282,8 +290,14 @@ def parse(self) -> List[str]:
282290
if self._fmt is None:
283291
return []
284292

285-
if isinstance(self._style, str) and self._style == ",":
286-
return [field.strip() for field in self._fmt.split(",") if field.strip()]
293+
if isinstance(self._style, str):
294+
if self._style == "__sequence__":
295+
raise RuntimeError("Must not call parse when fmt is a sequence of strings")
296+
297+
if self._style == ",":
298+
return [field.strip() for field in self._fmt.split(",") if field.strip()]
299+
300+
raise ValueError(f"Style {self._style!r} is not supported")
287301

288302
if isinstance(self._style, logging.StringTemplateStyle):
289303
formatter_style_pattern = STYLE_STRING_TEMPLATE_REGEX

tests/test_formatters.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,32 @@ def test_comma_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
184184
return
185185

186186

187+
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
188+
def test_sequence_list_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
189+
env.set_formatter(class_(["levelname", "message", "filename", "lineno", "asctime"]))
190+
191+
msg = "testing logging format"
192+
env.logger.info(msg)
193+
log_json = env.load_json()
194+
195+
assert log_json["message"] == msg
196+
assert log_json.keys() == {"levelname", "message", "filename", "lineno", "asctime"}
197+
return
198+
199+
200+
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
201+
def test_sequence_tuple_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
202+
env.set_formatter(class_(("levelname", "message", "filename", "lineno", "asctime")))
203+
204+
msg = "testing logging format"
205+
env.logger.info(msg)
206+
log_json = env.load_json()
207+
208+
assert log_json["message"] == msg
209+
assert log_json.keys() == {"levelname", "message", "filename", "lineno", "asctime"}
210+
return
211+
212+
187213
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
188214
def test_defaults_field(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
189215
env.set_formatter(class_(defaults={"first": 1, "second": 2}))

0 commit comments

Comments
 (0)