feat: Phase 2 - Service Management CRUD API (Backend)
Backend Implementation:
- Service model with comprehensive fields (name, url, type, status, health_endpoint)
- Complete CRUD API endpoints for service management
- Health check mechanism with httpx and response time tracking
- Service status tracking (healthy/unhealthy/unknown)
- Service type categorization (backend, frontend, database, cache, etc.)
API Endpoints:
- GET /api/services - Get all services
- POST /api/services - Create new service
- GET /api/services/{id} - Get service by ID
- PUT /api/services/{id} - Update service
- DELETE /api/services/{id} - Delete service
- POST /api/services/{id}/health-check - Check specific service health
- POST /api/services/health-check/all - Check all services health
Frontend Preparation:
- TypeScript type definitions for Service
- Service API client with full CRUD methods
- Health check client methods
Files Added:
- backend/app/models/service.py - Service data model
- backend/app/schemas/service.py - Request/response schemas
- backend/app/services/service_service.py - Business logic
- backend/app/routes/services.py - API route handlers
- frontend/src/types/service.ts - TypeScript types
- frontend/src/api/service.ts - API client
Updated:
- backend/app/main.py - Added services router
- docs/PROGRESS.md - Added Phase 2 status
Next: Frontend UI implementation (Services list page, Add/Edit modal, Health monitoring)
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -6,8 +6,8 @@
|
|||||||
## Current Status
|
## Current Status
|
||||||
- **Date Started**: 2025-09-09
|
- **Date Started**: 2025-09-09
|
||||||
- **Last Updated**: 2025-10-28
|
- **Last Updated**: 2025-10-28
|
||||||
- **Current Phase**: Phase 1 Complete ✅ (Authentication System)
|
- **Current Phase**: Phase 2 In Progress 🔄 (Service Management CRUD - Backend Complete)
|
||||||
- **Next Action**: Phase 2 - Service Management CRUD
|
- **Next Action**: Phase 2 - Frontend UI Implementation
|
||||||
|
|
||||||
## Completed Checkpoints
|
## Completed Checkpoints
|
||||||
|
|
||||||
@ -92,6 +92,44 @@ All authentication endpoints tested and working:
|
|||||||
- ✅ Duplicate email prevention
|
- ✅ Duplicate email prevention
|
||||||
- ✅ Unauthorized access blocking
|
- ✅ Unauthorized access blocking
|
||||||
|
|
||||||
|
### Phase 2: Service Management CRUD 🔄
|
||||||
|
**Started Date**: 2025-10-28
|
||||||
|
**Status**: Backend Complete, Frontend In Progress
|
||||||
|
|
||||||
|
#### Backend (FastAPI + MongoDB) ✅
|
||||||
|
✅ Service model with comprehensive fields
|
||||||
|
✅ Service CRUD API endpoints (Create, Read, Update, Delete)
|
||||||
|
✅ Health check mechanism with httpx
|
||||||
|
✅ Response time measurement
|
||||||
|
✅ Status tracking (healthy/unhealthy/unknown)
|
||||||
|
✅ Service type categorization (backend, frontend, database, etc.)
|
||||||
|
|
||||||
|
**API Endpoints**:
|
||||||
|
- GET `/api/services` - Get all services
|
||||||
|
- POST `/api/services` - Create new service
|
||||||
|
- GET `/api/services/{id}` - Get service by ID
|
||||||
|
- PUT `/api/services/{id}` - Update service
|
||||||
|
- DELETE `/api/services/{id}` - Delete service
|
||||||
|
- POST `/api/services/{id}/health-check` - Check specific service health
|
||||||
|
- POST `/api/services/health-check/all` - Check all services health
|
||||||
|
|
||||||
|
**Files Created**:
|
||||||
|
- `/services/console/backend/app/models/service.py` - Service model
|
||||||
|
- `/services/console/backend/app/schemas/service.py` - Service schemas
|
||||||
|
- `/services/console/backend/app/services/service_service.py` - Business logic
|
||||||
|
- `/services/console/backend/app/routes/services.py` - API routes
|
||||||
|
|
||||||
|
#### Frontend (React + TypeScript) 🔄
|
||||||
|
✅ TypeScript type definitions
|
||||||
|
✅ Service API client
|
||||||
|
⏳ Services list page (pending)
|
||||||
|
⏳ Add/Edit service modal (pending)
|
||||||
|
⏳ Health status display (pending)
|
||||||
|
|
||||||
|
**Files Created**:
|
||||||
|
- `/services/console/frontend/src/types/service.ts` - TypeScript types
|
||||||
|
- `/services/console/frontend/src/api/service.ts` - API client
|
||||||
|
|
||||||
### Earlier Checkpoints
|
### Earlier Checkpoints
|
||||||
✅ Project structure planning (CLAUDE.md)
|
✅ Project structure planning (CLAUDE.md)
|
||||||
✅ Implementation plan created (docs/PLAN.md)
|
✅ Implementation plan created (docs/PLAN.md)
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import logging
|
|||||||
|
|
||||||
from .core.config import settings
|
from .core.config import settings
|
||||||
from .db.mongodb import MongoDB
|
from .db.mongodb import MongoDB
|
||||||
from .routes import auth
|
from .routes import auth, services
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@ -56,6 +56,7 @@ app.add_middleware(
|
|||||||
|
|
||||||
# Include routers
|
# Include routers
|
||||||
app.include_router(auth.router)
|
app.include_router(auth.router)
|
||||||
|
app.include_router(services.router)
|
||||||
|
|
||||||
|
|
||||||
# Health check endpoints
|
# Health check endpoints
|
||||||
|
|||||||
81
services/console/backend/app/models/service.py
Normal file
81
services/console/backend/app/models/service.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
|
from bson import ObjectId
|
||||||
|
from pydantic_core import core_schema
|
||||||
|
|
||||||
|
|
||||||
|
class PyObjectId(str):
|
||||||
|
"""Custom ObjectId type for Pydantic v2"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __get_pydantic_core_schema__(cls, source_type, handler):
|
||||||
|
return core_schema.union_schema([
|
||||||
|
core_schema.is_instance_schema(ObjectId),
|
||||||
|
core_schema.chain_schema([
|
||||||
|
core_schema.str_schema(),
|
||||||
|
core_schema.no_info_plain_validator_function(cls.validate),
|
||||||
|
])
|
||||||
|
],
|
||||||
|
serialization=core_schema.plain_serializer_function_ser_schema(
|
||||||
|
lambda x: str(x)
|
||||||
|
))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls, v):
|
||||||
|
if not ObjectId.is_valid(v):
|
||||||
|
raise ValueError("Invalid ObjectId")
|
||||||
|
return ObjectId(v)
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceStatus:
|
||||||
|
"""Service status constants"""
|
||||||
|
HEALTHY = "healthy"
|
||||||
|
UNHEALTHY = "unhealthy"
|
||||||
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceType:
|
||||||
|
"""Service type constants"""
|
||||||
|
BACKEND = "backend"
|
||||||
|
FRONTEND = "frontend"
|
||||||
|
DATABASE = "database"
|
||||||
|
CACHE = "cache"
|
||||||
|
MESSAGE_QUEUE = "message_queue"
|
||||||
|
OTHER = "other"
|
||||||
|
|
||||||
|
|
||||||
|
class Service(BaseModel):
|
||||||
|
"""Service model for MongoDB"""
|
||||||
|
id: Optional[PyObjectId] = Field(alias="_id", default=None)
|
||||||
|
name: str = Field(..., min_length=1, max_length=100)
|
||||||
|
description: Optional[str] = Field(default=None, max_length=500)
|
||||||
|
service_type: str = Field(default=ServiceType.BACKEND)
|
||||||
|
url: str = Field(..., min_length=1)
|
||||||
|
health_endpoint: Optional[str] = Field(default="/health")
|
||||||
|
status: str = Field(default=ServiceStatus.UNKNOWN)
|
||||||
|
last_health_check: Optional[datetime] = None
|
||||||
|
response_time_ms: Optional[float] = None
|
||||||
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
metadata: Dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
populate_by_name=True,
|
||||||
|
arbitrary_types_allowed=True,
|
||||||
|
json_encoders={ObjectId: str},
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"name": "News API",
|
||||||
|
"description": "News generation and management API",
|
||||||
|
"service_type": "backend",
|
||||||
|
"url": "http://news-api:8050",
|
||||||
|
"health_endpoint": "/health",
|
||||||
|
"status": "healthy",
|
||||||
|
"metadata": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"port": 8050
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
113
services/console/backend/app/routes/services.py
Normal file
113
services/console/backend/app/routes/services.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
from typing import List
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
|
||||||
|
from app.models.service import Service
|
||||||
|
from app.models.user import User
|
||||||
|
from app.schemas.service import (
|
||||||
|
ServiceCreate,
|
||||||
|
ServiceUpdate,
|
||||||
|
ServiceResponse,
|
||||||
|
ServiceHealthCheck
|
||||||
|
)
|
||||||
|
from app.services.service_service import ServiceService
|
||||||
|
from app.core.security import get_current_user
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/services", tags=["services"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=ServiceResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_service(
|
||||||
|
service_data: ServiceCreate,
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a new service
|
||||||
|
|
||||||
|
Requires authentication.
|
||||||
|
"""
|
||||||
|
service = await ServiceService.create_service(service_data)
|
||||||
|
return service.model_dump(by_alias=True)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=List[ServiceResponse])
|
||||||
|
async def get_all_services(
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get all services
|
||||||
|
|
||||||
|
Requires authentication.
|
||||||
|
"""
|
||||||
|
services = await ServiceService.get_all_services()
|
||||||
|
return [service.model_dump(by_alias=True) for service in services]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{service_id}", response_model=ServiceResponse)
|
||||||
|
async def get_service(
|
||||||
|
service_id: str,
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get a service by ID
|
||||||
|
|
||||||
|
Requires authentication.
|
||||||
|
"""
|
||||||
|
service = await ServiceService.get_service(service_id)
|
||||||
|
return service.model_dump(by_alias=True)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{service_id}", response_model=ServiceResponse)
|
||||||
|
async def update_service(
|
||||||
|
service_id: str,
|
||||||
|
service_data: ServiceUpdate,
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Update a service
|
||||||
|
|
||||||
|
Requires authentication.
|
||||||
|
"""
|
||||||
|
service = await ServiceService.update_service(service_id, service_data)
|
||||||
|
return service.model_dump(by_alias=True)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{service_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def delete_service(
|
||||||
|
service_id: str,
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Delete a service
|
||||||
|
|
||||||
|
Requires authentication.
|
||||||
|
"""
|
||||||
|
await ServiceService.delete_service(service_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{service_id}/health-check", response_model=ServiceHealthCheck)
|
||||||
|
async def check_service_health(
|
||||||
|
service_id: str,
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Check health of a specific service
|
||||||
|
|
||||||
|
Requires authentication.
|
||||||
|
"""
|
||||||
|
result = await ServiceService.check_service_health(service_id)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/health-check/all", response_model=List[ServiceHealthCheck])
|
||||||
|
async def check_all_services_health(
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Check health of all services
|
||||||
|
|
||||||
|
Requires authentication.
|
||||||
|
"""
|
||||||
|
results = await ServiceService.check_all_services_health()
|
||||||
|
return results
|
||||||
93
services/console/backend/app/schemas/service.py
Normal file
93
services/console/backend/app/schemas/service.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceCreate(BaseModel):
|
||||||
|
"""Schema for creating a new service"""
|
||||||
|
name: str = Field(..., min_length=1, max_length=100)
|
||||||
|
description: Optional[str] = Field(default=None, max_length=500)
|
||||||
|
service_type: str = Field(default="backend")
|
||||||
|
url: str = Field(..., min_length=1)
|
||||||
|
health_endpoint: Optional[str] = Field(default="/health")
|
||||||
|
metadata: Dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"name": "News API",
|
||||||
|
"description": "News generation and management API",
|
||||||
|
"service_type": "backend",
|
||||||
|
"url": "http://news-api:8050",
|
||||||
|
"health_endpoint": "/health",
|
||||||
|
"metadata": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"port": 8050
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceUpdate(BaseModel):
|
||||||
|
"""Schema for updating a service"""
|
||||||
|
name: Optional[str] = Field(default=None, min_length=1, max_length=100)
|
||||||
|
description: Optional[str] = Field(default=None, max_length=500)
|
||||||
|
service_type: Optional[str] = None
|
||||||
|
url: Optional[str] = Field(default=None, min_length=1)
|
||||||
|
health_endpoint: Optional[str] = None
|
||||||
|
metadata: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"description": "Updated description",
|
||||||
|
"metadata": {
|
||||||
|
"version": "1.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceResponse(BaseModel):
|
||||||
|
"""Schema for service response"""
|
||||||
|
id: str = Field(alias="_id")
|
||||||
|
name: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
service_type: str
|
||||||
|
url: str
|
||||||
|
health_endpoint: Optional[str] = None
|
||||||
|
status: str
|
||||||
|
last_health_check: Optional[datetime] = None
|
||||||
|
response_time_ms: Optional[float] = None
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
metadata: Dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
populate_by_name=True,
|
||||||
|
from_attributes=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceHealthCheck(BaseModel):
|
||||||
|
"""Schema for health check result"""
|
||||||
|
service_id: str
|
||||||
|
service_name: str
|
||||||
|
status: str
|
||||||
|
response_time_ms: Optional[float] = None
|
||||||
|
checked_at: datetime
|
||||||
|
error_message: Optional[str] = None
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"service_id": "507f1f77bcf86cd799439011",
|
||||||
|
"service_name": "News API",
|
||||||
|
"status": "healthy",
|
||||||
|
"response_time_ms": 45.2,
|
||||||
|
"checked_at": "2025-10-28T10:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
212
services/console/backend/app/services/service_service.py
Normal file
212
services/console/backend/app/services/service_service.py
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Optional
|
||||||
|
import time
|
||||||
|
import httpx
|
||||||
|
from bson import ObjectId
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
|
from app.db.mongodb import MongoDB
|
||||||
|
from app.models.service import Service, ServiceStatus
|
||||||
|
from app.schemas.service import ServiceCreate, ServiceUpdate, ServiceHealthCheck
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceService:
|
||||||
|
"""Service management business logic"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def create_service(service_data: ServiceCreate) -> Service:
|
||||||
|
"""Create a new service"""
|
||||||
|
db = MongoDB.db
|
||||||
|
|
||||||
|
# Check if service with same name already exists
|
||||||
|
existing = await db.services.find_one({"name": service_data.name})
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Service with this name already exists"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create service document
|
||||||
|
service = Service(
|
||||||
|
**service_data.model_dump(),
|
||||||
|
status=ServiceStatus.UNKNOWN,
|
||||||
|
created_at=datetime.utcnow(),
|
||||||
|
updated_at=datetime.utcnow()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Insert into database
|
||||||
|
result = await db.services.insert_one(service.model_dump(by_alias=True, exclude={"id"}))
|
||||||
|
service.id = str(result.inserted_id)
|
||||||
|
|
||||||
|
return service
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_service(service_id: str) -> Service:
|
||||||
|
"""Get service by ID"""
|
||||||
|
db = MongoDB.db
|
||||||
|
|
||||||
|
if not ObjectId.is_valid(service_id):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid service ID"
|
||||||
|
)
|
||||||
|
|
||||||
|
service_doc = await db.services.find_one({"_id": ObjectId(service_id)})
|
||||||
|
if not service_doc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Service not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
return Service(**service_doc)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_all_services() -> List[Service]:
|
||||||
|
"""Get all services"""
|
||||||
|
db = MongoDB.db
|
||||||
|
|
||||||
|
cursor = db.services.find()
|
||||||
|
services = []
|
||||||
|
async for doc in cursor:
|
||||||
|
services.append(Service(**doc))
|
||||||
|
|
||||||
|
return services
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def update_service(service_id: str, service_data: ServiceUpdate) -> Service:
|
||||||
|
"""Update a service"""
|
||||||
|
db = MongoDB.db
|
||||||
|
|
||||||
|
if not ObjectId.is_valid(service_id):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid service ID"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get existing service
|
||||||
|
existing = await db.services.find_one({"_id": ObjectId(service_id)})
|
||||||
|
if not existing:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Service not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update only provided fields
|
||||||
|
update_data = service_data.model_dump(exclude_unset=True)
|
||||||
|
if update_data:
|
||||||
|
update_data["updated_at"] = datetime.utcnow()
|
||||||
|
|
||||||
|
# Check for name conflict if name is being updated
|
||||||
|
if "name" in update_data:
|
||||||
|
name_conflict = await db.services.find_one({
|
||||||
|
"name": update_data["name"],
|
||||||
|
"_id": {"$ne": ObjectId(service_id)}
|
||||||
|
})
|
||||||
|
if name_conflict:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Service with this name already exists"
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.services.update_one(
|
||||||
|
{"_id": ObjectId(service_id)},
|
||||||
|
{"$set": update_data}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return updated service
|
||||||
|
updated_doc = await db.services.find_one({"_id": ObjectId(service_id)})
|
||||||
|
return Service(**updated_doc)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def delete_service(service_id: str) -> bool:
|
||||||
|
"""Delete a service"""
|
||||||
|
db = MongoDB.db
|
||||||
|
|
||||||
|
if not ObjectId.is_valid(service_id):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid service ID"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await db.services.delete_one({"_id": ObjectId(service_id)})
|
||||||
|
if result.deleted_count == 0:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Service not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def check_service_health(service_id: str) -> ServiceHealthCheck:
|
||||||
|
"""Check health of a specific service"""
|
||||||
|
db = MongoDB.db
|
||||||
|
|
||||||
|
# Get service
|
||||||
|
service = await ServiceService.get_service(service_id)
|
||||||
|
|
||||||
|
# Perform health check
|
||||||
|
start_time = time.time()
|
||||||
|
status_result = ServiceStatus.UNKNOWN
|
||||||
|
error_message = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
health_url = f"{service.url.rstrip('/')}{service.health_endpoint or '/health'}"
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||||
|
response = await client.get(health_url)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
status_result = ServiceStatus.HEALTHY
|
||||||
|
else:
|
||||||
|
status_result = ServiceStatus.UNHEALTHY
|
||||||
|
error_message = f"HTTP {response.status_code}"
|
||||||
|
|
||||||
|
except httpx.TimeoutException:
|
||||||
|
status_result = ServiceStatus.UNHEALTHY
|
||||||
|
error_message = "Request timeout"
|
||||||
|
|
||||||
|
except httpx.RequestError as e:
|
||||||
|
status_result = ServiceStatus.UNHEALTHY
|
||||||
|
error_message = f"Connection error: {str(e)}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
status_result = ServiceStatus.UNHEALTHY
|
||||||
|
error_message = f"Error: {str(e)}"
|
||||||
|
|
||||||
|
response_time = (time.time() - start_time) * 1000 # Convert to ms
|
||||||
|
checked_at = datetime.utcnow()
|
||||||
|
|
||||||
|
# Update service status in database
|
||||||
|
await db.services.update_one(
|
||||||
|
{"_id": ObjectId(service_id)},
|
||||||
|
{
|
||||||
|
"$set": {
|
||||||
|
"status": status_result,
|
||||||
|
"last_health_check": checked_at,
|
||||||
|
"response_time_ms": response_time if status_result == ServiceStatus.HEALTHY else None,
|
||||||
|
"updated_at": checked_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return ServiceHealthCheck(
|
||||||
|
service_id=service_id,
|
||||||
|
service_name=service.name,
|
||||||
|
status=status_result,
|
||||||
|
response_time_ms=response_time if status_result == ServiceStatus.HEALTHY else None,
|
||||||
|
checked_at=checked_at,
|
||||||
|
error_message=error_message
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def check_all_services_health() -> List[ServiceHealthCheck]:
|
||||||
|
"""Check health of all services"""
|
||||||
|
services = await ServiceService.get_all_services()
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for service in services:
|
||||||
|
result = await ServiceService.check_service_health(str(service.id))
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
return results
|
||||||
45
services/console/frontend/src/api/service.ts
Normal file
45
services/console/frontend/src/api/service.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import api from './auth';
|
||||||
|
import type { Service, ServiceCreate, ServiceUpdate, ServiceHealthCheck } from '../types/service';
|
||||||
|
|
||||||
|
export const serviceAPI = {
|
||||||
|
// Get all services
|
||||||
|
getAll: async (): Promise<Service[]> => {
|
||||||
|
const { data } = await api.get<Service[]>('/api/services');
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get service by ID
|
||||||
|
getById: async (id: string): Promise<Service> => {
|
||||||
|
const { data } = await api.get<Service>(`/api/services/${id}`);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create new service
|
||||||
|
create: async (serviceData: ServiceCreate): Promise<Service> => {
|
||||||
|
const { data } = await api.post<Service>('/api/services', serviceData);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update service
|
||||||
|
update: async (id: string, serviceData: ServiceUpdate): Promise<Service> => {
|
||||||
|
const { data} = await api.put<Service>(`/api/services/${id}`, serviceData);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete service
|
||||||
|
delete: async (id: string): Promise<void> => {
|
||||||
|
await api.delete(`/api/services/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check service health
|
||||||
|
checkHealth: async (id: string): Promise<ServiceHealthCheck> => {
|
||||||
|
const { data } = await api.post<ServiceHealthCheck>(`/api/services/${id}/health-check`);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check all services health
|
||||||
|
checkAllHealth: async (): Promise<ServiceHealthCheck[]> => {
|
||||||
|
const { data } = await api.post<ServiceHealthCheck[]>('/api/services/health-check/all');
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
56
services/console/frontend/src/types/service.ts
Normal file
56
services/console/frontend/src/types/service.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
export interface Service {
|
||||||
|
_id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
service_type: ServiceType;
|
||||||
|
url: string;
|
||||||
|
health_endpoint?: string;
|
||||||
|
status: ServiceStatus;
|
||||||
|
last_health_check?: string;
|
||||||
|
response_time_ms?: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ServiceType {
|
||||||
|
BACKEND = 'backend',
|
||||||
|
FRONTEND = 'frontend',
|
||||||
|
DATABASE = 'database',
|
||||||
|
CACHE = 'cache',
|
||||||
|
MESSAGE_QUEUE = 'message_queue',
|
||||||
|
OTHER = 'other',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ServiceStatus {
|
||||||
|
HEALTHY = 'healthy',
|
||||||
|
UNHEALTHY = 'unhealthy',
|
||||||
|
UNKNOWN = 'unknown',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceCreate {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
service_type: ServiceType;
|
||||||
|
url: string;
|
||||||
|
health_endpoint?: string;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceUpdate {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
service_type?: ServiceType;
|
||||||
|
url?: string;
|
||||||
|
health_endpoint?: string;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceHealthCheck {
|
||||||
|
service_id: string;
|
||||||
|
service_name: string;
|
||||||
|
status: ServiceStatus;
|
||||||
|
response_time_ms?: number;
|
||||||
|
checked_at: string;
|
||||||
|
error_message?: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user