add health endpoint for gateway and everos
This commit is contained in:
65
README.md
65
README.md
@ -111,7 +111,55 @@ resource:{user_id}:{resource_id}
|
|||||||
|
|
||||||
除 `POST /users` 外,所有业务 API 都需要携带 `user_id` 和 `user_key`。认证失败返回 `401`。
|
除 `POST /users` 外,所有业务 API 都需要携带 `user_id` 和 `user_key`。认证失败返回 `401`。
|
||||||
|
|
||||||
### 1. 创建用户
|
### 1. 健康检查
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /health
|
||||||
|
```
|
||||||
|
|
||||||
|
该接口不需要 `user_id` 或 `user_key`,用于确认 Gateway API 是否可响应,以及上游 EverOS 是否可访问。
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:8010/health
|
||||||
|
```
|
||||||
|
|
||||||
|
EverOS 正常时响应示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"api": {
|
||||||
|
"status": "ok"
|
||||||
|
},
|
||||||
|
"everos": {
|
||||||
|
"status": "ok",
|
||||||
|
"base_url": "http://127.0.0.1:8000",
|
||||||
|
"data": {
|
||||||
|
"status": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
EverOS 不可访问时仍返回 HTTP 200,但 `status` 会变成 `degraded`,便于区分“Gateway API 活着”和“上游 EverOS 故障”:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "degraded",
|
||||||
|
"api": {
|
||||||
|
"status": "ok"
|
||||||
|
},
|
||||||
|
"everos": {
|
||||||
|
"status": "unavailable",
|
||||||
|
"base_url": "http://127.0.0.1:8000",
|
||||||
|
"error": "Connection refused"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 创建用户
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /users
|
POST /users
|
||||||
@ -144,7 +192,7 @@ curl -X POST http://127.0.0.1:8010/users \
|
|||||||
|
|
||||||
`user_key` 需要由调用方保存,后续上传、查询、搜索、修改和删除都要传入。如果同一个 `user_id` 已存在,接口会返回已有 `user_key`。
|
`user_key` 需要由调用方保存,后续上传、查询、搜索、修改和删除都要传入。如果同一个 `user_id` 已存在,接口会返回已有 `user_key`。
|
||||||
|
|
||||||
### 2. 上传资源
|
### 3. 上传资源
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /resources
|
POST /resources
|
||||||
@ -212,7 +260,7 @@ curl -X POST http://127.0.0.1:8010/resources \
|
|||||||
|
|
||||||
对外返回的 `uri` 永远是 `resource://{user_id}/{resource_id}`,不会泄露内部 `file://` 路径。
|
对外返回的 `uri` 永远是 `resource://{user_id}/{resource_id}`,不会泄露内部 `file://` 路径。
|
||||||
|
|
||||||
### 3. 查询资源列表
|
### 4. 查询资源列表
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /resources?user_id={user_id}&user_key={user_key}
|
GET /resources?user_id={user_id}&user_key={user_key}
|
||||||
@ -264,7 +312,7 @@ curl "http://127.0.0.1:8010/resources?user_id=u_123&user_key=uk_xxx"
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 查询资源详情
|
### 5. 查询资源详情
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /resources/{resource_id}?user_id={user_id}&user_key={user_key}
|
GET /resources/{resource_id}?user_id={user_id}&user_key={user_key}
|
||||||
@ -322,7 +370,7 @@ curl "http://127.0.0.1:8010/resources/r_xxx?user_id=u_123&user_key=uk_xxx"
|
|||||||
|
|
||||||
这种设计避免通过资源 ID 探测其他用户的数据。`uri` 同样只返回公开 `resource://{user_id}/{resource_id}`,不会泄露内部 URI。
|
这种设计避免通过资源 ID 探测其他用户的数据。`uri` 同样只返回公开 `resource://{user_id}/{resource_id}`,不会泄露内部 URI。
|
||||||
|
|
||||||
### 5. 删除资源
|
### 6. 删除资源
|
||||||
|
|
||||||
```http
|
```http
|
||||||
DELETE /resources/{resource_id}?user_id={user_id}&user_key={user_key}
|
DELETE /resources/{resource_id}?user_id={user_id}&user_key={user_key}
|
||||||
@ -352,7 +400,7 @@ curl -X DELETE "http://127.0.0.1:8010/resources/r_xxx?user_id=u_123&user_key=uk_
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. 搜索记忆
|
### 7. 搜索记忆
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /memories/search
|
POST /memories/search
|
||||||
@ -429,7 +477,7 @@ curl -X POST http://127.0.0.1:8010/memories/search \
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7. 修改记忆
|
### 8. 修改记忆
|
||||||
|
|
||||||
```http
|
```http
|
||||||
PATCH /memories/{memory_id}
|
PATCH /memories/{memory_id}
|
||||||
@ -470,7 +518,7 @@ curl -X PATCH http://127.0.0.1:8010/memories/mem_abc \
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 8. 删除记忆
|
### 9. 删除记忆
|
||||||
|
|
||||||
```http
|
```http
|
||||||
DELETE /memories/{memory_id}
|
DELETE /memories/{memory_id}
|
||||||
@ -518,6 +566,7 @@ Gateway 内部通过 `core/everos_client.py` 调用 EverOS:
|
|||||||
- `add_memory(payload)` -> `POST /api/v1/memory/add`
|
- `add_memory(payload)` -> `POST /api/v1/memory/add`
|
||||||
- `flush_memory(session_id, app_id, project_id)` -> `POST /api/v1/memory/flush`
|
- `flush_memory(session_id, app_id, project_id)` -> `POST /api/v1/memory/flush`
|
||||||
- `search_memory(payload)` -> `POST /api/v1/memory/search`
|
- `search_memory(payload)` -> `POST /api/v1/memory/search`
|
||||||
|
- `health_check()` -> `GET /health`
|
||||||
|
|
||||||
## 运行测试
|
## 运行测试
|
||||||
|
|
||||||
|
|||||||
24
core/api.py
24
core/api.py
@ -66,6 +66,30 @@ def create_app(
|
|||||||
if not service.authenticate_user(user_id, user_key):
|
if not service.authenticate_user(user_id, user_key):
|
||||||
raise HTTPException(status_code=401, detail="invalid user credentials")
|
raise HTTPException(status_code=401, detail="invalid user credentials")
|
||||||
|
|
||||||
|
@router.get("/health")
|
||||||
|
async def health() -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
everos_health = await client.health_check()
|
||||||
|
except Exception as exc:
|
||||||
|
return {
|
||||||
|
"status": "degraded",
|
||||||
|
"api": {"status": "ok"},
|
||||||
|
"everos": {
|
||||||
|
"status": "unavailable",
|
||||||
|
"base_url": cfg.everos_base_url,
|
||||||
|
"error": str(exc),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"api": {"status": "ok"},
|
||||||
|
"everos": {
|
||||||
|
"status": "ok",
|
||||||
|
"base_url": cfg.everos_base_url,
|
||||||
|
"data": everos_health,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
@router.post("/users")
|
@router.post("/users")
|
||||||
async def create_user(request: UserCreateRequest) -> dict[str, Any]:
|
async def create_user(request: UserCreateRequest) -> dict[str, Any]:
|
||||||
return service.create_user(request.user_id)
|
return service.create_user(request.user_id)
|
||||||
|
|||||||
@ -31,6 +31,15 @@ class EverOSClient:
|
|||||||
async def search_memory(self, payload: dict[str, Any]) -> dict[str, Any]:
|
async def search_memory(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
return await self._post("/api/v1/memory/search", payload)
|
return await self._post("/api/v1/memory/search", payload)
|
||||||
|
|
||||||
|
async def health_check(self) -> dict[str, Any]:
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
base_url=self.base_url,
|
||||||
|
timeout=self.timeout,
|
||||||
|
) as client:
|
||||||
|
response = await client.get("/health")
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
async def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
async def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
async with httpx.AsyncClient(
|
async with httpx.AsyncClient(
|
||||||
base_url=self.base_url,
|
base_url=self.base_url,
|
||||||
|
|||||||
@ -13,11 +13,16 @@ from core.repository import MemoryRepository
|
|||||||
|
|
||||||
|
|
||||||
class FakeEverOSClient:
|
class FakeEverOSClient:
|
||||||
def __init__(self, search_results: list[dict[str, Any]] | None = None) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
search_results: list[dict[str, Any]] | None = None,
|
||||||
|
health_error: Exception | None = None,
|
||||||
|
) -> None:
|
||||||
self.add_calls: list[dict[str, Any]] = []
|
self.add_calls: list[dict[str, Any]] = []
|
||||||
self.flush_calls: list[dict[str, str]] = []
|
self.flush_calls: list[dict[str, str]] = []
|
||||||
self.search_calls: list[dict[str, Any]] = []
|
self.search_calls: list[dict[str, Any]] = []
|
||||||
self.search_results = search_results or []
|
self.search_results = search_results or []
|
||||||
|
self.health_error = health_error
|
||||||
|
|
||||||
async def add_memory(self, payload: dict[str, Any]) -> dict[str, Any]:
|
async def add_memory(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
self.add_calls.append(payload)
|
self.add_calls.append(payload)
|
||||||
@ -38,6 +43,11 @@ class FakeEverOSClient:
|
|||||||
self.search_calls.append(payload)
|
self.search_calls.append(payload)
|
||||||
return {"request_id": "search", "data": {"episodes": self.search_results}}
|
return {"request_id": "search", "data": {"episodes": self.search_results}}
|
||||||
|
|
||||||
|
async def health_check(self) -> dict[str, Any]:
|
||||||
|
if self.health_error is not None:
|
||||||
|
raise self.health_error
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def config(tmp_path: Path) -> GatewayConfig:
|
def config(tmp_path: Path) -> GatewayConfig:
|
||||||
@ -72,6 +82,43 @@ async def create_user(client: httpx.AsyncClient, user_id: str = "u_123") -> str:
|
|||||||
return body["user_key"]
|
return body["user_key"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_health_reports_api_and_everos_ok(
|
||||||
|
config: GatewayConfig,
|
||||||
|
) -> None:
|
||||||
|
everos = FakeEverOSClient()
|
||||||
|
async with app_client(config, everos) as client:
|
||||||
|
response = await client.get("/health")
|
||||||
|
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == {
|
||||||
|
"status": "ok",
|
||||||
|
"api": {"status": "ok"},
|
||||||
|
"everos": {
|
||||||
|
"status": "ok",
|
||||||
|
"base_url": "http://everos.test",
|
||||||
|
"data": {"status": "ok"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_health_reports_degraded_when_everos_fails(
|
||||||
|
config: GatewayConfig,
|
||||||
|
) -> None:
|
||||||
|
everos = FakeEverOSClient(health_error=RuntimeError("everos down"))
|
||||||
|
async with app_client(config, everos) as client:
|
||||||
|
response = await client.get("/health")
|
||||||
|
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
body = response.json()
|
||||||
|
assert body["status"] == "degraded"
|
||||||
|
assert body["api"] == {"status": "ok"}
|
||||||
|
assert body["everos"]["status"] == "unavailable"
|
||||||
|
assert body["everos"]["base_url"] == "http://everos.test"
|
||||||
|
assert body["everos"]["error"] == "everos down"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_user_generates_and_persists_user_key(
|
async def test_create_user_generates_and_persists_user_key(
|
||||||
config: GatewayConfig,
|
config: GatewayConfig,
|
||||||
|
|||||||
Reference in New Issue
Block a user