Skip to content

Commit 1ae24f9

Browse files
authored
Merge pull request #31 from community-of-python/feature/health-checks-support
Add health checks support
2 parents ad3fcb8 + 93ac0bb commit 1ae24f9

File tree

10 files changed

+199
-8
lines changed

10 files changed

+199
-8
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ With <b>microbootstrap</b>, you receive an application with lightweight built-in
4141
- `logging`
4242
- `cors`
4343
- `swagger` - with additional offline version support
44+
- `health checks` - with additional offline version support
4445

4546
Those instruments can be bootstrapped for:
4647

@@ -62,6 +63,7 @@ Interested? Let's dive right in ⚡
6263
- [Logging](#logging)
6364
- [CORS](#cors)
6465
- [Swagger](#swagger)
66+
- [Health checks](#health-checks)
6567
- [Configuration](#configuration)
6668
- [Instruments configuration](#instruments-configuration)
6769
- [Application configuration](#application-configuration)
@@ -405,6 +407,27 @@ Parameter descriptions:
405407
- `swagger_offline_docs` - A boolean value that, when set to True, allows the Swagger JS bundles to be accessed offline. This is because the service starts to host via static.
406408
- `swagger_extra_params` - Additional parameters to pass into the OpenAPI configuration.
407409

410+
### Health checks
411+
412+
```python
413+
from microbootstrap.settings import BaseServiceSettings
414+
415+
416+
class YourSettings(BaseServiceSettings):
417+
service_name: str = "micro-service"
418+
service_version: str = "1.0.0"
419+
420+
health_checks_enabled: bool = True
421+
health_checks_path: str = "/health/"
422+
```
423+
424+
Parameter descriptions:
425+
426+
- `service_name` - Will be displayed in health check response.
427+
- `service_version` - Will be displayed in health check response.
428+
- `health_checks_enabled` - Must be True to enable health checks.
429+
- `health_checks_path` - Path for health check handler.
430+
408431
## Configuration
409432

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

microbootstrap/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from microbootstrap.instruments.cors_instrument import CorsConfig
2+
from microbootstrap.instruments.health_checks_instrument import HealthChecksConfig
23
from microbootstrap.instruments.logging_instrument import LoggingConfig
34
from microbootstrap.instruments.opentelemetry_instrument import OpentelemetryConfig
45
from microbootstrap.instruments.prometheus_instrument import FastApiPrometheusConfig, LitestarPrometheusConfig
@@ -13,9 +14,9 @@
1314
"FastApiPrometheusConfig",
1415
"LitestarPrometheusConfig",
1516
"LoggingConfig",
16-
"LitestarBootstrapper",
1717
"LitestarSettings",
1818
"FastApiSettings",
1919
"CorsConfig",
2020
"SwaggerConfig",
21+
"HealthChecksConfig",
2122
)

microbootstrap/bootstrappers/fastapi.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
import typing_extensions
55
from fastapi.middleware.cors import CORSMiddleware
66
from fastapi_offline_docs import enable_offline_docs
7+
from health_checks.fastapi_healthcheck import build_fastapi_health_check_router
78
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
89
from prometheus_fastapi_instrumentator import Instrumentator
910
from sentry_sdk.integrations.fastapi import FastApiIntegration
1011

1112
from microbootstrap.bootstrappers.base import ApplicationBootstrapper
1213
from microbootstrap.config.fastapi import FastApiConfig
1314
from microbootstrap.instruments.cors_instrument import CorsInstrument
15+
from microbootstrap.instruments.health_checks_instrument import HealthChecksInstrument
1416
from microbootstrap.instruments.logging_instrument import LoggingInstrument
1517
from microbootstrap.instruments.opentelemetry_instrument import OpentelemetryInstrument
1618
from microbootstrap.instruments.prometheus_instrument import FastApiPrometheusConfig, PrometheusInstrument
@@ -114,3 +116,12 @@ def bootstrap_after(self, application: fastapi.FastAPI) -> fastapi.FastAPI:
114116
@classmethod
115117
def get_config_type(cls) -> type[FastApiPrometheusConfig]:
116118
return FastApiPrometheusConfig
119+
120+
121+
@FastApiBootstrapper.use_instrument()
122+
class FastApiHealthChecksInstrument(HealthChecksInstrument):
123+
def bootstrap_after(self, application: fastapi.FastAPI) -> fastapi.FastAPI:
124+
application.include_router(
125+
build_fastapi_health_check_router(self.health_check, self.instrument_config.health_checks_path),
126+
)
127+
return application

microbootstrap/bootstrappers/litestar.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import litestar
55
import litestar.types
66
import typing_extensions
7+
from health_checks.litestar_healthcheck import build_litestar_health_check_router
78
from litestar import openapi
89
from litestar.config.cors import CORSConfig as LitestarCorsConfig
910
from litestar.contrib.opentelemetry.config import OpenTelemetryConfig as LitestarOpentelemetryConfig
@@ -15,6 +16,7 @@
1516
from microbootstrap.bootstrappers.base import ApplicationBootstrapper
1617
from microbootstrap.config.litestar import LitestarConfig
1718
from microbootstrap.instruments.cors_instrument import CorsInstrument
19+
from microbootstrap.instruments.health_checks_instrument import HealthChecksInstrument
1820
from microbootstrap.instruments.logging_instrument import LoggingInstrument
1921
from microbootstrap.instruments.opentelemetry_instrument import OpentelemetryInstrument
2022
from microbootstrap.instruments.prometheus_instrument import LitestarPrometheusConfig, PrometheusInstrument
@@ -75,7 +77,7 @@ def bootstrap_before(self) -> dict[str, typing.Any]:
7577
} | self.instrument_config.swagger_extra_params
7678

7779
bootstrap_result: typing.Final[dict[str, typing.Any]] = {
78-
"openapi_config": openapi.OpenAPIConfig(**all_swagger_params)
80+
"openapi_config": openapi.OpenAPIConfig(**all_swagger_params),
7981
}
8082
if self.instrument_config.swagger_offline_docs:
8183
bootstrap_result["static_files_config"] = [
@@ -137,3 +139,16 @@ class LitestarPrometheusController(PrometheusController):
137139
@classmethod
138140
def get_config_type(cls) -> type[LitestarPrometheusConfig]:
139141
return LitestarPrometheusConfig
142+
143+
144+
@LitestarBootstrapper.use_instrument()
145+
class LitestarHealthChecksInstrument(HealthChecksInstrument):
146+
def bootstrap_before(self) -> dict[str, typing.Any]:
147+
return {
148+
"route_handlers": [
149+
build_litestar_health_check_router(
150+
self.health_check,
151+
self.instrument_config.health_checks_path,
152+
),
153+
],
154+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from __future__ import annotations
2+
3+
from health_checks.http_based import DefaultHTTPHealthCheck
4+
5+
from microbootstrap.instruments.base import BaseInstrumentConfig, Instrument
6+
7+
8+
class HealthChecksConfig(BaseInstrumentConfig):
9+
service_name: str = "micro-service"
10+
service_version: str = "1.0.0"
11+
12+
health_checks_enabled: bool = True
13+
health_checks_path: str = "/health/"
14+
15+
16+
class HealthChecksInstrument(Instrument[HealthChecksConfig]):
17+
instrument_name = "Health checks"
18+
ready_condition = "Set health_checks_enabled to True"
19+
20+
def bootstrap(self) -> None:
21+
self.health_check = DefaultHTTPHealthCheck(
22+
service_version=self.instrument_config.service_version,
23+
service_name=self.instrument_config.service_name,
24+
)
25+
26+
def is_ready(self) -> bool:
27+
return self.instrument_config.health_checks_enabled
28+
29+
@classmethod
30+
def get_config_type(cls) -> type[HealthChecksConfig]:
31+
return HealthChecksConfig

microbootstrap/settings.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ class BaseServiceSettings(
2828
service_debug: bool = False
2929
service_environment: str | None = None
3030
service_name: str = pydantic.Field(
31-
"micro-service", validation_alias=pydantic.AliasChoices("SERVICE_NAME", f"{ENV_PREFIX}SERVICE_NAME")
31+
"micro-service",
32+
validation_alias=pydantic.AliasChoices("SERVICE_NAME", f"{ENV_PREFIX}SERVICE_NAME"),
3233
)
3334
service_description: str = "Micro service description"
3435
service_version: str = pydantic.Field(
35-
"1.0.0", validation_alias=pydantic.AliasChoices("CI_COMMIT_TAG", f"{ENV_PREFIX}SERVICE_VERSION")
36+
"1.0.0",
37+
validation_alias=pydantic.AliasChoices("CI_COMMIT_TAG", f"{ENV_PREFIX}SERVICE_VERSION"),
3638
)
3739
service_static_path: str = "/static"
3840

@@ -50,24 +52,24 @@ class BaseServiceSettings(
5052

5153

5254
class LitestarSettings(
53-
BaseServiceSettings,
5455
LoggingConfig,
5556
OpentelemetryConfig,
5657
SentryConfig,
5758
LitestarPrometheusConfig,
5859
SwaggerConfig,
5960
CorsConfig,
61+
BaseServiceSettings,
6062
):
6163
"""Settings for a litestar botstrap."""
6264

6365

6466
class FastApiSettings(
65-
BaseServiceSettings,
6667
LoggingConfig,
6768
OpentelemetryConfig,
6869
SentryConfig,
6970
FastApiPrometheusConfig,
7071
SwaggerConfig,
7172
CorsConfig,
73+
BaseServiceSettings,
7274
):
7375
"""Settings for a fastapi botstrap."""

poetry.lock

Lines changed: 32 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ fastapi = { version = "^0.111.0", optional = true }
6666
prometheus-fastapi-instrumentator = { version = "^6.1.0", optional = true }
6767
opentelemetry-instrumentation-fastapi = { version = "^0.46b0", optional = true }
6868
fastapi-offline-docs = { version = "^1.0.1", optional = true }
69+
health-checks = "^1.0.0"
6970

7071
[tool.poetry.group.dev.dependencies]
7172
pytest = "^8.2.2"

tests/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
)
1717
from microbootstrap.console_writer import ConsoleWriter
1818
from microbootstrap.instruments.cors_instrument import CorsConfig
19+
from microbootstrap.instruments.health_checks_instrument import HealthChecksConfig
1920
from microbootstrap.instruments.prometheus_instrument import BasePrometheusConfig
2021
from microbootstrap.instruments.swagger_instrument import SwaggerConfig
2122
from microbootstrap.settings import BaseServiceSettings
@@ -69,6 +70,11 @@ def minimal_cors_config() -> CorsConfig:
6970
return CorsConfig(cors_allowed_origins=["*"])
7071

7172

73+
@pytest.fixture
74+
def minimal_health_checks_config() -> HealthChecksConfig:
75+
return HealthChecksConfig()
76+
77+
7278
@pytest.fixture
7379
def minimal_opentelemetry_config() -> OpentelemetryConfig:
7480
return OpentelemetryConfig(
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import typing
2+
3+
import fastapi
4+
import litestar
5+
from httpx import AsyncClient
6+
from litestar import status_codes
7+
from litestar.testing import AsyncTestClient
8+
9+
from microbootstrap.bootstrappers.fastapi import FastApiHealthChecksInstrument
10+
from microbootstrap.bootstrappers.litestar import LitestarHealthChecksInstrument
11+
from microbootstrap.instruments.health_checks_instrument import (
12+
HealthChecksConfig,
13+
HealthChecksInstrument,
14+
)
15+
16+
17+
def test_health_checks_is_ready(minimal_health_checks_config: HealthChecksConfig) -> None:
18+
health_checks_instrument: typing.Final = HealthChecksInstrument(minimal_health_checks_config)
19+
assert health_checks_instrument.is_ready()
20+
21+
22+
def test_health_checks_bootstrap_is_not_ready(minimal_health_checks_config: HealthChecksConfig) -> None:
23+
minimal_health_checks_config.health_checks_enabled = False
24+
health_checks_instrument: typing.Final = HealthChecksInstrument(minimal_health_checks_config)
25+
assert not health_checks_instrument.is_ready()
26+
27+
28+
def test_health_checks_bootstrap_after(
29+
default_litestar_app: litestar.Litestar,
30+
minimal_health_checks_config: HealthChecksConfig,
31+
) -> None:
32+
health_checks_instrument: typing.Final = HealthChecksInstrument(minimal_health_checks_config)
33+
assert health_checks_instrument.bootstrap_after(default_litestar_app) == default_litestar_app
34+
35+
36+
def test_health_checks_teardown(
37+
minimal_health_checks_config: HealthChecksConfig,
38+
) -> None:
39+
health_checks_instrument: typing.Final = HealthChecksInstrument(minimal_health_checks_config)
40+
assert health_checks_instrument.teardown() is None # type: ignore[func-returns-value]
41+
42+
43+
async def test_litestar_health_checks_bootstrap() -> None:
44+
test_health_checks_path: typing.Final = "/test-path/"
45+
heatlh_checks_config: typing.Final = HealthChecksConfig(health_checks_path=test_health_checks_path)
46+
health_checks_instrument: typing.Final = LitestarHealthChecksInstrument(heatlh_checks_config)
47+
48+
health_checks_instrument.bootstrap()
49+
litestar_application: typing.Final = litestar.Litestar(
50+
**health_checks_instrument.bootstrap_before(),
51+
)
52+
53+
async with AsyncTestClient(app=litestar_application) as async_client:
54+
response = await async_client.get(heatlh_checks_config.health_checks_path)
55+
assert response.status_code == status_codes.HTTP_200_OK
56+
57+
58+
async def test_fastapi_health_checks_bootstrap() -> None:
59+
test_health_checks_path: typing.Final = "/test-path/"
60+
heatlh_checks_config: typing.Final = HealthChecksConfig(health_checks_path=test_health_checks_path)
61+
health_checks_instrument: typing.Final = FastApiHealthChecksInstrument(heatlh_checks_config)
62+
63+
health_checks_instrument.bootstrap()
64+
fastapi_application = fastapi.FastAPI(
65+
**health_checks_instrument.bootstrap_before(),
66+
)
67+
fastapi_application = health_checks_instrument.bootstrap_after(fastapi_application)
68+
69+
async with AsyncClient(app=fastapi_application, base_url="http://testserver") as async_client:
70+
response = await async_client.get(heatlh_checks_config.health_checks_path)
71+
assert response.status_code == status_codes.HTTP_200_OK

0 commit comments

Comments
 (0)