# Health Checks

Composable health check patterns for monitoring application dependencies and system health.

## Overview

FraiseQL provides a **composable health check utility** that allows applications to register custom checks for databases, caches, external services, and other dependencies. Unlike opinionated frameworks that dictate what to monitor, FraiseQL provides the pattern and lets you control what checks to include.

**Key Features:**

- **Composable**: Register only the checks your application needs
- **Pre-built checks**: Ready-to-use functions for common dependencies
- **Custom checks**: Easy pattern for application-specific monitoring
- **Async-first**: Built for modern Python async applications
- **FastAPI integration**: Natural integration with FastAPI health endpoints

## Table of Contents

- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [Pre-built Checks](#pre-built-checks)
- [Custom Checks](#custom-checks)
- [FastAPI Integration](#fastapi-integration)
- [Production Patterns](#production-patterns)

## Quick Start

### Basic Health Endpoint

```python
from fastapi import FastAPI
from fraiseql.monitoring import HealthCheck, check_database, check_pool_stats

app = FastAPI()

# Create health check instance
health = HealthCheck()

# Register pre-built checks
health.add_check("database", check_database)
health.add_check("pool", check_pool_stats)

@app.get("/health")
async def health_endpoint():
    """Health check endpoint for monitoring and orchestration."""
    return await health.run_checks()
```

**Response Example:**

```json
{
  "status": "healthy",
  "checks": {
    "database": {
      "status": "healthy",
      "message": "Database connection successful (PostgreSQL 16.3)",
      "metadata": {
        "database_version": "16.3",
        "full_version": "PostgreSQL 16.3 (Ubuntu 16.3-1.pgdg22.04+1) on x86_64-pc-linux-gnu"
      }
    },
    "pool": {
      "status": "healthy",
      "message": "Pool healthy (45.0% utilized - 9/20 active)",
      "metadata": {
        "pool_size": 9,
        "active_connections": 9,
        "idle_connections": 0,
        "max_connections": 20,
        "min_connections": 5,
        "usage_percentage": 45.0
      }
    }
  }
}
```

## Core Concepts

### HealthCheck Class

The `HealthCheck` class is a runner that executes registered checks and aggregates results:

```python
from fraiseql.monitoring import HealthCheck

health = HealthCheck()
```

**Methods:**

- `add_check(name: str, check_fn: CheckFunction)` - Register a health check
- `run_checks() -> dict` - Execute all checks and return aggregated results

### CheckResult Dataclass

Health checks return a `CheckResult` with status and metadata:

```python
from fraiseql.monitoring import CheckResult, HealthStatus

result = CheckResult(
    name="database",
    status=HealthStatus.HEALTHY,
    message="Connection successful",
    metadata={"version": "16.3", "pool_size": 10}
)
```

**Attributes:**

- `name` - Check identifier
- `status` - `HealthStatus.HEALTHY`, `UNHEALTHY`, or `DEGRADED`
- `message` - Human-readable description
- `metadata` - Optional dictionary with additional context

### Health Statuses

```python
from fraiseql.monitoring import HealthStatus

# Individual check statuses
HealthStatus.HEALTHY    # Check passed
HealthStatus.UNHEALTHY  # Check failed
HealthStatus.DEGRADED   # Partial failure (unused in individual checks)

# Overall system status (from run_checks)
# - HEALTHY: All checks passed
# - DEGRADED: One or more checks failed
```

## Pre-built Checks

FraiseQL provides ready-to-use health checks for common dependencies.

### check_database

Verifies database connectivity and retrieves version information.

**Import:**

```python
from fraiseql.monitoring.health_checks import check_database
```

**What it checks:**

- Database connection pool availability
- Ability to execute queries (SELECT version())
- PostgreSQL version

**Usage:**

```python
health = HealthCheck()
health.add_check("database", check_database)
```

**Returns:**

```json
{
  "status": "healthy",
  "message": "Database connection successful (PostgreSQL 16.3)",
  "metadata": {
    "database_version": "16.3",
    "full_version": "PostgreSQL 16.3..."
  }
}
```

### check_pool_stats

Monitors database connection pool health and utilization.

**Import:**

```python
from fraiseql.monitoring.health_checks import check_pool_stats
```

**What it checks:**

- Pool availability
- Connection utilization (active vs idle)
- Pool saturation percentage

**Usage:**

```python
health = HealthCheck()
health.add_check("pool", check_pool_stats)
```

**Returns:**

```json
{
  "status": "healthy",
  "message": "Pool healthy (45.0% utilized - 9/20 active)",
  "metadata": {
    "pool_size": 9,
    "active_connections": 9,
    "idle_connections": 0,
    "max_connections": 20,
    "min_connections": 5,
    "usage_percentage": 45.0
  }
}
```

**Interpretation:**

- `< 75%` - "Pool healthy"
- `75-90%` - "Pool moderately utilized"
- `> 90%` - "Pool highly utilized" (consider scaling)

## Custom Checks

Create application-specific health checks following the pattern.

### Basic Custom Check

```python
from fraiseql.monitoring import CheckResult, HealthStatus

async def check_redis() -> CheckResult:
    """Check Redis cache connectivity."""
    try:
        redis = get_redis_client()
        await redis.ping()

        return CheckResult(
            name="redis",
            status=HealthStatus.HEALTHY,
            message="Redis connection successful"
        )

    except Exception as e:
        return CheckResult(
            name="redis",
            status=HealthStatus.UNHEALTHY,
            message=f"Redis connection failed: {e}"
        )

# Register the check
health.add_check("redis", check_redis)
```

### Check with Metadata

```python
async def check_s3_bucket() -> CheckResult:
    """Check S3 bucket accessibility."""
    try:
        s3_client = get_s3_client()

        # Test bucket access
        response = s3_client.head_bucket(Bucket="my-bucket")

        # Get bucket metadata
        objects = s3_client.list_objects_v2(
            Bucket="my-bucket",
            MaxKeys=1
        )
        object_count = objects.get('KeyCount', 0)

        return CheckResult(
            name="s3",
            status=HealthStatus.HEALTHY,
            message="S3 bucket accessible",
            metadata={
                "bucket": "my-bucket",
                "region": s3_client.meta.region_name,
                "object_count": object_count
            }
        )

    except Exception as e:
        return CheckResult(
            name="s3",
            status=HealthStatus.UNHEALTHY,
            message=f"S3 bucket check failed: {e}"
        )
```

### External Service Check

```python
import httpx

async def check_payment_gateway() -> CheckResult:
    """Check external payment gateway availability."""
    try:
        async with httpx.AsyncClient() as client:
            response = await client.get(
                "https://api.stripe.com/v1/health",
                timeout=5.0
            )

            if response.status_code == 200:
                return CheckResult(
                    name="stripe",
                    status=HealthStatus.HEALTHY,
                    message="Payment gateway operational",
                    metadata={
                        "latency_ms": response.elapsed.total_seconds() * 1000,
                        "status_code": response.status_code
                    }
                )
            else:
                return CheckResult(
                    name="stripe",
                    status=HealthStatus.UNHEALTHY,
                    message=f"Payment gateway returned {response.status_code}"
                )

    except httpx.TimeoutException:
        return CheckResult(
            name="stripe",
            status=HealthStatus.UNHEALTHY,
            message="Payment gateway timeout (> 5s)"
        )

    except Exception as e:
        return CheckResult(
            name="stripe",
            status=HealthStatus.UNHEALTHY,
            message=f"Payment gateway error: {e}"
        )
```

## FastAPI Integration

### Standard Health Endpoint

```python
from fastapi import FastAPI
from fraiseql.monitoring import HealthCheck, check_database, check_pool_stats

app = FastAPI()
health = HealthCheck()

# Register checks
health.add_check("database", check_database)
health.add_check("pool", check_pool_stats)

@app.get("/health")
async def health_check():
    """Kubernetes/orchestrator health check endpoint."""
    return await health.run_checks()
```

### Kubernetes-Style Liveness/Readiness

```python
from fastapi import FastAPI, Response, status
from fraiseql.monitoring import HealthCheck, check_database

app = FastAPI()

# Liveness: Is the app running?
@app.get("/health/live")
async def liveness():
    """Liveness probe - always returns 200 if app is running."""
    return {"status": "alive"}

# Readiness: Can the app serve traffic?
readiness_checks = HealthCheck()
readiness_checks.add_check("database", check_database)

@app.get("/health/ready")
async def readiness(response: Response):
    """Readiness probe - returns 200 if dependencies are healthy."""
    result = await readiness_checks.run_checks()

    if result["status"] != "healthy":
        response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE

    return result
```

### Comprehensive Health with Versioning

```python
from fastapi import FastAPI
from fraiseql.monitoring import HealthCheck, check_database, check_pool_stats
import os

app = FastAPI()

# Different check sets for different purposes
liveness = HealthCheck()  # Minimal checks

readiness = HealthCheck()  # Critical dependencies
readiness.add_check("database", check_database)

comprehensive = HealthCheck()  # All dependencies
comprehensive.add_check("database", check_database)
comprehensive.add_check("pool", check_pool_stats)
# ... add custom checks

@app.get("/health")
async def health():
    """Comprehensive health check with version info."""
    result = await comprehensive.run_checks()

    # Add application metadata
    result["version"] = os.getenv("APP_VERSION", "unknown")
    result["environment"] = os.getenv("ENV", "development")

    return result

@app.get("/health/live")
async def live():
    """Liveness - minimal check."""
    return await liveness.run_checks()

@app.get("/health/ready")
async def ready(response: Response):
    """Readiness - critical dependencies."""
    result = await readiness.run_checks()

    if result["status"] != "healthy":
        response.status_code = 503

    return result
```

## Production Patterns

### Monitoring Integration

```python
from fraiseql.monitoring import HealthCheck, check_database, check_pool_stats
import logging

logger = logging.getLogger(__name__)

health = HealthCheck()
health.add_check("database", check_database)
health.add_check("pool", check_pool_stats)

@app.get("/health")
async def health_endpoint():
    """Health check with monitoring integration."""
    result = await health.run_checks()

    # Log degraded status for alerting
    if result["status"] == "degraded":
        failed_checks = [
            name
            for name, check in result["checks"].items()
            if check["status"] != "healthy"
        ]
        logger.warning(
            f"Health check degraded: {', '.join(failed_checks)}",
            extra={
                "failed_checks": failed_checks,
                "health_status": result
            }
        )

    return result
```

### Alerting on Degradation

```python
from fraiseql.monitoring import HealthCheck, HealthStatus
from fraiseql.monitoring.sentry import capture_message

health = HealthCheck()
# ... register checks

@app.get("/health")
async def health_with_alerts():
    """Health check with automatic alerting."""
    result = await health.run_checks()

    if result["status"] == "degraded":
        # Alert to Sentry
        failed_checks = {
            name: check
            for name, check in result["checks"].items()
            if check["status"] != "healthy"
        }

        capture_message(
            f"Health check degraded: {len(failed_checks)} checks failing",
            level="warning",
            extra={"failed_checks": failed_checks}
        )

    return result
```

### Response Caching

```python
from fastapi import FastAPI
from fraiseql.monitoring import HealthCheck, check_database
import time

app = FastAPI()
health = HealthCheck()
health.add_check("database", check_database)

# Cache for high-frequency health checks
_health_cache = {"result": None, "timestamp": 0}
CACHE_TTL = 5  # seconds

@app.get("/health")
async def cached_health():
    """Health check with caching to reduce database load."""
    now = time.time()

    # Return cached result if fresh
    if _health_cache["result"] and (now - _health_cache["timestamp"]) < CACHE_TTL:
        return _health_cache["result"]

    # Run checks
    result = await health.run_checks()

    # Update cache
    _health_cache["result"] = result
    _health_cache["timestamp"] = now

    return result
```

### Environment-Specific Checks

```python
from fraiseql.monitoring import HealthCheck, check_database
import os

def create_health_checks() -> HealthCheck:
    """Create health checks based on environment."""
    health = HealthCheck()

    # Always check database
    health.add_check("database", check_database)

    # Production-specific checks
    if os.getenv("ENV") == "production":
        health.add_check("redis", check_redis)
        health.add_check("s3", check_s3_bucket)
        health.add_check("stripe", check_payment_gateway)

    return health

health = create_health_checks()
```

## Best Practices

### 1. Separate Liveness and Readiness

```python
# Liveness: App is running (no external dependencies)
@app.get("/health/live")
async def liveness():
    return {"status": "alive"}

# Readiness: App can serve traffic (check dependencies)
@app.get("/health/ready")
async def readiness():
    return await health.run_checks()
```

### 2. Include Metadata for Debugging

```python
async def check_with_metadata() -> CheckResult:
    """Include diagnostic information."""
    return CheckResult(
        name="service",
        status=HealthStatus.HEALTHY,
        message="Service operational",
        metadata={
            "version": "1.2.3",
            "uptime_seconds": get_uptime(),
            "last_request": get_last_request_time()
        }
    )
```

### 3. Timeout Long-Running Checks

```python
import asyncio

async def check_with_timeout() -> CheckResult:
    """Prevent health checks from hanging."""
    try:
        # Timeout after 5 seconds
        async with asyncio.timeout(5.0):
            result = await slow_external_check()

        return CheckResult(
            name="external_api",
            status=HealthStatus.HEALTHY,
            message="External API responding"
        )

    except asyncio.TimeoutError:
        return CheckResult(
            name="external_api",
            status=HealthStatus.UNHEALTHY,
            message="External API timeout (> 5s)"
        )
```

### 4. Don't Check on Every Request

```python
# ❌ Bad: Health check runs on every GraphQL request
@app.middleware("http")
async def health_middleware(request, call_next):
    await health.run_checks()  # Expensive!
    return await call_next(request)

# ✅ Good: Dedicated health endpoint
@app.get("/health")
async def health_endpoint():
    return await health.run_checks()
```

## See Also

- [Production Deployment](../production/deployment.md) - Kubernetes health probes
- [Monitoring](../production/monitoring.md) - Metrics and observability
- [Sentry Integration](../production/monitoring.md#sentry-integration-legacyoptional) - Error tracking
