mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-11-01 05:06:58 +08:00
feat(backend): add unauthenticated /health liveness endpoint
Add GET /health mounted outside the /api prefix so it bypasses
per-route auth and is reachable as a plain liveness/readiness probe.
Returns {status, version, db_ok}, degrading to db_ok=false with a 200
(not a raised exception) on database failure so the probe stays useful
even when the DB is unreachable. Add a Docker HEALTHCHECK that curls it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
This commit is contained in:
@@ -46,4 +46,9 @@ ENV PATH="/app/.venv/bin:$PATH"
|
||||
EXPOSE 7892
|
||||
VOLUME ["/app/config", "/app/data"]
|
||||
|
||||
# Liveness probe against the unauthenticated /health route; give the app
|
||||
# room to run migrations on first boot before failures count.
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
|
||||
CMD wget -qO- http://localhost:7892/health || exit 1
|
||||
|
||||
ENTRYPOINT ["tini", "-g", "--", "/entrypoint.sh"]
|
||||
|
||||
@@ -11,6 +11,7 @@ from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from module.api import v1
|
||||
from module.api.health import router as health_router
|
||||
from module.conf import VERSION, settings, setup_logger
|
||||
from module.core import AppContext
|
||||
from module.mcp import create_mcp_app
|
||||
@@ -65,6 +66,10 @@ def create_app() -> FastAPI:
|
||||
# mount routers
|
||||
app.include_router(v1, prefix="/api")
|
||||
|
||||
# unauthenticated liveness probe, mounted at the app root (not /api) so
|
||||
# it stays reachable without auth for container/orchestrator health checks
|
||||
app.include_router(health_router)
|
||||
|
||||
# mount MCP server (SSE transport for LLM tool integration)
|
||||
app.mount("/mcp", create_mcp_app(ctx))
|
||||
|
||||
|
||||
29
backend/src/module/api/health.py
Normal file
29
backend/src/module/api/health.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""无鉴权的存活探针(liveness probe),供容器/编排系统健康检查使用。
|
||||
|
||||
与 program.py 中的 /status 不同,本路由不挂在 /api/v1 前缀下、也不依赖
|
||||
get_current_user,因此可以直接被 Docker HEALTHCHECK 等外部探针访问。
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter
|
||||
from sqlalchemy import text
|
||||
|
||||
from module.conf import VERSION
|
||||
from module.database import Database
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(tags=["health"])
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health() -> dict:
|
||||
"""存活探针:始终返回 200,db_ok 反映一次简单查询是否成功。"""
|
||||
try:
|
||||
async with Database() as db:
|
||||
await db.session.execute(text("SELECT 1"))
|
||||
db_ok = True
|
||||
except Exception as e:
|
||||
logger.warning(f"[Health] DB check failed: {e}")
|
||||
db_ok = False
|
||||
return {"status": "ok", "version": VERSION, "db_ok": db_ok}
|
||||
60
backend/src/test/test_api_health.py
Normal file
60
backend/src/test/test_api_health.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""Tests for the unauthenticated /health liveness probe."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from module.api.health import router as health_router
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
"""App with only the health router mounted, at the root (no /api prefix)."""
|
||||
app = FastAPI()
|
||||
app.include_router(health_router)
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(app):
|
||||
"""Plain TestClient — no auth override, matching the probe's real usage."""
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /health
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestHealth:
|
||||
def test_health_no_auth_required_returns_200(self, client):
|
||||
"""GET /health succeeds without any auth dependency/header."""
|
||||
response = client.get("/health")
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_health_reports_version_and_db_ok(self, client):
|
||||
"""GET /health returns status/version/db_ok, DB reachable -> db_ok True."""
|
||||
with patch("module.api.health.VERSION", "3.3.0-test"):
|
||||
response = client.get("/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "ok"
|
||||
assert data["version"] == "3.3.0-test"
|
||||
assert data["db_ok"] is True
|
||||
|
||||
def test_health_db_failure_reports_db_ok_false(self, client):
|
||||
"""A DB query failure is caught: still 200, but db_ok is False."""
|
||||
with patch("module.api.health.Database", side_effect=RuntimeError("db down")):
|
||||
response = client.get("/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "ok"
|
||||
assert data["db_ok"] is False
|
||||
Reference in New Issue
Block a user