Loading src/open_exposure_gateway/core/logging.py +2 −1 Changes for src/open_exposure_gateway/core/logging.py: 2 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -26,7 +26,8 @@ def configure_logging(log_level: str = "INFO") -> None: formatter = structlog.stdlib.ProcessorFormatter( processors=[ structlog.stdlib.ProcessorFormatter.remove_processors_meta, structlog.dev.ConsoleRenderer(), structlog.processors.format_exc_info, structlog.processors.JSONRenderer(), ], foreign_pre_chain=shared_processors, ) Loading src/open_exposure_gateway/main.py +2 −0 Changes for src/open_exposure_gateway/main.py: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -421,8 +421,10 @@ async def default_lifespan(app: FastAPI) -> AsyncGenerator[None, None]: await db_engine.dispose() logger.info("Database engine disposed") _PROBE_PATHS = frozenset({"/platform/healthz", "/platform/readyz"}) def create_app(lifespan: Optional[Lifespan[FastAPI]] = None) -> FastAPI: """Build the application. Loading tests/unit/test_logging.py 0 → 100644 +51 −0 Changes for tests/unit/test_logging.py: 51 added lines, 0 removed lines. Original line number Diff line number Diff line """Unit tests for configure_logging: every line is one JSON object.""" import json import logging from collections.abc import Iterator from typing import Any import pytest import structlog from open_exposure_gateway.core.logging import configure_logging @pytest.fixture() def output(capsys: pytest.CaptureFixture[str]) -> Iterator[pytest.CaptureFixture[str]]: # Configured by each test: pytest swaps sys.stdout between fixture setup and the test. root = logging.getLogger() handlers, level = root.handlers[:], root.level yield capsys structlog.reset_defaults() structlog.contextvars.clear_contextvars() root.handlers[:] = handlers root.setLevel(level) def _line(output: pytest.CaptureFixture[str]) -> dict[str, Any]: line: dict[str, Any] = json.loads(output.readouterr().out) return line def test_line_is_json_with_bound_caller(output: pytest.CaptureFixture[str]) -> None: configure_logging("INFO") structlog.contextvars.bind_contextvars(tenant_id="acme-labs") structlog.get_logger("test").info("http_request", status=201) assert { "event": "http_request", "status": 201, "level": "info", "tenant_id": "acme-labs", }.items() <= _line(output).items() def test_traceback_is_kept(output: pytest.CaptureFixture[str]) -> None: configure_logging("INFO") try: raise ValueError("boom") except ValueError: structlog.get_logger("test").exception("Unhandled exception") assert "ValueError: boom" in _line(output)["exception"] Loading
src/open_exposure_gateway/core/logging.py +2 −1 Changes for src/open_exposure_gateway/core/logging.py: 2 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -26,7 +26,8 @@ def configure_logging(log_level: str = "INFO") -> None: formatter = structlog.stdlib.ProcessorFormatter( processors=[ structlog.stdlib.ProcessorFormatter.remove_processors_meta, structlog.dev.ConsoleRenderer(), structlog.processors.format_exc_info, structlog.processors.JSONRenderer(), ], foreign_pre_chain=shared_processors, ) Loading
src/open_exposure_gateway/main.py +2 −0 Changes for src/open_exposure_gateway/main.py: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -421,8 +421,10 @@ async def default_lifespan(app: FastAPI) -> AsyncGenerator[None, None]: await db_engine.dispose() logger.info("Database engine disposed") _PROBE_PATHS = frozenset({"/platform/healthz", "/platform/readyz"}) def create_app(lifespan: Optional[Lifespan[FastAPI]] = None) -> FastAPI: """Build the application. Loading
tests/unit/test_logging.py 0 → 100644 +51 −0 Changes for tests/unit/test_logging.py: 51 added lines, 0 removed lines. Original line number Diff line number Diff line """Unit tests for configure_logging: every line is one JSON object.""" import json import logging from collections.abc import Iterator from typing import Any import pytest import structlog from open_exposure_gateway.core.logging import configure_logging @pytest.fixture() def output(capsys: pytest.CaptureFixture[str]) -> Iterator[pytest.CaptureFixture[str]]: # Configured by each test: pytest swaps sys.stdout between fixture setup and the test. root = logging.getLogger() handlers, level = root.handlers[:], root.level yield capsys structlog.reset_defaults() structlog.contextvars.clear_contextvars() root.handlers[:] = handlers root.setLevel(level) def _line(output: pytest.CaptureFixture[str]) -> dict[str, Any]: line: dict[str, Any] = json.loads(output.readouterr().out) return line def test_line_is_json_with_bound_caller(output: pytest.CaptureFixture[str]) -> None: configure_logging("INFO") structlog.contextvars.bind_contextvars(tenant_id="acme-labs") structlog.get_logger("test").info("http_request", status=201) assert { "event": "http_request", "status": 201, "level": "info", "tenant_id": "acme-labs", }.items() <= _line(output).items() def test_traceback_is_kept(output: pytest.CaptureFixture[str]) -> None: configure_logging("INFO") try: raise ValueError("boom") except ValueError: structlog.get_logger("test").exception("Unhandled exception") assert "ValueError: boom" in _line(output)["exception"]