austial-terminus 0.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- austial_terminus-0.1.5/.gitignore +11 -0
- austial_terminus-0.1.5/PKG-INFO +7 -0
- austial_terminus-0.1.5/pyproject.toml +26 -0
- austial_terminus-0.1.5/src/austial/terminus/__init__.py +14 -0
- austial_terminus-0.1.5/src/austial/terminus/database_health_indicator.py +26 -0
- austial_terminus-0.1.5/src/austial/terminus/health_check_service.py +42 -0
- austial_terminus-0.1.5/src/austial/terminus/health_indicator.py +28 -0
- austial_terminus-0.1.5/src/austial/terminus/memory_health_indicator.py +20 -0
- austial_terminus-0.1.5/test/__init__.py +0 -0
- austial_terminus-0.1.5/test/health_check_service_spec.py +48 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: austial-terminus
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: Health-check aggregator and indicators (memory, database) -- mirrors @nestjs/terminus (no main-monorepo equivalent).
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: austial-common
|
|
7
|
+
Requires-Dist: austial-orm
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "austial-terminus"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Health-check aggregator and indicators (memory, database) -- mirrors @nestjs/terminus (no main-monorepo equivalent)."
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"austial-common",
|
|
8
|
+
"austial-orm",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
13
|
+
build-backend = "hatchling.build"
|
|
14
|
+
|
|
15
|
+
[tool.hatch.build.targets.wheel]
|
|
16
|
+
packages = ["src/austial"]
|
|
17
|
+
|
|
18
|
+
[tool.uv.sources]
|
|
19
|
+
austial-common = { workspace = true }
|
|
20
|
+
austial-orm = { workspace = true }
|
|
21
|
+
|
|
22
|
+
[tool.hatch.version]
|
|
23
|
+
source = "vcs"
|
|
24
|
+
|
|
25
|
+
[tool.hatch.version.raw-options]
|
|
26
|
+
root = "../.."
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from austial.terminus.database_health_indicator import DatabaseHealthIndicator
|
|
2
|
+
from austial.terminus.health_check_service import HealthCheckService, HealthIndicatorFunction
|
|
3
|
+
from austial.terminus.health_indicator import HealthCheckError, HealthIndicator, HealthIndicatorResult
|
|
4
|
+
from austial.terminus.memory_health_indicator import MemoryHealthIndicator
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"DatabaseHealthIndicator",
|
|
8
|
+
"HealthCheckService",
|
|
9
|
+
"HealthIndicatorFunction",
|
|
10
|
+
"HealthCheckError",
|
|
11
|
+
"HealthIndicator",
|
|
12
|
+
"HealthIndicatorResult",
|
|
13
|
+
"MemoryHealthIndicator",
|
|
14
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""``DatabaseHealthIndicator`` -- mirrors ``@nestjs/terminus``'s ``TypeOrmHealthIndicator``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import text
|
|
8
|
+
|
|
9
|
+
from austial.common.decorators.injectable import Inject, Injectable
|
|
10
|
+
from austial.orm.database_module import DATABASE_SESSION_FACTORY
|
|
11
|
+
from austial.terminus.health_indicator import HealthCheckError, HealthIndicator, HealthIndicatorResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@Injectable()
|
|
15
|
+
class DatabaseHealthIndicator(HealthIndicator):
|
|
16
|
+
def __init__(self, session_factory: Any = Inject(DATABASE_SESSION_FACTORY)):
|
|
17
|
+
self._session_factory = session_factory
|
|
18
|
+
|
|
19
|
+
async def ping_check(self, key: str) -> HealthIndicatorResult:
|
|
20
|
+
try:
|
|
21
|
+
async with self._session_factory() as session:
|
|
22
|
+
await session.execute(text("SELECT 1"))
|
|
23
|
+
return self.get_status(key, True)
|
|
24
|
+
except Exception as exc: # noqa: BLE001
|
|
25
|
+
result = self.get_status(key, False, {"message": str(exc)})
|
|
26
|
+
raise HealthCheckError("Database health check failed", result) from exc
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""``HealthCheckService`` -- mirrors ``@nestjs/terminus``'s ``HealthCheckService``.
|
|
2
|
+
|
|
3
|
+
Usage, identical in shape to real Nest/Terminus code::
|
|
4
|
+
|
|
5
|
+
result = await self.health_check_service.check([
|
|
6
|
+
lambda: self.memory.check_heap("memory_heap", 300 * 1024 * 1024),
|
|
7
|
+
lambda: self.database.ping_check("database"),
|
|
8
|
+
])
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Awaitable, Callable
|
|
14
|
+
|
|
15
|
+
from austial.common.decorators.injectable import Injectable
|
|
16
|
+
from austial.terminus.health_indicator import HealthCheckError, HealthIndicatorResult
|
|
17
|
+
|
|
18
|
+
HealthIndicatorFunction = Callable[[], Awaitable[HealthIndicatorResult]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@Injectable()
|
|
22
|
+
class HealthCheckService:
|
|
23
|
+
async def check(self, indicators: list[HealthIndicatorFunction]) -> dict:
|
|
24
|
+
info: HealthIndicatorResult = {}
|
|
25
|
+
error: HealthIndicatorResult = {}
|
|
26
|
+
details: HealthIndicatorResult = {}
|
|
27
|
+
|
|
28
|
+
for indicator in indicators:
|
|
29
|
+
try:
|
|
30
|
+
result = await indicator()
|
|
31
|
+
info.update(result)
|
|
32
|
+
details.update(result)
|
|
33
|
+
except HealthCheckError as exc:
|
|
34
|
+
error.update(exc.causes)
|
|
35
|
+
details.update(exc.causes)
|
|
36
|
+
except Exception as exc: # noqa: BLE001 - an indicator blew up unexpectedly
|
|
37
|
+
failure = {"unknown": {"status": "down", "message": str(exc)}}
|
|
38
|
+
error.update(failure)
|
|
39
|
+
details.update(failure)
|
|
40
|
+
|
|
41
|
+
status = "error" if error else "ok"
|
|
42
|
+
return {"status": status, "info": info, "error": error, "details": details}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Base types for health indicators -- mirrors ``@nestjs/terminus``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
HealthIndicatorResult = dict[str, dict[str, Any]]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HealthCheckError(Exception):
|
|
11
|
+
"""Raised by an indicator to signal it's unhealthy; carries the same
|
|
12
|
+
``HealthIndicatorResult`` shape that a healthy check would have returned."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, message: str, causes: HealthIndicatorResult):
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
self.causes = causes
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HealthIndicator:
|
|
20
|
+
"""Base class for health indicators. Subclasses expose one or more
|
|
21
|
+
``async def xxx_check(self, key: str, ...) -> HealthIndicatorResult``
|
|
22
|
+
methods built on top of :meth:`get_status`."""
|
|
23
|
+
|
|
24
|
+
def get_status(self, key: str, is_healthy: bool, data: dict | None = None) -> HealthIndicatorResult:
|
|
25
|
+
payload: dict[str, Any] = {"status": "up" if is_healthy else "down"}
|
|
26
|
+
if data:
|
|
27
|
+
payload.update(data)
|
|
28
|
+
return {key: payload}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""``MemoryHealthIndicator`` -- mirrors ``@nestjs/terminus``'s ``MemoryHealthIndicator``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import resource
|
|
6
|
+
|
|
7
|
+
from austial.common.decorators.injectable import Injectable
|
|
8
|
+
from austial.terminus.health_indicator import HealthCheckError, HealthIndicator, HealthIndicatorResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@Injectable()
|
|
12
|
+
class MemoryHealthIndicator(HealthIndicator):
|
|
13
|
+
async def check_heap(self, key: str, threshold_bytes: int) -> HealthIndicatorResult:
|
|
14
|
+
# ru_maxrss is KB on Linux, bytes on macOS -- close enough for a demo indicator.
|
|
15
|
+
used_bytes = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss * 1024
|
|
16
|
+
is_healthy = used_bytes < threshold_bytes
|
|
17
|
+
result = self.get_status(key, is_healthy, {"used_bytes": used_bytes, "threshold_bytes": threshold_bytes})
|
|
18
|
+
if not is_healthy:
|
|
19
|
+
raise HealthCheckError("Memory usage exceeded threshold", result)
|
|
20
|
+
return result
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Unit test for ``HealthCheckService`` -- the framework's own Terminus-style
|
|
2
|
+
health-check aggregator, exercised directly (no consumer app required): this
|
|
3
|
+
is a library test, so it builds indicators inline instead of importing a
|
|
4
|
+
sample app's ``HealthService``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from austial.terminus import (
|
|
12
|
+
HealthCheckError,
|
|
13
|
+
HealthCheckService,
|
|
14
|
+
HealthIndicator,
|
|
15
|
+
HealthIndicatorResult,
|
|
16
|
+
MemoryHealthIndicator,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.asyncio
|
|
21
|
+
async def test_reports_ok_status_when_all_indicators_pass():
|
|
22
|
+
service = HealthCheckService()
|
|
23
|
+
memory = MemoryHealthIndicator()
|
|
24
|
+
|
|
25
|
+
result = await service.check([lambda: memory.check_heap("memory_heap", 1024 * 1024 * 1024)])
|
|
26
|
+
|
|
27
|
+
assert result["status"] == "ok"
|
|
28
|
+
assert result["error"] == {}
|
|
29
|
+
assert result["info"]["memory_heap"]["status"] == "up"
|
|
30
|
+
assert result["details"] == result["info"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.asyncio
|
|
34
|
+
async def test_reports_error_status_when_an_indicator_fails():
|
|
35
|
+
service = HealthCheckService()
|
|
36
|
+
|
|
37
|
+
class _AlwaysDownIndicator(HealthIndicator):
|
|
38
|
+
async def ping(self, key: str) -> HealthIndicatorResult:
|
|
39
|
+
raise HealthCheckError("simulated failure", self.get_status(key, False, {"reason": "simulated"}))
|
|
40
|
+
|
|
41
|
+
indicator = _AlwaysDownIndicator()
|
|
42
|
+
|
|
43
|
+
result = await service.check([lambda: indicator.ping("dependency")])
|
|
44
|
+
|
|
45
|
+
assert result["status"] == "error"
|
|
46
|
+
assert result["info"] == {}
|
|
47
|
+
assert result["error"]["dependency"]["status"] == "down"
|
|
48
|
+
assert result["details"] == result["error"]
|