Claude Code Claude Opus 4.6 commited on
Commit
8fdfa0c
·
1 Parent(s): 560527a

Claude Code: add robust /health endpoint with brain, persistence, and frontend checks

Browse files

- Explicit status keys: brain, persistence, frontend, agent_router
- Returns 503 for critical brain import failures
- Detailed checks dictionary for debugging
- Frontend and persistence issues return degraded status

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +87 -44
app.py CHANGED
@@ -353,81 +353,124 @@ async def health():
353
  """
354
  Health check endpoint with proper HTTP status codes.
355
  Returns 200 if healthy, 503 if unhealthy with descriptive error.
 
 
 
 
 
356
  """
357
  from fastapi import status as http_status
358
 
359
- details = {"checks": {}}
360
- brain_available = False
361
- brain_error = None
362
 
363
  # 1. Check brain_minimal can be imported
 
364
  try:
365
  from openclaw.agents.brain_minimal import BrainMinimal
366
  brain = BrainMinimal(agent_name="cain", legacy_mode=True)
367
- brain_available = True
368
- details["checks"]["brain_minimal"] = "ok"
369
  except ImportError as e:
370
- brain_error = f"ImportError: {str(e)}"
371
- details["checks"]["brain_minimal"] = f"failed: {brain_error}"
 
372
  except Exception as e:
373
- brain_error = f"{type(e).__name__}: {str(e)}"
374
- details["checks"]["brain_minimal"] = f"failed: {brain_error}"
375
-
376
- # If brain import fails, return 503 immediately
377
- if not brain_available:
378
- return JSONResponse(
379
- status_code=http_status.HTTP_503_SERVICE_UNAVAILABLE,
380
- content={
381
- "status": "error",
382
- "error": brain_error or "brain_minimal import failed",
383
- "details": details
384
- }
385
- )
386
 
387
- # 2. Check cain_status.json is accessible and valid
 
388
  cain_status_path = "/app/openclaw/.openclaw/agents/cain_status.json"
389
- status_file_ok = True
390
- status_error = None
 
391
  try:
392
  with open(cain_status_path, "r") as f:
393
  status_data = json.load(f)
394
  current_state = status_data.get("current_state", "unknown")
395
- details["cain_status_state"] = current_state
396
  if current_state == "error":
397
- status_error = "cain_status.json reports error state"
398
- status_file_ok = False
399
- elif current_state == "unknown":
400
- details["cain_status"] = "ok (unknown state)"
 
 
 
 
 
 
 
 
 
 
 
 
401
  except FileNotFoundError:
402
- status_error = "cain_status.json not found"
403
- status_file_ok = False
404
  except json.JSONDecodeError as e:
405
- status_error = f"cain_status.json invalid JSON: {str(e)}"
406
- status_file_ok = False
407
  except Exception as e:
408
- status_error = f"cain_status.json read error: {type(e).__name__}: {str(e)}"
409
- status_file_ok = False
410
 
411
- details["checks"]["cain_status"] = "ok" if status_file_ok else f"failed: {status_error}"
412
 
413
- # 3. Check agent router heartbeat
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  now = time.time()
415
  cain_state = agent_router.get_state(AgentRole.CAIN)
416
  heartbeat_age = now - cain_state.last_heartbeat
417
  router_ok = heartbeat_age < 30
418
- details["checks"]["agent_router"] = "ok" if router_ok else f"stale ({heartbeat_age:.1f}s)"
419
 
420
- # Return healthy response if brain is available
421
- response = {
422
- "status": "healthy",
423
- "agent": "cain",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  "uptime_seconds": round(time.time() - START_TIME, 2),
425
- "details": details,
426
  "timestamp": datetime.utcnow().isoformat() + "+00:00"
427
  }
428
 
429
- return response
430
-
431
 
432
  @app.get("/debug/health")
433
  async def debug_health():
 
353
  """
354
  Health check endpoint with proper HTTP status codes.
355
  Returns 200 if healthy, 503 if unhealthy with descriptive error.
356
+
357
+ Checks:
358
+ 1. Brain import status
359
+ 2. Data persistence files (JSON-based storage)
360
+ 3. Frontend asset availability (static/index.html)
361
  """
362
  from fastapi import status as http_status
363
 
364
+ checks = {}
365
+ is_healthy = True
366
+ error_message = None
367
 
368
  # 1. Check brain_minimal can be imported
369
+ brain_ok = False
370
  try:
371
  from openclaw.agents.brain_minimal import BrainMinimal
372
  brain = BrainMinimal(agent_name="cain", legacy_mode=True)
373
+ brain_ok = True
374
+ checks["brain_import"] = "ok"
375
  except ImportError as e:
376
+ checks["brain_import"] = f"failed: ImportError: {str(e)}"
377
+ is_healthy = False
378
+ error_message = f"Brain import failed: {str(e)}"
379
  except Exception as e:
380
+ checks["brain_import"] = f"failed: {type(e).__name__}: {str(e)}"
381
+ is_healthy = False
382
+ error_message = f"Brain import failed: {type(e).__name__}: {str(e)}"
 
 
 
 
 
 
 
 
 
 
383
 
384
+ # 2. Check data persistence files (JSON-based)
385
+ persistence_ok = True
386
  cain_status_path = "/app/openclaw/.openclaw/agents/cain_status.json"
387
+ registry_path = "/app/openclaw/.openclaw/agents/registry.json"
388
+
389
+ # Check cain_status.json
390
  try:
391
  with open(cain_status_path, "r") as f:
392
  status_data = json.load(f)
393
  current_state = status_data.get("current_state", "unknown")
394
+ checks["persistence_cain_status"] = "ok"
395
  if current_state == "error":
396
+ checks["persistence_cain_status"] = "ok (error state)"
397
+ except FileNotFoundError:
398
+ checks["persistence_cain_status"] = "failed: file not found"
399
+ persistence_ok = False
400
+ except json.JSONDecodeError as e:
401
+ checks["persistence_cain_status"] = f"failed: invalid JSON: {str(e)}"
402
+ persistence_ok = False
403
+ except Exception as e:
404
+ checks["persistence_cain_status"] = f"failed: {type(e).__name__}: {str(e)}"
405
+ persistence_ok = False
406
+
407
+ # Check registry.json
408
+ try:
409
+ with open(registry_path, "r") as f:
410
+ json.load(f)
411
+ checks["persistence_registry"] = "ok"
412
  except FileNotFoundError:
413
+ checks["persistence_registry"] = "failed: file not found"
414
+ persistence_ok = False
415
  except json.JSONDecodeError as e:
416
+ checks["persistence_registry"] = f"failed: invalid JSON: {str(e)}"
417
+ persistence_ok = False
418
  except Exception as e:
419
+ checks["persistence_registry"] = f"failed: {type(e).__name__}: {str(e)}"
420
+ persistence_ok = False
421
 
422
+ checks["persistence"] = "ok" if persistence_ok else "degraded"
423
 
424
+ # 3. Check frontend asset availability
425
+ frontend_ok = False
426
+ index_path = f"{static_dir}/index.html"
427
+ fallback_path = "/app/index.html"
428
+
429
+ if os.path.exists(index_path):
430
+ checks["frontend_assets"] = "ok"
431
+ frontend_ok = True
432
+ elif os.path.exists(fallback_path):
433
+ checks["frontend_assets"] = "ok (fallback)"
434
+ frontend_ok = True
435
+ else:
436
+ checks["frontend_assets"] = "failed: index.html not found"
437
+ # Frontend missing is degraded, not critical failure
438
+
439
+ # 4. Check agent router heartbeat
440
  now = time.time()
441
  cain_state = agent_router.get_state(AgentRole.CAIN)
442
  heartbeat_age = now - cain_state.last_heartbeat
443
  router_ok = heartbeat_age < 30
444
+ checks["agent_router"] = "ok" if router_ok else f"degraded: stale ({heartbeat_age:.1f}s)"
445
 
446
+ # Determine overall status
447
+ # Critical failure: brain not available
448
+ if not brain_ok:
449
+ return JSONResponse(
450
+ status_code=http_status.HTTP_503_SERVICE_UNAVAILABLE,
451
+ content={
452
+ "status": "critical",
453
+ "brain": "unavailable",
454
+ "persistence": "ok" if persistence_ok else "error",
455
+ "frontend": "available" if frontend_ok else "unavailable",
456
+ "checks": checks,
457
+ "error": error_message,
458
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
459
+ }
460
+ )
461
+
462
+ # Healthy response
463
+ return {
464
+ "status": "healthy" if is_healthy else "degraded",
465
+ "brain": "available",
466
+ "persistence": "ok" if persistence_ok else "error",
467
+ "frontend": "available" if frontend_ok else "unavailable",
468
+ "agent_router": "ok" if router_ok else "stale",
469
  "uptime_seconds": round(time.time() - START_TIME, 2),
470
+ "checks": checks,
471
  "timestamp": datetime.utcnow().isoformat() + "+00:00"
472
  }
473
 
 
 
474
 
475
  @app.get("/debug/health")
476
  async def debug_health():