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
23 changes: 10 additions & 13 deletions lite_bootstrap/instruments/logging_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import sys
import typing

import orjson

from lite_bootstrap import import_checker
from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument

Expand All @@ -13,9 +15,7 @@


if import_checker.is_structlog_installed:
import orjson
import structlog
from structlog.processors import ExceptionRenderer


ScopeType = typing.MutableMapping[str, typing.Any]
Expand Down Expand Up @@ -97,15 +97,6 @@ class LoggingConfig(BaseConfig):
)


class CustomExceptionRenderer(ExceptionRenderer):
def __call__(self, logger: "WrappedLogger", name: str, event_dict: "EventDict") -> "EventDict":
exc_info = event_dict.get("exc_info")
event_dict = super().__call__(logger=logger, name=name, event_dict=event_dict)
if exc_info:
event_dict["exc_info"] = exc_info
return event_dict


@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class LoggingInstrument(BaseInstrument):
bootstrap_config: LoggingConfig
Expand All @@ -121,7 +112,7 @@ def structlog_pre_chain_processors(self) -> list[typing.Any]:
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
structlog.processors.StackInfoRenderer(),
CustomExceptionRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
]

Expand Down Expand Up @@ -162,7 +153,8 @@ def _configure_foreign_loggers(self) -> None:
foreign_pre_chain=self.structlog_pre_chain_processors,
processors=[
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
structlog.processors.JSONRenderer(),
*self.bootstrap_config.logging_extra_processors,
structlog.processors.JSONRenderer(serializer=_serialize_log_with_orjson_to_string),
],
logger=root_logger,
)
Expand All @@ -177,3 +169,8 @@ def bootstrap(self) -> None:

def teardown(self) -> None:
structlog.reset_defaults()
root_logger = logging.getLogger()
for h in root_logger.handlers[:]:
root_logger.removeHandler(h)
h.close()
root_logger.setLevel(logging.WARNING)
2 changes: 1 addition & 1 deletion lite_bootstrap/instruments/opentelemetry_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def bootstrap(self) -> None:
tracer_provider = TracerProvider(resource=resource)
if self.bootstrap_config.opentelemetry_log_traces:
tracer_provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
if self.bootstrap_config.opentelemetry_endpoint:
if self.bootstrap_config.opentelemetry_endpoint: # pragma: no cover
tracer_provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ classifiers = [
"Topic :: Software Development :: Libraries",
]
version = "0"
dependencies = [
"orjson",
]

[project.urls]
repository = "https://github.com/modern-python/lite-bootstrap"
Expand All @@ -45,7 +48,6 @@ otl = [
]
logging = [
"structlog",
"orjson",
]
free-all = [
"lite-bootstrap[sentry,otl,logging]",
Expand Down
20 changes: 16 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import sys
import typing
from importlib import reload
from unittest.mock import Mock

import pytest
import sentry_sdk
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore[attr-defined]
from structlog.testing import capture_logs
from structlog.typing import EventDict
Expand All @@ -20,9 +20,21 @@ def _uninstrument(self, **kwargs: typing.Mapping[str, typing.Any]) -> None:
pass


@pytest.fixture(autouse=True)
def mock_sentry_init(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr("sentry_sdk.init", Mock)
P = typing.ParamSpec("P")


class SentryTestTransport(sentry_sdk.Transport):
def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: # noqa: ANN401
super().__init__(*args, **kwargs)
self.mock_envelopes: list[sentry_sdk.envelope.Envelope] = []

def capture_envelope(self, envelope: sentry_sdk.envelope.Envelope) -> None:
self.mock_envelopes.append(envelope)


@pytest.fixture
def sentry_mock() -> SentryTestTransport:
return SentryTestTransport()


@contextlib.contextmanager
Expand Down
35 changes: 30 additions & 5 deletions tests/instruments/test_logging_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,79 @@

import structlog
from opentelemetry.trace import get_tracer
from structlog.testing import LogCapture

from lite_bootstrap.instruments.logging_instrument import LoggingConfig, LoggingInstrument, MemoryLoggerFactory
from lite_bootstrap.instruments.opentelemetry_instrument import OpentelemetryConfig, OpenTelemetryInstrument


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


def test_logging_instrument_simple() -> None:
log_capture = LogCapture()
logging_instrument = LoggingInstrument(
bootstrap_config=LoggingConfig(
logging_unset_handlers=["uvicorn"], logging_buffer_capacity=0, service_debug=False
logging_unset_handlers=["uvicorn"],
logging_buffer_capacity=0,
service_debug=False,
logging_extra_processors=[log_capture],
)
)
try:
logging_instrument.bootstrap()

logger = structlog.getLogger(__name__)
logger.info("testing structlog", key="value")
std_logger.info("testing std logger", extra={"key": "value"})
try:
msg = "some error"
raise ValueError(msg) # noqa: TRY301
except ValueError:
logger.exception("logging error")
std_logger.info("testing std logger", extra={"key": "value"})

events_number = 2
assert len(log_capture.entries) == events_number
finally:
logging_instrument.teardown()


def test_logging_instrument_tracer_injection() -> None:
log_capture = LogCapture()
logging_instrument = LoggingInstrument(
bootstrap_config=LoggingConfig(logging_unset_handlers=["uvicorn"], logging_buffer_capacity=0)
bootstrap_config=LoggingConfig(
logging_unset_handlers=["uvicorn"],
logging_buffer_capacity=0,
logging_extra_processors=[log_capture],
)
)
opentelemetry_instrument = OpenTelemetryInstrument(
bootstrap_config=OpentelemetryConfig(
opentelemetry_endpoint="otl",
opentelemetry_log_traces=True,
)
)
try:
logging_instrument.bootstrap()
opentelemetry_instrument.bootstrap()

logger = structlog.getLogger(__name__)
tracer = get_tracer(__name__)
logger.info("testing tracer injection without spans")
with tracer.start_as_current_span("my_fake_span") as span:
logger.info("testing tracer injection without span attributes")
span.set_attribute("example_attribute", "value")
span.add_event("example_event", {"event_attr": 1})
logger.info("testing tracer injection with span attributes")

assert log_capture.entries[0]["event"] == "testing tracer injection without spans"

assert log_capture.entries[1]["event"] == "testing tracer injection without span attributes"
assert log_capture.entries[2]["event"] == "testing tracer injection with span attributes"

tracing1 = log_capture.entries[1]["tracing"]
tracing2 = log_capture.entries[2]["tracing"]
assert tracing1
assert tracing1 == tracing2
finally:
logging_instrument.teardown()
opentelemetry_instrument.teardown()
Expand Down
2 changes: 0 additions & 2 deletions tests/instruments/test_opentelemetry_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
def test_opentelemetry_instrument() -> None:
opentelemetry_instrument = OpenTelemetryInstrument(
bootstrap_config=OpentelemetryConfig(
opentelemetry_endpoint="otl",
opentelemetry_instrumentors=[
InstrumentorWithParams(instrumentor=CustomInstrumentor(), additional_params={"key": "value"}),
CustomInstrumentor(),
Expand All @@ -26,7 +25,6 @@ def test_opentelemetry_instrument() -> None:
def test_opentelemetry_instrument_empty_instruments() -> None:
opentelemetry_instrument = OpenTelemetryInstrument(
bootstrap_config=OpentelemetryConfig(
opentelemetry_endpoint="otl",
opentelemetry_log_traces=True,
)
)
Expand Down
41 changes: 36 additions & 5 deletions tests/instruments/test_sentry_instrument.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,41 @@
from lite_bootstrap.instruments.sentry_instrument import SentryConfig, SentryInstrument
import logging
import typing

import pytest
import sentry_sdk

def test_sentry_instrument() -> None:
SentryInstrument(
bootstrap_config=SentryConfig(sentry_dsn="https://testdsn@localhost/1", sentry_tags={"tag": "value"})
).bootstrap()
from tests.conftest import SentryTestTransport


if typing.TYPE_CHECKING:
pass

from lite_bootstrap.instruments.sentry_instrument import (
SentryConfig,
SentryInstrument,
)


logger = logging.getLogger(__name__)


@pytest.fixture
def minimal_sentry_config(sentry_mock: SentryTestTransport) -> SentryConfig:
return SentryConfig(
sentry_dsn="https://testdsn@localhost/1",
sentry_tags={"test": "test"},
sentry_additional_params={"transport": sentry_mock},
)


def test_sentry_instrument_with_raise(minimal_sentry_config: SentryConfig, sentry_mock: SentryTestTransport) -> None:
SentryInstrument(bootstrap_config=minimal_sentry_config).bootstrap()

try:
logger.error("some error")
assert len(sentry_mock.mock_envelopes) == 1
finally:
sentry_sdk.init()


def test_sentry_instrument_empty_dsn() -> None:
Expand Down
6 changes: 3 additions & 3 deletions tests/test_fastapi_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from starlette.testclient import TestClient

from lite_bootstrap import FastAPIBootstrapper, FastAPIConfig
from tests.conftest import CustomInstrumentor, emulate_package_missing
from tests.conftest import CustomInstrumentor, SentryTestTransport, emulate_package_missing


logger = structlog.getLogger(__name__)
Expand All @@ -25,12 +25,12 @@ def fastapi_config() -> FastAPIConfig:
cors_allowed_origins=["http://test"],
health_checks_path="/custom-health/",
logging_buffer_capacity=0,
opentelemetry_endpoint="otl",
opentelemetry_instrumentors=[CustomInstrumentor()],
opentelemetry_log_traces=True,
opentelemetry_generate_health_check_spans=False,
prometheus_metrics_path="/custom-metrics/",
sentry_dsn="https://testdsn@localhost/1",
sentry_additional_params={"transport": SentryTestTransport()},
swagger_offline_docs=True,
)

Expand Down Expand Up @@ -75,7 +75,7 @@ async def home() -> str:
test_client.get("/")

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


Expand Down
4 changes: 2 additions & 2 deletions tests/test_faststream_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from starlette.testclient import TestClient

from lite_bootstrap import FastStreamBootstrapper, FastStreamConfig
from tests.conftest import CustomInstrumentor, emulate_package_missing
from tests.conftest import CustomInstrumentor, SentryTestTransport, emulate_package_missing


logger = structlog.getLogger(__name__)
Expand All @@ -30,13 +30,13 @@ def build_faststream_config(
service_version="2.0.0",
service_environment="test",
service_debug=False,
opentelemetry_endpoint="otl",
opentelemetry_instrumentors=[CustomInstrumentor()],
opentelemetry_log_traces=True,
opentelemetry_middleware_cls=RedisTelemetryMiddleware,
prometheus_metrics_path="/custom-metrics/",
prometheus_middleware_cls=RedisPrometheusMiddleware,
sentry_dsn="https://testdsn@localhost/1",
sentry_additional_params={"transport": SentryTestTransport()},
health_checks_path="/custom-health/",
logging_buffer_capacity=0,
application=faststream.asgi.AsgiFastStream(
Expand Down
5 changes: 2 additions & 3 deletions tests/test_free_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from structlog.typing import EventDict

from lite_bootstrap import FreeBootstrapper, FreeBootstrapperConfig
from tests.conftest import CustomInstrumentor, emulate_package_missing
from tests.conftest import CustomInstrumentor, SentryTestTransport, emulate_package_missing


logger = structlog.getLogger(__name__)
Expand All @@ -13,7 +13,6 @@
def free_bootstrapper_config() -> FreeBootstrapperConfig:
return FreeBootstrapperConfig(
service_debug=False,
opentelemetry_endpoint="otl",
opentelemetry_instrumentors=[CustomInstrumentor()],
opentelemetry_log_traces=True,
sentry_dsn="https://testdsn@localhost/1",
Expand All @@ -34,10 +33,10 @@ def test_free_bootstrap_logging_not_ready(log_output: list[EventDict]) -> None:
FreeBootstrapper(
bootstrap_config=FreeBootstrapperConfig(
service_debug=True,
opentelemetry_endpoint="otl",
opentelemetry_instrumentors=[CustomInstrumentor()],
opentelemetry_log_traces=True,
sentry_dsn="https://testdsn@localhost/1",
sentry_additional_params={"transport": SentryTestTransport()},
logging_buffer_capacity=0,
),
)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_litestar_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from litestar.testing import TestClient

from lite_bootstrap import LitestarBootstrapper, LitestarConfig
from tests.conftest import CustomInstrumentor, emulate_package_missing
from tests.conftest import CustomInstrumentor, SentryTestTransport, emulate_package_missing


logger = structlog.getLogger(__name__)
Expand All @@ -19,12 +19,12 @@ def litestar_config() -> LitestarConfig:
service_debug=False,
cors_allowed_origins=["http://test"],
health_checks_path="/custom-health/",
opentelemetry_endpoint="otl",
opentelemetry_instrumentors=[CustomInstrumentor()],
opentelemetry_log_traces=True,
opentelemetry_generate_health_check_spans=False,
prometheus_metrics_path="/custom-metrics/",
sentry_dsn="https://testdsn@localhost/1",
sentry_additional_params={"transport": SentryTestTransport()},
swagger_offline_docs=True,
logging_buffer_capacity=0,
)
Expand Down
Loading