Skip to content
Merged
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
21 changes: 16 additions & 5 deletions haystack/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def exception(
msg: str,
*,
_: Any = None,
exc_info: Any = None,
exc_info: Any = True,
stack_info: Any = False,
stacklevel: int = 1,
**kwargs: Any,
Expand Down Expand Up @@ -152,12 +152,23 @@ def setLevel(self, level: int) -> None:
"""Set the logging level."""


def patch_log_method_to_kwargs_only(func: typing.Callable) -> typing.Callable:
"""A decorator to make sure that a function is only called with keyword arguments."""
def patch_log_method_to_kwargs_only(func: typing.Callable, *, default_exc_info: Any = None) -> typing.Callable:
"""
A decorator to make sure that a function is only called with keyword arguments.

:param func: The log method to wrap.
:param default_exc_info: The `exc_info` to pass when the caller omits it. Use `True` for `exception`.
"""

@functools.wraps(func)
def _log_only_with_kwargs(
msg: str, *, _: Any = None, exc_info: Any = None, stack_info: Any = False, stacklevel: int = 1, **kwargs: Any
msg: str,
*,
_: Any = None,
exc_info: Any = default_exc_info,
stack_info: Any = False,
stacklevel: int = 1,
**kwargs: Any,
) -> typing.Callable: # we need the `_` to avoid a syntax error
existing_extra = kwargs.pop("extra", {})
return func(
Expand Down Expand Up @@ -273,7 +284,7 @@ def getLogger(name: str) -> PatchedLogger:
logger.warning = patch_log_method_to_kwargs_only(logger.warning) # type: ignore
logger.error = patch_log_method_to_kwargs_only(logger.error) # type: ignore
logger.critical = patch_log_method_to_kwargs_only(logger.critical) # type: ignore
logger.exception = patch_log_method_to_kwargs_only(logger.exception) # type: ignore
logger.exception = patch_log_method_to_kwargs_only(logger.exception, default_exc_info=True) # type: ignore
logger.fatal = patch_log_method_to_kwargs_only(logger.fatal) # type: ignore
logger.log = patch_log_with_level_method_to_kwargs_only(logger.log) # type: ignore

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
fixes:
- |
``logger.exception()`` on loggers from ``haystack.logging.getLogger`` now logs the active traceback, as the
standard library does. Previously it logged only the message, without the stack trace, in both JSON and console
output. Passing ``exc_info`` explicitly still works.
24 changes: 23 additions & 1 deletion test/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,28 @@ def test_various_levels(self, capfd: CaptureFixture, method: str, expected_level
"module": "haystack.test_logging",
}

def test_exception_logs_traceback(self, caplog: LogCaptureFixture) -> None:
logger = haystack_logging.getLogger("haystack.test_logging")

try:
1 / 0
except ZeroDivisionError:
logger.exception("An error happened")

record = caplog.records[0]
assert record.exc_info is not None
assert record.exc_info[0] is ZeroDivisionError

def test_exception_respects_explicit_exc_info(self, caplog: LogCaptureFixture) -> None:
logger = haystack_logging.getLogger("haystack.test_logging")

try:
1 / 0
except ZeroDivisionError:
logger.exception("An error happened", exc_info=False)

assert not caplog.records[0].exc_info

def test_log(self, capfd: CaptureFixture) -> None:
haystack_logging.configure_logging(use_json=True)

Expand Down Expand Up @@ -641,7 +663,7 @@ def test_haystack_logger_with_positional_args(self, method: str, expected_level:
logger = haystack_logging.getLogger("haystack.test_logging")
logger.setLevel(logging.DEBUG)

with pytest.raises(TypeError):
with pytest.raises(TypeError, match=rf"Logger\.{method}\(\) takes 1 positional argument"):
getattr(logger, method)("Hello, structured logging %s!", "logging")

@pytest.mark.parametrize(
Expand Down
Loading