From 46f016f661ac51be295635640d6a0171c9dce4de Mon Sep 17 00:00:00 2001 From: swve Date: Sat, 23 Nov 2024 20:52:24 +0100 Subject: [PATCH] feat: better healthcheck --- apps/api/src/router.py | 3 ++ apps/api/src/routers/health.py | 11 ++++++ apps/api/src/services/health/__init__.py | 0 apps/api/src/services/health/health.py | 21 +++++++++++ apps/web/app/api/health/route.ts | 39 +++++++++++++++++++++ apps/web/app/orgs/[orgslug]/health/page.tsx | 9 ----- apps/web/middleware.ts | 5 +++ apps/web/services/utils/health.ts | 33 +++++++++++++++++ 8 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 apps/api/src/routers/health.py create mode 100644 apps/api/src/services/health/__init__.py create mode 100644 apps/api/src/services/health/health.py create mode 100644 apps/web/app/api/health/route.ts delete mode 100644 apps/web/app/orgs/[orgslug]/health/page.tsx create mode 100644 apps/web/services/utils/health.ts diff --git a/apps/api/src/router.py b/apps/api/src/router.py index ef3e226c..2725a263 100644 --- a/apps/api/src/router.py +++ b/apps/api/src/router.py @@ -1,5 +1,6 @@ import os from fastapi import APIRouter, Depends +from src.routers import health from src.routers import usergroups from src.routers import dev, trail, users, auth, orgs, roles from src.routers.ai import ai @@ -42,6 +43,8 @@ if os.environ.get("CLOUD_INTERNAL_KEY"): dependencies=[Depends(cloud_internal.check_internal_cloud_key)], ) +v1_router.include_router(health.router, prefix="/health", tags=["health"]) + # Dev Routes v1_router.include_router( dev.router, diff --git a/apps/api/src/routers/health.py b/apps/api/src/routers/health.py new file mode 100644 index 00000000..68d78588 --- /dev/null +++ b/apps/api/src/routers/health.py @@ -0,0 +1,11 @@ +from fastapi import Depends, APIRouter +from sqlmodel import Session +from src.services.health.health import check_health +from src.core.events.database import get_db_session + + +router = APIRouter() + +@router.get("") +async def health(db_session: Session = Depends(get_db_session)): + return await check_health(db_session) \ No newline at end of file diff --git a/apps/api/src/services/health/__init__.py b/apps/api/src/services/health/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/api/src/services/health/health.py b/apps/api/src/services/health/health.py new file mode 100644 index 00000000..8daccbd9 --- /dev/null +++ b/apps/api/src/services/health/health.py @@ -0,0 +1,21 @@ +from fastapi import HTTPException +from sqlmodel import Session, select +from src.db.organizations import Organization + +async def check_database_health(db_session: Session) -> bool: + statement = select(Organization) + result = db_session.exec(statement) + + if not result: + return False + + return True + +async def check_health(db_session: Session) -> bool: + # Check database health + database_healthy = await check_database_health(db_session) + + if not database_healthy: + raise HTTPException(status_code=503, detail="Database is not healthy") + + return True diff --git a/apps/web/app/api/health/route.ts b/apps/web/app/api/health/route.ts new file mode 100644 index 00000000..182501ae --- /dev/null +++ b/apps/web/app/api/health/route.ts @@ -0,0 +1,39 @@ +export const dynamic = 'force-dynamic' // defaults to auto +export const revalidate = 0 + +import { NextResponse } from 'next/server'; +import { checkHealth } from '@services/utils/health'; + +export async function GET() { + const health = await checkHealth() + if (health.success === true) { + return NextResponse.json( + { + status: 'healthy', + timestamp: new Date().toISOString(), + health: health.data, + }, + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + } + ) + } else { + return NextResponse.json( + { + status: 'unhealthy', + timestamp: new Date().toISOString(), + health: null, + error: health.HTTPmessage, + }, + { + status: 503, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } +} diff --git a/apps/web/app/orgs/[orgslug]/health/page.tsx b/apps/web/app/orgs/[orgslug]/health/page.tsx deleted file mode 100644 index e9d89171..00000000 --- a/apps/web/app/orgs/[orgslug]/health/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react' - -function HealthPage() { - return ( -
OK
- ) -} - -export default HealthPage \ No newline at end of file diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index f98a41b0..423f6a5d 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -102,6 +102,11 @@ export default async function middleware(req: NextRequest) { return NextResponse.rewrite(redirectUrl) } + // Health Check + if (pathname.startsWith('/health')) { + return NextResponse.rewrite(new URL(`/api/health`, req.url)) + } + // Auth Redirects if (pathname == '/redirect_from_auth') { if (cookie_orgslug) { diff --git a/apps/web/services/utils/health.ts b/apps/web/services/utils/health.ts new file mode 100644 index 00000000..78cd8b63 --- /dev/null +++ b/apps/web/services/utils/health.ts @@ -0,0 +1,33 @@ +import { getAPIUrl } from '@services/config/config' +import { + RequestBody, + getResponseMetadata, +} from '@services/utils/ts/requests' + +export async function checkHealth() { + try { + const result = await fetch( + `${getAPIUrl()}health`, + RequestBody('GET', null, null) + ) + + if (!result.ok) { + return { + success: false, + status: result.status, + HTTPmessage: result.statusText, + data: null + } + } + + const res = await getResponseMetadata(result) + return res + } catch (error) { + return { + success: false, + status: 503, + HTTPmessage: 'Service unavailable', + data: null + } + } +}