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
8 changes: 8 additions & 0 deletions microbootstrap/instruments/logging_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import pydantic
import structlog
import typing_extensions
from opentelemetry import trace

from microbootstrap.instruments.base import BaseInstrumentConfig, Instrument
Expand Down Expand Up @@ -128,6 +129,13 @@ class LoggingConfig(BaseInstrumentConfig):
logging_exclude_endpoints: list[str] = pydantic.Field(default_factory=lambda: ["/health/", "/metrics"])
logging_turn_off_middleware: bool = False

@pydantic.model_validator(mode="after")
def remove_trailing_slashes_from_logging_exclude_endpoints(self) -> typing_extensions.Self:
self.logging_exclude_endpoints = [
one_endpoint.removesuffix("/") for one_endpoint in self.logging_exclude_endpoints
]
return self


class LoggingInstrument(Instrument[LoggingConfig]):
instrument_name = "Logging"
Expand Down
3 changes: 2 additions & 1 deletion microbootstrap/middlewares/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ async def dispatch(
request: fastapi.Request,
call_next: RequestResponseEndpoint,
) -> fastapi.Response:
request_path: typing.Final = request.url.path.removesuffix("/")
should_log: typing.Final = not any(
exclude_endpoint in str(request.url) for exclude_endpoint in exclude_endpoints
exclude_endpoint == request_path for exclude_endpoint in exclude_endpoints
)
start_time: typing.Final = time.perf_counter_ns()
try:
Expand Down
5 changes: 2 additions & 3 deletions microbootstrap/middlewares/litestar.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ async def __call__(
start_time: typing.Final[int] = time.perf_counter_ns()

async def log_message_wrapper(message: litestar.types.Message) -> None:
should_log: typing.Final = not any(
exclude_endpoint in str(request.url) for exclude_endpoint in exclude_endpoints
)
request_path = request.url.path.removesuffix("/")
should_log: typing.Final = not any(one_endpoint == request_path for one_endpoint in exclude_endpoints)
if message["type"] == "http.response.start" and should_log:
log_level: str = "info" if message["status"] < HTTP_500_INTERNAL_SERVER_ERROR else "exception"
fill_log_message(log_level, request, message["status"], start_time)
Expand Down
48 changes: 44 additions & 4 deletions tests/instruments/test_logging.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import logging
import typing
from io import StringIO
from unittest import mock

import fastapi
import litestar
import pytest
from fastapi.testclient import TestClient as FastAPITestClient
from litestar.testing import TestClient as LitestarTestClient
from opentelemetry import trace
Expand Down Expand Up @@ -51,7 +53,9 @@ def test_litestar_logging_bootstrap(minimal_logging_config: LoggingConfig) -> No
assert len(bootsrap_result["middleware"]) == 1


def test_litestar_logging_bootstrap_working(minimal_logging_config: LoggingConfig) -> None:
def test_litestar_logging_bootstrap_working(
monkeypatch: pytest.MonkeyPatch, minimal_logging_config: LoggingConfig
) -> None:
logging_instrument: typing.Final = LitestarLoggingInstrument(minimal_logging_config)

@litestar.get("/test-handler")
Expand All @@ -63,11 +67,28 @@ async def error_handler() -> str:
route_handlers=[error_handler],
**logging_instrument.bootstrap_before(),
)
monkeypatch.setattr("microbootstrap.middlewares.litestar.fill_log_message", fill_log_mock := mock.Mock())

with LitestarTestClient(app=litestar_application) as test_client:
test_client.get("/test-handler?test-query=1")
test_client.get("/test-handler")

assert fill_log_mock.call_count == 2 # noqa: PLR2004


def test_litestar_logging_bootstrap_ignores_health(
monkeypatch: pytest.MonkeyPatch, minimal_logging_config: LoggingConfig
) -> None:
logging_instrument: typing.Final = LitestarLoggingInstrument(minimal_logging_config)
logging_instrument.bootstrap()
litestar_application: typing.Final = litestar.Litestar(**logging_instrument.bootstrap_before())
monkeypatch.setattr("microbootstrap.middlewares.litestar.fill_log_message", fill_log_mock := mock.Mock())

with LitestarTestClient(app=litestar_application) as test_client:
test_client.get("/health")

assert fill_log_mock.call_count == 0


def test_litestar_logging_bootstrap_tracer_injection(minimal_logging_config: LoggingConfig) -> None:
trace.set_tracer_provider(TracerProvider())
Expand Down Expand Up @@ -133,18 +154,37 @@ def test_memory_logger_factory_error() -> None:
assert error_message in test_stream.getvalue()


def test_fastapi_logging_bootstrap_working(minimal_logging_config: LoggingConfig) -> None:
logging_instrument: typing.Final = FastApiLoggingInstrument(minimal_logging_config)

def test_fastapi_logging_bootstrap_working(
monkeypatch: pytest.MonkeyPatch, minimal_logging_config: LoggingConfig
) -> None:
fastapi_application: typing.Final = fastapi.FastAPI()

@fastapi_application.get("/test-handler")
async def test_handler() -> str:
return "Ok"

logging_instrument: typing.Final = FastApiLoggingInstrument(minimal_logging_config)
logging_instrument.bootstrap()
logging_instrument.bootstrap_after(fastapi_application)
monkeypatch.setattr("microbootstrap.middlewares.fastapi.fill_log_message", fill_log_mock := mock.Mock())

with FastAPITestClient(app=fastapi_application) as test_client:
test_client.get("/test-handler?test-query=1")
test_client.get("/test-handler")

assert fill_log_mock.call_count == 2 # noqa: PLR2004


def test_fastapi_logging_bootstrap_ignores_health(
monkeypatch: pytest.MonkeyPatch, minimal_logging_config: LoggingConfig
) -> None:
fastapi_application: typing.Final = fastapi.FastAPI()
logging_instrument: typing.Final = FastApiLoggingInstrument(minimal_logging_config)
logging_instrument.bootstrap()
logging_instrument.bootstrap_after(fastapi_application)
monkeypatch.setattr("microbootstrap.middlewares.fastapi.fill_log_message", fill_log_mock := mock.Mock())

with FastAPITestClient(app=fastapi_application) as test_client:
test_client.get("/health")

assert fill_log_mock.call_count == 0