Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions docs/getting-started/first-run.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,28 @@ curl http://localhost:8000/api/v1/health
Expected response:
```json
{
"status": "healthy",
"timestamp": "2024-01-01T12:00:00Z"
"status":"healthy",
"environment":"local",
"version":"0.1.0",
"timestamp":"2025-10-21T14:40:14+00:00"
}
```

**Ready Check:**
```bash
curl http://localhost:8000/api/v1/ready
```

Expected response:
```json
{
"status":"healthy",
"environment":"local",
"version":"0.1.0",
"app":"healthy",
"database":"healthy",
"redis":"healthy",
"timestamp":"2025-10-21T14:40:47+00:00"
}
```

Expand Down
10 changes: 8 additions & 2 deletions docs/getting-started/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ Visit these URLs to confirm everything is working:
- **API Documentation**: [http://localhost:8000/docs](http://localhost:8000/docs)
- **Alternative Docs**: [http://localhost:8000/redoc](http://localhost:8000/redoc)
- **Health Check**: [http://localhost:8000/api/v1/health](http://localhost:8000/api/v1/health)
- **Ready Check**: [http://localhost:8000/api/v1/ready](http://localhost:8000/api/v1/ready)

## You're Ready!

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

### 2. Create a User
### 2. Ready Check
```bash
curl http://localhost:8000/api/v1/ready
```

### 3. Create a User
```bash
curl -X POST "http://localhost:8000/api/v1/users" \
-H "Content-Type: application/json" \
Expand All @@ -138,7 +144,7 @@ curl -X POST "http://localhost:8000/api/v1/users" \
}'
```

### 3. Login
### 4. Login
```bash
curl -X POST "http://localhost:8000/api/v1/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
Expand Down
7 changes: 4 additions & 3 deletions docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,10 @@ After installation, verify everything works:

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

## Troubleshooting

Expand Down
15 changes: 0 additions & 15 deletions docs/user-guide/configuration/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -526,21 +526,6 @@ if settings.ENABLE_ADVANCED_CACHING:
pass
```

### Health Checks

Configure health check endpoints:

```python
@app.get("/health")
async def health_check():
return {
"status": "healthy",
"database": await check_database_health(),
"redis": await check_redis_health(),
"version": settings.APP_VERSION
}
```

## Configuration Validation

### Environment Validation
Expand Down
50 changes: 7 additions & 43 deletions docs/user-guide/production.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,13 @@ http {
access_log off;
}

# Ready check endpoint (no rate limiting)
location /ready {
proxy_pass http://fastapi_backend;
proxy_set_header Host $host;
access_log off;
}

# Static files (if any)
location /static/ {
alias /code/static/;
Expand Down Expand Up @@ -554,49 +561,6 @@ DEFAULT_RATE_LIMIT_LIMIT = 100 # requests per period
DEFAULT_RATE_LIMIT_PERIOD = 3600 # 1 hour
```

### Health Checks

#### Application Health Check

```python
# src/app/api/v1/health.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from ...core.db.database import async_get_db
from ...core.utils.cache import redis_client

router = APIRouter()

@router.get("/health")
async def health_check():
return {"status": "healthy", "timestamp": datetime.utcnow()}

@router.get("/health/detailed")
async def detailed_health_check(db: AsyncSession = Depends(async_get_db)):
health_status = {"status": "healthy", "services": {}}

# Check database
try:
await db.execute("SELECT 1")
health_status["services"]["database"] = "healthy"
except Exception:
health_status["services"]["database"] = "unhealthy"
health_status["status"] = "unhealthy"

# Check Redis
try:
await redis_client.ping()
health_status["services"]["redis"] = "healthy"
except Exception:
health_status["services"]["redis"] = "unhealthy"
health_status["status"] = "unhealthy"

if health_status["status"] == "unhealthy":
raise HTTPException(status_code=503, detail=health_status)

return health_status
```

### Deployment Process

#### CI/CD Pipeline (GitHub Actions)
Expand Down
2 changes: 2 additions & 0 deletions src/app/api/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fastapi import APIRouter

from .health import router as health_router
from .login import router as login_router
from .logout import router as logout_router
from .posts import router as posts_router
Expand All @@ -9,6 +10,7 @@
from .users import router as users_router

router = APIRouter(prefix="/v1")
router.include_router(health_router)
router.include_router(login_router)
router.include_router(logout_router)
router.include_router(users_router)
Expand Down
57 changes: 57 additions & 0 deletions src/app/api/v1/health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import logging
from datetime import UTC, datetime
from typing import Annotated

from fastapi import APIRouter, Depends, status
from fastapi.responses import JSONResponse
from redis.asyncio import Redis
from sqlalchemy.ext.asyncio import AsyncSession

from ...core.config import settings
from ...core.db.database import async_get_db
from ...core.health import check_database_health, check_redis_health
from ...core.schemas import HealthCheck, ReadyCheck
from ...core.utils.cache import async_get_redis

router = APIRouter(tags=["health"])

STATUS_HEALTHY = "healthy"
STATUS_UNHEALTHY = "unhealthy"

LOGGER = logging.getLogger(__name__)


@router.get("/health", response_model=HealthCheck)
async def health():
http_status = status.HTTP_200_OK
response = {
"status": STATUS_HEALTHY,
"environment": settings.ENVIRONMENT.value,
"version": settings.APP_VERSION,
"timestamp": datetime.now(UTC).isoformat(timespec="seconds"),
}

return JSONResponse(status_code=http_status, content=response)


@router.get("/ready", response_model=ReadyCheck)
async def ready(redis: Annotated[Redis, Depends(async_get_redis)], db: Annotated[AsyncSession, Depends(async_get_db)]):
database_status = await check_database_health(db=db)
LOGGER.debug(f"Database health check status: {database_status}")
redis_status = await check_redis_health(redis=redis)
LOGGER.debug(f"Redis health check status: {redis_status}")

overall_status = STATUS_HEALTHY if database_status and redis_status else STATUS_UNHEALTHY
http_status = status.HTTP_200_OK if overall_status == STATUS_HEALTHY else status.HTTP_503_SERVICE_UNAVAILABLE

response = {
"status": overall_status,
"environment": settings.ENVIRONMENT.value,
"version": settings.APP_VERSION,
"app": STATUS_HEALTHY,
"database": STATUS_HEALTHY if database_status else STATUS_UNHEALTHY,
"redis": STATUS_HEALTHY if redis_status else STATUS_UNHEALTHY,
"timestamp": datetime.now(UTC).isoformat(timespec="seconds"),
}

return JSONResponse(status_code=http_status, content=response)
25 changes: 25 additions & 0 deletions src/app/core/health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import logging

from redis.asyncio import Redis
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession

LOGGER = logging.getLogger(__name__)


async def check_database_health(db: AsyncSession) -> bool:
try:
await db.execute(text("SELECT 1"))
return True
except Exception as e:
LOGGER.exception(f"Database health check failed with error: {e}")
return False


async def check_redis_health(redis: Redis) -> bool:
try:
await redis.ping()
return True
except Exception as e:
LOGGER.exception(f"Redis health check failed with error: {e}")
return False
17 changes: 14 additions & 3 deletions src/app/core/schemas.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import uuid as uuid_pkg
from uuid6 import uuid7
from datetime import UTC, datetime
from typing import Any

from pydantic import BaseModel, Field, field_serializer
from uuid6 import uuid7


class HealthCheck(BaseModel):
name: str
status: str
environment: str
version: str
timestamp: str


class ReadyCheck(BaseModel):
status: str
environment: str
version: str
description: str
app: str
database: str
redis: str
timestamp: str


# -------------- mixins --------------
Expand Down
17 changes: 13 additions & 4 deletions src/app/core/utils/cache.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import functools
import json
import re
from collections.abc import Callable
from collections.abc import AsyncGenerator, Callable
from typing import Any

from fastapi import Request
Expand Down Expand Up @@ -173,13 +173,13 @@ async def _delete_keys_by_pattern(pattern: str) -> None:
"""
if client is None:
return
cursor = 0

cursor = 0
while True:
cursor, keys = await client.scan(cursor, match=pattern, count=100)
if keys:
await client.delete(*keys)
if cursor == 0:
if cursor == 0:
break


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

return wrapper


async def async_get_redis() -> AsyncGenerator[Redis, None]:
"""Get a Redis client from the pool for each request."""
client = Redis(connection_pool=pool)
try:
yield client
finally:
await client.aclose() # type: ignore