171 lines
5.3 KiB
Python
171 lines
5.3 KiB
Python
from __future__ import annotations
|
|
|
|
from importlib import import_module
|
|
from pathlib import Path
|
|
from tempfile import NamedTemporaryFile
|
|
|
|
from alembic.config import Config
|
|
from alembic.script import ScriptDirectory
|
|
from fastapi import APIRouter, Response, status
|
|
from sqlalchemy import text
|
|
|
|
from app.core.config import get_settings
|
|
from app.db.session import get_engine
|
|
from app.providers.registry import list_provider_capabilities
|
|
from app.schemas.health import (
|
|
HealthResponse,
|
|
SystemCapabilities,
|
|
SystemCapabilitiesEnvelope,
|
|
)
|
|
from app.services.model_registry_service import ModelRegistryService
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _dependency_enabled(module: str) -> bool:
|
|
try:
|
|
import_module(module)
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def _expected_migration_heads() -> list[str]:
|
|
backend_root = Path(__file__).resolve().parents[3]
|
|
config = Config(str(backend_root / "alembic.ini"))
|
|
config.set_main_option("script_location", str(backend_root / "alembic"))
|
|
return list(ScriptDirectory.from_config(config).get_heads())
|
|
|
|
|
|
def _database_checks() -> dict[str, str]:
|
|
checks = {
|
|
"database": "degraded",
|
|
"postgis": "degraded",
|
|
"migration": "degraded",
|
|
}
|
|
try:
|
|
with get_engine().connect() as connection:
|
|
connection.execute(text("SELECT 1"))
|
|
checks["database"] = "ok"
|
|
postgis_version = connection.execute(
|
|
text("SELECT PostGIS_Version()")
|
|
).scalar_one()
|
|
checks["postgis"] = f"ok:{postgis_version}"
|
|
database_head = connection.execute(
|
|
text("SELECT version_num FROM alembic_version")
|
|
).scalar_one()
|
|
expected_heads = _expected_migration_heads()
|
|
if len(expected_heads) == 1 and database_head == expected_heads[0]:
|
|
checks["migration"] = f"ok:{database_head}"
|
|
else:
|
|
checks["migration"] = (
|
|
f"degraded:database={database_head};"
|
|
f"expected={','.join(expected_heads) or 'none'}"
|
|
)
|
|
except Exception:
|
|
return checks
|
|
return checks
|
|
|
|
|
|
def _storage_check(storage_root: str) -> str:
|
|
root = Path(storage_root).expanduser()
|
|
try:
|
|
root.mkdir(parents=True, exist_ok=True)
|
|
with NamedTemporaryFile(
|
|
prefix=".geointel-readiness-",
|
|
dir=root,
|
|
delete=True,
|
|
) as handle:
|
|
handle.write(b"ok")
|
|
handle.flush()
|
|
return "ok"
|
|
except OSError:
|
|
return "degraded"
|
|
|
|
|
|
def _readiness_payload() -> HealthResponse:
|
|
settings = get_settings()
|
|
checks = _database_checks()
|
|
checks["storage"] = _storage_check(settings.storage_root)
|
|
ready = all(
|
|
value == "ok" or value.startswith("ok:")
|
|
for value in checks.values()
|
|
)
|
|
return HealthResponse(
|
|
status="ok" if ready else "degraded",
|
|
service="geointel-backend",
|
|
version=settings.app_version,
|
|
build_sha=settings.build_sha,
|
|
build_time=settings.build_time,
|
|
database=checks["database"],
|
|
postgis=checks["postgis"],
|
|
migration=checks["migration"],
|
|
storage=checks["storage"],
|
|
checks=checks,
|
|
)
|
|
|
|
|
|
@router.get("/health/live", response_model=HealthResponse)
|
|
def liveness() -> HealthResponse:
|
|
settings = get_settings()
|
|
return HealthResponse(
|
|
status="ok",
|
|
service="geointel-backend",
|
|
version=settings.app_version,
|
|
build_sha=settings.build_sha,
|
|
build_time=settings.build_time,
|
|
)
|
|
|
|
|
|
def _readiness_response(response: Response) -> HealthResponse:
|
|
payload = _readiness_payload()
|
|
if payload.status != "ok":
|
|
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
|
|
return payload
|
|
|
|
|
|
@router.get("/health", response_model=HealthResponse)
|
|
def readiness(response: Response) -> HealthResponse:
|
|
return _readiness_response(response)
|
|
|
|
|
|
@router.get("/health/ready", response_model=HealthResponse)
|
|
def readiness_explicit(response: Response) -> HealthResponse:
|
|
return _readiness_response(response)
|
|
|
|
|
|
@router.get(
|
|
"/api/v1/system/capabilities",
|
|
response_model=SystemCapabilitiesEnvelope,
|
|
)
|
|
def capabilities() -> SystemCapabilitiesEnvelope:
|
|
settings = get_settings()
|
|
providers = [item.to_dict() for item in list_provider_capabilities()]
|
|
configured_yolo = ModelRegistryService.get_model_capability(
|
|
settings.yolo_model_id,
|
|
settings=settings,
|
|
)
|
|
yolo_configured = bool(configured_yolo and configured_yolo.configured)
|
|
yolo_status = configured_yolo.status if configured_yolo else "not_configured"
|
|
configured_sam = ModelRegistryService.get_model_capability(
|
|
settings.sam_model_id,
|
|
settings=settings,
|
|
task_type="segmentation",
|
|
)
|
|
postgis_ready = _database_checks()["postgis"].startswith("ok:")
|
|
return SystemCapabilitiesEnvelope(
|
|
data=SystemCapabilities(
|
|
postgis=postgis_ready,
|
|
rasterio=_dependency_enabled("rasterio"),
|
|
geopandas=_dependency_enabled("geopandas"),
|
|
yolo=yolo_configured,
|
|
yolo_status=yolo_status,
|
|
sam=bool(configured_sam and configured_sam.configured),
|
|
grb="bounded",
|
|
sentinel="planned",
|
|
version=settings.app_version,
|
|
build_sha=settings.build_sha,
|
|
providers=providers,
|
|
)
|
|
)
|