sageapi/sageapi/api/health.py
Hermes Agent 5936a2f328 feat: implement sync engine, API handlers, DAPI auth, HTTP client
- 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
2026-05-20 18:22:23 +08:00

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)