diff --git a/lite_bootstrap/instruments/logging_instrument.py b/lite_bootstrap/instruments/logging_instrument.py index e540996..87f8db3 100644 --- a/lite_bootstrap/instruments/logging_instrument.py +++ b/lite_bootstrap/instruments/logging_instrument.py @@ -19,10 +19,6 @@ class _RenderedLine(str): __slots__ = () -def _mark_rendered(_: "WrappedLogger", __: str, rendered_line: str) -> _RenderedLine: - return _RenderedLine(rendered_line) - - if import_checker.is_structlog_installed: import structlog @@ -141,12 +137,24 @@ def _build_excluded_paths(self) -> tuple[str, ...]: @property def structlog_processors(self) -> list[typing.Any]: - return [ - structlog.stdlib.filter_by_level, + processors = [ *self.structlog_pre_chain_processors, *self.bootstrap_config.logging_extra_processors, - structlog.processors.JSONRenderer(serializer=_serialize_log_to_string), - _mark_rendered, + ] + render_json = structlog.processors.JSONRenderer(serializer=_serialize_log_to_string) + + def render_for_stdlib( + logger: "WrappedLogger", method: str, event_dict: "EventDict" + ) -> tuple[tuple[_RenderedLine], dict[str, typing.Any]]: + exc_info = event_dict.get("exc_info") + for processor in processors: + event_dict = processor(logger, method, event_dict) + line = _RenderedLine(render_json(logger, method, event_dict)) + return (line,), {"exc_info": exc_info} + + return [ + structlog.stdlib.filter_by_level, + render_for_stdlib, ] def _configure_structlog_loggers(self) -> None: diff --git a/tests/instruments/test_sentry_instrument.py b/tests/instruments/test_sentry_instrument.py index 57d4aaf..9be0362 100644 --- a/tests/instruments/test_sentry_instrument.py +++ b/tests/instruments/test_sentry_instrument.py @@ -1,5 +1,6 @@ import copy import dataclasses +import json import logging import typing import uuid @@ -556,3 +557,37 @@ def before_send(event: "sentry_types.Event", _: "sentry_types.Hint") -> "sentry_ finally: logs.teardown() sentry.teardown() + + +def test_structlog_stored_exception_reaches_sentry_and_stdout( + minimal_sentry_config: SentryConfig, + sentry_mock: SentryTestTransport, + capsys: pytest.CaptureFixture[str], +) -> None: + sentry = SentryInstrument(bootstrap_config=minimal_sentry_config) + logs = LoggingInstrument(bootstrap_config=LoggingConfig()) + sentry.bootstrap() + logs.bootstrap() + try: + try: + int("original stored error") + except ValueError as exc: + stored = exc + + logger.error("stored exception", exc_info=stored, component="worker") + + line = json.loads(capsys.readouterr().out.splitlines()[-1]) + assert "Traceback (most recent call last)" in line["exception"] + assert "ValueError: invalid literal for int() with base 10: 'original stored error'" in line["exception"] + event = sentry_mock.mock_envelopes[0].get_event() + assert event is not None + (exception,) = event["exception"]["values"] + assert exception["type"] == "ValueError" + assert exception["value"] == "invalid literal for int() with base 10: 'original stored error'" + assert exception["stacktrace"]["frames"][-1]["function"] == ( + "test_structlog_stored_exception_reaches_sentry_and_stdout" + ) + assert event["contexts"]["structlog"] == {"component": "worker"} + finally: + logs.teardown() + sentry.teardown()