Files
site11/services/console/backend/app/services/service_service.py
jungwoo choi e60e531cdc 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>
2025-10-28 16:44:33 +09:00

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