mirror of
https://github.com/fusion44/blitz_api.git
synced 2026-08-13 11:52:45 +02:00
feat(api): implement GET /system/health readiness endpoint (#145)
Make /system/health unauthenticated, compute real readiness from the shared startup state via build_health_info, and return 503 (body still a SystemHealthInfo) when a subsystem is not ready. Drop the now-dead per-backend get_system_health delegation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0e77b6a924
commit
6e3238a5a7
6 changed files with 52 additions and 24 deletions
|
|
@ -17,7 +17,6 @@ from app.system.models import (
|
|||
ConnectionInfo,
|
||||
LoginInput,
|
||||
RawDebugLogData,
|
||||
SystemHealthInfo,
|
||||
SystemInfo,
|
||||
)
|
||||
|
||||
|
|
@ -55,10 +54,6 @@ class NativePythonSystem(SystemBase):
|
|||
chain=lninfo.chains[0].network,
|
||||
)
|
||||
|
||||
@logger.catch(exclude=(HTTPException,))
|
||||
async def get_system_health(self, verbose: bool) -> SystemHealthInfo:
|
||||
return SystemHealthInfo(healthy=True)
|
||||
|
||||
@logger.catch(exclude=(HTTPException,))
|
||||
async def shutdown(self, reboot: bool) -> bool:
|
||||
logger.info("Shutdown / reboot not supported in native_python mode.")
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ from app.system.models import (
|
|||
ConnectionInfo,
|
||||
LoginInput,
|
||||
RawDebugLogData,
|
||||
SystemHealthInfo,
|
||||
SystemInfo,
|
||||
)
|
||||
|
||||
|
|
@ -105,9 +104,6 @@ class RaspiBlitzSystem(SystemBase):
|
|||
chain=data_chain,
|
||||
)
|
||||
|
||||
async def get_system_health(self, verbose: bool) -> SystemHealthInfo:
|
||||
return SystemHealthInfo(healthy=True)
|
||||
|
||||
async def shutdown(self, reboot: bool) -> bool:
|
||||
params = ""
|
||||
if reboot:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ from app.system.models import (
|
|||
ConnectionInfo,
|
||||
LoginInput,
|
||||
RawDebugLogData,
|
||||
SystemHealthInfo,
|
||||
SystemInfo,
|
||||
)
|
||||
|
||||
|
|
@ -17,10 +16,6 @@ class SystemBase:
|
|||
async def get_system_info(self) -> SystemInfo:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def get_system_health(self, verbose: bool) -> SystemHealthInfo:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def shutdown(self, reboot: bool) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
|
|
|||
|
|
@ -137,16 +137,21 @@ async def get_debug_logs_raw_route() -> RawDebugLogData:
|
|||
@router.get(
|
||||
"/health",
|
||||
name=f"{_PREFIX}.health",
|
||||
summary="Returns info about the systems health",
|
||||
dependencies=[Depends(JWTBearer())],
|
||||
summary="Returns info about the system's health",
|
||||
response_model=SystemHealthInfo,
|
||||
responses={503: {"description": "One or more subsystems are not ready"}},
|
||||
)
|
||||
async def get_system_health(
|
||||
response: Response,
|
||||
verbose: bool = Query(
|
||||
False,
|
||||
description="Returns info about each subsytem running on this node if true. Currently not implemented.",
|
||||
description="If true, include a per-subsystem (api, bitcoind, lightning) health breakdown.",
|
||||
),
|
||||
) -> SystemHealthInfo:
|
||||
return await system_health(verbose)
|
||||
result = await system_health(verbose)
|
||||
if not result.healthy:
|
||||
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
return result
|
||||
|
||||
|
||||
@router.post(
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ from loguru import logger
|
|||
|
||||
from app.api.config import config
|
||||
from app.api.error_report.report import Frame
|
||||
from app.api.startup_status import api_startup_status
|
||||
from app.api.utils import Event, broadcast_msg
|
||||
from app.external.result_type.src.result.result import Err, Ok
|
||||
from app.system.health import build_health_info
|
||||
from app.system.models import (
|
||||
APIPlatform,
|
||||
ConnectionInfo,
|
||||
|
|
@ -57,12 +59,8 @@ async def get_system_info() -> SystemInfo:
|
|||
|
||||
|
||||
async def system_health(verbose: bool) -> SystemHealthInfo:
|
||||
try:
|
||||
return await system.get_system_health(verbose)
|
||||
except HTTPException:
|
||||
raise
|
||||
except NotImplementedError as r:
|
||||
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0])
|
||||
# Readiness is platform independent: read the shared startup state directly.
|
||||
return build_health_info(api_startup_status, verbose)
|
||||
|
||||
|
||||
async def get_hardware_info() -> map:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ from starlette.testclient import TestClient
|
|||
|
||||
from app.main import app
|
||||
from tests.routers.utils import call_route
|
||||
import app.main as main
|
||||
from app.api.models import StartupState
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
|
@ -11,3 +13,40 @@ def test_route_authentications_latest():
|
|||
|
||||
for prefix in prefixes:
|
||||
call_route(client, f"{prefix}/refresh-token", method="p")
|
||||
|
||||
|
||||
def _set_status(bitcoin, lightning):
|
||||
main.api_startup_status.bitcoin = bitcoin
|
||||
main.api_startup_status.lightning = lightning
|
||||
|
||||
|
||||
def test_health_is_unauthenticated_and_200_when_ready():
|
||||
_set_status(StartupState.DONE, StartupState.DONE)
|
||||
# no `with`: don't start lifespan/background tasks
|
||||
c = TestClient(main.app)
|
||||
resp = c.get("/system/health") # no Authorization header
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["healthy"] is True
|
||||
assert body["subsystems"] == []
|
||||
|
||||
|
||||
def test_health_503_when_not_ready_body_is_health_info():
|
||||
_set_status(StartupState.BOOTSTRAPPING, StartupState.DONE)
|
||||
c = TestClient(main.app)
|
||||
resp = c.get("/system/health")
|
||||
assert resp.status_code == 503
|
||||
body = resp.json()
|
||||
# body is a SystemHealthInfo, NOT an ErrorMessage
|
||||
assert body["healthy"] is False
|
||||
assert "error_code" not in body
|
||||
assert body["message"] == "bitcoind not ready: bootstrapping"
|
||||
|
||||
|
||||
def test_health_verbose_lists_subsystems():
|
||||
_set_status(StartupState.DONE, StartupState.DONE)
|
||||
c = TestClient(main.app)
|
||||
resp = c.get("/system/health?verbose=true")
|
||||
assert resp.status_code == 200
|
||||
names = [s["name"] for s in resp.json()["subsystems"]]
|
||||
assert names == ["api", "bitcoind", "lightning"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue