57dc91585d
Internal SmartGift build of a Claude Code monitoring dashboard. Lanes: a durable unit of parallel agent work, one per working directory, tracked across session restarts. Managed lanes are git worktrees the dashboard provisions and can reset or remove behind a three-check destroy guard and a counted preflight; adopted lanes are directories you already own and are never destroyable. Pipelines: a lane moves through pipeline stages. A stage the agent declares with evidence renders green; a stage inferred from the tool-event stream renders dashed amber and never counts as done. Detection is forward-only within a 30-minute window, and never writes the declared stage. Workspace: one page at /run with a lane grid, the selected lane's pipeline, and a full Claude console behind a disclosure.
343 lines
13 KiB
Bash
Executable File
343 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# health-check.sh – Comprehensive health check for Claude Code Agent Monitor
|
||
#
|
||
# Usage:
|
||
# ./health-check.sh --url http://localhost:4820
|
||
# ./health-check.sh --url http://host:port --retries 30 --interval 5
|
||
# ./health-check.sh --url http://host:port --json
|
||
# ./health-check.sh --help
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||
set -euo pipefail
|
||
|
||
# ── Colors & logging ───────────────────────────────────────────────────────
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
CYAN='\033[0;36m'
|
||
BOLD='\033[1m'
|
||
NC='\033[0m'
|
||
|
||
log() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $*"; }
|
||
info() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${BLUE}ℹ${NC} $*"; }
|
||
ok() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${GREEN}✔${NC} $*"; }
|
||
warn() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${YELLOW}⚠${NC} $*" >&2; }
|
||
err() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${RED}✖${NC} $*" >&2; }
|
||
|
||
# ── Defaults ────────────────────────────────────────────────────────────────
|
||
BASE_URL=""
|
||
RETRIES=30
|
||
INTERVAL=5
|
||
TIMEOUT=5
|
||
RESPONSE_THRESHOLD=2000 # milliseconds
|
||
JSON_OUTPUT=false
|
||
CHECK_WEBSOCKET=true
|
||
HEALTH_PATH="/api/health"
|
||
|
||
# ── Usage ───────────────────────────────────────────────────────────────────
|
||
usage() {
|
||
cat <<HELP
|
||
${BOLD}Usage:${NC}
|
||
$(basename "$0") --url <base-url> [options]
|
||
|
||
${BOLD}Required:${NC}
|
||
--url, -u Base URL (e.g., http://localhost:4820)
|
||
|
||
${BOLD}Options:${NC}
|
||
--retries, -r Max retry attempts (default: 30)
|
||
--interval, -i Seconds between retries (default: 5)
|
||
--timeout HTTP request timeout in seconds (default: 5)
|
||
--threshold Max response time in ms (default: 2000)
|
||
--path Health endpoint path (default: /api/health)
|
||
--no-websocket Skip WebSocket connectivity check
|
||
--json Output results as JSON
|
||
--help, -h Show this help message
|
||
|
||
${BOLD}Exit codes:${NC}
|
||
0 All checks passed
|
||
1 One or more checks failed
|
||
|
||
${BOLD}Examples:${NC}
|
||
$(basename "$0") --url http://localhost:4820
|
||
$(basename "$0") --url https://monitor.example.com --retries 10 --json
|
||
$(basename "$0") --url http://10.0.1.5:4820 --threshold 500 --no-websocket
|
||
|
||
HELP
|
||
exit 0
|
||
}
|
||
|
||
# ── Argument parsing ────────────────────────────────────────────────────────
|
||
parse_args() {
|
||
[[ $# -eq 0 ]] && usage
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--url|-u) BASE_URL="$2"; shift 2 ;;
|
||
--retries|-r) RETRIES="$2"; shift 2 ;;
|
||
--interval|-i) INTERVAL="$2"; shift 2 ;;
|
||
--timeout) TIMEOUT="$2"; shift 2 ;;
|
||
--threshold) RESPONSE_THRESHOLD="$2"; shift 2 ;;
|
||
--path) HEALTH_PATH="$2"; shift 2 ;;
|
||
--no-websocket) CHECK_WEBSOCKET=false; shift ;;
|
||
--json) JSON_OUTPUT=true; shift ;;
|
||
--help|-h) usage ;;
|
||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||
esac
|
||
done
|
||
|
||
[[ -z "$BASE_URL" ]] && { echo "Missing required argument: --url" >&2; exit 1; }
|
||
|
||
# Strip trailing slash
|
||
BASE_URL="${BASE_URL%/}"
|
||
}
|
||
|
||
# ── HTTP health check ──────────────────────────────────────────────────────
|
||
check_http_health() {
|
||
local url="${BASE_URL}${HEALTH_PATH}"
|
||
local attempt=0
|
||
local http_ok=false
|
||
local status_code=""
|
||
local response_body=""
|
||
local response_time_ms=0
|
||
|
||
info "Checking HTTP health: ${url}"
|
||
|
||
while [[ $attempt -lt $RETRIES ]]; do
|
||
attempt=$((attempt + 1))
|
||
|
||
# Measure response time and capture output
|
||
local start_ns
|
||
start_ns=$(date +%s%N 2>/dev/null || echo "0")
|
||
|
||
local http_response
|
||
http_response=$(curl -sf \
|
||
--max-time "${TIMEOUT}" \
|
||
--write-out "\n%{http_code}\n%{time_total}" \
|
||
"${url}" 2>/dev/null) || true
|
||
|
||
local end_ns
|
||
end_ns=$(date +%s%N 2>/dev/null || echo "0")
|
||
|
||
if [[ -n "$http_response" ]]; then
|
||
response_body=$(echo "$http_response" | head -n -2)
|
||
status_code=$(echo "$http_response" | tail -2 | head -1)
|
||
local time_total
|
||
time_total=$(echo "$http_response" | tail -1)
|
||
# Convert seconds to milliseconds
|
||
response_time_ms=$(echo "$time_total" | awk '{printf "%.0f", $1 * 1000}' 2>/dev/null || echo "0")
|
||
|
||
if [[ "$status_code" == "200" ]] && echo "$response_body" | grep -q '"status":"ok"'; then
|
||
http_ok=true
|
||
break
|
||
fi
|
||
fi
|
||
|
||
if [[ $attempt -lt $RETRIES ]]; then
|
||
info "Attempt ${attempt}/${RETRIES} – waiting ${INTERVAL}s..."
|
||
sleep "${INTERVAL}"
|
||
fi
|
||
done
|
||
|
||
# Results
|
||
HTTP_OK="$http_ok"
|
||
HTTP_STATUS="$status_code"
|
||
HTTP_BODY="$response_body"
|
||
HTTP_RESPONSE_TIME_MS="$response_time_ms"
|
||
HTTP_ATTEMPTS="$attempt"
|
||
|
||
if [[ "$http_ok" == true ]]; then
|
||
ok "HTTP health check passed (${response_time_ms}ms, ${attempt} attempt(s))"
|
||
else
|
||
err "HTTP health check failed after ${attempt} attempts"
|
||
fi
|
||
}
|
||
|
||
# ── Response time check ────────────────────────────────────────────────────
|
||
check_response_time() {
|
||
if [[ "$HTTP_OK" != true ]]; then
|
||
LATENCY_OK=false
|
||
return
|
||
fi
|
||
|
||
if [[ "$HTTP_RESPONSE_TIME_MS" -le "$RESPONSE_THRESHOLD" ]]; then
|
||
LATENCY_OK=true
|
||
ok "Response time ${HTTP_RESPONSE_TIME_MS}ms within threshold (${RESPONSE_THRESHOLD}ms)"
|
||
else
|
||
LATENCY_OK=false
|
||
warn "Response time ${HTTP_RESPONSE_TIME_MS}ms exceeds threshold (${RESPONSE_THRESHOLD}ms)"
|
||
fi
|
||
}
|
||
|
||
# ── WebSocket connectivity check ───────────────────────────────────────────
|
||
check_websocket() {
|
||
WS_OK=false
|
||
|
||
if [[ "$CHECK_WEBSOCKET" == false ]]; then
|
||
info "WebSocket check skipped"
|
||
WS_OK=true # treat as pass when skipped
|
||
return
|
||
fi
|
||
|
||
# Construct WebSocket URL
|
||
local ws_url="${BASE_URL}"
|
||
ws_url="${ws_url/http:/ws:}"
|
||
ws_url="${ws_url/https:/wss:}"
|
||
ws_url="${ws_url}/ws"
|
||
|
||
info "Checking WebSocket: ${ws_url}"
|
||
|
||
# Check if we have a WebSocket testing tool
|
||
if command -v websocat &>/dev/null; then
|
||
if echo "" | websocat --one-message -t "${ws_url}" 2>/dev/null; then
|
||
WS_OK=true
|
||
ok "WebSocket connection succeeded (websocat)"
|
||
return
|
||
fi
|
||
fi
|
||
|
||
# Fallback: use curl with upgrade headers to test the handshake
|
||
local ws_status
|
||
ws_status=$(curl -sf \
|
||
--max-time "${TIMEOUT}" \
|
||
-o /dev/null \
|
||
-w "%{http_code}" \
|
||
-H "Upgrade: websocket" \
|
||
-H "Connection: Upgrade" \
|
||
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
|
||
-H "Sec-WebSocket-Version: 13" \
|
||
"${BASE_URL}/ws" 2>/dev/null) || ws_status="000"
|
||
|
||
# 101 = Switching Protocols (WebSocket upgrade success)
|
||
# 400 = Bad Request (server recognized WS but rejected – still proves WS is available)
|
||
case "$ws_status" in
|
||
101)
|
||
WS_OK=true
|
||
ok "WebSocket handshake succeeded (HTTP 101)"
|
||
;;
|
||
400|426)
|
||
WS_OK=true
|
||
ok "WebSocket endpoint reachable (HTTP ${ws_status} – server recognized upgrade)"
|
||
;;
|
||
*)
|
||
# Try Node.js one-liner as last resort
|
||
if command -v node &>/dev/null; then
|
||
local node_result
|
||
node_result=$(node -e "
|
||
const ws = new (require('ws'))('${ws_url}');
|
||
const t = setTimeout(() => { process.stdout.write('timeout'); process.exit(1); }, 5000);
|
||
ws.on('open', () => { clearTimeout(t); process.stdout.write('ok'); ws.close(); process.exit(0); });
|
||
ws.on('error', (e) => { clearTimeout(t); process.stdout.write('error:' + e.message); process.exit(1); });
|
||
" 2>/dev/null) || node_result="error"
|
||
|
||
if [[ "$node_result" == "ok" ]]; then
|
||
WS_OK=true
|
||
ok "WebSocket connection verified (node)"
|
||
else
|
||
WS_OK=false
|
||
warn "WebSocket check failed: ${node_result}"
|
||
fi
|
||
else
|
||
warn "WebSocket check inconclusive (no ws testing tool available, HTTP status: ${ws_status})"
|
||
WS_OK=true # Don't fail the whole check for this
|
||
fi
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ── Output results ──────────────────────────────────────────────────────────
|
||
output_results() {
|
||
local overall_healthy=true
|
||
[[ "$HTTP_OK" != true ]] && overall_healthy=false
|
||
[[ "$LATENCY_OK" != true ]] && overall_healthy=false
|
||
[[ "$WS_OK" != true ]] && overall_healthy=false
|
||
|
||
if [[ "$JSON_OUTPUT" == true ]]; then
|
||
cat <<JSON
|
||
{
|
||
"healthy": ${overall_healthy},
|
||
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||
"url": "${BASE_URL}",
|
||
"checks": {
|
||
"http": {
|
||
"passed": ${HTTP_OK},
|
||
"status_code": "${HTTP_STATUS:-null}",
|
||
"response_time_ms": ${HTTP_RESPONSE_TIME_MS:-0},
|
||
"attempts": ${HTTP_ATTEMPTS:-0},
|
||
"body": $(echo "${HTTP_BODY:-null}" | head -c 500 | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))' 2>/dev/null || echo '"null"')
|
||
},
|
||
"latency": {
|
||
"passed": ${LATENCY_OK},
|
||
"response_time_ms": ${HTTP_RESPONSE_TIME_MS:-0},
|
||
"threshold_ms": ${RESPONSE_THRESHOLD}
|
||
},
|
||
"websocket": {
|
||
"passed": ${WS_OK},
|
||
"checked": ${CHECK_WEBSOCKET}
|
||
}
|
||
}
|
||
}
|
||
JSON
|
||
else
|
||
echo ""
|
||
echo -e "${BOLD}Health Check Summary${NC}"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
|
||
local http_icon=$([[ "$HTTP_OK" == true ]] && echo "${GREEN}✔${NC}" || echo "${RED}✖${NC}")
|
||
local latency_icon=$([[ "$LATENCY_OK" == true ]] && echo "${GREEN}✔${NC}" || echo "${YELLOW}⚠${NC}")
|
||
local ws_icon=$([[ "$WS_OK" == true ]] && echo "${GREEN}✔${NC}" || echo "${RED}✖${NC}")
|
||
|
||
echo -e " ${http_icon} HTTP /api/health (${HTTP_STATUS:-???}, ${HTTP_RESPONSE_TIME_MS:-?}ms, ${HTTP_ATTEMPTS:-?} attempts)"
|
||
echo -e " ${latency_icon} Response time (${HTTP_RESPONSE_TIME_MS:-?}ms / ${RESPONSE_THRESHOLD}ms threshold)"
|
||
echo -e " ${ws_icon} WebSocket (checked: ${CHECK_WEBSOCKET})"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
|
||
if [[ "$overall_healthy" == true ]]; then
|
||
echo -e " ${GREEN}${BOLD}Overall: HEALTHY${NC}"
|
||
else
|
||
echo -e " ${RED}${BOLD}Overall: UNHEALTHY${NC}"
|
||
fi
|
||
echo ""
|
||
fi
|
||
|
||
if [[ "$overall_healthy" == true ]]; then
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# ── Main ────────────────────────────────────────────────────────────────────
|
||
main() {
|
||
parse_args "$@"
|
||
|
||
# Initialize result variables
|
||
HTTP_OK=false
|
||
HTTP_STATUS=""
|
||
HTTP_BODY=""
|
||
HTTP_RESPONSE_TIME_MS=0
|
||
HTTP_ATTEMPTS=0
|
||
LATENCY_OK=false
|
||
WS_OK=false
|
||
|
||
[[ "$JSON_OUTPUT" != true ]] && {
|
||
echo ""
|
||
echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${BOLD}${GREEN}║ Claude Code Agent Monitor – Health Check ║${NC}"
|
||
echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
info "Target: ${BASE_URL}"
|
||
info "Config: retries=${RETRIES}, interval=${INTERVAL}s, threshold=${RESPONSE_THRESHOLD}ms"
|
||
echo ""
|
||
}
|
||
|
||
check_http_health
|
||
check_response_time
|
||
check_websocket
|
||
|
||
output_results
|
||
}
|
||
|
||
main "$@"
|