diff --git a/lite_bootstrap/bootstrappers/base.py b/lite_bootstrap/bootstrappers/base.py index 9d0121b..5381894 100644 --- a/lite_bootstrap/bootstrappers/base.py +++ b/lite_bootstrap/bootstrappers/base.py @@ -1,5 +1,6 @@ import abc import typing +import warnings from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument from lite_bootstrap.types import ApplicationT @@ -14,16 +15,28 @@ class BaseBootstrapper(abc.ABC, typing.Generic[ApplicationT]): bootstrap_config: BaseConfig def __init__(self, bootstrap_config: BaseConfig) -> None: + if not self.is_ready(): + raise RuntimeError(self.not_ready_message) + self.bootstrap_config = bootstrap_config self.instruments = [] for instrument_type in self.instruments_types: instrument = instrument_type(bootstrap_config=bootstrap_config) if instrument.is_ready(): self.instruments.append(instrument) + else: + warnings.warn(instrument.not_ready_message, stacklevel=2) + + @property + @abc.abstractmethod + def not_ready_message(self) -> str: ... @abc.abstractmethod def _prepare_application(self) -> ApplicationT: ... + @abc.abstractmethod + def is_ready(self) -> bool: ... + def bootstrap(self) -> ApplicationT: for one_instrument in self.instruments: one_instrument.bootstrap() diff --git a/lite_bootstrap/bootstrappers/fastapi_bootstrapper.py b/lite_bootstrap/bootstrappers/fastapi_bootstrapper.py index dbc4456..ece14cd 100644 --- a/lite_bootstrap/bootstrappers/fastapi_bootstrapper.py +++ b/lite_bootstrap/bootstrappers/fastapi_bootstrapper.py @@ -1,7 +1,7 @@ -import contextlib import dataclasses import typing +from lite_bootstrap import import_checker from lite_bootstrap.bootstrappers.base import BaseBootstrapper from lite_bootstrap.instruments.healthchecks_instrument import ( HealthChecksConfig, @@ -14,16 +14,20 @@ from lite_bootstrap.instruments.sentry_instrument import SentryConfig, SentryInstrument -with contextlib.suppress(ImportError): +if import_checker.is_fastapi_installed: import fastapi + +if import_checker.is_opentelemetry_installed: from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from opentelemetry.trace import get_tracer_provider + +if import_checker.is_prometheus_fastapi_instrumentator_installed: from prometheus_fastapi_instrumentator import Instrumentator @dataclasses.dataclass(kw_only=True, slots=True, frozen=True) class FastAPIConfig(HealthChecksConfig, LoggingConfig, OpentelemetryConfig, PrometheusConfig, SentryConfig): - application: fastapi.FastAPI = dataclasses.field(default_factory=fastapi.FastAPI) + application: "fastapi.FastAPI" = dataclasses.field(default_factory=fastapi.FastAPI) opentelemetry_excluded_urls: list[str] = dataclasses.field(default_factory=list) prometheus_instrumentator_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict) prometheus_instrument_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict) @@ -34,7 +38,7 @@ class FastAPIConfig(HealthChecksConfig, LoggingConfig, OpentelemetryConfig, Prom class FastAPIHealthChecksInstrument(HealthChecksInstrument): bootstrap_config: FastAPIConfig - def build_fastapi_health_check_router(self) -> fastapi.APIRouter: + def build_fastapi_health_check_router(self) -> "fastapi.APIRouter": fastapi_router = fastapi.APIRouter( tags=["probes"], include_in_schema=self.bootstrap_config.health_checks_include_in_schema, @@ -87,6 +91,12 @@ class FastAPISentryInstrument(SentryInstrument): @dataclasses.dataclass(kw_only=True, frozen=True) class FastAPIPrometheusInstrument(PrometheusInstrument): bootstrap_config: FastAPIConfig + not_ready_message = ( + PrometheusInstrument.not_ready_message + " or prometheus_fastapi_instrumentator is not installed" + ) + + def is_ready(self) -> bool: + return super().is_ready() and import_checker.is_prometheus_fastapi_instrumentator_installed def bootstrap(self) -> None: Instrumentator(**self.bootstrap_config.prometheus_instrument_params).instrument( @@ -101,6 +111,8 @@ def bootstrap(self) -> None: class FastAPIBootstrapper(BaseBootstrapper[fastapi.FastAPI]): + __slots__ = "bootstrap_config", "instruments" + instruments_types: typing.ClassVar = [ FastAPIOpenTelemetryInstrument, FastAPISentryInstrument, @@ -109,7 +121,10 @@ class FastAPIBootstrapper(BaseBootstrapper[fastapi.FastAPI]): FastAPIPrometheusInstrument, ] bootstrap_config: FastAPIConfig - __slots__ = "bootstrap_config", "instruments" + not_ready_message = "fastapi is not installed" + + def is_ready(self) -> bool: + return import_checker.is_fastapi_installed def _prepare_application(self) -> fastapi.FastAPI: return self.bootstrap_config.application diff --git a/lite_bootstrap/bootstrappers/faststream_bootstrapper.py b/lite_bootstrap/bootstrappers/faststream_bootstrapper.py index 686edf1..c4c9317 100644 --- a/lite_bootstrap/bootstrappers/faststream_bootstrapper.py +++ b/lite_bootstrap/bootstrappers/faststream_bootstrapper.py @@ -1,9 +1,9 @@ from __future__ import annotations -import contextlib import dataclasses import json import typing +from lite_bootstrap import import_checker from lite_bootstrap.bootstrappers.base import BaseBootstrapper from lite_bootstrap.instruments.healthchecks_instrument import HealthChecksConfig, HealthChecksInstrument from lite_bootstrap.instruments.logging_instrument import LoggingConfig, LoggingInstrument @@ -12,12 +12,16 @@ from lite_bootstrap.instruments.sentry_instrument import SentryConfig, SentryInstrument -with contextlib.suppress(ImportError): +if import_checker.is_faststream_installed: import faststream - import prometheus_client from faststream.asgi import AsgiFastStream, AsgiResponse from faststream.asgi import get as handle_get from faststream.broker.core.usecase import BrokerUsecase + +if import_checker.is_prometheus_client_installed: + import prometheus_client + +if import_checker.is_opentelemetry_installed: from opentelemetry.metrics import Meter, MeterProvider from opentelemetry.trace import TracerProvider, get_tracer_provider @@ -87,9 +91,10 @@ class FastStreamLoggingInstrument(LoggingInstrument): @dataclasses.dataclass(kw_only=True, frozen=True) class FastStreamOpenTelemetryInstrument(OpenTelemetryInstrument): bootstrap_config: FastStreamConfig + not_ready_message = OpenTelemetryInstrument.not_ready_message + " or opentelemetry_middleware_cls is empty" def is_ready(self) -> bool: - return bool(self.bootstrap_config.opentelemetry_middleware_cls and super().is_ready()) + return super().is_ready() and bool(self.bootstrap_config.opentelemetry_middleware_cls) def bootstrap(self) -> None: if self.bootstrap_config.opentelemetry_middleware_cls and self.bootstrap_config.application.broker: @@ -109,9 +114,18 @@ class FastStreamPrometheusInstrument(PrometheusInstrument): collector_registry: prometheus_client.CollectorRegistry = dataclasses.field( default_factory=prometheus_client.CollectorRegistry, init=False ) + not_ready_message = ( + PrometheusInstrument.not_ready_message + + " or prometheus_middleware_cls is missing or prometheus_client is not installed" + ) def is_ready(self) -> bool: - return bool(self.bootstrap_config.prometheus_middleware_cls and super().is_ready()) + return ( + super().is_ready() + and import_checker.is_prometheus_client_installed + and bool(self.bootstrap_config.prometheus_middleware_cls) + and import_checker.is_prometheus_client_installed + ) def bootstrap(self) -> None: self.bootstrap_config.application.mount( @@ -124,6 +138,8 @@ def bootstrap(self) -> None: class FastStreamBootstrapper(BaseBootstrapper[AsgiFastStream]): + __slots__ = "bootstrap_config", "instruments" + instruments_types: typing.ClassVar = [ FastStreamOpenTelemetryInstrument, FastStreamSentryInstrument, @@ -132,7 +148,10 @@ class FastStreamBootstrapper(BaseBootstrapper[AsgiFastStream]): FastStreamPrometheusInstrument, ] bootstrap_config: FastStreamConfig - __slots__ = "bootstrap_config", "instruments" + not_ready_message = "faststream is not installed" + + def is_ready(self) -> bool: + return import_checker.is_faststream_installed def __init__(self, bootstrap_config: FastStreamConfig) -> None: super().__init__(bootstrap_config) diff --git a/lite_bootstrap/bootstrappers/free_bootstrapper.py b/lite_bootstrap/bootstrappers/free_bootstrapper.py index 13f9bc2..9237fc6 100644 --- a/lite_bootstrap/bootstrappers/free_bootstrapper.py +++ b/lite_bootstrap/bootstrappers/free_bootstrapper.py @@ -12,13 +12,18 @@ class FreeBootstrapperConfig(LoggingConfig, OpentelemetryConfig, SentryConfig): class FreeBootstrapper(BaseBootstrapper[None]): + __slots__ = "bootstrap_config", "instruments" + instruments_types: typing.ClassVar = [ OpenTelemetryInstrument, SentryInstrument, LoggingInstrument, ] bootstrap_config: FreeBootstrapperConfig - __slots__ = "bootstrap_config", "instruments" + not_ready_message = "" + + def is_ready(self) -> bool: + return True def __init__(self, bootstrap_config: FreeBootstrapperConfig) -> None: super().__init__(bootstrap_config) diff --git a/lite_bootstrap/bootstrappers/litestar_bootstrapper.py b/lite_bootstrap/bootstrappers/litestar_bootstrapper.py index 2c5d931..a90a835 100644 --- a/lite_bootstrap/bootstrappers/litestar_bootstrapper.py +++ b/lite_bootstrap/bootstrappers/litestar_bootstrapper.py @@ -1,9 +1,7 @@ -import contextlib import dataclasses import typing -from litestar.plugins.prometheus import PrometheusConfig, PrometheusController - +from lite_bootstrap import import_checker from lite_bootstrap.bootstrappers.base import BaseBootstrapper from lite_bootstrap.instruments.healthchecks_instrument import ( HealthChecksConfig, @@ -21,10 +19,13 @@ from lite_bootstrap.instruments.sentry_instrument import SentryConfig, SentryInstrument -with contextlib.suppress(ImportError): +if import_checker.is_litestar_installed: import litestar from litestar.config.app import AppConfig from litestar.contrib.opentelemetry import OpenTelemetryConfig + from litestar.plugins.prometheus import PrometheusConfig, PrometheusController + +if import_checker.is_opentelemetry_installed: from opentelemetry.trace import get_tracer_provider @@ -32,7 +33,7 @@ class LitestarConfig( HealthChecksConfig, LoggingConfig, OpentelemetryConfig, PrometheusBootstrapperConfig, SentryConfig ): - application_config: AppConfig = dataclasses.field(default_factory=AppConfig) + application_config: "AppConfig" = dataclasses.field(default_factory=AppConfig) opentelemetry_excluded_urls: list[str] = dataclasses.field(default_factory=list) prometheus_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict) @@ -41,7 +42,7 @@ class LitestarConfig( class LitestarHealthChecksInstrument(HealthChecksInstrument): bootstrap_config: LitestarConfig - def build_litestar_health_check_router(self) -> litestar.Router: + def build_litestar_health_check_router(self) -> "litestar.Router": @litestar.get(media_type=litestar.MediaType.JSON) async def health_check_handler() -> HealthCheckTypedDict: return self.render_health_check_data() @@ -91,6 +92,10 @@ class LitestarSentryInstrument(SentryInstrument): @dataclasses.dataclass(kw_only=True, frozen=True) class LitestarPrometheusInstrument(PrometheusInstrument): bootstrap_config: LitestarConfig + not_ready_message = PrometheusInstrument.not_ready_message + " or prometheus_client is not installed" + + def is_ready(self) -> bool: + return super().is_ready() and import_checker.is_prometheus_client_installed def bootstrap(self) -> None: class LitestarPrometheusController(PrometheusController): @@ -108,6 +113,8 @@ class LitestarPrometheusController(PrometheusController): class LitestarBootstrapper(BaseBootstrapper[litestar.Litestar]): + __slots__ = "bootstrap_config", "instruments" + instruments_types: typing.ClassVar = [ LitestarOpenTelemetryInstrument, LitestarSentryInstrument, @@ -116,7 +123,10 @@ class LitestarBootstrapper(BaseBootstrapper[litestar.Litestar]): LitestarPrometheusInstrument, ] bootstrap_config: LitestarConfig - __slots__ = "bootstrap_config", "instruments" + not_ready_message = "litestar is not installed" + + def is_ready(self) -> bool: + return import_checker.is_litestar_installed def __init__(self, bootstrap_config: LitestarConfig) -> None: super().__init__(bootstrap_config) diff --git a/lite_bootstrap/import_checker.py b/lite_bootstrap/import_checker.py new file mode 100644 index 0000000..a1c655e --- /dev/null +++ b/lite_bootstrap/import_checker.py @@ -0,0 +1,11 @@ +from importlib.util import find_spec + + +is_opentelemetry_installed = find_spec("opentelemetry") is not None +is_sentry_installed = find_spec("sentry_sdk") is not None +is_structlog_installed = find_spec("structlog") is not None +is_prometheus_client_installed = find_spec("prometheus_client") is not None +is_fastapi_installed = find_spec("fastapi") is not None +is_litestar_installed = find_spec("litestar") is not None +is_faststream_installed = find_spec("faststream") is not None +is_prometheus_fastapi_instrumentator_installed = find_spec("prometheus_fastapi_instrumentator") is not None diff --git a/lite_bootstrap/instruments/base.py b/lite_bootstrap/instruments/base.py index cbdddfb..c9fee10 100644 --- a/lite_bootstrap/instruments/base.py +++ b/lite_bootstrap/instruments/base.py @@ -14,6 +14,10 @@ class BaseConfig: class BaseInstrument(abc.ABC): bootstrap_config: BaseConfig + @property + @abc.abstractmethod + def not_ready_message(self) -> str: ... + def bootstrap(self) -> None: ... # noqa: B027 def teardown(self) -> None: ... # noqa: B027 diff --git a/lite_bootstrap/instruments/healthchecks_instrument.py b/lite_bootstrap/instruments/healthchecks_instrument.py index 431aa72..36e608e 100644 --- a/lite_bootstrap/instruments/healthchecks_instrument.py +++ b/lite_bootstrap/instruments/healthchecks_instrument.py @@ -21,6 +21,7 @@ class HealthChecksConfig(BaseConfig): @dataclasses.dataclass(kw_only=True, slots=True, frozen=True) class HealthChecksInstrument(BaseInstrument): bootstrap_config: HealthChecksConfig + not_ready_message = "health_checks_enabled is False" def is_ready(self) -> bool: return self.bootstrap_config.health_checks_enabled diff --git a/lite_bootstrap/instruments/logging_instrument.py b/lite_bootstrap/instruments/logging_instrument.py index da5171c..c9cd1aa 100644 --- a/lite_bootstrap/instruments/logging_instrument.py +++ b/lite_bootstrap/instruments/logging_instrument.py @@ -1,9 +1,10 @@ -import contextlib import dataclasses import logging import logging.handlers +import sys import typing +from lite_bootstrap import import_checker from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument @@ -11,7 +12,7 @@ from structlog.typing import EventDict, WrappedLogger -with contextlib.suppress(ImportError): +if import_checker.is_structlog_installed: import structlog @@ -48,20 +49,6 @@ def tracer_injection(_: "WrappedLogger", __: str, event_dict: "EventDict") -> "E return event_dict -DEFAULT_STRUCTLOG_PROCESSORS: typing.Final[list[typing.Any]] = [ - 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(), -] -DEFAULT_STRUCTLOG_FORMATTER_PROCESSOR: typing.Final = structlog.processors.JSONRenderer() - - class MemoryLoggerFactory(structlog.stdlib.LoggerFactory): def __init__( self, @@ -106,19 +93,35 @@ class LoggingConfig(BaseConfig): @dataclasses.dataclass(kw_only=True, slots=True, frozen=True) class LoggingInstrument(BaseInstrument): bootstrap_config: LoggingConfig + not_ready_message = "service_debug is True or structlog is not installed" def is_ready(self) -> bool: - return not self.bootstrap_config.service_debug + return not self.bootstrap_config.service_debug and import_checker.is_structlog_installed def bootstrap(self) -> None: + # Configure basic logging to allow structlog to catch its events + logging.basicConfig( + format="%(message)s", + stream=sys.stdout, + level=logging.INFO, + ) + for unset_handlers_logger in self.bootstrap_config.logging_unset_handlers: logging.getLogger(unset_handlers_logger).handlers = [] structlog.configure( processors=[ - *DEFAULT_STRUCTLOG_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.bootstrap_config.logging_extra_processors, - DEFAULT_STRUCTLOG_FORMATTER_PROCESSOR, + structlog.processors.JSONRenderer(), ], context_class=dict, logger_factory=MemoryLoggerFactory( diff --git a/lite_bootstrap/instruments/opentelemetry_instrument.py b/lite_bootstrap/instruments/opentelemetry_instrument.py index 31b8f62..a9855e6 100644 --- a/lite_bootstrap/instruments/opentelemetry_instrument.py +++ b/lite_bootstrap/instruments/opentelemetry_instrument.py @@ -1,22 +1,26 @@ -import contextlib import dataclasses import typing +from lite_bootstrap import import_checker from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument -with contextlib.suppress(ImportError): - from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +if typing.TYPE_CHECKING: from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore[attr-defined] + from opentelemetry.sdk.trace.export import SpanExporter + + +if import_checker.is_opentelemetry_installed: + from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk import resources from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter + from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.trace import set_tracer_provider @dataclasses.dataclass(kw_only=True, slots=True, frozen=True) class InstrumentorWithParams: - instrumentor: BaseInstrumentor + instrumentor: "BaseInstrumentor" additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict) @@ -27,18 +31,19 @@ class OpentelemetryConfig(BaseConfig): opentelemetry_endpoint: str | None = None opentelemetry_namespace: str | None = None opentelemetry_insecure: bool = True - opentelemetry_instrumentors: list[InstrumentorWithParams | BaseInstrumentor] = dataclasses.field( + opentelemetry_instrumentors: list[typing.Union[InstrumentorWithParams, "BaseInstrumentor"]] = dataclasses.field( default_factory=list ) - opentelemetry_span_exporter: SpanExporter | None = None + opentelemetry_span_exporter: typing.Optional["SpanExporter"] = None @dataclasses.dataclass(kw_only=True, slots=True, frozen=True) class OpenTelemetryInstrument(BaseInstrument): bootstrap_config: OpentelemetryConfig + not_ready_message = "opentelemetry_endpoint is empty or opentelemetry is not installed" def is_ready(self) -> bool: - return bool(self.bootstrap_config.opentelemetry_endpoint) + return bool(self.bootstrap_config.opentelemetry_endpoint) and import_checker.is_opentelemetry_installed def bootstrap(self) -> None: attributes = { diff --git a/lite_bootstrap/instruments/prometheus_instrument.py b/lite_bootstrap/instruments/prometheus_instrument.py index 90dfcc4..3d7ae1a 100644 --- a/lite_bootstrap/instruments/prometheus_instrument.py +++ b/lite_bootstrap/instruments/prometheus_instrument.py @@ -21,6 +21,7 @@ class PrometheusConfig(BaseConfig): @dataclasses.dataclass(kw_only=True, slots=True, frozen=True) class PrometheusInstrument(BaseInstrument): bootstrap_config: PrometheusConfig + not_ready_message = "prometheus_metrics_path is empty or not valid" def is_ready(self) -> bool: return bool(self.bootstrap_config.prometheus_metrics_path) and _is_valid_path( diff --git a/lite_bootstrap/instruments/sentry_instrument.py b/lite_bootstrap/instruments/sentry_instrument.py index 8dd2d6d..84c4119 100644 --- a/lite_bootstrap/instruments/sentry_instrument.py +++ b/lite_bootstrap/instruments/sentry_instrument.py @@ -1,15 +1,18 @@ -import contextlib import dataclasses import typing +from lite_bootstrap import import_checker from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument -with contextlib.suppress(ImportError): - import sentry_sdk +if typing.TYPE_CHECKING: from sentry_sdk.integrations import Integration +if import_checker.is_sentry_installed: + import sentry_sdk + + @dataclasses.dataclass(kw_only=True, frozen=True) class SentryConfig(BaseConfig): sentry_dsn: str | None = None @@ -18,7 +21,7 @@ class SentryConfig(BaseConfig): sentry_max_breadcrumbs: int = 15 sentry_max_value_length: int = 16384 sentry_attach_stacktrace: bool = True - sentry_integrations: list[Integration] = dataclasses.field(default_factory=list) + 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 @@ -26,9 +29,10 @@ class SentryConfig(BaseConfig): @dataclasses.dataclass(kw_only=True, slots=True, frozen=True) class SentryInstrument(BaseInstrument): bootstrap_config: SentryConfig + not_ready_message = "sentry_dsn is empty or sentry_sdk is not installed" def is_ready(self) -> bool: - return bool(self.bootstrap_config.sentry_dsn) + return bool(self.bootstrap_config.sentry_dsn) and import_checker.is_sentry_installed def bootstrap(self) -> None: sentry_sdk.init( diff --git a/tests/test_fastapi_bootstrap.py b/tests/test_fastapi_bootstrap.py index 2e9b21e..6e0da8c 100644 --- a/tests/test_fastapi_bootstrap.py +++ b/tests/test_fastapi_bootstrap.py @@ -1,9 +1,10 @@ +import pytest import structlog from opentelemetry.sdk.trace.export import ConsoleSpanExporter from starlette import status from starlette.testclient import TestClient -from lite_bootstrap import FastAPIBootstrapper, FastAPIConfig +from lite_bootstrap import FastAPIBootstrapper, FastAPIConfig, import_checker from tests.conftest import CustomInstrumentor @@ -11,6 +12,8 @@ def test_fastapi_bootstrap() -> None: + health_checks_path = "/custom-health/" + prometheus_metrics_path = "/custom-metrics/" bootstrapper = FastAPIBootstrapper( bootstrap_config=FastAPIConfig( service_name="microservice", @@ -20,31 +23,33 @@ def test_fastapi_bootstrap() -> None: opentelemetry_endpoint="otl", opentelemetry_instrumentors=[CustomInstrumentor()], opentelemetry_span_exporter=ConsoleSpanExporter(), + prometheus_metrics_path=prometheus_metrics_path, sentry_dsn="https://testdsn@localhost/1", - health_checks_path="/health/", + health_checks_path=health_checks_path, logging_buffer_capacity=0, ), ) - fastapi_app = bootstrapper.bootstrap() + application = bootstrapper.bootstrap() + test_client = TestClient(application) + logger.info("testing logging", key="value") try: - response = TestClient(fastapi_app).get("/health/") + response = test_client.get(health_checks_path) assert response.status_code == status.HTTP_200_OK assert response.json() == {"health_status": True, "service_name": "microservice", "service_version": "2.0.0"} + + response = test_client.get(prometheus_metrics_path) + assert response.status_code == status.HTTP_200_OK + assert response.text finally: bootstrapper.teardown() -def test_fastapi_prometheus_instrument() -> None: - prometheus_metrics_path = "/custom-metrics-path" - bootstrapper = FastAPIBootstrapper( - bootstrap_config=FastAPIConfig( - prometheus_metrics_path=prometheus_metrics_path, - ), - ) - fastapi_app = bootstrapper.bootstrap() - - response = TestClient(fastapi_app).get(prometheus_metrics_path) - assert response.status_code == status.HTTP_200_OK - assert response.text +def test_fastapi_bootstrapper_not_ready() -> None: + import_checker.is_fastapi_installed = False + try: + with pytest.raises(RuntimeError, match="fastapi is not installed"): + FastAPIBootstrapper(bootstrap_config=FastAPIConfig()) + finally: + import_checker.is_fastapi_installed = True diff --git a/tests/test_faststream_bootstrap.py b/tests/test_faststream_bootstrap.py index 16d8ff2..1aadf7c 100644 --- a/tests/test_faststream_bootstrap.py +++ b/tests/test_faststream_bootstrap.py @@ -7,7 +7,7 @@ from starlette import status from starlette.testclient import TestClient -from lite_bootstrap import FastStreamBootstrapper, FastStreamConfig +from lite_bootstrap import FastStreamBootstrapper, FastStreamConfig, import_checker from tests.conftest import CustomInstrumentor @@ -20,8 +20,8 @@ def broker() -> RedisBroker: async def test_faststream_bootstrap(broker: RedisBroker) -> None: - prometheus_metrics_path = "/test-metrics-path" - health_check_path = "/custom-health-check-path" + prometheus_metrics_path = "/custom-metrics/" + health_check_path = "/custom-health/" bootstrapper = FastStreamBootstrapper( bootstrap_config=FastStreamConfig( broker=broker, @@ -41,17 +41,18 @@ async def test_faststream_bootstrap(broker: RedisBroker) -> None: ), ) application = bootstrapper.bootstrap() - logger.info("testing logging", key="value") test_client = TestClient(app=application) - async with TestRedisBroker(broker): - response = test_client.get(prometheus_metrics_path) - assert response.status_code == status.HTTP_200_OK + logger.info("testing logging", key="value") + async with TestRedisBroker(broker): response = test_client.get(health_check_path) assert response.status_code == status.HTTP_200_OK assert response.json() == {"health_status": True, "service_name": "microservice", "service_version": "2.0.0"} + response = test_client.get(prometheus_metrics_path) + assert response.status_code == status.HTTP_200_OK + async def test_faststream_bootstrap_health_check_wo_broker() -> None: health_check_path = "/custom-health-check-path" @@ -64,15 +65,27 @@ async def test_faststream_bootstrap_health_check_wo_broker() -> None: opentelemetry_endpoint="otl", opentelemetry_instrumentors=[CustomInstrumentor()], opentelemetry_span_exporter=ConsoleSpanExporter(), + opentelemetry_middleware_cls=RedisTelemetryMiddleware, + prometheus_middleware_cls=RedisPrometheusMiddleware, sentry_dsn="https://testdsn@localhost/1", health_checks_path=health_check_path, logging_buffer_capacity=0, ), ) application = bootstrapper.bootstrap() - logger.info("testing logging", key="value") test_client = TestClient(app=application) response = test_client.get(health_check_path) assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR assert response.text == "Service is unhealthy" + + +def test_faststream_bootstrapper_not_ready() -> None: + import_checker.is_faststream_installed = False + try: + with pytest.raises(RuntimeError, match="faststream is not installed"): + FastStreamBootstrapper( + bootstrap_config=FastStreamConfig(), + ) + finally: + import_checker.is_faststream_installed = True diff --git a/tests/test_free_bootstrap.py b/tests/test_free_bootstrap.py index 2e58b5b..13c3fde 100644 --- a/tests/test_free_bootstrap.py +++ b/tests/test_free_bootstrap.py @@ -1,3 +1,4 @@ +import pytest import structlog from opentelemetry.sdk.trace.export import ConsoleSpanExporter @@ -11,6 +12,7 @@ def test_free_bootstrap() -> None: bootstrapper = FreeBootstrapper( bootstrap_config=FreeBootstrapperConfig( + service_debug=False, opentelemetry_endpoint="otl", opentelemetry_instrumentors=[CustomInstrumentor()], opentelemetry_span_exporter=ConsoleSpanExporter(), @@ -23,3 +25,14 @@ def test_free_bootstrap() -> None: logger.info("testing logging", key="value") finally: bootstrapper.teardown() + + +def test_free_bootstrap_logging_not_ready() -> None: + with pytest.warns(UserWarning, match="service_debug is True or structlog is not installed"): + FreeBootstrapper( + bootstrap_config=FreeBootstrapperConfig( + service_debug=True, + opentelemetry_endpoint="otl", + sentry_dsn="https://testdsn@localhost/1", + ), + ) diff --git a/tests/test_litestar_bootstrap.py b/tests/test_litestar_bootstrap.py index 379f3f8..2922318 100644 --- a/tests/test_litestar_bootstrap.py +++ b/tests/test_litestar_bootstrap.py @@ -1,9 +1,10 @@ +import pytest import structlog from litestar import status_codes from litestar.testing import TestClient from opentelemetry.sdk.trace.export import ConsoleSpanExporter -from lite_bootstrap import LitestarBootstrapper, LitestarConfig +from lite_bootstrap import LitestarBootstrapper, LitestarConfig, import_checker from tests.conftest import CustomInstrumentor @@ -11,43 +12,50 @@ def test_litestar_bootstrap() -> None: + health_checks_path = "/custom-health/" + prometheus_metrics_path = "/custom-metrics/" bootstrapper = LitestarBootstrapper( bootstrap_config=LitestarConfig( service_name="microservice", service_version="2.0.0", service_environment="test", + service_debug=False, opentelemetry_endpoint="otl", opentelemetry_instrumentors=[CustomInstrumentor()], opentelemetry_span_exporter=ConsoleSpanExporter(), + prometheus_metrics_path=prometheus_metrics_path, sentry_dsn="https://testdsn@localhost/1", - health_checks_path="/health/", + health_checks_path=health_checks_path, logging_buffer_capacity=0, ), ) application = bootstrapper.bootstrap() - logger.info("testing logging", key="value") try: + logger.info("testing logging", key="value") + with TestClient(app=application) as test_client: - response = test_client.get("/health/") + response = test_client.get(health_checks_path) assert response.status_code == status_codes.HTTP_200_OK assert response.json() == { "health_status": True, "service_name": "microservice", "service_version": "2.0.0", } + + response = test_client.get(prometheus_metrics_path) + assert response.status_code == status_codes.HTTP_200_OK + assert response.text finally: bootstrapper.teardown() -def test_litestar_prometheus_bootstrap() -> None: - prometheus_metrics_path = "/custom-metrics-path" - bootstrapper = LitestarBootstrapper( - bootstrap_config=LitestarConfig(prometheus_metrics_path=prometheus_metrics_path), - ) - application = bootstrapper.bootstrap() - - with TestClient(app=application) as test_client: - response = test_client.get(prometheus_metrics_path) - assert response.status_code == status_codes.HTTP_200_OK - assert response.text +def test_litestar_bootstrapper_not_ready() -> None: + import_checker.is_litestar_installed = False + try: + with pytest.raises(RuntimeError, match="litestar is not installed"): + LitestarBootstrapper( + bootstrap_config=LitestarConfig(), + ) + finally: + import_checker.is_litestar_installed = True