From ffde7213a677dab14a23de6d3174e1eef33a49ea Mon Sep 17 00:00:00 2001 From: Lev Vereshchagin Date: Tue, 17 Mar 2026 12:07:53 +0300 Subject: [PATCH 1/8] Route structlog JSON output to stdlib for Sentry --- .../instruments/logging_instrument.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/microbootstrap/instruments/logging_instrument.py b/microbootstrap/instruments/logging_instrument.py index 07839a2..bf17cfe 100644 --- a/microbootstrap/instruments/logging_instrument.py +++ b/microbootstrap/instruments/logging_instrument.py @@ -93,6 +93,18 @@ def _serialize_log_with_orjson_to_string(value: typing.Any, **kwargs: typing.Any serializer=_serialize_log_with_orjson_to_string ) +_FAKER_STDLIB_LOGGER = logging.getLogger("microbootstrap.structlog") +_FAKER_STDLIB_LOGGER.propagate = False +_FAKER_STDLIB_LOGGER.handlers = [] +_FAKER_STDLIB_LOGGER.filters = [] +_FAKER_STDLIB_LOGGER.addHandler(logging.NullHandler()) + + +def redirect_json_log_to_stdlib(_: WrappedLogger, __: str, event_dict: EventDict) -> EventDict: + __tracebackhide__ = True + getattr(_FAKER_STDLIB_LOGGER, event_dict["level"])(STRUCTLOG_FORMATTER_PROCESSOR(_, __, event_dict)) + return event_dict + class MemoryLoggerFactory(structlog.stdlib.LoggerFactory): def __init__( @@ -161,6 +173,13 @@ def _unset_handlers(self) -> None: def _configure_structlog_loggers(self) -> None: if self.instrument_config.service_debug: + structlog.configure( + processors=[ + *structlog.get_config()["processors"][:-1], + redirect_json_log_to_stdlib, # ensure log is sent to Sentry + structlog.get_config()["processors"][-1], + ] + ) return structlog.configure( processors=[ From e33bb9d8e91e6713b34032aa3df472b4e686a202 Mon Sep 17 00:00:00 2001 From: Lev Vereshchagin Date: Tue, 17 Mar 2026 12:07:57 +0300 Subject: [PATCH 2/8] Add manual test for Sentry service_debug modes --- t.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 t.py diff --git a/t.py b/t.py new file mode 100644 index 0000000..1d91022 --- /dev/null +++ b/t.py @@ -0,0 +1,39 @@ +"""Simple manual test for VA-6754: Sentry events with service_debug=True vs False.""" + +import sentry_sdk +import structlog + +from microbootstrap import InstrumentsSetupperSettings +from microbootstrap.instruments_setupper import InstrumentsSetupper +from sentry_sdk.integrations.logging import LoggingIntegration + +# Change this to test both modes +SERVICE_DEBUG = False # Set to False to test production mode + +settings = InstrumentsSetupperSettings( + service_debug=SERVICE_DEBUG, + # sentry_dsn="YOUR_DSN_HERE", # Replace with real DSN + # logging_log_level=10, # DEBUG +) + +setupper = InstrumentsSetupper(settings) +setupper.setup() + +logger = structlog.get_logger() + +# Test 1: Log error via structlog +for one in range(10): + logger.error("test_error_from_structlog", key="value", service_debug=SERVICE_DEBUG) +# # Test 2: Capture exception directly +# try: +# raise ValueError("test_exception_direct") +# except ValueError: +# sentry_sdk.capture_exception() + +# # Test 3: Log exception via structlog +# try: +# raise RuntimeError("test_exception_structlog") +# except RuntimeError: +# logger.exception("caught_exception") + +# print(f"\nDone. Check Sentry for events. service_debug={SERVICE_DEBUG}") From 3e1952e898f110181c9fb33b974a0fcb0a176381 Mon Sep 17 00:00:00 2001 From: Lev Vereshchagin Date: Tue, 17 Mar 2026 12:11:25 +0300 Subject: [PATCH 3/8] Pass exc_info to stdlib logger and enable debug --- microbootstrap/instruments/logging_instrument.py | 4 +++- t.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/microbootstrap/instruments/logging_instrument.py b/microbootstrap/instruments/logging_instrument.py index bf17cfe..c1ef52d 100644 --- a/microbootstrap/instruments/logging_instrument.py +++ b/microbootstrap/instruments/logging_instrument.py @@ -102,7 +102,9 @@ def _serialize_log_with_orjson_to_string(value: typing.Any, **kwargs: typing.Any def redirect_json_log_to_stdlib(_: WrappedLogger, __: str, event_dict: EventDict) -> EventDict: __tracebackhide__ = True - getattr(_FAKER_STDLIB_LOGGER, event_dict["level"])(STRUCTLOG_FORMATTER_PROCESSOR(_, __, event_dict)) + getattr(_FAKER_STDLIB_LOGGER, event_dict["level"])( + STRUCTLOG_FORMATTER_PROCESSOR(_, __, event_dict), exc_info=event_dict.get("exc_info", False) + ) return event_dict diff --git a/t.py b/t.py index 1d91022..37a86ac 100644 --- a/t.py +++ b/t.py @@ -8,7 +8,7 @@ from sentry_sdk.integrations.logging import LoggingIntegration # Change this to test both modes -SERVICE_DEBUG = False # Set to False to test production mode +SERVICE_DEBUG = True # Set to False to test production mode settings = InstrumentsSetupperSettings( service_debug=SERVICE_DEBUG, From 051cfc7d333b2cda57e0c09db735c1650244e99b Mon Sep 17 00:00:00 2001 From: Lev Vereshchagin Date: Tue, 17 Mar 2026 12:25:42 +0300 Subject: [PATCH 4/8] Add tests for Sentry capturing structlog logs --- tests/instruments/test_sentry.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/instruments/test_sentry.py b/tests/instruments/test_sentry.py index e0e4818..b2e6219 100644 --- a/tests/instruments/test_sentry.py +++ b/tests/instruments/test_sentry.py @@ -5,9 +5,11 @@ import litestar import pytest +import structlog from litestar.testing import TestClient as LitestarTestClient from microbootstrap.bootstrappers.litestar import LitestarSentryInstrument +from microbootstrap.instruments.logging_instrument import LoggingConfig, LoggingInstrument from microbootstrap.instruments.sentry_instrument import ( SENTRY_EXTRA_OTEL_TRACE_ID_KEY, SENTRY_EXTRA_OTEL_TRACE_URL_KEY, @@ -160,3 +162,29 @@ def test_add_trace_url_creates_contexts(self, faker: faker.Faker, event: sentry_ assert SENTRY_EXTRA_OTEL_TRACE_URL_KEY in result["extra"] assert SENTRY_EXTRA_OTEL_TRACE_ID_KEY in result["extra"] + + +@pytest.mark.parametrize("is_exception", [True, False]) +@pytest.mark.parametrize("service_debug", [True, False]) +def test_sentry_captures_structlog_logs( + minimal_sentry_config: SentryConfig, + faker: faker.Faker, + monkeypatch: pytest.MonkeyPatch, + is_exception: bool, + service_debug: bool, +) -> None: + monkeypatch.setattr("sentry_sdk.Scope.capture_event", capture_mock := mock.Mock()) + SentryInstrument(minimal_sentry_config).bootstrap() + LoggingInstrument(LoggingConfig(service_debug=service_debug)).bootstrap() + + if is_exception: + try: + _ = 1 / 0 + except ZeroDivisionError: + structlog.get_logger(__name__).exception("in exception") + else: + structlog.get_logger(__name__).error(faker.pystr()) + + assert capture_mock.mock_calls + if is_exception: + assert capture_mock.mock_calls[0].get("exception") From 4c47e6fc6c84f3e2ce69287366c9f4d5205596a7 Mon Sep 17 00:00:00 2001 From: Lev Vereshchagin Date: Tue, 17 Mar 2026 12:32:49 +0300 Subject: [PATCH 5/8] Parametrize Sentry log tests for structlog and logging --- tests/instruments/test_sentry.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/instruments/test_sentry.py b/tests/instruments/test_sentry.py index b2e6219..1d83754 100644 --- a/tests/instruments/test_sentry.py +++ b/tests/instruments/test_sentry.py @@ -1,5 +1,6 @@ from __future__ import annotations import copy +import logging import typing from unittest import mock @@ -164,14 +165,16 @@ def test_add_trace_url_creates_contexts(self, faker: faker.Faker, event: sentry_ assert SENTRY_EXTRA_OTEL_TRACE_ID_KEY in result["extra"] +@pytest.mark.parametrize("logger_instance", [structlog.get_logger(__name__), logging.getLogger(__name__)]) @pytest.mark.parametrize("is_exception", [True, False]) @pytest.mark.parametrize("service_debug", [True, False]) -def test_sentry_captures_structlog_logs( - minimal_sentry_config: SentryConfig, - faker: faker.Faker, - monkeypatch: pytest.MonkeyPatch, +def test_sentry_captures_structlog_logs( # noqa: PLR0913 + logger_instance: logging.Logger, is_exception: bool, service_debug: bool, + monkeypatch: pytest.MonkeyPatch, + faker: faker.Faker, + minimal_sentry_config: SentryConfig, ) -> None: monkeypatch.setattr("sentry_sdk.Scope.capture_event", capture_mock := mock.Mock()) SentryInstrument(minimal_sentry_config).bootstrap() @@ -181,10 +184,9 @@ def test_sentry_captures_structlog_logs( try: _ = 1 / 0 except ZeroDivisionError: - structlog.get_logger(__name__).exception("in exception") + logger_instance.exception("in exception") else: - structlog.get_logger(__name__).error(faker.pystr()) + logger_instance.error(faker.pystr()) assert capture_mock.mock_calls - if is_exception: - assert capture_mock.mock_calls[0].get("exception") + assert bool(capture_mock.mock_calls[0].args[0].get("exception")) == is_exception From e02c3635b9c698a3da34686b0d9d91a65159e7d6 Mon Sep 17 00:00:00 2001 From: Lev Vereshchagin Date: Tue, 17 Mar 2026 12:34:40 +0300 Subject: [PATCH 6/8] Clear structlog faker logger handlers and filters --- microbootstrap/instruments/logging_instrument.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/microbootstrap/instruments/logging_instrument.py b/microbootstrap/instruments/logging_instrument.py index c1ef52d..cb6966a 100644 --- a/microbootstrap/instruments/logging_instrument.py +++ b/microbootstrap/instruments/logging_instrument.py @@ -95,8 +95,6 @@ def _serialize_log_with_orjson_to_string(value: typing.Any, **kwargs: typing.Any _FAKER_STDLIB_LOGGER = logging.getLogger("microbootstrap.structlog") _FAKER_STDLIB_LOGGER.propagate = False -_FAKER_STDLIB_LOGGER.handlers = [] -_FAKER_STDLIB_LOGGER.filters = [] _FAKER_STDLIB_LOGGER.addHandler(logging.NullHandler()) From 7b7f94b546ede4dac6f88cad890fbba1fe8c0e2a Mon Sep 17 00:00:00 2001 From: Lev Vereshchagin Date: Tue, 17 Mar 2026 12:35:23 +0300 Subject: [PATCH 7/8] Remove manual Sentry test script t.py --- t.py | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 t.py diff --git a/t.py b/t.py deleted file mode 100644 index 37a86ac..0000000 --- a/t.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Simple manual test for VA-6754: Sentry events with service_debug=True vs False.""" - -import sentry_sdk -import structlog - -from microbootstrap import InstrumentsSetupperSettings -from microbootstrap.instruments_setupper import InstrumentsSetupper -from sentry_sdk.integrations.logging import LoggingIntegration - -# Change this to test both modes -SERVICE_DEBUG = True # Set to False to test production mode - -settings = InstrumentsSetupperSettings( - service_debug=SERVICE_DEBUG, - # sentry_dsn="YOUR_DSN_HERE", # Replace with real DSN - # logging_log_level=10, # DEBUG -) - -setupper = InstrumentsSetupper(settings) -setupper.setup() - -logger = structlog.get_logger() - -# Test 1: Log error via structlog -for one in range(10): - logger.error("test_error_from_structlog", key="value", service_debug=SERVICE_DEBUG) -# # Test 2: Capture exception directly -# try: -# raise ValueError("test_exception_direct") -# except ValueError: -# sentry_sdk.capture_exception() - -# # Test 3: Log exception via structlog -# try: -# raise RuntimeError("test_exception_structlog") -# except RuntimeError: -# logger.exception("caught_exception") - -# print(f"\nDone. Check Sentry for events. service_debug={SERVICE_DEBUG}") From d64c534113c90f6ad5f4c6242b3286f8e63b9e41 Mon Sep 17 00:00:00 2001 From: Lev Vereshchagin Date: Tue, 17 Mar 2026 12:40:23 +0300 Subject: [PATCH 8/8] Refine exc_info logic and scope Sentry test assertion --- microbootstrap/instruments/logging_instrument.py | 3 ++- tests/instruments/test_sentry.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/microbootstrap/instruments/logging_instrument.py b/microbootstrap/instruments/logging_instrument.py index cb6966a..be4f81a 100644 --- a/microbootstrap/instruments/logging_instrument.py +++ b/microbootstrap/instruments/logging_instrument.py @@ -101,7 +101,8 @@ def _serialize_log_with_orjson_to_string(value: typing.Any, **kwargs: typing.Any def redirect_json_log_to_stdlib(_: WrappedLogger, __: str, event_dict: EventDict) -> EventDict: __tracebackhide__ = True getattr(_FAKER_STDLIB_LOGGER, event_dict["level"])( - STRUCTLOG_FORMATTER_PROCESSOR(_, __, event_dict), exc_info=event_dict.get("exc_info", False) + STRUCTLOG_FORMATTER_PROCESSOR(_, __, event_dict), + exc_info=event_dict.get("exc_info", bool(event_dict.get("exception", False))), ) return event_dict diff --git a/tests/instruments/test_sentry.py b/tests/instruments/test_sentry.py index 1d83754..bdc541d 100644 --- a/tests/instruments/test_sentry.py +++ b/tests/instruments/test_sentry.py @@ -189,4 +189,5 @@ def test_sentry_captures_structlog_logs( # noqa: PLR0913 logger_instance.error(faker.pystr()) assert capture_mock.mock_calls - assert bool(capture_mock.mock_calls[0].args[0].get("exception")) == is_exception + if service_debug: + assert bool(capture_mock.mock_calls[0].args[0].get("exception")) == is_exception