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>
213 lines
6.9 KiB
Python
213 lines
6.9 KiB
Python
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
|