2bd778117f
Replace persistent Cloudflare tunnels (API-based) with temporary tunnels using 'cloudflared tunnel --url'. This removes the need for Cloudflare API tokens, DNS records, and persistent tunnel management. Changes: - Install cloudflared binary in API Dockerfile - Add start_cloudflared_tunnel() and stop_cloudflared_tunnel() to docker.py - Update instance start/stop/restart/delete to use temporary tunnels - Store tunnel PID in tunnel_id field, temporary URL in url/public_url - Remove Cloudflare API service (cloudflare_tunnel.py) - Remove cloudflared container from docker-compose - Remove Cloudflare env vars (CLOUDFLARE_API_TOKEN, ZONE_ID, etc.) - Remove Cloudflare configuration from config.py - Remove Cloudflare startup check from main.py - Remove /health/cloudflare endpoint
150 lines
4.5 KiB
Python
150 lines
4.5 KiB
Python
"""Health check endpoints and models."""
|
|
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, status
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy import text
|
|
|
|
from src.config import Settings
|
|
from src.database import SessionLocal
|
|
|
|
router = APIRouter()
|
|
|
|
# Track start time for uptime
|
|
_start_time = time.time()
|
|
|
|
|
|
class DatabaseHealth(BaseModel):
|
|
"""Database health check result."""
|
|
|
|
status: str = Field(description="Database health status", examples=["healthy"])
|
|
response_time_ms: float = Field(description="Query response time in milliseconds", examples=[5.2])
|
|
|
|
|
|
class DiskHealth(BaseModel):
|
|
"""Disk space health check result."""
|
|
|
|
status: str = Field(description="Disk health status", examples=["healthy"])
|
|
free_gb: float = Field(description="Free disk space in GB", examples=[45.2])
|
|
total_gb: float = Field(description="Total disk space in GB", examples=[100.0])
|
|
|
|
|
|
class HealthChecks(BaseModel):
|
|
"""Individual health checks."""
|
|
|
|
database: DatabaseHealth | None = None
|
|
disk: DiskHealth | None = None
|
|
|
|
|
|
class HealthResponse(BaseModel):
|
|
"""Overall health check response."""
|
|
|
|
status: str = Field(description="Overall health status", examples=["healthy"])
|
|
timestamp: str = Field(description="ISO 8601 timestamp", examples=["2026-05-19T12:00:00Z"])
|
|
version: str = Field(description="API version", examples=["0.1.0"])
|
|
checks: HealthChecks = Field(description="Individual health checks")
|
|
uptime_seconds: float = Field(description="Server uptime in seconds", examples=[3600.0])
|
|
|
|
|
|
class DatabaseHealthResponse(BaseModel):
|
|
"""Database-specific health check response."""
|
|
|
|
status: str = Field(description="Database health status", examples=["healthy"])
|
|
response_time_ms: float = Field(description="Query response time in milliseconds", examples=[5.2])
|
|
|
|
|
|
@router.get(
|
|
"/health",
|
|
response_model=HealthResponse,
|
|
summary="Health check",
|
|
description="Returns overall system health status including database and disk checks.",
|
|
tags=["Health"],
|
|
)
|
|
async def health_check() -> dict[str, Any]:
|
|
"""Check overall system health.
|
|
|
|
Returns:
|
|
HealthResponse with status, timestamp, version, checks, and uptime.
|
|
"""
|
|
checks = HealthChecks()
|
|
overall_status = "healthy"
|
|
|
|
# Database check
|
|
try:
|
|
import time as time_module
|
|
|
|
start = time_module.perf_counter()
|
|
async with SessionLocal() as session:
|
|
await session.execute(text("SELECT 1"))
|
|
db_time = (time_module.perf_counter() - start) * 1000
|
|
checks.database = DatabaseHealth(
|
|
status="healthy",
|
|
response_time_ms=round(db_time, 2),
|
|
)
|
|
except Exception:
|
|
checks.database = DatabaseHealth(
|
|
status="unhealthy",
|
|
response_time_ms=0.0,
|
|
)
|
|
overall_status = "degraded"
|
|
|
|
# Disk check
|
|
try:
|
|
import shutil
|
|
|
|
disk = shutil.disk_usage("/")
|
|
free_gb = disk.free / (1024**3)
|
|
total_gb = disk.total / (1024**3)
|
|
disk_status = "healthy" if free_gb > 1.0 else "degraded"
|
|
if disk_status == "degraded":
|
|
overall_status = "degraded"
|
|
checks.disk = DiskHealth(
|
|
status=disk_status,
|
|
free_gb=round(free_gb, 2),
|
|
total_gb=round(total_gb, 2),
|
|
)
|
|
except Exception:
|
|
checks.disk = None
|
|
|
|
return HealthResponse(
|
|
status=overall_status,
|
|
timestamp=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
version="0.1.0",
|
|
checks=checks,
|
|
uptime_seconds=round(time.time() - _start_time, 2),
|
|
).model_dump()
|
|
|
|
|
|
@router.get(
|
|
"/health/db",
|
|
response_model=DatabaseHealthResponse,
|
|
summary="Database health check",
|
|
description="Returns database-specific health status with response time.",
|
|
tags=["Health"],
|
|
)
|
|
async def health_check_db() -> dict[str, Any]:
|
|
"""Check database health.
|
|
|
|
Returns:
|
|
DatabaseHealthResponse with status and response time.
|
|
"""
|
|
import time as time_module
|
|
|
|
try:
|
|
start = time_module.perf_counter()
|
|
async with SessionLocal() as session:
|
|
await session.execute(text("SELECT 1"))
|
|
db_time = (time_module.perf_counter() - start) * 1000
|
|
return DatabaseHealthResponse(
|
|
status="healthy",
|
|
response_time_ms=round(db_time, 2),
|
|
).model_dump()
|
|
except Exception:
|
|
return DatabaseHealthResponse(
|
|
status="unhealthy",
|
|
response_time_ms=0.0,
|
|
).model_dump()
|