Skip to content

Commit 2f3f56f

Browse files
committed
feat: health check
1 parent 3c09d93 commit 2f3f56f

File tree

8 files changed

+111
-0
lines changed

8 files changed

+111
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- Поддержка `chat/completions` API с потоковой передачей сообщений
1616
- Поддержка файлов в сообщениях исключая повторные загрузки в GigaChat
1717
- Docker-образ
18+
- Healthcheck API (ready, live)
1819

1920
### Запуск
2021

src/core/settings.py

+12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1+
import tomllib
2+
13
from pydantic import Field
24
from pydantic_settings import BaseSettings
35

46

7+
def get_version() -> str:
8+
"""Получает версию из pyproject.toml"""
9+
try:
10+
with open("pyproject.toml", "rb") as f:
11+
return tomllib.load(f)["project"]["version"]
12+
except (FileNotFoundError, KeyError):
13+
return "unknown"
14+
15+
516
class AppSettings(BaseSettings):
617
debug: bool = False
718
environment: str = "production"
@@ -10,6 +21,7 @@ class AppSettings(BaseSettings):
1021
description="Bearer token is required to authorize requests.",
1122
)
1223
cors_allowed_hosts: list[str] | None = ["http://localhost:5173"]
24+
version: str = Field(default_factory=get_version)
1325

1426
class Config:
1527
env_file = ".env"

src/endpoints.py

+24
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
from fastapi.security import HTTPBearer
44

55
from .core.logging import local_logger
6+
from .core.settings import AppSettings, get_app_settings
67
from .core.verify_token import verify_token
78
from .gigachat_service import gigachat_service
89
from .models.completion import ChatCompletionRequest, ChatCompletionResponse
910
from .models.files import FilePurpose, FileUploadResponse
11+
from .models.health import HealthResponse
1012
from .models.models import ListModelsResponse
1113

1214
router = APIRouter()
@@ -63,3 +65,25 @@ async def upload_file(
6365
purpose=purpose.value if purpose != FilePurpose.FINE_TUNE else "general",
6466
)
6567
return uploaded
68+
69+
70+
@router.get("/health/liveness", response_model=HealthResponse)
71+
async def liveness(settings: AppSettings = Depends(get_app_settings)) -> HealthResponse:
72+
"""
73+
Liveness probe для kubernetes.
74+
Проверяет что сервис жив и отвечает на запросы.
75+
"""
76+
return HealthResponse(status="ok", version=settings.version)
77+
78+
79+
@router.get("/health/readiness", response_model=HealthResponse)
80+
async def readiness(
81+
settings: AppSettings = Depends(get_app_settings),
82+
) -> HealthResponse:
83+
"""
84+
Readiness probe для kubernetes.
85+
Проверяет что сервис готов обрабатывать запросы,
86+
включая проверку подключения к GigaChat API.
87+
"""
88+
await gigachat_service.get_models()
89+
return HealthResponse(status="ok", version=settings.version)

src/endpoints/health.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

src/main.py

+15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import gigachat.exceptions
12
from fastapi import FastAPI
23
from fastapi.exceptions import RequestValidationError
34
from fastapi.middleware.cors import CORSMiddleware
@@ -18,6 +19,7 @@ def get_application() -> FastAPI:
1819
description="Connector to GigaChat API using OpenAI",
1920
debug=settings.debug,
2021
cors_allowed_origins=settings.cors_allowed_hosts,
22+
version=settings.version,
2123
)
2224

2325
if settings.cors_allowed_hosts:
@@ -70,6 +72,19 @@ async def exception_handler(request, exc):
7072
).model_dump(),
7173
)
7274

75+
@app.exception_handler(gigachat.exceptions.ResponseError)
76+
async def response_error_handler(request, exc):
77+
return JSONResponse(
78+
status_code=500,
79+
content=ErrorResponse(
80+
error=ErrorDetail(
81+
message=str(exc),
82+
type="http",
83+
code="HTTP_EXCEPTION",
84+
)
85+
).model_dump(),
86+
)
87+
7388
return app
7489

7590

src/models/health.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from pydantic import BaseModel, Field
2+
3+
4+
class HealthResponse(BaseModel):
5+
status: str = Field("ok", description="The status of the service")
6+
version: str = Field(..., description="The version of the service")

tests/conftest.py

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
TEST_BEARER_TOKEN = "test_token"
1010
os.environ["BEARER_TOKEN"] = TEST_BEARER_TOKEN
1111
os.environ["GIGACHAT_CREDENTIALS"] = "test_credentials"
12+
os.environ["DEBUG"] = "false"
1213

1314

1415
from src.main import get_application # noqa: E402

tests/test_health.py

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from fastapi import status
2+
from pytest_httpx import HTTPXMock
3+
4+
5+
def test_liveness(client):
6+
"""Test liveness endpoint returns 200 and correct version"""
7+
response = client.get("/health/liveness")
8+
assert response.status_code == status.HTTP_200_OK
9+
data = response.json()
10+
assert data["status"] == "ok"
11+
assert isinstance(data["version"], str)
12+
13+
14+
def test_readiness(client, httpx_mock: HTTPXMock):
15+
"""Test readiness endpoint returns 200 when GigaChat API is available"""
16+
# Mock GigaChat models API call
17+
httpx_mock.add_response(
18+
json={
19+
"data": [
20+
{
21+
"id": "GigaChat",
22+
"object": "model",
23+
"owned_by": "salutedevices",
24+
"created": 1735689600,
25+
}
26+
],
27+
"object": "list",
28+
},
29+
url="https://gigachat.devices.sberbank.ru/api/v1/models",
30+
)
31+
32+
response = client.get("/health/readiness")
33+
assert response.status_code == status.HTTP_200_OK
34+
data = response.json()
35+
assert data["status"] == "ok"
36+
assert isinstance(data["version"], str)
37+
38+
39+
def test_readiness_failure(client, httpx_mock: HTTPXMock):
40+
"""Test readiness endpoint returns 500 when GigaChat API is unavailable"""
41+
# Mock GigaChat models API call failure
42+
httpx_mock.add_response(
43+
status_code=500,
44+
url="https://gigachat.devices.sberbank.ru/api/v1/models",
45+
)
46+
47+
response = client.get("/health/readiness")
48+
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
49+
data = response.json()
50+
assert data["error"]["type"] == "http"
51+
assert data["error"]["code"] == "HTTP_EXCEPTION"

0 commit comments

Comments
 (0)