community-of-python / microbootstrap

Bootstrap your microservices in a second!
27 stars 2 forks source link


microbootstrap assists you in creating applications with all the necessary instruments already set up.

# settings.py
from microbootstrap import LitestarSettings

class YourSettings(LitestarSettings):
    ...  # Your settings are stored here

settings = YourSettings()

# application.py
import litestar
from microbootstrap.bootstrappers.litestar import LitestarBootstrapper

from your_application.settings import settings

# Use the Litestar application!
application: litestar.Litestar = LitestarBootstrapper(settings).bootstrap()

With microbootstrap, you receive an application with lightweight built-in support for:

Those instruments can be bootstrapped for:

Interested? Let's dive right in ⚡

Table of Contents

Installation

You can install the package using either pip or poetry. Also, you can specify extras during installation for concrete framework:

For poetry:

$ poetry add microbootstrap -E fastapi

For pip:

$ pip install microbootstrap[fastapi]

Quickstart

To configure your application, you can use the settings object.

from microbootstrap import LitestarSettings

class YourSettings(LitestarSettings):
    # General settings
    service_debug: bool = False
    service_name: str = "my-awesome-service"

    # Sentry settings
    sentry_dsn: str = "your-sentry-dsn"

    # Prometheus settings
    prometheus_metrics_path: str = "/my-path"

    # Opentelemetry settings
    opentelemetry_container_name: str = "your-container"
    opentelemetry_endpoint: str = "/opentelemetry-endpoint"

settings = YourSettings()

Next, use the Bootstrapper object to create an application based on your settings.

import litestar
from microbootstrap.bootstrappers.litestar import LitestarBootstrapper

application: litestar.Litestar = LitestarBootstrapper(settings).bootstrap()

This approach will provide you with an application that has all the essential instruments already set up for you.

Settings

The settings object is the core of microbootstrap.

All framework-related settings inherit from the BaseServiceSettings object. BaseServiceSettings defines parameters for the service and various instruments.

However, the number of parameters is not confined to those defined in BaseServiceSettings. You can add as many as you need.

These parameters can be sourced from your environment. By default, no prefix is added to these parameters.

Example:

class YourSettings(BaseServiceSettings):
    service_debug: bool = True
    service_name: str = "micro-service"

    your_awesome_parameter: str = "really awesome"

    ... # Other settings here

To source your_awesome_parameter from the environment, set the environment variable named YOUR_AWESOME_PARAMETER.

If you prefer to use a prefix when sourcing parameters, set the ENVIRONMENT_PREFIX environment variable in advance.

Example:

$ export ENVIRONMENT_PREFIX=YOUR_PREFIX_

Then the settings object will attempt to source the variable named YOUR_PREFIX_YOUR_AWESOME_PARAMETER.

Service settings

Each settings object for every framework includes service parameters that can be utilized by various instruments.

You can configure them manually, or set the corresponding environment variables and let microbootstrap to source them automatically.

from microbootstrap.settings import BaseServiceSettings

class ServiceSettings(BaseServiceSettings):
    service_debug: bool = True
    service_environment: str | None = None
    service_name: str = "micro-service"
    service_description: str = "Micro service description"
    service_version: str = "1.0.0"

    ... # Other settings here

Instruments

At present, the following instruments are supported for bootstrapping:

Let's clarify the process required to bootstrap these instruments.

Sentry

To bootstrap Sentry, you must provide at least the sentry_dsn. Additional parameters can also be supplied through the settings object.

from microbootstrap.settings import BaseServiceSettings

class YourSettings(BaseServiceSettings):
    service_environment: str | None = None

    sentry_dsn: str | None = None
    sentry_traces_sample_rate: float | None = None
    sentry_sample_rate: float = pydantic.Field(default=1.0, le=1.0, ge=0.0)
    sentry_max_breadcrumbs: int = 15
    sentry_max_value_length: int = 16384
    sentry_attach_stacktrace: bool = True
    sentry_integrations: list[Integration] = []
    sentry_additional_params: dict[str, typing.Any] = {}

    ... # Other settings here

These settings are subsequently passed to the sentry-sdk package, finalizing your Sentry integration.

Prometheus

Prometheus integration presents a challenge because the underlying libraries for FastAPI and Litestar differ significantly, making it impossible to unify them under a single interface. As a result, the Prometheus settings for FastAPI and Litestar must be configured separately.

Fastapi

To bootstrap prometheus you have to provide prometheus_metrics_path

from microbootstrap.settings import FastApiSettings

class YourFastApiSettings(FastApiSettings):
    service_name: str

    prometheus_metrics_path: str = "/metrics"
    prometheus_metrics_include_in_schema: bool = False
    prometheus_instrumentator_params: dict[str, typing.Any] = {}
    prometheus_instrument_params: dict[str, typing.Any] = {}
    prometheus_expose_params: dict[str, typing.Any] = {}

    ... # Other settings here

Parameters description:

FastApi prometheus bootstrapper uses prometheus-fastapi-instrumentator that's why there are three different dict for parameters.

Fastapi

To bootstrap prometheus you have to provide prometheus_metrics_path

from microbootstrap.settings import LitestarSettings

class YourFastApiSettings(LitestarSettings):
    service_name: str

    prometheus_metrics_path: str = "/metrics"
    prometheus_additional_params: dict[str, typing.Any] = {}

    ... # Other settings here

Parameters description:

Opentelemetry

To bootstrap Opentelemetry, you must provide several parameters:

However, additional parameters can also be supplied if needed.

from microbootstrap.settings import BaseServiceSettings
from microbootstrap.instruments.opentelemetry_instrument import OpenTelemetryInstrumentor

class YourSettings(BaseServiceSettings):
    service_name: str
    service_version: str

    opentelemetry_container_name: str | None = None
    opentelemetry_endpoint: str | None = None
    opentelemetry_namespace: str | None = None
    opentelemetry_insecure: bool = True
    opentelemetry_instrumentors: list[OpenTelemetryInstrumentor] = []
    opentelemetry_exclude_urls: list[str] = []

    ... # Other settings here

Parameters description:

These settings are subsequently passed to opentelemetry, finalizing your Opentelemetry integration.

Logging

microbootstrap provides in-memory JSON logging through the use of structlog. For more information on in-memory logging, refer to MemoryHandler.

To utilize this feature, your application must be in non-debug mode, meaning service_debug should be set to False.

import logging

from microbootstrap.settings import BaseServiceSettings

class YourSettings(BaseServiceSettings):
    service_debug: bool = False

    logging_log_level: int = logging.INFO
    logging_flush_level: int = logging.ERROR
    logging_buffer_capacity: int = 10
    logging_unset_handlers: list[str] = ["uvicorn", "uvicorn.access"]
    logging_extra_processors: list[typing.Any] = []
    logging_exclude_endpoints: list[str] = []

Parameters description:

CORS

from microbootstrap.settings import BaseServiceSettings

class YourSettings(BaseServiceSettings):
    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

Parameter descriptions:

Swagger

from microbootstrap.settings import BaseServiceSettings

class YourSettings(BaseServiceSettings):
    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] = {}

Parameter descriptions:

Health checks

from microbootstrap.settings import BaseServiceSettings

class YourSettings(BaseServiceSettings):
    service_name: str = "micro-service"
    service_version: str = "1.0.0"

    health_checks_enabled: bool = True
    health_checks_path: str = "/health/"
    health_checks_include_in_schema: bool = False

Parameter descriptions:

Configuration

While settings provide a convenient mechanism, it's not always feasible to store everything within them.

There may be cases where you need to configure a tool directly. Here's how it can be done.

Instruments configuration

To manually configure an instrument, you need to import one of the available configurations from microbootstrap:

These configurations can then be passed into the .configure_instrument or .configure_instruments bootstrapper methods.

import litestar

from microbootstrap.bootstrappers.litestar import LitestarBootstrapper
from microbootstrap import SentryConfig, OpentelemetryConfig

application: litestar.Litestar = (
    LitestarBootstrapper(settings)
    .configure_instrument(SentryConfig(sentry_dsn="https://new-dsn"))
    .configure_instrument(OpentelemetryConfig(opentelemetry_endpoint="/new-endpoint"))
    .bootstrap()
)

Alternatively,

import litestar

from microbootstrap.bootstrappers.litestar import LitestarBootstrapper
from microbootstrap import SentryConfig, OpentelemetryConfig

application: litestar.Litestar = (
    LitestarBootstrapper(settings)
    .configure_instruments(
        SentryConfig(sentry_dsn="https://examplePublicKey@o0.ingest.sentry.io/0"),
        OpentelemetryConfig(opentelemetry_endpoint="/new-endpoint")
    )
    .bootstrap()
)

Application configuration

The application can be configured in a similar manner:

import litestar

from microbootstrap.config.litestar import LitestarConfig
from microbootstrap.bootstrappers.litestar import LitestarBootstrapper
from microbootstrap import SentryConfig, OpentelemetryConfig

@litestar.get("/my-handler")
async def my_handler() -> str:
    return "Ok"

application: litestar.Litestar = (
    LitestarBootstrapper(settings)
    .configure_application(LitestarConfig(route_handlers=[my_handler]))
    .bootstrap()
)

Important

When configuring parameters with simple data types such as: str, int, float, etc., these variables overwrite previous values.

Example:

from microbootstrap import LitestarSettings, SentryConfig

class YourSettings(LitestarSettings):
    sentry_dsn: str = "https://my-sentry-dsn"

application: litestar.Litestar = (
    LitestarBootstrapper(YourSettings())
    .configure_instrument(
        SentryConfig(sentry_dsn="https://my-new-configured-sentry-dsn")
    )
    .bootstrap()
)

In this example, the application will be bootstrapped with the new https://my-new-configured-sentry-dsn Sentry DSN, replacing the old one.

However, when you configure parameters with complex data types such as: list, tuple, dict, or set, they are expanded or merged.

Example:

from microbootstrap import LitestarSettings, PrometheusConfig

class YourSettings(LitestarSettings):
    prometheus_additional_params: dict[str, Any] = {"first_value": 1}

application: litestar.Litestar = (
    LitestarBootstrapper(YourSettings())
    .configure_instrument(
        PrometheusConfig(prometheus_additional_params={"second_value": 2})
    )
    .bootstrap()
)

In this case, Prometheus will receive {"first_value": 1, "second_value": 2} inside prometheus_additional_params. This is also true for list, tuple, and set.

Using microbootstrap without a framework

When working on projects that don't use Litestar or FastAPI, you can still take advantage of monitoring and logging capabilities using InstrumentsSetupper. This class sets up Sentry, OpenTelemetry, and Logging instruments in a way that's easy to integrate with your project.

You can use InstrumentsSetupper as a context manager, like this:

from microbootstrap.instruments_setupper import InstrumentsSetupper
from microbootstrap import InstrumentsSetupperSettings

class YourSettings(InstrumentsSetupperSettings):
    ...

with InstrumentsSetupper(YourSettings()):
    while True:
        print("doing something useful")
        time.sleep(1)

Alternatively, you can use the setup() and teardown() methods instead of a context manager:

current_setupper = InstrumentsSetupper(YourSettings())
current_setupper.setup()
try:
    while True:
        print("doing something useful")
        time.sleep(1)
finally:
    current_setupper.teardown()

Like bootstrappers, you can reconfigure instruments using the configure_instrument() and configure_instruments() methods.

Advanced

If you miss some instrument, you can add your own. Essentially, Instrument is just a class with some abstractmethods. Every instrument uses some config, so that's first thing, you have to define.

from microbootstrap.instruments.base import BaseInstrumentConfig

class MyInstrumentConfig(BaseInstrumentConfig):
    your_string_parameter: str
    your_list_parameter: list

Next, you can create an instrument class that inherits from Instrument and accepts your configuration as a generic parameter.

from microbootstrap.instruments.base import Instrument

class MyInstrument(Instrument[MyInstrumentConfig]):
    instrument_name: str
    ready_condition: str

    def is_ready(self) -> bool:
        pass

    def teardown(self) -> None:
        pass

    def bootstrap(self) -> None:
        pass

    @classmethod
    def get_config_type(cls) -> type[MyInstrumentConfig]:
        return MyInstrumentConfig

Now, you can define the behavior of your instrument.

Attributes:

Methods:

Once you have the framework of the instrument, you can adapt it for any existing framework. For instance, let's adapt it for litestar.

import litestar

from microbootstrap.bootstrappers.litestar import LitestarBootstrapper

@LitestarBootstrapper.use_instrument()
class LitestarMyInstrument(MyInstrument):
    def bootstrap_before(self) -> dict[str, typing.Any]:
        pass

    def bootstrap_after(self, application: litestar.Litestar) -> dict[str, typing.Any]:
        pass

To bind the instrument to a bootstrapper, use the .use_instrument decorator.

To add extra parameters to the application, you can use:

Afterwards, you can use your instrument during the bootstrap process.

import litestar

from microbootstrap.bootstrappers.litestar import LitestarBootstrapper
from microbootstrap import SentryConfig, OpentelemetryConfig

from your_app import MyInstrumentConfig

application: litestar.Litestar = (
    LitestarBootstrapper(settings)
    .configure_instrument(
        MyInstrumentConfig(
            your_string_parameter="very-nice-parameter",
            your_list_parameter=["very-special-list"],
        )
    )
    .bootstrap()
)

Alternatively, you can fill these parameters within your main settings object.

from microbootstrap import LitestarSettings
from microbootstrap.bootstrappers.litestar import LitestarBootstrapper

from your_app import MyInstrumentConfig

class YourSettings(LitestarSettings, MyInstrumentConfig):
    your_string_parameter: str = "very-nice-parameter"
    your_list_parameter: list = ["very-special-list"]

settings = YourSettings()

application: litestar.Litestar = LitestarBootstrapper(settings).bootstrap()