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
54 changes: 37 additions & 17 deletions lite_bootstrap/instruments/logging_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,36 +98,35 @@ class LoggingInstrument(BaseInstrument):
not_ready_message = "service_debug is True"
missing_dependency_message = "structlog is not installed"

@property
def structlog_pre_chain_processors(self) -> list[typing.Any]:
return [
structlog.stdlib.add_log_level,
structlog.stdlib.add_logger_name,
tracer_injection,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
]

def is_ready(self) -> bool:
return not self.bootstrap_config.service_debug and import_checker.is_structlog_installed

@staticmethod
def check_dependencies() -> bool:
return import_checker.is_structlog_installed

def bootstrap(self) -> None:
# Configure basic logging to allow structlog to catch its events
logging.basicConfig(
format="%(levelname)s [%(asctime)s] %(module)s %(pathname)s - %(message)s",
stream=sys.stdout,
datefmt="%Y-%m-%d %H:%M:%S",
level=self.bootstrap_config.logging_log_level,
)

def _unset_handlers(self) -> None:
for unset_handlers_logger in self.bootstrap_config.logging_unset_handlers:
logging.getLogger(unset_handlers_logger).handlers = []

def _configure_structlog_loggers(self) -> None:
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_log_level,
structlog.stdlib.add_logger_name,
tracer_injection,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
*self.structlog_pre_chain_processors,
*self.bootstrap_config.logging_extra_processors,
structlog.processors.JSONRenderer(),
],
Expand All @@ -141,5 +140,26 @@ def bootstrap(self) -> None:
cache_logger_on_first_use=True,
)

def _configure_foreign_loggers(self) -> None:
root_logger: typing.Final = logging.getLogger()
stream_handler: typing.Final = logging.StreamHandler(sys.stdout)
stream_handler.setFormatter(
structlog.stdlib.ProcessorFormatter(
foreign_pre_chain=self.structlog_pre_chain_processors,
processors=[
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
structlog.processors.JSONRenderer(),
],
logger=root_logger,
)
)
root_logger.addHandler(stream_handler)
root_logger.setLevel(self.bootstrap_config.logging_log_level)

def bootstrap(self) -> None:
self._unset_handlers()
self._configure_structlog_loggers()
self._configure_foreign_loggers()

def teardown(self) -> None:
structlog.reset_defaults()
1 change: 1 addition & 0 deletions lite_bootstrap/instruments/sentry_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class SentryConfig(BaseConfig):
sentry_integrations: list["Integration"] = dataclasses.field(default_factory=list)
sentry_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
sentry_tags: dict[str, str] | None = None
sentry_default_integrations: bool = True


@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
Expand Down
20 changes: 20 additions & 0 deletions tests/test_fastapi_bootstrap.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import dataclasses
import logging

import fastapi
import pytest
Expand All @@ -11,6 +12,7 @@


logger = structlog.getLogger(__name__)
std_logger = logging.getLogger()


@pytest.fixture
Expand Down Expand Up @@ -58,6 +60,24 @@ def test_fastapi_bootstrap(fastapi_config: FastAPIConfig) -> None:
assert not bootstrapper.is_bootstrapped


def test_fastapi_bootstrap_std_logger(fastapi_config: FastAPIConfig, capsys: pytest.CaptureFixture[str]) -> None:
bootstrapper = FastAPIBootstrapper(bootstrap_config=fastapi_config)
application = bootstrapper.bootstrap()

@application.get("/")
async def home() -> str:
std_logger.info("std logger")
logger.info("structlog logger")
return ""

with TestClient(application) as test_client:
test_client.get("/")

stdout = capsys.readouterr().out
assert '"event": "std logger", "level": "info", "logger": "root"' in stdout
assert stdout.count("std logger") == 1


def test_fastapi_bootstrapper_not_ready() -> None:
with emulate_package_missing("fastapi"), pytest.raises(RuntimeError, match="fastapi is not installed"):
FastAPIBootstrapper(bootstrap_config=FastAPIConfig())
Expand Down
Loading