Skip to content

Commit

Permalink
Merge pull request #11 from community-of-python/feature/add-offline-d…
Browse files Browse the repository at this point in the history
…ocs-and-cors

Add offline swagger and CORS support
  • Loading branch information
insani7y authored Aug 6, 2024
2 parents db2a0a0 + b3139d2 commit 9326643
Show file tree
Hide file tree
Showing 21 changed files with 572 additions and 208 deletions.
86 changes: 75 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,15 @@ from your_application.settings import settings
application: litestar.Litestar = LitestarBootstrapper(settings).bootstrap()
```

With <b>microbootstrap</b>, you get an application with built-in support for:
Only `litestar` is supported yet.
With <b>microbootstrap</b>, you get an application with lightweight built-in support for:

- `sentry`
- `prometheus`
- `opentelemetry`
- `logging`

<b>microbootstrap</b> supports only `litestar` framework for now.
- `cors`
- `swagger` - additional offline version support

Interested? Let's jump right into it ⚡

Expand All @@ -58,7 +59,7 @@ $ poetry add microbootstrap -E litestar
pip:

```bash
$ poetry add microbootstrap[litestar]
$ pip install microbootstrap[litestar]
```

## Quickstart
Expand Down Expand Up @@ -163,6 +164,8 @@ Currently, these instruments are already supported for bootstrapping:
- `prometheus`
- `opentelemetry`
- `logging`
- `cors`
- `swagger`

Let's make it clear, what it takes to bootstrap them.

Expand Down Expand Up @@ -247,7 +250,7 @@ class YourSettings(BaseBootstrapSettings):

All these settings are then passed to [opentelemetry](https://opentelemetry.io/), completing your Opentelemetry integration.

## Logging
### Logging

<b>microbootstrap</b> provides in-memory json logging using [structlog](https://pypi.org/project/structlog/).
To learn more about in-memory logging, check out [MemoryHandler](https://docs.python.org/3/library/logging.handlers.html#memoryhandler)
Expand Down Expand Up @@ -280,6 +283,59 @@ Parameters description:
- `logging_extra_processors` - set additional structlog processors if you have some.
- `logging_exclude_endpoints` - remove logging on certain endpoints.

### Swagger

```python
from microbootstrap.bootstrappers.litestar import BaseBootstrapSettings


class YourSettings(BaseBootstrapSettings):
service_name: str = "micro-service"
service_description: str = "Micro service description"
service_version: str = "1.0.0"
service_static_path: str = "/static"

swagger_path: str = "/docs"
swagger_offline_docs: bool = False
swagger_extra_params: dict[str, Any] = {}
```

Parameters description:

- `service_environment` - will be displayed in docs.
- `service_name` - will be displayed in docs.
- `service_description` - will be displayed in docs.
- `service_static_path` - set additional structlog processors if you have some.
- `swagger_path` - path of the docs.
- `swagger_offline_docs` - makes swagger js bundles access offline, because service starts to host via static.
- `swagger_extra_params` - additional params to pass into openapi config.

### Cors

```python
from microbootstrap.bootstrappers.litestar import BaseBootstrapSettings


class YourSettings(BaseBootstrapSettings):
cors_allowed_origins: list[str] = pydantic.Field(default_factory=list)
cors_allowed_methods: list[str] = pydantic.Field(default_factory=list)
cors_allowed_headers: list[str] = pydantic.Field(default_factory=list)
cors_exposed_headers: list[str] = pydantic.Field(default_factory=list)
cors_allowed_credentials: bool = False
cors_allowed_origin_regex: str | None = None
cors_max_age: int = 600
```

Parameters description:

- `cors_allowed_origins` - list of origins that are allowed.
- `cors_allowed_methods` - list of allowed HTTP methods.
- `cors_allowed_headers` - list of allowed headers.
- `cors_exposed_headers` - list of headers that are exposed via the 'Access-Control-Expose-Headers' header.
- `cors_allowed_credentials` - boolean dictating whether or not to set the 'Access-Control-Allow-Credentials' header.
- `cors_allowed_origin_regex` - regex to match origins against.
- `cors_max_age` - response caching TTL in seconds, defaults to 600.

## Configuration

Despite settings being pretty convenient mechanism, it's not always possible to store everything in settings.
Expand All @@ -294,6 +350,8 @@ To configure instruemt manually, you have to import one of available configs fro
- `OpentelemetryConfig`
- `PrometheusConfig`
- `LoggingConfig`
- `SwaggerConfig`
- `CorsConfig`

And pass them into `.configure_instrument` or `.configure_instruments` bootstrapper method.

Expand Down Expand Up @@ -428,8 +486,8 @@ from microbootstrap.instruments.base import Instrument
class MyInstrument(Instrument[MyInstrumentConfig]):
def write_status(self, console_writer: ConsoleWriter) -> None:
pass
instrument_name: str
ready_condition: str
def is_ready(self) -> bool:
pass
Expand All @@ -447,10 +505,16 @@ class MyInstrument(Instrument[MyInstrumentConfig]):
And now you can define behaviour of your instrument
- `write_status` - writes status to console, indicating, is instrument bootstrapped.
- `is_ready` - defines ready for bootstrapping state of instrument, based on it's config values.
- `teardown` - graceful shutdown for instrument during application shutdown.
- `bootstrap` - main instrument's logic.
Attributes:
- `instrument_name` - Will be displayed in your console during bootstrap.
- `ready_condition` - Will be displayed in your console during bootstrap if instument is not ready.
Methods:
- `is_ready` - defines ready for bootstrapping state of instrument, based on it's config values. Required.
- `teardown` - graceful shutdown for instrument during application shutdown. Not required.
- `bootstrap` - main instrument's logic. Not required.
When you have a carcass of instrument, you can adapt it for every framework existing.
Let's adapt it for litestar for example
Expand Down
4 changes: 4 additions & 0 deletions microbootstrap/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from microbootstrap.instruments.cors_instrument import CorsConfig
from microbootstrap.instruments.logging_instrument import LoggingConfig
from microbootstrap.instruments.opentelemetry_instrument import OpentelemetryConfig
from microbootstrap.instruments.prometheus_instrument import PrometheusConfig
from microbootstrap.instruments.sentry_instrument import SentryConfig
from microbootstrap.instruments.swagger_instrument import SwaggerConfig
from microbootstrap.settings import LitestarSettings


Expand All @@ -12,4 +14,6 @@
"LoggingConfig",
"LitestarBootstrapper",
"LitestarSettings",
"CorsConfig",
"SwaggerConfig",
)
53 changes: 52 additions & 1 deletion microbootstrap/bootstrappers/litestar.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@
import litestar.types
import sentry_sdk
import typing_extensions
from litestar import status_codes
from litestar import openapi, status_codes
from litestar.config.app import AppConfig as LitestarConfig
from litestar.config.cors import CORSConfig as LitestarCorsConfig
from litestar.contrib.opentelemetry.config import OpenTelemetryConfig as LitestarOpentelemetryConfig
from litestar.contrib.prometheus import PrometheusConfig as LitestarPrometheusConfig
from litestar.contrib.prometheus import PrometheusController
from litestar.exceptions.http_exceptions import HTTPException
from litestar_offline_docs import generate_static_files_config

from microbootstrap.bootstrappers.base import ApplicationBootstrapper
from microbootstrap.instruments.cors_instrument import CorsInstrument
from microbootstrap.instruments.logging_instrument import LoggingInstrument
from microbootstrap.instruments.opentelemetry_instrument import OpentelemetryInstrument
from microbootstrap.instruments.prometheus_instrument import PrometheusInstrument
from microbootstrap.instruments.sentry_instrument import SentryInstrument
from microbootstrap.instruments.swagger_instrument import SwaggerInstrument
from microbootstrap.middlewares.litestar import build_litestar_logging_middleware
from microbootstrap.settings import LitestarSettings

Expand Down Expand Up @@ -52,6 +56,53 @@ def bootstrap_before(self) -> dict[str, typing.Any]:
return {"after_exception": [self.sentry_exception_catcher_hook]}


@LitestarBootstrapper.use_instrument()
class LitestarSwaggerInstrument(SwaggerInstrument):
def bootstrap_before(self) -> dict[str, typing.Any]:
class LitestarOpenAPIController(openapi.OpenAPIController):
path = self.instrument_config.swagger_path
if self.instrument_config.swagger_offline_docs:
swagger_ui_standalone_preset_js_url = (
f"{self.instrument_config.service_static_path}/swagger-ui-standalone-preset.js"
)
swagger_bundle_path: str = f"{self.instrument_config.service_static_path}/swagger-ui-bundle.js"
swagger_css_url: str = f"{self.instrument_config.service_static_path}/swagger-ui.css"

openapi_config: typing.Final = openapi.OpenAPIConfig(
title=self.instrument_config.service_name,
version=self.instrument_config.service_version,
description=self.instrument_config.service_description,
openapi_controller=LitestarOpenAPIController,
**self.instrument_config.swagger_extra_params,
)

bootstrap_result = {}
if self.instrument_config.swagger_offline_docs:
bootstrap_result["static_files_config"] = [
generate_static_files_config(static_files_handler_path=self.instrument_config.service_static_path),
]
return {
**bootstrap_result,
"openapi_config": openapi_config,
}


@LitestarBootstrapper.use_instrument()
class LitestarCorsInstrument(CorsInstrument):
def bootstrap_before(self) -> dict[str, typing.Any]:
return {
"cors_config": LitestarCorsConfig(
allow_origins=self.instrument_config.cors_allowed_origins,
allow_methods=self.instrument_config.cors_allowed_methods,
allow_headers=self.instrument_config.cors_allowed_headers,
allow_credentials=self.instrument_config.cors_allowed_credentials,
allow_origin_regex=self.instrument_config.cors_allowed_origin_regex,
expose_headers=self.instrument_config.cors_exposed_headers,
max_age=self.instrument_config.cors_max_age,
),
}


@LitestarBootstrapper.use_instrument()
class LitetstarOpentelemetryInstrument(OpentelemetryInstrument):
def bootstrap_before(self) -> dict[str, typing.Any]:
Expand Down
7 changes: 0 additions & 7 deletions microbootstrap/instruments/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +0,0 @@
from microbootstrap.instruments.logging_instrument import LoggingConfig
from microbootstrap.instruments.opentelemetry_instrument import OpentelemetryConfig
from microbootstrap.instruments.prometheus_instrument import PrometheusConfig
from microbootstrap.instruments.sentry_instrument import SentryConfig


__all__ = ("SentryConfig", "OpentelemetryConfig", "PrometheusConfig", "LoggingConfig")
23 changes: 13 additions & 10 deletions microbootstrap/instruments/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,34 +23,37 @@ class BaseInstrumentConfig(pydantic.BaseModel):
@dataclasses.dataclass
class Instrument(abc.ABC, typing.Generic[InstrumentConfigT]):
instrument_config: InstrumentConfigT
instrument_name: typing.ClassVar[str]
ready_condition: typing.ClassVar[str]

def configure_instrument(
self,
incoming_config: InstrumentConfigT,
) -> None:
self.instrument_config = merge_pydantic_configs(self.instrument_config, incoming_config)

@abc.abstractmethod
def write_status(self, console_writer: ConsoleWriter) -> None:
raise NotImplementedError
console_writer.write_instrument_status(
self.instrument_name,
is_enabled=self.is_ready(),
disable_reason=None if self.is_ready() else self.ready_condition,
)

@abc.abstractmethod
def is_ready(self) -> bool:
raise NotImplementedError

@abc.abstractmethod
def bootstrap(self) -> None:
raise NotImplementedError

@abc.abstractmethod
def teardown(self) -> None:
raise NotImplementedError

@classmethod
@abc.abstractmethod
def get_config_type(cls) -> type[InstrumentConfigT]:
raise NotImplementedError

def bootstrap(self) -> None:
return None

def teardown(self) -> None:
return None

def bootstrap_before(self) -> dict[str, typing.Any]:
"""Add some framework-related parameters to final bootstrap result before application creation."""
return {}
Expand Down
29 changes: 29 additions & 0 deletions microbootstrap/instruments/cors_instrument.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from __future__ import annotations

import pydantic

from microbootstrap.instruments.base import BaseInstrumentConfig, Instrument


class CorsConfig(BaseInstrumentConfig):
cors_allowed_origins: list[str] = pydantic.Field(default_factory=list)
cors_allowed_methods: list[str] = pydantic.Field(default_factory=list)
cors_allowed_headers: list[str] = pydantic.Field(default_factory=list)
cors_exposed_headers: list[str] = pydantic.Field(default_factory=list)
cors_allowed_credentials: bool = False
cors_allowed_origin_regex: str | None = None
cors_max_age: int = 600


class CorsInstrument(Instrument[CorsConfig]):
instrument_name = "Cors"
ready_condition = "Provide allowed origins or regex"

def is_ready(self) -> bool:
return bool(self.instrument_config.cors_allowed_origins) or bool(
self.instrument_config.cors_allowed_origin_regex,
)

@classmethod
def get_config_type(cls) -> type[CorsConfig]:
return CorsConfig
21 changes: 6 additions & 15 deletions microbootstrap/instruments/logging_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
import litestar
from structlog.typing import EventDict, WrappedLogger

from microbootstrap.console_writer import ConsoleWriter


ScopeType = typing.MutableMapping[str, typing.Any]

Expand Down Expand Up @@ -120,26 +118,19 @@ def __call__(self, *args: typing.Any) -> logging.Logger: # noqa: ANN401
class LoggingConfig(BaseInstrumentConfig):
service_debug: bool = True

logging_log_level: int = pydantic.Field(default=logging.INFO)
logging_flush_level: int = pydantic.Field(default=logging.ERROR)
logging_buffer_capacity: int = pydantic.Field(default=10)
logging_log_level: int = logging.INFO
logging_flush_level: int = logging.ERROR
logging_buffer_capacity: int = 10
logging_extra_processors: list[typing.Any] = pydantic.Field(default_factory=list)
logging_unset_handlers: list[str] = pydantic.Field(
default_factory=lambda: ["uvicorn", "uvicorn.access"],
)
logging_exclude_endpoints: list[str] = pydantic.Field(default_factory=lambda: ["/health"])
logging_exclude_endpoints: list[str] = pydantic.Field(default_factory=list)


class LoggingInstrument(Instrument[LoggingConfig]):
def write_status(self, console_writer: ConsoleWriter) -> None:
if self.is_ready():
console_writer.write_instrument_status("Logging", is_enabled=True)
else:
console_writer.write_instrument_status(
"Logging",
is_enabled=False,
disable_reason="Works only in non-debug mode",
)
instrument_name = "Logging"
ready_condition = "Works only in non-debug mode"

def is_ready(self) -> bool:
return not self.instrument_config.service_debug
Expand Down
Loading

0 comments on commit 9326643

Please sign in to comment.