06/05 Optimize app: Add Plaid web hook, healthcheck
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Health check endpoint — no authentication required.
|
||||
Used by uptime monitors (UptimeRobot, etc.) to verify the app and subsystems.
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from flask import Blueprint, jsonify
|
||||
from sqlalchemy import func, text
|
||||
|
||||
from app.extensions import db
|
||||
|
||||
health_bp = Blueprint('health', __name__)
|
||||
log = logging.getLogger('app.health')
|
||||
|
||||
|
||||
@health_bp.route('/health')
|
||||
def health_check():
|
||||
db_result = _check_db()
|
||||
cron_result = _check_crons()
|
||||
|
||||
status = 'ok'
|
||||
if db_result['status'] != 'ok':
|
||||
status = 'degraded'
|
||||
if any(v.get('overdue') for v in cron_result.values() if isinstance(v, dict)):
|
||||
status = 'degraded'
|
||||
|
||||
return jsonify({
|
||||
'status': status,
|
||||
'timestamp': datetime.utcnow().isoformat() + 'Z',
|
||||
'db': db_result,
|
||||
'crons': cron_result,
|
||||
}), 200 if status == 'ok' else 503
|
||||
|
||||
|
||||
def _check_db():
|
||||
t0 = time.monotonic()
|
||||
try:
|
||||
db.session.execute(text('SELECT 1'))
|
||||
ms = round((time.monotonic() - t0) * 1000, 1)
|
||||
return {'status': 'ok', 'latency_ms': ms}
|
||||
except Exception as e:
|
||||
log.error('[health] DB check failed: %s', e)
|
||||
return {'status': 'error', 'error': str(e)}
|
||||
|
||||
|
||||
def _check_crons():
|
||||
from app.models.fx_rate import FxRate
|
||||
from app.models.ai_insight import AiInsight
|
||||
from app.models.net_worth_snapshot import NetWorthSnapshot
|
||||
from app.models.investment import Investment
|
||||
|
||||
return {
|
||||
# expected daily — overdue after 48 h
|
||||
'fetch_fx_rate': _stat(
|
||||
db.session.query(func.max(FxRate.fetched_at)).scalar(),
|
||||
max_hours=48,
|
||||
),
|
||||
# expected daily — overdue after 48 h
|
||||
'daily_ai_insight': _stat(
|
||||
db.session.query(func.max(AiInsight.created_at))
|
||||
.filter(AiInsight.insight_type == 'daily_summary').scalar(),
|
||||
max_hours=48,
|
||||
),
|
||||
# expected monthly — overdue after 35 days
|
||||
'daily_snapshot': _stat(
|
||||
db.session.query(func.max(NetWorthSnapshot.created_at)).scalar(),
|
||||
max_hours=35 * 24,
|
||||
),
|
||||
# expected weekdays — overdue after 4 days
|
||||
'fetch_prices': _stat(
|
||||
db.session.query(func.max(Investment.last_price_update)).scalar(),
|
||||
max_hours=4 * 24,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _stat(last_run_dt, max_hours):
|
||||
if last_run_dt is None:
|
||||
return {'last_run': None, 'age_hours': None, 'overdue': False}
|
||||
age = round((datetime.utcnow() - last_run_dt).total_seconds() / 3600, 1)
|
||||
return {
|
||||
'last_run': last_run_dt.isoformat() + 'Z',
|
||||
'age_hours': age,
|
||||
'overdue': age > max_hours,
|
||||
}
|
||||
Reference in New Issue
Block a user