11from contextlib import asynccontextmanager
22import datetime
33import os
4- from typing import Annotated , Any , Dict
4+ from typing import Annotated , Any , Dict , Optional
55
66from fastapi import Depends , FastAPI
77from fastapi .middleware .cors import CORSMiddleware
8- from fastapi_health import health
8+ from fastapi . responses import JSONResponse
99from loguru import logger
10+ from pydantic import BaseModel , Field
1011
1112from config import Settings
1213from src .interface_adapters import api_router
2122running = False
2223
2324
24- async def isRunning () -> bool :
25+ def isRunning () -> bool :
2526 logger .info (f"running: { running } " )
2627 return running
2728
@@ -31,15 +32,71 @@ async def isRunning() -> bool:
3132async def lifespan (app : FastAPI ):
3233 global running
3334 running = True
35+ logger .info ("Lifespan started" )
3436 yield
3537 running = False
38+ logger .info ("Lifespan stopped" )
3639
3740
38- async def isConfigured () :
41+ def is_configured () -> bool :
3942 logger .info (f"configured: { Settings .get_settings ().env_smoke_test == "configured" } " )
4043 return Settings .get_settings ().env_smoke_test == "configured"
4144
4245
46+ """ {
47+ "status": "pass|fail|warn", // Required
48+ "version": "1.0", // Optional
49+ "description": "...", // Optional
50+ "checks": { // Optional, for detailed component status
51+ "database": {
52+ "status": "up",
53+ "responseTime": "23ms"
54+ },
55+ "cache": {
56+ "status": "up",
57+ "responseTime": "5ms"
58+ }
59+ }
60+ } """
61+
62+
63+ class HealthCheck (BaseModel ):
64+ """RFC Health Check response format."""
65+
66+ status : str = Field (
67+ ..., description = "Required. Status of the service: pass, fail, or warn"
68+ )
69+ version : Optional [str ] = Field (None , description = "Version of the service" )
70+ description : Optional [str ] = Field (
71+ None , description = "Human-friendly description of the service"
72+ )
73+ checks : Optional [Dict [str , Dict [str , Any ]]] = Field (
74+ None , description = "Additional checks"
75+ )
76+
77+
78+ def create_health_response (is_healthy : bool , check_name : str ) -> JSONResponse :
79+ """Create a standardized health check response."""
80+ status = "pass" if is_healthy else "fail"
81+ response = HealthCheck (
82+ status = status ,
83+ version = Settings .get_settings ().project_name ,
84+ description = f"Service { status } " ,
85+ checks = {
86+ check_name : {
87+ "status" : status ,
88+ "time" : datetime .datetime .now ().isoformat (),
89+ }
90+ },
91+ )
92+
93+ return JSONResponse (
94+ status_code = 200 if is_healthy else 503 ,
95+ content = response .model_dump (),
96+ media_type = "application/health+json" ,
97+ )
98+
99+
43100app = FastAPI (
44101 lifespan = lifespan ,
45102 title = Settings .get_settings ().project_name ,
@@ -80,18 +137,44 @@ async def info(
80137 "system_time" : datetime .datetime .now (),
81138 "execution_mode" : settings .execution_mode ,
82139 "env_smoke_test" : settings .env_smoke_test ,
83- "items_per_user" : settings .project_name ,
84140 }
85141
86142
87- # https://docs.paperspace.com/gradient/deployments/healthchecks/
88- # https://github.com/Kludex/fastapi-health
89- # https://github.com/healthchecks/healthchecks
90- app .add_api_route ("/health" , health ([isRunning ]))
91- app .add_api_route ("/startup" , health ([isRunning ]))
92- app .add_api_route ("/readiness" , health ([isRunning ]))
93- app .add_api_route ("/liveness" , health ([isRunning ]))
94- app .add_api_route ("/smoke" , health ([isConfigured ]))
143+ # Health check endpoints
144+ # Startup: Signals if the application has completed its initial startup
145+ # Smoke: Verifies basic configuration and dependencies
146+ # Readiness: Shows if the application is ready to handle requests
147+ # Liveness: Indicates if the application is running and alive
148+ # https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check
149+ @app .get ("/health" , response_class = JSONResponse )
150+ async def health_check () -> JSONResponse :
151+ """Overall service health check."""
152+ return create_health_response (isRunning (), "service" )
153+
154+
155+ @app .get ("/liveness" , response_class = JSONResponse )
156+ async def liveness_check () -> JSONResponse :
157+ """Service liveness check."""
158+ return create_health_response (isRunning (), "liveness" )
159+
160+
161+ @app .get ("/startup" , response_class = JSONResponse )
162+ async def startup_check () -> JSONResponse :
163+ """Startup health check."""
164+ return create_health_response (isRunning (), "startup" )
165+
166+
167+ @app .get ("/readiness" , response_class = JSONResponse )
168+ async def readiness_check () -> JSONResponse :
169+ """Service readiness check."""
170+ return create_health_response (isRunning (), "readiness" )
171+
172+
173+ @app .get ("/smoke" , response_class = JSONResponse )
174+ async def smoke_check () -> JSONResponse :
175+ """Configuration smoke test."""
176+ return create_health_response (is_configured (), "configuration" )
177+
95178
96179# add feature routers here
97180app .include_router (api_router , prefix = "/v1" )
@@ -104,6 +187,5 @@ async def info(
104187# https://fastapi.tiangolo.com/tutorial/metadata/
105188
106189# reverse proxy and system serice manager
107- # https://docs.sisk-framework.org/docs/deploying/production/
108190# https://medium.com/@kevinzeladacl/deploy-a-fastapi-app-with-nginx-and-gunicorn-b66ac14cdf5a
109191# https://fastapi.tiangolo.com/advanced/behind-a-proxy/
0 commit comments