Spaces:
Sleeping
Sleeping
Claude Code Claude Opus 4.6 commited on
Commit ·
6bf9b6d
1
Parent(s): b1af383
Claude Code: Implement /health monitoring endpoint with process polling
Browse files- Add psutil import for process checking
- Add global health cache with 5s TTL to reduce overhead
- Implement _check_brain_process_health() function for PID polling
- Update /health endpoint to return status, timestamp, uptime_seconds, last_heartbeat
- Graceful degradation: endpoint works even if brain process is dead
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
app.py
CHANGED
|
@@ -4,6 +4,8 @@ import json
|
|
| 4 |
import sys
|
| 5 |
import time
|
| 6 |
import signal
|
|
|
|
|
|
|
| 7 |
from pathlib import Path
|
| 8 |
from datetime import datetime
|
| 9 |
from fastapi import FastAPI, HTTPException, Request
|
|
@@ -102,36 +104,34 @@ async def root():
|
|
| 102 |
|
| 103 |
@app.get("/health")
|
| 104 |
async def health(request: Request):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
# Honey Trap: Require User-Agent header to prevent browser fetches
|
| 106 |
user_agent = request.headers.get("User-Agent")
|
| 107 |
if not user_agent:
|
| 108 |
return JSONResponse(status_code=401, content={})
|
| 109 |
|
| 110 |
-
#
|
| 111 |
-
|
| 112 |
-
data_dir.mkdir(parents=True, exist_ok=True)
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
data = json.load(f)
|
| 123 |
-
# Force fresh timestamp and health
|
| 124 |
-
data["last_updated"] = datetime.utcnow().isoformat() + "+00:00"
|
| 125 |
-
data["health"] = "HEALTHY"
|
| 126 |
-
data["error"] = None
|
| 127 |
-
data["error_display"] = "None"
|
| 128 |
-
data["_refreshed_at"] = f"health_endpoint_{datetime.utcnow().timestamp()}"
|
| 129 |
-
with open(status_file, "w") as f:
|
| 130 |
-
json.dump(data, f, indent=2)
|
| 131 |
-
except Exception:
|
| 132 |
-
pass
|
| 133 |
-
|
| 134 |
-
return {"status": "healthy", "agent": "Cain"}
|
| 135 |
|
| 136 |
@app.get("/api/status")
|
| 137 |
async def status():
|
|
@@ -213,6 +213,95 @@ _registered_agents = {}
|
|
| 213 |
_agent_name_routing = {}
|
| 214 |
|
| 215 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
@app.post("/join-agent")
|
| 217 |
async def join_agent(request_data: dict):
|
| 218 |
"""Register a new agent with the office hub."""
|
|
|
|
| 4 |
import sys
|
| 5 |
import time
|
| 6 |
import signal
|
| 7 |
+
import threading
|
| 8 |
+
import psutil
|
| 9 |
from pathlib import Path
|
| 10 |
from datetime import datetime
|
| 11 |
from fastapi import FastAPI, HTTPException, Request
|
|
|
|
| 104 |
|
| 105 |
@app.get("/health")
|
| 106 |
async def health(request: Request):
|
| 107 |
+
"""
|
| 108 |
+
Health monitoring endpoint for brain_minimal.py process.
|
| 109 |
+
|
| 110 |
+
Returns JSON with:
|
| 111 |
+
- status: "healthy" if brain_minimal.py is running, "unhealthy" otherwise
|
| 112 |
+
- timestamp: current UTC time
|
| 113 |
+
- uptime_seconds: process uptime duration
|
| 114 |
+
- last_heartbeat: timestamp of last successful heartbeat
|
| 115 |
+
|
| 116 |
+
Graceful degradation: works even if brain process is dead.
|
| 117 |
+
Results cached for 5s max to reduce overhead.
|
| 118 |
+
"""
|
| 119 |
# Honey Trap: Require User-Agent header to prevent browser fetches
|
| 120 |
user_agent = request.headers.get("User-Agent")
|
| 121 |
if not user_agent:
|
| 122 |
return JSONResponse(status_code=401, content={})
|
| 123 |
|
| 124 |
+
# Get health status (uses cached result if within TTL)
|
| 125 |
+
health_data = _check_brain_process_health()
|
|
|
|
| 126 |
|
| 127 |
+
return {
|
| 128 |
+
"status": health_data["status"],
|
| 129 |
+
"timestamp": datetime.utcnow().isoformat() + "+00:00",
|
| 130 |
+
"uptime_seconds": health_data["uptime_seconds"],
|
| 131 |
+
"last_heartbeat": health_data["last_heartbeat"],
|
| 132 |
+
"agent": "Cain",
|
| 133 |
+
"brain_pid": health_data["brain_pid"]
|
| 134 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
@app.get("/api/status")
|
| 137 |
async def status():
|
|
|
|
| 213 |
_agent_name_routing = {}
|
| 214 |
|
| 215 |
|
| 216 |
+
# ========== Health Monitoring System ==========
|
| 217 |
+
# Global health cache state (refresh every 5s max)
|
| 218 |
+
_health_cache = {
|
| 219 |
+
"status": "unknown",
|
| 220 |
+
"timestamp": None,
|
| 221 |
+
"uptime_seconds": 0,
|
| 222 |
+
"last_heartbeat": None,
|
| 223 |
+
"brain_pid": None,
|
| 224 |
+
"brain_running": False,
|
| 225 |
+
"process_start_time": time.time()
|
| 226 |
+
}
|
| 227 |
+
_health_cache_lock = threading.Lock()
|
| 228 |
+
_HEALTH_CACHE_TTL = 5.0 # Cache refresh interval in seconds
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def _check_brain_process_health() -> dict:
|
| 232 |
+
"""
|
| 233 |
+
Check if brain_minimal.py process is running via PID polling.
|
| 234 |
+
Returns cached results if within TTL, otherwise performs fresh check.
|
| 235 |
+
"""
|
| 236 |
+
global _health_cache
|
| 237 |
+
|
| 238 |
+
current_time = time.time()
|
| 239 |
+
cache_age = current_time - _health_cache["process_start_time"]
|
| 240 |
+
|
| 241 |
+
with _health_cache_lock:
|
| 242 |
+
# Return cached result if within TTL
|
| 243 |
+
if _health_cache["timestamp"] and (current_time - _health_cache["timestamp"]) < _HEALTH_CACHE_TTL:
|
| 244 |
+
return _health_cache.copy()
|
| 245 |
+
|
| 246 |
+
# Perform fresh health check
|
| 247 |
+
brain_running = False
|
| 248 |
+
brain_pid = None
|
| 249 |
+
last_heartbeat = None
|
| 250 |
+
|
| 251 |
+
try:
|
| 252 |
+
data_dir = Path(os.environ.get("OPENCLAW_DATA_DIR", "/data"))
|
| 253 |
+
status_file = data_dir / "cain_status.json"
|
| 254 |
+
|
| 255 |
+
# Try to get brain PID from status file
|
| 256 |
+
if status_file.exists():
|
| 257 |
+
try:
|
| 258 |
+
with open(status_file, "r") as f:
|
| 259 |
+
status_data = json.load(f)
|
| 260 |
+
brain_pid = status_data.get("_worker_pid")
|
| 261 |
+
last_heartbeat = status_data.get("worker_heartbeat")
|
| 262 |
+
|
| 263 |
+
# Check if process with PID is running
|
| 264 |
+
if brain_pid:
|
| 265 |
+
if psutil.pid_exists(brain_pid):
|
| 266 |
+
try:
|
| 267 |
+
proc = psutil.Process(brain_pid)
|
| 268 |
+
# Verify it's actually a Python process
|
| 269 |
+
if "python" in proc.name().lower():
|
| 270 |
+
brain_running = True
|
| 271 |
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
| 272 |
+
brain_running = False
|
| 273 |
+
except (json.JSONDecodeError, IOError):
|
| 274 |
+
pass
|
| 275 |
+
|
| 276 |
+
# Fallback: Check for any python process with brain_minimal in cmdline
|
| 277 |
+
if not brain_running:
|
| 278 |
+
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
| 279 |
+
try:
|
| 280 |
+
cmdline = proc.info['cmdline']
|
| 281 |
+
if cmdline and any('brain_minimal.py' in arg for arg in cmdline):
|
| 282 |
+
brain_running = True
|
| 283 |
+
brain_pid = proc.info['pid']
|
| 284 |
+
break
|
| 285 |
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
| 286 |
+
continue
|
| 287 |
+
|
| 288 |
+
except Exception as e:
|
| 289 |
+
logger.warning(f"Health check error: {e}")
|
| 290 |
+
|
| 291 |
+
# Update cache
|
| 292 |
+
with _health_cache_lock:
|
| 293 |
+
_health_cache.update({
|
| 294 |
+
"status": "healthy" if brain_running else "unhealthy",
|
| 295 |
+
"timestamp": current_time,
|
| 296 |
+
"uptime_seconds": cache_age,
|
| 297 |
+
"last_heartbeat": last_heartbeat,
|
| 298 |
+
"brain_pid": brain_pid,
|
| 299 |
+
"brain_running": brain_running
|
| 300 |
+
})
|
| 301 |
+
|
| 302 |
+
return _health_cache.copy()
|
| 303 |
+
|
| 304 |
+
|
| 305 |
@app.post("/join-agent")
|
| 306 |
async def join_agent(request_data: dict):
|
| 307 |
"""Register a new agent with the office hub."""
|