- Sync engine: BaseSync abstract class + 4 sync modules (users/pricing/uapi/llmage) - Checkpoint management via sync_state table - Batch processing with retry and exponential backoff - Incremental fetch from Sage DB via sqlor - UPSERT to local cache tables - API handlers: balance/accounting/users/pricing/health - Balance: cache lookup + Sage fallback - Accounting: create with idempotency, query with filters/pagination - Users: keyword search, org filter - Pricing: filter by ppid/llmid/type/status - Health: basic + readiness checks (DB connectivity) - DAPI auth: middleware + authenticate_request function - HMAC-SHA256 signature verification - Timestamp window validation - Sage downapikey table lookup - HTTP client: SageHttpClient with aiohttp - Auto DAPI signature injection - Connection pooling, retry, timeout - Router: 12 routes registered - Module init: load_sageapi() wires everything to ServerEnv
110 lines
3.2 KiB
Python
110 lines
3.2 KiB
Python
"""Health check API handler.
|
|
|
|
Provides endpoints for service health and readiness checks.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import time
|
|
from typing import Any
|
|
|
|
from appPublic.log import debug, error
|
|
from sqlor.dbpools import DBPools
|
|
from ahserver.serverenv import ServerEnv
|
|
|
|
_START_TIME = time.time()
|
|
|
|
|
|
async def health_check() -> str:
|
|
"""Basic health check - returns service status."""
|
|
uptime = time.time() - _START_TIME
|
|
|
|
result = {
|
|
'status': 'ok',
|
|
'service': 'sageapi',
|
|
'uptime_seconds': round(uptime, 1),
|
|
'timestamp': time.strftime('%Y-%m-%dT%H:%M:%S%z'),
|
|
}
|
|
return json.dumps(result, ensure_ascii=False, default=str)
|
|
|
|
|
|
async def readiness_check() -> str:
|
|
"""Readiness check - verifies database connectivity."""
|
|
result: dict[str, Any] = {
|
|
'status': 'unknown',
|
|
'checks': {},
|
|
}
|
|
|
|
# Check cache database connection
|
|
try:
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('sageapi')
|
|
if not dbname:
|
|
result['checks']['cache_db'] = {
|
|
'status': 'fail',
|
|
'error': 'No database configured for sageapi module',
|
|
}
|
|
else:
|
|
async with DBPools().sqlorContext(dbname) as sor:
|
|
rows = await sor.sqlExe('SELECT 1 as ping')
|
|
result['checks']['cache_db'] = {
|
|
'status': 'ok',
|
|
'dbname': dbname,
|
|
}
|
|
except Exception as e:
|
|
error(f'readiness_check cache_db error: {e}')
|
|
result['checks']['cache_db'] = {
|
|
'status': 'fail',
|
|
'error': str(e),
|
|
}
|
|
|
|
# Check Sage database connection
|
|
try:
|
|
from sqlor.dbpools import get_sor_context
|
|
async with get_sor_context(env, 'sage') as sor:
|
|
rows = await sor.sqlExe('SELECT 1 as ping')
|
|
result['checks']['sage_db'] = {'status': 'ok'}
|
|
except Exception as e:
|
|
error(f'readiness_check sage_db error: {e}')
|
|
result['checks']['sage_db'] = {
|
|
'status': 'fail',
|
|
'error': str(e),
|
|
}
|
|
|
|
# Check sync state
|
|
try:
|
|
async with DBPools().sqlorContext(dbname) as sor:
|
|
sql = """
|
|
SELECT entity_type, sync_status, last_sync_time
|
|
FROM sync_state
|
|
ORDER BY last_sync_time DESC
|
|
"""
|
|
rows = await sor.sqlExe(sql)
|
|
if isinstance(rows, list):
|
|
result['checks']['sync_status'] = {
|
|
'status': 'ok',
|
|
'entities': [
|
|
{
|
|
'entity_type': r.get('entity_type', ''),
|
|
'sync_status': r.get('sync_status', ''),
|
|
'last_sync_time': str(r.get('last_sync_time', '')),
|
|
}
|
|
for r in rows
|
|
],
|
|
}
|
|
except Exception as e:
|
|
result['checks']['sync_status'] = {
|
|
'status': 'fail',
|
|
'error': str(e),
|
|
}
|
|
|
|
# Overall status
|
|
all_ok = all(
|
|
check.get('status') == 'ok'
|
|
for check in result['checks'].values()
|
|
)
|
|
result['status'] = 'ready' if all_ok else 'degraded'
|
|
|
|
return json.dumps(result, ensure_ascii=False, default=str)
|