Files
headquarter/apps/api/src/api/health.py
T
Fusion 2bd778117f feat(tunnels): switch to temporary Cloudflare tunnels
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
2026-05-20 16:23:16 +02:00

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()