feat: deep pipeline health check script

Checks 12 aspects of the running deployment:
- Pod health (replicas, restarts, OOMKills)
- PostgreSQL connectivity & stats
- Redis connectivity & queue backlog
- MinIO connectivity
- vLLM model availability
- Alpaca broker account status
- Pipeline flow (ingestion→extraction→aggregation→recommendation)
- Model quality gate freshness
- Paper trading portfolio status
- API endpoint health
- Resource usage
- End-to-end latency (doc processing, trend/rec freshness)

Usage: ./scripts/pipeline_health_check.sh [namespace]
This commit is contained in:
Celes Renata
2026-07-11 22:35:04 +00:00
parent 2f966b1caf
commit 84634a365e
+503
View File
@@ -0,0 +1,503 @@
#!/usr/bin/env bash
# Stonks Oracle — Deep Pipeline Health Check
# Verifies every component is ACTUALLY working, not just "running"
#
# Usage: ./scripts/pipeline_health_check.sh [namespace]
# Default namespace: stonks-beta
set -uo pipefail
NS="${1:-stonks-beta}"
PASS=0
WARN=0
FAIL=0
DETAILS=""
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
pass() { ((PASS++)); echo -e " ${GREEN}${NC} $1"; }
warn() { ((WARN++)); echo -e " ${YELLOW}${NC} $1"; }
fail() { ((FAIL++)); echo -e " ${RED}${NC} $1"; }
header() { echo -e "\n${BLUE}━━━ $1 ━━━${NC}"; }
# ─── 1. POD HEALTH ─────────────────────────────────────────────
header "Pod Health (namespace: $NS)"
# Check all deployments are at desired replicas
while IFS= read -r line; do
name=$(echo "$line" | awk '{print $1}')
ready=$(echo "$line" | awk '{print $2}')
desired=$(echo "$ready" | cut -d/ -f2)
actual=$(echo "$ready" | cut -d/ -f1)
if [[ "$desired" == "0" ]]; then
continue # intentionally scaled down
fi
if [[ "$actual" == "$desired" ]]; then
pass "$name: $ready"
else
fail "$name: $ready (not fully ready)"
fi
done < <(kubectl get deployments -n "$NS" --no-headers 2>/dev/null)
# Check for restarts in the last hour
RESTART_PODS=$(kubectl get pods -n "$NS" --no-headers -o custom-columns=NAME:.metadata.name,RESTARTS:.status.containerStatuses[0].restartCount 2>/dev/null | awk '$2 > 0 {print $1 "(" $2 ")"}')
if [[ -z "$RESTART_PODS" ]]; then
pass "No pod restarts"
else
for p in $RESTART_PODS; do
warn "Restart detected: $p"
done
fi
# Check for OOMKilled in last state
OOM_PODS=$(kubectl get pods -n "$NS" -o json 2>/dev/null | python3 -c "
import json, sys
data = json.load(sys.stdin)
for pod in data.get('items', []):
for cs in pod.get('status', {}).get('containerStatuses', []):
last = cs.get('lastState', {}).get('terminated', {})
if last.get('reason') == 'OOMKilled':
print(pod['metadata']['name'])
" 2>/dev/null)
if [[ -z "$OOM_PODS" ]]; then
pass "No OOMKilled containers"
else
for p in $OOM_PODS; do
fail "OOMKilled: $p"
done
fi
# ─── 2. DATABASE CONNECTIVITY & HEALTH ────────────────────────
header "PostgreSQL"
DB_CHECK=$(kubectl exec -n "$NS" deployment/query-api -- python3 -c "
import asyncio, asyncpg, os, json
async def check():
pool = await asyncpg.create_pool(dsn=f\"postgresql://{os.environ['POSTGRES_USER']}:{os.environ['POSTGRES_PASSWORD']}@{os.environ['POSTGRES_HOST']}:{os.environ.get('POSTGRES_PORT','5432')}/{os.environ['POSTGRES_DB']}\", min_size=1, max_size=2)
result = {}
result['connected'] = True
# DB size
row = await pool.fetchrow('SELECT pg_database_size(current_database()) as size')
result['db_size_mb'] = round(row['size'] / 1024 / 1024)
# Table count
result['table_count'] = await pool.fetchval(\"SELECT count(*) FROM pg_tables WHERE schemaname='public'\")
# Active connections
result['connections'] = await pool.fetchval('SELECT count(*) FROM pg_stat_activity WHERE datname = current_database()')
await pool.close()
print(json.dumps(result))
asyncio.run(check())
" 2>/dev/null) || DB_CHECK='{"connected": false}'
if echo "$DB_CHECK" | python3 -c "import json,sys; d=json.load(sys.stdin); exit(0 if d.get('connected') else 1)" 2>/dev/null; then
DB_SIZE=$(echo "$DB_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin)['db_size_mb'])")
DB_TABLES=$(echo "$DB_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin)['table_count'])")
DB_CONNS=$(echo "$DB_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin)['connections'])")
pass "Connected (${DB_SIZE}MB, ${DB_TABLES} tables, ${DB_CONNS} connections)"
else
fail "Cannot connect to PostgreSQL"
fi
# ─── 3. REDIS CONNECTIVITY & QUEUES ───────────────────────────
header "Redis"
REDIS_CHECK=$(kubectl exec -n "$NS" deployment/query-api -- python3 -c "
import redis, os, json
r = redis.from_url(f'redis://:{os.environ.get(\"REDIS_PASSWORD\",\"\")}@{os.environ.get(\"REDIS_HOST\",\"redis-master.redis-service.svc.cluster.local\")}:6379/{os.environ.get(\"REDIS_DB\",\"1\")}')
result = {}
result['connected'] = True
result['memory'] = r.info('memory')['used_memory_human']
result['keys'] = r.dbsize()
# Check queues for backlog
queues = {}
for q in ['ingestion','parsing','extraction','aggregation','recommendation','trading','broker_orders','macro_classification']:
stage = os.environ.get('DEPLOY_STAGE', '')
prefix = f'stonks:{stage}' if stage else 'stonks'
key = f'{prefix}:queue:{q}'
length = r.llen(key)
if length > 0:
queues[q] = length
result['queue_backlog'] = queues
print(json.dumps(result))
" 2>/dev/null) || REDIS_CHECK='{"connected": false}'
if echo "$REDIS_CHECK" | python3 -c "import json,sys; d=json.load(sys.stdin); exit(0 if d.get('connected') else 1)" 2>/dev/null; then
R_MEM=$(echo "$REDIS_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin)['memory'])")
R_KEYS=$(echo "$REDIS_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin)['keys'])")
pass "Connected (${R_MEM}, ${R_KEYS} keys)"
BACKLOG=$(echo "$REDIS_CHECK" | python3 -c "import json,sys; d=json.load(sys.stdin)['queue_backlog']; print(' '.join(f'{k}={v}' for k,v in d.items()) if d else '')")
if [[ -z "$BACKLOG" ]]; then
pass "All queues drained (no backlog)"
else
warn "Queue backlog: $BACKLOG"
fi
else
fail "Cannot connect to Redis"
fi
# ─── 4. MINIO CONNECTIVITY ────────────────────────────────────
header "MinIO (S3)"
MINIO_CHECK=$(kubectl exec -n "$NS" deployment/query-api -- python3 -c "
from minio import Minio
import os, json
client = Minio(
os.environ.get('MINIO_ENDPOINT', 'minio.minio-service.svc.cluster.local:80'),
access_key=os.environ.get('MINIO_ACCESS_KEY', ''),
secret_key=os.environ.get('MINIO_SECRET_KEY', ''),
secure=False
)
buckets = client.list_buckets()
print(json.dumps({'connected': True, 'bucket_count': len(buckets)}))
" 2>/dev/null) || MINIO_CHECK='{"connected": false}'
if echo "$MINIO_CHECK" | python3 -c "import json,sys; d=json.load(sys.stdin); exit(0 if d.get('connected') else 1)" 2>/dev/null; then
BUCKETS=$(echo "$MINIO_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin)['bucket_count'])")
pass "Connected ($BUCKETS buckets)"
else
fail "Cannot connect to MinIO"
fi
# ─── 5. vLLM / AI MODEL ───────────────────────────────────────
header "vLLM (NuExtract3)"
VLLM_CHECK=$(kubectl exec -n "$NS" deployment/extractor -- python3 -c "
import httpx, json, os
url = os.environ.get('VLLM_BASE_URL', os.environ.get('OLLAMA_BASE_URL', ''))
try:
resp = httpx.get(f'{url}/v1/models', timeout=10)
models = resp.json().get('data', [])
names = [m['id'] for m in models]
print(json.dumps({'connected': True, 'models': names}))
except Exception as e:
print(json.dumps({'connected': False, 'error': str(e)}))
" 2>/dev/null) || VLLM_CHECK='{"connected": false}'
if echo "$VLLM_CHECK" | python3 -c "import json,sys; d=json.load(sys.stdin); exit(0 if d.get('connected') else 1)" 2>/dev/null; then
MODELS=$(echo "$VLLM_CHECK" | python3 -c "import json,sys; print(', '.join(json.load(sys.stdin)['models']))")
pass "Connected (models: $MODELS)"
else
fail "Cannot reach vLLM"
fi
# ─── 6. BROKER CONNECTIVITY ───────────────────────────────────
header "Alpaca Broker"
BROKER_CHECK=$(kubectl exec -n "$NS" deployment/broker-adapter -- python3 -c "
import httpx, json, os
key = os.environ.get('BROKER_API_KEY', '')
secret = os.environ.get('BROKER_API_SECRET', '')
base = os.environ.get('BROKER_BASE_URL', 'https://paper-api.alpaca.markets')
try:
resp = httpx.get(f'{base}/v2/account', headers={'APCA-API-KEY-ID': key, 'APCA-API-SECRET-KEY': secret}, timeout=10)
if resp.status_code == 200:
acct = resp.json()
print(json.dumps({'connected': True, 'equity': acct.get('equity'), 'buying_power': acct.get('buying_power'), 'status': acct.get('status')}))
else:
print(json.dumps({'connected': False, 'status_code': resp.status_code}))
except Exception as e:
print(json.dumps({'connected': False, 'error': str(e)}))
" 2>/dev/null) || BROKER_CHECK='{"connected": false}'
if echo "$BROKER_CHECK" | python3 -c "import json,sys; d=json.load(sys.stdin); exit(0 if d.get('connected') else 1)" 2>/dev/null; then
EQUITY=$(echo "$BROKER_CHECK" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'equity=\${float(d[\"equity\"]):,.0f} buying_power=\${float(d[\"buying_power\"]):,.0f}')")
pass "Connected ($EQUITY)"
else
fail "Cannot reach Alpaca API"
fi
# ─── 7. PIPELINE FLOW (the real test) ─────────────────────────
header "Pipeline Flow (last 6 hours)"
FLOW_CHECK=$(kubectl exec -n "$NS" deployment/query-api -- python3 -c "
import asyncio, asyncpg, os, json
from datetime import datetime, timezone
async def check():
pool = await asyncpg.create_pool(dsn=f\"postgresql://{os.environ['POSTGRES_USER']}:{os.environ['POSTGRES_PASSWORD']}@{os.environ['POSTGRES_HOST']}:{os.environ.get('POSTGRES_PORT','5432')}/{os.environ['POSTGRES_DB']}\", min_size=1, max_size=2)
r = {}
now = datetime.now(tz=timezone.utc)
# Ingestion: new docs created
r['docs_ingested_6h'] = await pool.fetchval(\"SELECT count(*) FROM documents WHERE created_at > now() - interval '6 hours'\")
# Extraction: docs extracted recently
r['docs_extracted_6h'] = await pool.fetchval(\"SELECT count(*) FROM documents WHERE status = 'extracted' AND updated_at > now() - interval '6 hours'\")
# Stuck docs
r['stuck_parsed'] = await pool.fetchval(\"SELECT count(*) FROM documents WHERE status = 'parsed' AND updated_at < now() - interval '1 hour'\")
r['stuck_ingested'] = await pool.fetchval(\"SELECT count(*) FROM documents WHERE status = 'ingested' AND updated_at < now() - interval '1 hour'\")
# Aggregation: trend history entries
r['trends_6h'] = await pool.fetchval(\"SELECT count(*) FROM trend_history WHERE generated_at > now() - interval '6 hours'\")
# Recommendations generated
r['recs_6h'] = await pool.fetchval(\"SELECT count(*) FROM recommendations WHERE created_at > now() - interval '6 hours'\")
# Trading decisions
r['decisions_6h'] = await pool.fetchval(\"SELECT count(*) FROM trading_decisions WHERE created_at > now() - interval '6 hours'\")
# Global events (macro pipeline)
r['global_events_6h'] = await pool.fetchval(\"SELECT count(*) FROM global_events WHERE created_at > now() - interval '6 hours'\")
# Prediction snapshots
r['snapshots_6h'] = await pool.fetchval(\"SELECT count(*) FROM prediction_snapshots WHERE created_at > now() - interval '6 hours'\")
# Market data freshness
latest_market = await pool.fetchval('SELECT max(captured_at) FROM market_snapshots')
if latest_market:
r['market_data_age_min'] = round((now - latest_market).total_seconds() / 60)
else:
r['market_data_age_min'] = -1
await pool.close()
print(json.dumps(r))
asyncio.run(check())
" 2>/dev/null) || FLOW_CHECK='{}'
if [[ -n "$FLOW_CHECK" && "$FLOW_CHECK" != "{}" ]]; then
DOCS_IN=$(echo "$FLOW_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin).get('docs_ingested_6h', 0))")
DOCS_EX=$(echo "$FLOW_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin).get('docs_extracted_6h', 0))")
STUCK_P=$(echo "$FLOW_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin).get('stuck_parsed', 0))")
STUCK_I=$(echo "$FLOW_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin).get('stuck_ingested', 0))")
TRENDS=$(echo "$FLOW_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin).get('trends_6h', 0))")
RECS=$(echo "$FLOW_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin).get('recs_6h', 0))")
DECISIONS=$(echo "$FLOW_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin).get('decisions_6h', 0))")
EVENTS=$(echo "$FLOW_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin).get('global_events_6h', 0))")
SNAPS=$(echo "$FLOW_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin).get('snapshots_6h', 0))")
MKT_AGE=$(echo "$FLOW_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin).get('market_data_age_min', -1))")
# Ingestion flowing?
if [[ "$DOCS_IN" -gt 0 ]]; then
pass "Ingestion: $DOCS_IN docs ingested"
else
fail "Ingestion: ZERO docs in last 6h"
fi
# Extraction flowing?
if [[ "$DOCS_EX" -gt 0 ]]; then
pass "Extraction: $DOCS_EX docs extracted"
else
fail "Extraction: ZERO docs extracted in last 6h"
fi
# Stuck documents?
if [[ "$STUCK_P" -eq 0 && "$STUCK_I" -eq 0 ]]; then
pass "No stuck documents"
else
[[ "$STUCK_P" -gt 0 ]] && warn "Stuck in 'parsed': $STUCK_P docs (>1h old)"
[[ "$STUCK_I" -gt 0 ]] && warn "Stuck in 'ingested': $STUCK_I docs (>1h old)"
fi
# Aggregation producing trends?
if [[ "$TRENDS" -gt 0 ]]; then
pass "Aggregation: $TRENDS trend entries"
else
warn "Aggregation: 0 trends (expected on weekends/off-hours)"
fi
# Recommendations flowing?
if [[ "$RECS" -gt 0 ]]; then
pass "Recommendations: $RECS generated"
else
fail "Recommendations: ZERO in last 6h"
fi
# Trading decisions happening?
if [[ "$DECISIONS" -gt 0 ]]; then
pass "Trading engine: $DECISIONS decisions"
else
warn "Trading engine: 0 decisions (market may be closed)"
fi
# Macro events?
if [[ "$EVENTS" -gt 0 ]]; then
pass "Macro pipeline: $EVENTS global events classified"
else
warn "Macro pipeline: 0 events (may be low news volume)"
fi
# Prediction snapshots?
if [[ "$SNAPS" -gt 0 ]]; then
pass "Validation: $SNAPS prediction snapshots"
else
warn "Validation: 0 snapshots in 6h"
fi
# Market data freshness
if [[ "$MKT_AGE" -ge 0 && "$MKT_AGE" -lt 30 ]]; then
pass "Market data: ${MKT_AGE}min old"
elif [[ "$MKT_AGE" -ge 30 && "$MKT_AGE" -lt 120 ]]; then
warn "Market data: ${MKT_AGE}min old (stale)"
elif [[ "$MKT_AGE" -ge 120 ]]; then
# Could be weekend/after hours
HOURS=$((MKT_AGE / 60))
warn "Market data: ${HOURS}h old (weekend/after-hours?)"
fi
else
fail "Could not query pipeline flow data"
fi
# ─── 8. QUALITY GATE STATUS ───────────────────────────────────
header "Model Quality Gate"
QG_CHECK=$(kubectl exec -n "$NS" deployment/query-api -- python3 -c "
import asyncio, asyncpg, os, json
from datetime import datetime, timezone
async def check():
pool = await asyncpg.create_pool(dsn=f\"postgresql://{os.environ['POSTGRES_USER']}:{os.environ['POSTGRES_PASSWORD']}@{os.environ['POSTGRES_HOST']}:{os.environ.get('POSTGRES_PORT','5432')}/{os.environ['POSTGRES_DB']}\", min_size=1, max_size=2)
row = await pool.fetchrow(\"\"\"
SELECT generated_at, prediction_count, win_rate, directional_accuracy, brier_score
FROM model_metric_snapshots
WHERE lookback_window = '30d' AND horizon = '7d'
ORDER BY generated_at DESC LIMIT 1
\"\"\")
r = {}
if row:
age_h = (datetime.now(tz=timezone.utc) - row['generated_at']).total_seconds() / 3600
r['age_hours'] = round(age_h, 1)
r['prediction_count'] = row['prediction_count']
r['win_rate'] = float(row['win_rate']) if row['win_rate'] else 0
r['directional_accuracy'] = float(row['directional_accuracy']) if row['directional_accuracy'] else 0
r['brier_score'] = float(row['brier_score']) if row['brier_score'] else 0
else:
r['age_hours'] = -1
await pool.close()
print(json.dumps(r))
asyncio.run(check())
" 2>/dev/null) || QG_CHECK='{}'
if [[ -n "$QG_CHECK" && "$QG_CHECK" != "{}" ]]; then
QG_AGE=$(echo "$QG_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin).get('age_hours', -1))")
QG_PREDS=$(echo "$QG_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin).get('prediction_count', 0))")
QG_WR=$(echo "$QG_CHECK" | python3 -c "import json,sys; print(f\"{json.load(sys.stdin).get('win_rate', 0)*100:.1f}%\")")
QG_DA=$(echo "$QG_CHECK" | python3 -c "import json,sys; print(f\"{json.load(sys.stdin).get('directional_accuracy', 0)*100:.1f}%\")")
if python3 -c "exit(0 if $QG_AGE >= 0 and $QG_AGE <= 48 else 1)" 2>/dev/null; then
pass "Metric snapshot: ${QG_AGE}h old (predictions=$QG_PREDS, win_rate=$QG_WR, accuracy=$QG_DA)"
elif python3 -c "exit(0 if $QG_AGE > 48 else 1)" 2>/dev/null; then
fail "Metric snapshot STALE: ${QG_AGE}h old (max 48h) — quality gate failing"
else
fail "No metric snapshot exists"
fi
fi
# ─── 9. PORTFOLIO STATUS ──────────────────────────────────────
header "Paper Trading Portfolio"
PORT_CHECK=$(kubectl exec -n "$NS" deployment/query-api -- python3 -c "
import asyncio, asyncpg, os, json
async def check():
pool = await asyncpg.create_pool(dsn=f\"postgresql://{os.environ['POSTGRES_USER']}:{os.environ['POSTGRES_PASSWORD']}@{os.environ['POSTGRES_HOST']}:{os.environ.get('POSTGRES_PORT','5432')}/{os.environ['POSTGRES_DB']}\", min_size=1, max_size=2)
r = {}
positions = await pool.fetch('SELECT ticker, quantity, avg_entry_price, current_price, unrealized_pnl FROM positions')
r['position_count'] = len(positions)
r['total_invested'] = sum(float(p['avg_entry_price']) * float(p['quantity']) for p in positions)
r['unrealized_pnl'] = sum(float(p['unrealized_pnl'] or 0) for p in positions)
r['tickers'] = [p['ticker'] for p in positions]
# Recent orders
r['orders_24h'] = await pool.fetchval(\"SELECT count(*) FROM orders WHERE created_at > now() - interval '24 hours'\")
r['total_orders'] = await pool.fetchval('SELECT count(*) FROM orders')
await pool.close()
print(json.dumps(r))
asyncio.run(check())
" 2>/dev/null) || PORT_CHECK='{}'
if [[ -n "$PORT_CHECK" && "$PORT_CHECK" != "{}" ]]; then
POS_CNT=$(echo "$PORT_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin).get('position_count', 0))")
INVESTED=$(echo "$PORT_CHECK" | python3 -c "import json,sys; print(f\"\${json.load(sys.stdin).get('total_invested', 0):,.0f}\")")
PNL=$(echo "$PORT_CHECK" | python3 -c "import json,sys; print(f\"\${json.load(sys.stdin).get('unrealized_pnl', 0):,.0f}\")")
TICKERS=$(echo "$PORT_CHECK" | python3 -c "import json,sys; print(', '.join(json.load(sys.stdin).get('tickers', [])))")
ORDERS_24=$(echo "$PORT_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin).get('orders_24h', 0))")
TOTAL_ORD=$(echo "$PORT_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total_orders', 0))")
pass "$POS_CNT positions ($TICKERS)"
pass "Invested: $INVESTED | Unrealized P&L: $PNL"
pass "Orders: $ORDERS_24 today, $TOTAL_ORD lifetime"
fi
# ─── 10. API ENDPOINTS ────────────────────────────────────────
header "API Endpoints"
# Query API health
QA_STATUS=$(kubectl exec -n "$NS" deployment/query-api -- curl -sf http://localhost:8000/health 2>/dev/null) && pass "Query API: healthy" || fail "Query API: unreachable"
# Symbol Registry health
SR_STATUS=$(kubectl exec -n "$NS" deployment/symbol-registry -- curl -sf http://localhost:8000/health 2>/dev/null) && pass "Symbol Registry: healthy" || fail "Symbol Registry: unreachable"
# Trading Engine health
TE_STATUS=$(kubectl exec -n "$NS" deployment/trading-engine -- curl -sf http://localhost:8000/health 2>/dev/null) && pass "Trading Engine: healthy" || fail "Trading Engine: unreachable"
# Risk Engine health
RISK_STATUS=$(kubectl exec -n "$NS" deployment/risk -- curl -sf http://localhost:8000/health 2>/dev/null) && pass "Risk Engine: healthy" || fail "Risk Engine: unreachable"
# ─── 11. RESOURCE USAGE ───────────────────────────────────────
header "Resource Usage"
TOTAL_MEM=$(kubectl top pods -n "$NS" --no-headers 2>/dev/null | awk '{sum += $3} END {print sum}')
TOTAL_CPU=$(kubectl top pods -n "$NS" --no-headers 2>/dev/null | awk '{sum += $2} END {print sum}')
pass "Total: ${TOTAL_CPU}m CPU, ${TOTAL_MEM}Mi memory"
# Any pod over 80% of its limit?
HIGH_MEM=$(kubectl top pods -n "$NS" --no-headers 2>/dev/null | awk '$3+0 > 200 {print $1 "=" $3}')
if [[ -n "$HIGH_MEM" ]]; then
warn "High memory pods: $HIGH_MEM"
fi
# ─── 12. END-TO-END LATENCY CHECK ─────────────────────────────
header "End-to-End Pipeline Latency"
# Copy helper script to pod and run it
LATENCY_CHECK=$(kubectl exec -n "$NS" deployment/query-api -- python3 -c '
import asyncio, asyncpg, os, json
from datetime import datetime, timezone, timedelta
async def check():
dsn = "postgresql://{}:{}@{}:{}/{}".format(os.environ["POSTGRES_USER"], os.environ["POSTGRES_PASSWORD"], os.environ["POSTGRES_HOST"], os.environ.get("POSTGRES_PORT","5432"), os.environ["POSTGRES_DB"])
pool = await asyncpg.create_pool(dsn=dsn, min_size=1, max_size=2)
r = {}
now = datetime.now(tz=timezone.utc)
cutoff = now - timedelta(hours=24)
row = await pool.fetchrow("SELECT avg(extract(epoch from (updated_at - created_at))) as avg_sec, max(extract(epoch from (updated_at - created_at))) as max_sec FROM documents WHERE status = $1 AND updated_at > $2 AND updated_at > created_at", "extracted", cutoff)
if row and row["avg_sec"]:
r["avg_extraction_sec"] = round(float(row["avg_sec"]), 1)
r["max_extraction_sec"] = round(float(row["max_sec"]), 1)
latest_trend = await pool.fetchval("SELECT max(generated_at) FROM trend_windows")
if latest_trend:
r["trend_age_min"] = round((now - latest_trend).total_seconds() / 60)
latest_rec = await pool.fetchval("SELECT max(created_at) FROM recommendations")
if latest_rec:
r["rec_age_min"] = round((now - latest_rec).total_seconds() / 60)
await pool.close()
print(json.dumps(r))
asyncio.run(check())
' 2>/dev/null) || LATENCY_CHECK='{}'
if [[ -n "$LATENCY_CHECK" && "$LATENCY_CHECK" != "{}" ]]; then
AVG_EXT=$(echo "$LATENCY_CHECK" | python3 -c "import json,sys; v=json.load(sys.stdin).get('avg_extraction_sec'); print(f'{v}s' if v else 'N/A')")
MAX_EXT=$(echo "$LATENCY_CHECK" | python3 -c "import json,sys; v=json.load(sys.stdin).get('max_extraction_sec'); print(f'{v}s' if v else 'N/A')")
TREND_AGE=$(echo "$LATENCY_CHECK" | python3 -c "import json,sys; v=json.load(sys.stdin).get('trend_age_min'); print(f'{v}min' if v else 'N/A')")
REC_AGE=$(echo "$LATENCY_CHECK" | python3 -c "import json,sys; v=json.load(sys.stdin).get('rec_age_min'); print(f'{v}min' if v else 'N/A')")
pass "Doc ingestion→extraction: avg=${AVG_EXT}, max=${MAX_EXT}"
if echo "$LATENCY_CHECK" | python3 -c "import json,sys; v=json.load(sys.stdin).get('trend_age_min',999); exit(0 if v < 30 else 1)" 2>/dev/null; then
pass "Latest trend: ${TREND_AGE} ago"
else
warn "Latest trend: ${TREND_AGE} ago"
fi
if echo "$LATENCY_CHECK" | python3 -c "import json,sys; v=json.load(sys.stdin).get('rec_age_min',999); exit(0 if v < 30 else 1)" 2>/dev/null; then
pass "Latest recommendation: ${REC_AGE} ago"
else
warn "Latest recommendation: ${REC_AGE} ago"
fi
else
warn "Could not measure pipeline latency"
fi
# ─── SUMMARY ──────────────────────────────────────────────────
header "Summary"
echo -e " ${GREEN}PASS: $PASS${NC} ${YELLOW}WARN: $WARN${NC} ${RED}FAIL: $FAIL${NC}"
echo ""
if [[ "$FAIL" -gt 0 ]]; then
echo -e "${RED}Pipeline has failures that need attention.${NC}"
exit 1
elif [[ "$WARN" -gt 3 ]]; then
echo -e "${YELLOW}Pipeline is running but has multiple warnings.${NC}"
exit 0
else
echo -e "${GREEN}Pipeline is healthy and flowing.${NC}"
exit 0
fi