Skip to content

Commit 8824867

Browse files
authored
Fix Sentry detection of structlog logs when service_debug=True (#138)
* Route structlog JSON output to stdlib for Sentry * Add manual test for Sentry service_debug modes * Pass exc_info to stdlib logger and enable debug * Add tests for Sentry capturing structlog logs * Parametrize Sentry log tests for structlog and logging * Clear structlog faker logger handlers and filters * Remove manual Sentry test script t.py * Refine exc_info logic and scope Sentry test assertion
1 parent 74ffca2 commit 8824867

2 files changed

Lines changed: 51 additions & 0 deletions

File tree

microbootstrap/instruments/logging_instrument.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,19 @@ def _serialize_log_with_orjson_to_string(value: typing.Any, **kwargs: typing.Any
9393
serializer=_serialize_log_with_orjson_to_string
9494
)
9595

96+
_FAKER_STDLIB_LOGGER = logging.getLogger("microbootstrap.structlog")
97+
_FAKER_STDLIB_LOGGER.propagate = False
98+
_FAKER_STDLIB_LOGGER.addHandler(logging.NullHandler())
99+
100+
101+
def redirect_json_log_to_stdlib(_: WrappedLogger, __: str, event_dict: EventDict) -> EventDict:
102+
__tracebackhide__ = True
103+
getattr(_FAKER_STDLIB_LOGGER, event_dict["level"])(
104+
STRUCTLOG_FORMATTER_PROCESSOR(_, __, event_dict),
105+
exc_info=event_dict.get("exc_info", bool(event_dict.get("exception", False))),
106+
)
107+
return event_dict
108+
96109

97110
class MemoryLoggerFactory(structlog.stdlib.LoggerFactory):
98111
def __init__(
@@ -161,6 +174,13 @@ def _unset_handlers(self) -> None:
161174

162175
def _configure_structlog_loggers(self) -> None:
163176
if self.instrument_config.service_debug:
177+
structlog.configure(
178+
processors=[
179+
*structlog.get_config()["processors"][:-1],
180+
redirect_json_log_to_stdlib, # ensure log is sent to Sentry
181+
structlog.get_config()["processors"][-1],
182+
]
183+
)
164184
return
165185
structlog.configure(
166186
processors=[

tests/instruments/test_sentry.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
from __future__ import annotations
22
import copy
3+
import logging
34
import typing
45
from unittest import mock
56

67
import litestar
78
import pytest
9+
import structlog
810
from litestar.testing import TestClient as LitestarTestClient
911

1012
from microbootstrap.bootstrappers.litestar import LitestarSentryInstrument
13+
from microbootstrap.instruments.logging_instrument import LoggingConfig, LoggingInstrument
1114
from microbootstrap.instruments.sentry_instrument import (
1215
SENTRY_EXTRA_OTEL_TRACE_ID_KEY,
1316
SENTRY_EXTRA_OTEL_TRACE_URL_KEY,
@@ -160,3 +163,31 @@ def test_add_trace_url_creates_contexts(self, faker: faker.Faker, event: sentry_
160163

161164
assert SENTRY_EXTRA_OTEL_TRACE_URL_KEY in result["extra"]
162165
assert SENTRY_EXTRA_OTEL_TRACE_ID_KEY in result["extra"]
166+
167+
168+
@pytest.mark.parametrize("logger_instance", [structlog.get_logger(__name__), logging.getLogger(__name__)])
169+
@pytest.mark.parametrize("is_exception", [True, False])
170+
@pytest.mark.parametrize("service_debug", [True, False])
171+
def test_sentry_captures_structlog_logs( # noqa: PLR0913
172+
logger_instance: logging.Logger,
173+
is_exception: bool,
174+
service_debug: bool,
175+
monkeypatch: pytest.MonkeyPatch,
176+
faker: faker.Faker,
177+
minimal_sentry_config: SentryConfig,
178+
) -> None:
179+
monkeypatch.setattr("sentry_sdk.Scope.capture_event", capture_mock := mock.Mock())
180+
SentryInstrument(minimal_sentry_config).bootstrap()
181+
LoggingInstrument(LoggingConfig(service_debug=service_debug)).bootstrap()
182+
183+
if is_exception:
184+
try:
185+
_ = 1 / 0
186+
except ZeroDivisionError:
187+
logger_instance.exception("in exception")
188+
else:
189+
logger_instance.error(faker.pystr())
190+
191+
assert capture_mock.mock_calls
192+
if service_debug:
193+
assert bool(capture_mock.mock_calls[0].args[0].get("exception")) == is_exception

0 commit comments

Comments
 (0)