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>

Files changed (1) hide show
  1. app.py +113 -24
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
- # Force refresh status file timestamp to break stale "unknown" display
111
- data_dir = Path(os.environ.get("OPENCLAW_DATA_DIR", "/data"))
112
- data_dir.mkdir(parents=True, exist_ok=True)
113
 
114
- # Update all status files with fresh timestamp
115
- for status_file in [
116
- data_dir / "cain_status.json",
117
- Path("/app/memory/cain_status.json"),
118
- ]:
119
- try:
120
- if status_file.exists():
121
- with open(status_file, "r") as f:
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."""