feat: expose API runtime identity in health
This commit is contained in:
parent
7fede4d082
commit
310e273aa5
@ -30,6 +30,8 @@ services:
|
|||||||
- APP_ENV=development
|
- APP_ENV=development
|
||||||
- ENVIRONMENT=development
|
- ENVIRONMENT=development
|
||||||
- START_MODE=dev
|
- START_MODE=dev
|
||||||
|
- CC_COMPOSE_PROJECT=api-dev
|
||||||
|
- CC_COMPOSE_SERVICE=backend-dev
|
||||||
- RUN_INIT=false
|
- RUN_INIT=false
|
||||||
- INIT_MODE=infra
|
- INIT_MODE=infra
|
||||||
ports:
|
ports:
|
||||||
@ -54,6 +56,9 @@ services:
|
|||||||
- BACKEND_DEV_MODE=true
|
- BACKEND_DEV_MODE=true
|
||||||
- APP_ENV=development
|
- APP_ENV=development
|
||||||
- ENVIRONMENT=development
|
- ENVIRONMENT=development
|
||||||
|
- START_MODE=dev
|
||||||
|
- CC_COMPOSE_PROJECT=api-dev
|
||||||
|
- CC_COMPOSE_SERVICE=backend-test
|
||||||
- API_HEALTH_URL=http://192.168.0.64:18000/health
|
- API_HEALTH_URL=http://192.168.0.64:18000/health
|
||||||
depends_on:
|
depends_on:
|
||||||
redis-dev:
|
redis-dev:
|
||||||
|
|||||||
@ -46,6 +46,11 @@ services:
|
|||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- REDIS_HOST=redis
|
- REDIS_HOST=redis
|
||||||
|
- START_MODE=prod
|
||||||
|
- APP_ENV=production
|
||||||
|
- ENVIRONMENT=production
|
||||||
|
- CC_COMPOSE_PROJECT=api
|
||||||
|
- CC_COMPOSE_SERVICE=backend
|
||||||
- RUN_INIT=${RUN_INIT:-false} # Set to 'true' to run init on startup
|
- RUN_INIT=${RUN_INIT:-false} # Set to 'true' to run init on startup
|
||||||
- INIT_MODE=${INIT_MODE:-infra} # Which init tasks to run
|
- INIT_MODE=${INIT_MODE:-infra} # Which init tasks to run
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
51
main.py
51
main.py
@ -10,6 +10,7 @@ logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH
|
|||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
import uvicorn
|
import uvicorn
|
||||||
import requests
|
import requests
|
||||||
|
from urllib.parse import urlparse
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
from modules.database.tools.neo4j_driver_tools import get_driver
|
from modules.database.tools.neo4j_driver_tools import get_driver
|
||||||
|
|
||||||
@ -22,15 +23,59 @@ from modules.queue_system import ServiceType
|
|||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
setup_cors(app)
|
setup_cors(app)
|
||||||
|
|
||||||
|
def _truthy_env(name: str) -> bool:
|
||||||
|
return os.getenv(name, "").lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
|
|
||||||
|
def _runtime_environment() -> str:
|
||||||
|
"""Return the API runtime role used for dev/prod backing-service selection."""
|
||||||
|
start_mode = os.getenv("START_MODE", "prod").lower()
|
||||||
|
if start_mode == "dev" or _truthy_env("BACKEND_DEV_MODE"):
|
||||||
|
return "dev"
|
||||||
|
return "prod"
|
||||||
|
|
||||||
|
|
||||||
|
def _url_host(url: Optional[str]) -> Optional[str]:
|
||||||
|
if not url:
|
||||||
|
return None
|
||||||
|
parsed = urlparse(url)
|
||||||
|
return parsed.hostname
|
||||||
|
|
||||||
|
|
||||||
|
def _runtime_identity() -> Dict[str, Any]:
|
||||||
|
"""Non-secret runtime identity for agents and smoke tests.
|
||||||
|
|
||||||
|
This intentionally exposes only modes, labels, and URL hosts. It must not
|
||||||
|
include API keys, passwords, bearer tokens, or full URLs that may embed
|
||||||
|
credentials.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"api_runtime_role": _runtime_environment(),
|
||||||
|
"start_mode": os.getenv("START_MODE", "prod"),
|
||||||
|
"app_environment": os.getenv("APP_ENV"),
|
||||||
|
"environment": os.getenv("ENVIRONMENT"),
|
||||||
|
"backend_dev_mode": _truthy_env("BACKEND_DEV_MODE"),
|
||||||
|
"compose_project": os.getenv("COMPOSE_PROJECT_NAME") or os.getenv("CC_COMPOSE_PROJECT"),
|
||||||
|
"compose_service": os.getenv("COMPOSE_SERVICE") or os.getenv("CC_COMPOSE_SERVICE"),
|
||||||
|
"supabase_url_host": _url_host(os.getenv("SUPABASE_URL")),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Health check endpoint
|
# Health check endpoint
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
async def health_check() -> Dict[str, Any]:
|
async def health_check() -> Dict[str, Any]:
|
||||||
"""Health check endpoint that verifies all service dependencies"""
|
"""Health check endpoint that verifies all service dependencies"""
|
||||||
|
runtime_identity = _runtime_identity()
|
||||||
health_status = {
|
health_status = {
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
|
"runtime": runtime_identity,
|
||||||
"services": {
|
"services": {
|
||||||
"neo4j": {"status": "healthy", "message": "Connected"},
|
"neo4j": {"status": "healthy", "message": "Connected"},
|
||||||
"supabase": {"status": "healthy", "message": "Connected"},
|
"supabase": {
|
||||||
|
"status": "healthy",
|
||||||
|
"message": "Connected",
|
||||||
|
"url_host": runtime_identity["supabase_url_host"],
|
||||||
|
},
|
||||||
"redis": {"status": "healthy", "message": "Connected"}
|
"redis": {"status": "healthy", "message": "Connected"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,8 +122,8 @@ async def health_check() -> Dict[str, Any]:
|
|||||||
# Check Redis using new Redis manager
|
# Check Redis using new Redis manager
|
||||||
from modules.redis_manager import get_redis_manager
|
from modules.redis_manager import get_redis_manager
|
||||||
|
|
||||||
# Determine environment
|
# Determine environment from explicit startup/runtime identity.
|
||||||
environment = 'dev' if os.getenv('BACKEND_DEV_MODE', 'true').lower() == 'true' else 'prod'
|
environment = runtime_identity["api_runtime_role"]
|
||||||
redis_manager = get_redis_manager(environment)
|
redis_manager = get_redis_manager(environment)
|
||||||
|
|
||||||
# Get comprehensive health check
|
# Get comprehensive health check
|
||||||
|
|||||||
@ -38,7 +38,18 @@ def test_dev_api_health_endpoint_is_healthy():
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
payload = response.json()
|
payload = response.json()
|
||||||
assert payload['status'] == 'healthy'
|
assert payload['status'] == 'healthy'
|
||||||
|
|
||||||
|
runtime = payload['runtime']
|
||||||
|
assert runtime['api_runtime_role'] == 'dev'
|
||||||
|
assert runtime['start_mode'] == 'dev'
|
||||||
|
assert runtime['app_environment'] == 'development'
|
||||||
|
assert runtime['environment'] == 'development'
|
||||||
|
assert runtime['backend_dev_mode'] is True
|
||||||
|
assert runtime['compose_project'] == 'api-dev'
|
||||||
|
assert runtime['supabase_url_host'] == '192.168.0.94'
|
||||||
|
|
||||||
assert payload['services']['supabase']['status'] == 'healthy'
|
assert payload['services']['supabase']['status'] == 'healthy'
|
||||||
|
assert payload['services']['supabase']['url_host'] == '192.168.0.94'
|
||||||
assert payload['services']['redis']['status'] == 'healthy'
|
assert payload['services']['redis']['status'] == 'healthy'
|
||||||
assert payload['services']['redis']['environment'] == 'dev'
|
assert payload['services']['redis']['environment'] == 'dev'
|
||||||
assert payload['services']['redis']['database'] == 0
|
assert payload['services']['redis']['database'] == 0
|
||||||
@ -53,3 +64,19 @@ def test_supabase_dev_seed_core_counts():
|
|||||||
def test_supabase_dev_seed_timetable_counts():
|
def test_supabase_dev_seed_timetable_counts():
|
||||||
assert _rest_count('classes') == 17
|
assert _rest_count('classes') == 17
|
||||||
assert _rest_count('taught_lessons') == 1462
|
assert _rest_count('taught_lessons') == 1462
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_identity_does_not_expose_secret_values():
|
||||||
|
health_url = os.getenv('API_HEALTH_URL', 'http://192.168.0.64:18000/health')
|
||||||
|
response = requests.get(health_url, timeout=15)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload_text = response.text
|
||||||
|
for secret_name in ('SERVICE_ROLE_KEY', 'ANON_KEY', 'SUPABASE_JWT_SECRET', 'REDIS_PASSWORD'):
|
||||||
|
secret_value = os.getenv(secret_name)
|
||||||
|
if secret_value:
|
||||||
|
assert secret_value not in payload_text
|
||||||
|
|
||||||
|
runtime = response.json()['runtime']
|
||||||
|
assert 'supabase_url' not in runtime
|
||||||
|
assert 'service_role_key' not in runtime
|
||||||
|
assert 'anon_key' not in runtime
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user