Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ repos:
language_version: python3.8

- repo: https://github.com/PyCQA/isort
rev: 5.9.1
rev: 5.9.2
hooks:
- id: isort
additional_dependencies: [toml]
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Changes:
- If the `better-exceptions <https://github.com/qix-/better-exceptions>`_ package is present, ``structlog.dev.ConsoleRenderer`` will now pretty-print exceptions using it.
Pass ``pretty_exceptions=False`` to disable.
This only works if ``format_exc_info`` is **absent** in the processor chain.
- ``KeyValueRenderer`` now accepts an optional ``repr_formatter`` parameter to handle log representation.
- Added logfmt_repr to make KeyValueRenderer output logfmt compatible.


----
Expand Down
13 changes: 13 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,19 @@ API Reference
>>> TimeStamper(fmt="%Y", key="year")(None, None, {}) # doctest: +SKIP
{'year': '2013'}

.. autofunction:: logfmt_repr

.. doctest::

>>> from structlog.processors import logfmt_repr
>>> logfmt_repr("string with withespace")
'"string with whitespace"'
>>> logfmt_repr("string")
'string'
>>> logfmt_repr(42)
'42'
>>> logfmt_repr(None)
'null'

`structlog.stdlib` Module
-------------------------
Expand Down
29 changes: 21 additions & 8 deletions src/structlog/processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import datetime
import json
import operator
import re
import sys
import time

Expand Down Expand Up @@ -43,9 +44,25 @@
"format_exc_info",
"ExceptionPrettyPrinter",
"StackInfoRenderer",
"logfmt_repr",
]


def logfmt_repr(inst: Any) -> str:
if inst is None:
return "null"
if isinstance(inst, str):
return json.dumps(inst) if re.search("\\s", inst) else inst
return repr(inst)


def _repr(inst: Any) -> str:
if isinstance(inst, str):
return inst
else:
return repr(inst)


class KeyValueRenderer:
"""
Render ``event_dict`` as a list of ``Key=repr(Value)`` pairs.
Expand All @@ -60,10 +77,12 @@ class KeyValueRenderer:
to native strings.
Setting this to ``False`` is useful if you want to have human-readable
non-ASCII output on Python 2.
:param repr_formatter: A callable to use as a custom :func:`repr()`.

.. versionadded:: 0.2.0 *key_order*
.. versionadded:: 16.1.0 *drop_missing*
.. versionadded:: 17.1.0 *repr_native_str*
.. versionadded:: 21.2.0 *repr_formatter*
"""

def __init__(
Expand All @@ -72,6 +91,7 @@ def __init__(
key_order: Optional[Sequence[str]] = None,
drop_missing: bool = False,
repr_native_str: bool = True,
repr_formatter: Callable = _repr,
):
# Use an optimized version for each case.
if key_order and sort_keys is True:
Expand Down Expand Up @@ -113,14 +133,7 @@ def ordered_items(event_dict: EventDict) -> List[Tuple[str, Any]]:
if repr_native_str is True:
self._repr = repr
else:

def _repr(inst: Any) -> str:
if isinstance(inst, str):
return inst
else:
return repr(inst)

self._repr = _repr
self._repr = repr_formatter

def __call__(
self, _: WrappedLogger, __: str, event_dict: EventDict
Expand Down
35 changes: 35 additions & 0 deletions tests/test_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
_figure_out_exc_info,
_json_fallback_handler,
format_exc_info,
logfmt_repr,
)
from structlog.threadlocal import wrap_dict

Expand Down Expand Up @@ -124,6 +125,16 @@ def test_repr_native_str(self, rns):
cnt = rv.count("哈")
assert 2 == cnt

def test_repr_formatter(self, event_dict):
"""
Injects a custom repr-like formatter when repr_native_str=False
"""
rv = KeyValueRenderer(
repr_formatter=logfmt_repr, repr_native_str=False
)(None, None, event_dict)

assert r"a=<A(\o/)> b=[3, 4] x=7 y=test z=(1, 2)" == rv


class TestJSONRenderer:
def test_renders_json(self, event_dict):
Expand Down Expand Up @@ -551,3 +562,27 @@ def test_py3_exception_no_traceback(self):
e = ValueError()

assert (e.__class__, e, None) == _figure_out_exc_info(e)


class TestLogFmtRepr:
@pytest.mark.parametrize(
"obj,expected",
[
("word", "word"), # plain word
("a sentence", '"a sentence"'), # a sentence with spaces
(
'a sentence "with quotes"',
'"a sentence \\"with quotes\\""',
), # a sentence in double quotes
(42, "42"), # int
(3.14, "3.14"), # float
("127.0.0.1", "127.0.0.1"), # an ip address
("user@email.test", "user@email.test"), # an email
(None, "null"), # NoneType
],
)
def test_logfmt_repr(self, obj, expected):
"""
Ensure logfmt_repr produces logfmt-compatible strings
"""
assert expected == logfmt_repr(obj)