Skip to content

Commit 21fa78f

Browse files
authored
Merge pull request #210 from rragundez/health-checks
Add health check endpoints
2 parents 38151b8 + 4a7a2f6 commit 21fa78f

File tree

10 files changed

+152
-72
lines changed

10 files changed

+152
-72
lines changed

docs/getting-started/first-run.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,28 @@ curl http://localhost:8000/api/v1/health
3838
Expected response:
3939
```json
4040
{
41-
"status": "healthy",
42-
"timestamp": "2024-01-01T12:00:00Z"
41+
"status":"healthy",
42+
"environment":"local",
43+
"version":"0.1.0",
44+
"timestamp":"2025-10-21T14:40:14+00:00"
45+
}
46+
```
47+
48+
**Ready Check:**
49+
```bash
50+
curl http://localhost:8000/api/v1/ready
51+
```
52+
53+
Expected response:
54+
```json
55+
{
56+
"status":"healthy",
57+
"environment":"local",
58+
"version":"0.1.0",
59+
"app":"healthy",
60+
"database":"healthy",
61+
"redis":"healthy",
62+
"timestamp":"2025-10-21T14:40:47+00:00"
4363
}
4464
```
4565

docs/getting-started/index.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ Visit these URLs to confirm everything is working:
104104
- **API Documentation**: [http://localhost:8000/docs](http://localhost:8000/docs)
105105
- **Alternative Docs**: [http://localhost:8000/redoc](http://localhost:8000/redoc)
106106
- **Health Check**: [http://localhost:8000/api/v1/health](http://localhost:8000/api/v1/health)
107+
- **Ready Check**: [http://localhost:8000/api/v1/ready](http://localhost:8000/api/v1/ready)
107108

108109
## You're Ready!
109110

@@ -126,7 +127,12 @@ Try these quick tests to see your API in action:
126127
curl http://localhost:8000/api/v1/health
127128
```
128129

129-
### 2. Create a User
130+
### 2. Ready Check
131+
```bash
132+
curl http://localhost:8000/api/v1/ready
133+
```
134+
135+
### 3. Create a User
130136
```bash
131137
curl -X POST "http://localhost:8000/api/v1/users" \
132138
-H "Content-Type: application/json" \
@@ -138,7 +144,7 @@ curl -X POST "http://localhost:8000/api/v1/users" \
138144
}'
139145
```
140146

141-
### 3. Login
147+
### 4. Login
142148
```bash
143149
curl -X POST "http://localhost:8000/api/v1/login" \
144150
-H "Content-Type: application/x-www-form-urlencoded" \

docs/getting-started/installation.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,9 +290,10 @@ After installation, verify everything works:
290290

291291
1. **API Documentation**: http://localhost:8000/docs
292292
2. **Health Check**: http://localhost:8000/api/v1/health
293-
3. **Database Connection**: Check logs for successful connection
294-
4. **Redis Connection**: Test caching functionality
295-
5. **Background Tasks**: Submit a test job
293+
3. **Ready Check**: http://localhost:8000/api/v1/ready
294+
4. **Database Connection**: Check logs for successful connection
295+
5. **Redis Connection**: Test caching functionality
296+
6. **Background Tasks**: Submit a test job
296297

297298
## Troubleshooting
298299

docs/user-guide/configuration/environment-variables.md

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -526,21 +526,6 @@ if settings.ENABLE_ADVANCED_CACHING:
526526
pass
527527
```
528528

529-
### Health Checks
530-
531-
Configure health check endpoints:
532-
533-
```python
534-
@app.get("/health")
535-
async def health_check():
536-
return {
537-
"status": "healthy",
538-
"database": await check_database_health(),
539-
"redis": await check_redis_health(),
540-
"version": settings.APP_VERSION
541-
}
542-
```
543-
544529
## Configuration Validation
545530

546531
### Environment Validation

docs/user-guide/production.md

Lines changed: 7 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,13 @@ http {
318318
access_log off;
319319
}
320320
321+
# Ready check endpoint (no rate limiting)
322+
location /ready {
323+
proxy_pass http://fastapi_backend;
324+
proxy_set_header Host $host;
325+
access_log off;
326+
}
327+
321328
# Static files (if any)
322329
location /static/ {
323330
alias /code/static/;
@@ -554,49 +561,6 @@ DEFAULT_RATE_LIMIT_LIMIT = 100 # requests per period
554561
DEFAULT_RATE_LIMIT_PERIOD = 3600 # 1 hour
555562
```
556563

557-
### Health Checks
558-
559-
#### Application Health Check
560-
561-
```python
562-
# src/app/api/v1/health.py
563-
from fastapi import APIRouter, Depends, HTTPException
564-
from sqlalchemy.ext.asyncio import AsyncSession
565-
from ...core.db.database import async_get_db
566-
from ...core.utils.cache import redis_client
567-
568-
router = APIRouter()
569-
570-
@router.get("/health")
571-
async def health_check():
572-
return {"status": "healthy", "timestamp": datetime.utcnow()}
573-
574-
@router.get("/health/detailed")
575-
async def detailed_health_check(db: AsyncSession = Depends(async_get_db)):
576-
health_status = {"status": "healthy", "services": {}}
577-
578-
# Check database
579-
try:
580-
await db.execute("SELECT 1")
581-
health_status["services"]["database"] = "healthy"
582-
except Exception:
583-
health_status["services"]["database"] = "unhealthy"
584-
health_status["status"] = "unhealthy"
585-
586-
# Check Redis
587-
try:
588-
await redis_client.ping()
589-
health_status["services"]["redis"] = "healthy"
590-
except Exception:
591-
health_status["services"]["redis"] = "unhealthy"
592-
health_status["status"] = "unhealthy"
593-
594-
if health_status["status"] == "unhealthy":
595-
raise HTTPException(status_code=503, detail=health_status)
596-
597-
return health_status
598-
```
599-
600564
### Deployment Process
601565

602566
#### CI/CD Pipeline (GitHub Actions)

src/app/api/v1/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from fastapi import APIRouter
22

3+
from .health import router as health_router
34
from .login import router as login_router
45
from .logout import router as logout_router
56
from .posts import router as posts_router
@@ -9,6 +10,7 @@
910
from .users import router as users_router
1011

1112
router = APIRouter(prefix="/v1")
13+
router.include_router(health_router)
1214
router.include_router(login_router)
1315
router.include_router(logout_router)
1416
router.include_router(users_router)

src/app/api/v1/health.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import logging
2+
from datetime import UTC, datetime
3+
from typing import Annotated
4+
5+
from fastapi import APIRouter, Depends, status
6+
from fastapi.responses import JSONResponse
7+
from redis.asyncio import Redis
8+
from sqlalchemy.ext.asyncio import AsyncSession
9+
10+
from ...core.config import settings
11+
from ...core.db.database import async_get_db
12+
from ...core.health import check_database_health, check_redis_health
13+
from ...core.schemas import HealthCheck, ReadyCheck
14+
from ...core.utils.cache import async_get_redis
15+
16+
router = APIRouter(tags=["health"])
17+
18+
STATUS_HEALTHY = "healthy"
19+
STATUS_UNHEALTHY = "unhealthy"
20+
21+
LOGGER = logging.getLogger(__name__)
22+
23+
24+
@router.get("/health", response_model=HealthCheck)
25+
async def health():
26+
http_status = status.HTTP_200_OK
27+
response = {
28+
"status": STATUS_HEALTHY,
29+
"environment": settings.ENVIRONMENT.value,
30+
"version": settings.APP_VERSION,
31+
"timestamp": datetime.now(UTC).isoformat(timespec="seconds"),
32+
}
33+
34+
return JSONResponse(status_code=http_status, content=response)
35+
36+
37+
@router.get("/ready", response_model=ReadyCheck)
38+
async def ready(redis: Annotated[Redis, Depends(async_get_redis)], db: Annotated[AsyncSession, Depends(async_get_db)]):
39+
database_status = await check_database_health(db=db)
40+
LOGGER.debug(f"Database health check status: {database_status}")
41+
redis_status = await check_redis_health(redis=redis)
42+
LOGGER.debug(f"Redis health check status: {redis_status}")
43+
44+
overall_status = STATUS_HEALTHY if database_status and redis_status else STATUS_UNHEALTHY
45+
http_status = status.HTTP_200_OK if overall_status == STATUS_HEALTHY else status.HTTP_503_SERVICE_UNAVAILABLE
46+
47+
response = {
48+
"status": overall_status,
49+
"environment": settings.ENVIRONMENT.value,
50+
"version": settings.APP_VERSION,
51+
"app": STATUS_HEALTHY,
52+
"database": STATUS_HEALTHY if database_status else STATUS_UNHEALTHY,
53+
"redis": STATUS_HEALTHY if redis_status else STATUS_UNHEALTHY,
54+
"timestamp": datetime.now(UTC).isoformat(timespec="seconds"),
55+
}
56+
57+
return JSONResponse(status_code=http_status, content=response)

src/app/core/health.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import logging
2+
3+
from redis.asyncio import Redis
4+
from sqlalchemy import text
5+
from sqlalchemy.ext.asyncio import AsyncSession
6+
7+
LOGGER = logging.getLogger(__name__)
8+
9+
10+
async def check_database_health(db: AsyncSession) -> bool:
11+
try:
12+
await db.execute(text("SELECT 1"))
13+
return True
14+
except Exception as e:
15+
LOGGER.exception(f"Database health check failed with error: {e}")
16+
return False
17+
18+
19+
async def check_redis_health(redis: Redis) -> bool:
20+
try:
21+
await redis.ping()
22+
return True
23+
except Exception as e:
24+
LOGGER.exception(f"Redis health check failed with error: {e}")
25+
return False

src/app/core/schemas.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
import uuid as uuid_pkg
2-
from uuid6 import uuid7
32
from datetime import UTC, datetime
43
from typing import Any
54

65
from pydantic import BaseModel, Field, field_serializer
6+
from uuid6 import uuid7
77

88

99
class HealthCheck(BaseModel):
10-
name: str
10+
status: str
11+
environment: str
12+
version: str
13+
timestamp: str
14+
15+
16+
class ReadyCheck(BaseModel):
17+
status: str
18+
environment: str
1119
version: str
12-
description: str
20+
app: str
21+
database: str
22+
redis: str
23+
timestamp: str
1324

1425

1526
# -------------- mixins --------------

src/app/core/utils/cache.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import functools
22
import json
33
import re
4-
from collections.abc import Callable
4+
from collections.abc import AsyncGenerator, Callable
55
from typing import Any
66

77
from fastapi import Request
@@ -173,13 +173,13 @@ async def _delete_keys_by_pattern(pattern: str) -> None:
173173
"""
174174
if client is None:
175175
return
176-
177-
cursor = 0
176+
177+
cursor = 0
178178
while True:
179179
cursor, keys = await client.scan(cursor, match=pattern, count=100)
180180
if keys:
181181
await client.delete(*keys)
182-
if cursor == 0:
182+
if cursor == 0:
183183
break
184184

185185

@@ -335,3 +335,12 @@ async def inner(request: Request, *args: Any, **kwargs: Any) -> Any:
335335
return inner
336336

337337
return wrapper
338+
339+
340+
async def async_get_redis() -> AsyncGenerator[Redis, None]:
341+
"""Get a Redis client from the pool for each request."""
342+
client = Redis(connection_pool=pool)
343+
try:
344+
yield client
345+
finally:
346+
await client.aclose() # type: ignore

0 commit comments

Comments
 (0)