claude-mpm 4.2.9__py3-none-any.whl → 4.2.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/cli/commands/dashboard.py +59 -126
- claude_mpm/cli/commands/monitor.py +71 -212
- claude_mpm/cli/commands/run.py +33 -33
- claude_mpm/dashboard/static/css/code-tree.css +8 -16
- claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
- claude_mpm/dashboard/static/dist/components/file-viewer.js +2 -0
- claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/unified-data-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/code-tree.js +692 -114
- claude_mpm/dashboard/static/js/components/file-viewer.js +538 -0
- claude_mpm/dashboard/static/js/components/module-viewer.js +26 -0
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +166 -14
- claude_mpm/dashboard/static/js/dashboard.js +108 -91
- claude_mpm/dashboard/static/js/socket-client.js +9 -7
- claude_mpm/dashboard/templates/index.html +2 -7
- claude_mpm/hooks/claude_hooks/hook_handler.py +1 -11
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +54 -59
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +112 -72
- claude_mpm/services/agents/deployment/agent_template_builder.py +0 -1
- claude_mpm/services/cli/unified_dashboard_manager.py +354 -0
- claude_mpm/services/monitor/__init__.py +20 -0
- claude_mpm/services/monitor/daemon.py +256 -0
- claude_mpm/services/monitor/event_emitter.py +279 -0
- claude_mpm/services/monitor/handlers/__init__.py +20 -0
- claude_mpm/services/monitor/handlers/code_analysis.py +334 -0
- claude_mpm/services/monitor/handlers/dashboard.py +298 -0
- claude_mpm/services/monitor/handlers/hooks.py +491 -0
- claude_mpm/services/monitor/management/__init__.py +18 -0
- claude_mpm/services/monitor/management/health.py +124 -0
- claude_mpm/services/monitor/management/lifecycle.py +298 -0
- claude_mpm/services/monitor/server.py +442 -0
- claude_mpm/tools/code_tree_analyzer.py +33 -17
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/RECORD +41 -36
- claude_mpm/cli/commands/socketio_monitor.py +0 -233
- claude_mpm/scripts/socketio_daemon.py +0 -571
- claude_mpm/scripts/socketio_daemon_hardened.py +0 -937
- claude_mpm/scripts/socketio_daemon_wrapper.py +0 -78
- claude_mpm/scripts/socketio_server_manager.py +0 -349
- claude_mpm/services/cli/dashboard_launcher.py +0 -423
- claude_mpm/services/cli/socketio_manager.py +0 -595
- claude_mpm/services/dashboard/stable_server.py +0 -1020
- claude_mpm/services/socketio/monitor_server.py +0 -505
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Claude Code Hooks Handler for Unified Monitor
|
|
3
|
+
=============================================
|
|
4
|
+
|
|
5
|
+
WHY: This handler ingests Claude Code hooks and events, providing integration
|
|
6
|
+
between Claude Code sessions and the unified monitor daemon. It processes
|
|
7
|
+
hook events and broadcasts them to dashboard clients.
|
|
8
|
+
|
|
9
|
+
DESIGN DECISIONS:
|
|
10
|
+
- Ingests Claude Code hooks via HTTP and WebSocket
|
|
11
|
+
- Processes and normalizes hook events
|
|
12
|
+
- Broadcasts events to connected dashboard clients
|
|
13
|
+
- Maintains event history and replay capability
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
from collections import deque
|
|
18
|
+
from typing import Dict, List
|
|
19
|
+
|
|
20
|
+
import socketio
|
|
21
|
+
|
|
22
|
+
from ....core.logging_config import get_logger
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HookHandler:
|
|
26
|
+
"""Event handler for Claude Code hooks integration.
|
|
27
|
+
|
|
28
|
+
WHY: Provides integration between Claude Code sessions and the unified
|
|
29
|
+
monitor daemon, allowing real-time monitoring of Claude Code activities.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, sio: socketio.AsyncServer):
|
|
33
|
+
"""Initialize the hooks handler.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
sio: Socket.IO server instance
|
|
37
|
+
"""
|
|
38
|
+
self.sio = sio
|
|
39
|
+
self.logger = get_logger(__name__)
|
|
40
|
+
|
|
41
|
+
# Event storage
|
|
42
|
+
self.event_history: deque = deque(maxlen=1000) # Keep last 1000 events
|
|
43
|
+
self.active_sessions: Dict[str, Dict] = {}
|
|
44
|
+
|
|
45
|
+
def register(self):
|
|
46
|
+
"""Register Socket.IO event handlers."""
|
|
47
|
+
try:
|
|
48
|
+
# Claude Code hook events (HTTP POST pathway)
|
|
49
|
+
self.sio.on("claude_event", self.handle_claude_event)
|
|
50
|
+
|
|
51
|
+
# Hook ingestion events (alternative format)
|
|
52
|
+
self.sio.on("hook:ingest", self.handle_hook_ingest)
|
|
53
|
+
self.sio.on("hook:session:start", self.handle_session_start)
|
|
54
|
+
self.sio.on("hook:session:end", self.handle_session_end)
|
|
55
|
+
|
|
56
|
+
# Event replay and history
|
|
57
|
+
self.sio.on("hook:history:get", self.handle_get_history)
|
|
58
|
+
self.sio.on("hook:replay:start", self.handle_replay_start)
|
|
59
|
+
|
|
60
|
+
# Session management
|
|
61
|
+
self.sio.on("hook:sessions:list", self.handle_list_sessions)
|
|
62
|
+
self.sio.on("hook:session:info", self.handle_session_info)
|
|
63
|
+
|
|
64
|
+
self.logger.info("Hook event handlers registered")
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
self.logger.error(f"Error registering hook handlers: {e}")
|
|
68
|
+
raise
|
|
69
|
+
|
|
70
|
+
async def handle_claude_event(self, sid: str, data: Dict):
|
|
71
|
+
"""Handle Claude Code hook events sent via 'claude_event'.
|
|
72
|
+
|
|
73
|
+
This is the primary integration point for Claude Code hooks.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
sid: Socket.IO session ID
|
|
77
|
+
data: Claude event data
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
self.logger.info(
|
|
81
|
+
f"Received Claude Code hook event: {data.get('type', 'unknown')}"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Process the Claude event
|
|
85
|
+
processed_event = self._process_claude_event(data)
|
|
86
|
+
|
|
87
|
+
# Store in history
|
|
88
|
+
self.event_history.append(processed_event)
|
|
89
|
+
|
|
90
|
+
# Update session tracking
|
|
91
|
+
session_id = processed_event.get("session_id")
|
|
92
|
+
if session_id:
|
|
93
|
+
self._update_session_tracking(session_id, processed_event)
|
|
94
|
+
|
|
95
|
+
# Broadcast to all dashboard clients
|
|
96
|
+
# Use only one event type to avoid duplication
|
|
97
|
+
await self.sio.emit("hook:event", processed_event)
|
|
98
|
+
|
|
99
|
+
self.logger.debug(
|
|
100
|
+
f"Claude hook event processed and broadcasted: {processed_event.get('type', 'unknown')}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
self.logger.error(f"Error processing Claude hook event: {e}")
|
|
105
|
+
await self.sio.emit(
|
|
106
|
+
"hook:error",
|
|
107
|
+
{"error": f"Claude event processing error: {e!s}"},
|
|
108
|
+
room=sid,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
async def handle_hook_ingest(self, sid: str, data: Dict):
|
|
112
|
+
"""Handle incoming Claude Code hook event.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
sid: Socket.IO session ID
|
|
116
|
+
data: Hook event data
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
# Validate hook data
|
|
120
|
+
if not self._validate_hook_data(data):
|
|
121
|
+
await self.sio.emit(
|
|
122
|
+
"hook:error", {"error": "Invalid hook data format"}, room=sid
|
|
123
|
+
)
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
# Process and normalize the hook event
|
|
127
|
+
processed_event = self._process_hook_event(data)
|
|
128
|
+
|
|
129
|
+
# Store in history
|
|
130
|
+
self.event_history.append(processed_event)
|
|
131
|
+
|
|
132
|
+
# Update session tracking
|
|
133
|
+
session_id = processed_event.get("session_id")
|
|
134
|
+
if session_id:
|
|
135
|
+
self._update_session_tracking(session_id, processed_event)
|
|
136
|
+
|
|
137
|
+
# Broadcast to all dashboard clients
|
|
138
|
+
await self.sio.emit("hook:event", processed_event)
|
|
139
|
+
|
|
140
|
+
self.logger.debug(
|
|
141
|
+
f"Hook event processed: {processed_event.get('type', 'unknown')}"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
except Exception as e:
|
|
145
|
+
self.logger.error(f"Error processing hook event: {e}")
|
|
146
|
+
await self.sio.emit(
|
|
147
|
+
"hook:error", {"error": f"Hook processing error: {e!s}"}, room=sid
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
async def handle_session_start(self, sid: str, data: Dict):
|
|
151
|
+
"""Handle Claude Code session start.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
sid: Socket.IO session ID
|
|
155
|
+
data: Session start data
|
|
156
|
+
"""
|
|
157
|
+
try:
|
|
158
|
+
session_id = data.get("session_id")
|
|
159
|
+
if not session_id:
|
|
160
|
+
await self.sio.emit(
|
|
161
|
+
"hook:error", {"error": "No session ID provided"}, room=sid
|
|
162
|
+
)
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
# Create session tracking
|
|
166
|
+
session_info = {
|
|
167
|
+
"session_id": session_id,
|
|
168
|
+
"start_time": asyncio.get_event_loop().time(),
|
|
169
|
+
"status": "active",
|
|
170
|
+
"event_count": 0,
|
|
171
|
+
"last_activity": asyncio.get_event_loop().time(),
|
|
172
|
+
"metadata": data.get("metadata", {}),
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
self.active_sessions[session_id] = session_info
|
|
176
|
+
|
|
177
|
+
# Broadcast session start
|
|
178
|
+
await self.sio.emit("hook:session:started", session_info)
|
|
179
|
+
|
|
180
|
+
self.logger.info(f"Claude Code session started: {session_id}")
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
self.logger.error(f"Error handling session start: {e}")
|
|
184
|
+
await self.sio.emit(
|
|
185
|
+
"hook:error", {"error": f"Session start error: {e!s}"}, room=sid
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
async def handle_session_end(self, sid: str, data: Dict):
|
|
189
|
+
"""Handle Claude Code session end.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
sid: Socket.IO session ID
|
|
193
|
+
data: Session end data
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
session_id = data.get("session_id")
|
|
197
|
+
if not session_id:
|
|
198
|
+
await self.sio.emit(
|
|
199
|
+
"hook:error", {"error": "No session ID provided"}, room=sid
|
|
200
|
+
)
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
# Update session status
|
|
204
|
+
if session_id in self.active_sessions:
|
|
205
|
+
session_info = self.active_sessions[session_id]
|
|
206
|
+
session_info["status"] = "ended"
|
|
207
|
+
session_info["end_time"] = asyncio.get_event_loop().time()
|
|
208
|
+
session_info["duration"] = (
|
|
209
|
+
session_info["end_time"] - session_info["start_time"]
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Broadcast session end
|
|
213
|
+
await self.sio.emit("hook:session:ended", session_info)
|
|
214
|
+
|
|
215
|
+
# Remove from active sessions after a delay
|
|
216
|
+
asyncio.create_task(
|
|
217
|
+
self._cleanup_session(session_id, delay=300)
|
|
218
|
+
) # 5 minutes
|
|
219
|
+
|
|
220
|
+
self.logger.info(f"Claude Code session ended: {session_id}")
|
|
221
|
+
else:
|
|
222
|
+
self.logger.warning(f"Session end for unknown session: {session_id}")
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
self.logger.error(f"Error handling session end: {e}")
|
|
226
|
+
await self.sio.emit(
|
|
227
|
+
"hook:error", {"error": f"Session end error: {e!s}"}, room=sid
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
async def handle_get_history(self, sid: str, data: Dict):
|
|
231
|
+
"""Handle request for event history.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
sid: Socket.IO session ID
|
|
235
|
+
data: History request data
|
|
236
|
+
"""
|
|
237
|
+
try:
|
|
238
|
+
limit = data.get("limit", 100)
|
|
239
|
+
event_type = data.get("type")
|
|
240
|
+
session_id = data.get("session_id")
|
|
241
|
+
|
|
242
|
+
# Filter events
|
|
243
|
+
filtered_events = list(self.event_history)
|
|
244
|
+
|
|
245
|
+
if event_type:
|
|
246
|
+
filtered_events = [
|
|
247
|
+
e for e in filtered_events if e.get("type") == event_type
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
if session_id:
|
|
251
|
+
filtered_events = [
|
|
252
|
+
e for e in filtered_events if e.get("session_id") == session_id
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
# Apply limit
|
|
256
|
+
if limit > 0:
|
|
257
|
+
filtered_events = filtered_events[-limit:]
|
|
258
|
+
|
|
259
|
+
await self.sio.emit(
|
|
260
|
+
"hook:history:response",
|
|
261
|
+
{
|
|
262
|
+
"events": filtered_events,
|
|
263
|
+
"total": len(filtered_events),
|
|
264
|
+
"filters": {
|
|
265
|
+
"type": event_type,
|
|
266
|
+
"session_id": session_id,
|
|
267
|
+
"limit": limit,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
room=sid,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
except Exception as e:
|
|
274
|
+
self.logger.error(f"Error getting event history: {e}")
|
|
275
|
+
await self.sio.emit(
|
|
276
|
+
"hook:error", {"error": f"History error: {e!s}"}, room=sid
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
async def handle_replay_start(self, sid: str, data: Dict):
|
|
280
|
+
"""Handle event replay request.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
sid: Socket.IO session ID
|
|
284
|
+
data: Replay request data
|
|
285
|
+
"""
|
|
286
|
+
try:
|
|
287
|
+
session_id = data.get("session_id")
|
|
288
|
+
speed = data.get("speed", 1.0) # Replay speed multiplier
|
|
289
|
+
|
|
290
|
+
if not session_id:
|
|
291
|
+
await self.sio.emit(
|
|
292
|
+
"hook:error",
|
|
293
|
+
{"error": "No session ID provided for replay"},
|
|
294
|
+
room=sid,
|
|
295
|
+
)
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
# Get events for session
|
|
299
|
+
session_events = [
|
|
300
|
+
e for e in self.event_history if e.get("session_id") == session_id
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
if not session_events:
|
|
304
|
+
await self.sio.emit(
|
|
305
|
+
"hook:error",
|
|
306
|
+
{"error": f"No events found for session: {session_id}"},
|
|
307
|
+
room=sid,
|
|
308
|
+
)
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
# Start replay
|
|
312
|
+
await self._replay_events(sid, session_events, speed)
|
|
313
|
+
|
|
314
|
+
except Exception as e:
|
|
315
|
+
self.logger.error(f"Error starting event replay: {e}")
|
|
316
|
+
await self.sio.emit(
|
|
317
|
+
"hook:error", {"error": f"Replay error: {e!s}"}, room=sid
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
async def handle_list_sessions(self, sid: str, data: Dict):
|
|
321
|
+
"""Handle request for active sessions list.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
sid: Socket.IO session ID
|
|
325
|
+
data: Request data
|
|
326
|
+
"""
|
|
327
|
+
try:
|
|
328
|
+
sessions = list(self.active_sessions.values())
|
|
329
|
+
|
|
330
|
+
await self.sio.emit(
|
|
331
|
+
"hook:sessions:response",
|
|
332
|
+
{"sessions": sessions, "total": len(sessions)},
|
|
333
|
+
room=sid,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
except Exception as e:
|
|
337
|
+
self.logger.error(f"Error listing sessions: {e}")
|
|
338
|
+
await self.sio.emit(
|
|
339
|
+
"hook:error", {"error": f"Sessions list error: {e!s}"}, room=sid
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
async def handle_session_info(self, sid: str, data: Dict):
|
|
343
|
+
"""Handle request for specific session info.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
sid: Socket.IO session ID
|
|
347
|
+
data: Request data
|
|
348
|
+
"""
|
|
349
|
+
try:
|
|
350
|
+
session_id = data.get("session_id")
|
|
351
|
+
if not session_id:
|
|
352
|
+
await self.sio.emit(
|
|
353
|
+
"hook:error", {"error": "No session ID provided"}, room=sid
|
|
354
|
+
)
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
session_info = self.active_sessions.get(session_id)
|
|
358
|
+
if not session_info:
|
|
359
|
+
await self.sio.emit(
|
|
360
|
+
"hook:error",
|
|
361
|
+
{"error": f"Session not found: {session_id}"},
|
|
362
|
+
room=sid,
|
|
363
|
+
)
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
await self.sio.emit("hook:session:info:response", session_info, room=sid)
|
|
367
|
+
|
|
368
|
+
except Exception as e:
|
|
369
|
+
self.logger.error(f"Error getting session info: {e}")
|
|
370
|
+
await self.sio.emit(
|
|
371
|
+
"hook:error", {"error": f"Session info error: {e!s}"}, room=sid
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def _validate_hook_data(self, data: Dict) -> bool:
|
|
375
|
+
"""Validate hook event data format.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
data: Hook event data
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
True if valid, False otherwise
|
|
382
|
+
"""
|
|
383
|
+
required_fields = ["type", "timestamp"]
|
|
384
|
+
return all(field in data for field in required_fields)
|
|
385
|
+
|
|
386
|
+
def _process_claude_event(self, data: Dict) -> Dict:
|
|
387
|
+
"""Process and normalize Claude Code hook event data.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
data: Raw Claude event data
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Processed event data
|
|
394
|
+
"""
|
|
395
|
+
processed = {
|
|
396
|
+
"type": data.get("type", "hook"),
|
|
397
|
+
"subtype": data.get("subtype", "unknown"),
|
|
398
|
+
"timestamp": data.get("timestamp", asyncio.get_event_loop().time()),
|
|
399
|
+
"session_id": data.get("session_id"),
|
|
400
|
+
"source": data.get("source", "claude_hooks"),
|
|
401
|
+
"data": data.get("data", {}),
|
|
402
|
+
"metadata": data.get("metadata", {}),
|
|
403
|
+
"processed_at": asyncio.get_event_loop().time(),
|
|
404
|
+
"original_event": data, # Keep original for debugging
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return processed
|
|
408
|
+
|
|
409
|
+
def _process_hook_event(self, data: Dict) -> Dict:
|
|
410
|
+
"""Process and normalize hook event data.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
data: Raw hook event data
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
Processed event data
|
|
417
|
+
"""
|
|
418
|
+
processed = {
|
|
419
|
+
"type": data.get("type"),
|
|
420
|
+
"timestamp": data.get("timestamp"),
|
|
421
|
+
"session_id": data.get("session_id"),
|
|
422
|
+
"data": data.get("data", {}),
|
|
423
|
+
"metadata": data.get("metadata", {}),
|
|
424
|
+
"processed_at": asyncio.get_event_loop().time(),
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return processed
|
|
428
|
+
|
|
429
|
+
def _update_session_tracking(self, session_id: str, event: Dict):
|
|
430
|
+
"""Update session tracking with new event.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
session_id: Session ID
|
|
434
|
+
event: Event data
|
|
435
|
+
"""
|
|
436
|
+
if session_id in self.active_sessions:
|
|
437
|
+
session = self.active_sessions[session_id]
|
|
438
|
+
session["event_count"] += 1
|
|
439
|
+
session["last_activity"] = event.get(
|
|
440
|
+
"timestamp", asyncio.get_event_loop().time()
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
async def _replay_events(self, sid: str, events: List[Dict], speed: float):
|
|
444
|
+
"""Replay events to a specific client.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
sid: Socket.IO session ID
|
|
448
|
+
events: Events to replay
|
|
449
|
+
speed: Replay speed multiplier
|
|
450
|
+
"""
|
|
451
|
+
try:
|
|
452
|
+
await self.sio.emit(
|
|
453
|
+
"hook:replay:started",
|
|
454
|
+
{"event_count": len(events), "speed": speed},
|
|
455
|
+
room=sid,
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
for i, event in enumerate(events):
|
|
459
|
+
# Calculate delay based on speed
|
|
460
|
+
if i > 0:
|
|
461
|
+
time_diff = event["timestamp"] - events[i - 1]["timestamp"]
|
|
462
|
+
delay = max(0.1, time_diff / speed) # Minimum 0.1s delay
|
|
463
|
+
await asyncio.sleep(delay)
|
|
464
|
+
|
|
465
|
+
# Emit replay event
|
|
466
|
+
await self.sio.emit(
|
|
467
|
+
"hook:replay:event",
|
|
468
|
+
{"index": i, "total": len(events), "event": event},
|
|
469
|
+
room=sid,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
await self.sio.emit(
|
|
473
|
+
"hook:replay:completed", {"event_count": len(events)}, room=sid
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
except Exception as e:
|
|
477
|
+
self.logger.error(f"Error during event replay: {e}")
|
|
478
|
+
await self.sio.emit(
|
|
479
|
+
"hook:error", {"error": f"Replay error: {e!s}"}, room=sid
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
async def _cleanup_session(self, session_id: str, delay: int = 300):
|
|
483
|
+
"""Cleanup session after delay.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
session_id: Session ID to cleanup
|
|
487
|
+
delay: Delay in seconds before cleanup
|
|
488
|
+
"""
|
|
489
|
+
await asyncio.sleep(delay)
|
|
490
|
+
self.active_sessions.pop(session_id, None)
|
|
491
|
+
self.logger.debug(f"Cleaned up session: {session_id}")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Daemon Management for Unified Monitor
|
|
3
|
+
=====================================
|
|
4
|
+
|
|
5
|
+
WHY: These modules provide daemon lifecycle management, health monitoring,
|
|
6
|
+
and process supervision for the unified monitor daemon.
|
|
7
|
+
|
|
8
|
+
DESIGN DECISIONS:
|
|
9
|
+
- Proper daemon process management with PID files
|
|
10
|
+
- Health monitoring and auto-restart capabilities
|
|
11
|
+
- Graceful shutdown and cleanup
|
|
12
|
+
- Production-ready daemon operation
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .health import HealthMonitor
|
|
16
|
+
from .lifecycle import DaemonLifecycle
|
|
17
|
+
|
|
18
|
+
__all__ = ["DaemonLifecycle", "HealthMonitor"]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Health Monitoring for Unified Monitor
|
|
3
|
+
=====================================
|
|
4
|
+
|
|
5
|
+
WHY: This module provides health monitoring for the unified monitor daemon.
|
|
6
|
+
It tracks system resources and service health to ensure stability.
|
|
7
|
+
|
|
8
|
+
DESIGN DECISIONS:
|
|
9
|
+
- Simple health monitoring without external dependencies
|
|
10
|
+
- Basic resource tracking and service health checks
|
|
11
|
+
- Configurable thresholds for alerts
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
from typing import Dict
|
|
17
|
+
|
|
18
|
+
from ....core.logging_config import get_logger
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HealthMonitor:
|
|
22
|
+
"""Health monitoring system for the unified monitor daemon."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, port: int = 8765):
|
|
25
|
+
"""Initialize health monitor.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
port: Port to monitor for service health
|
|
29
|
+
"""
|
|
30
|
+
self.port = port
|
|
31
|
+
self.logger = get_logger(__name__)
|
|
32
|
+
|
|
33
|
+
# Monitoring state
|
|
34
|
+
self.running = False
|
|
35
|
+
self.monitor_thread = None
|
|
36
|
+
|
|
37
|
+
# Health metrics
|
|
38
|
+
self.metrics = {
|
|
39
|
+
"service_responsive": True,
|
|
40
|
+
"last_check": time.time(),
|
|
41
|
+
"uptime": 0.0,
|
|
42
|
+
"error_count": 0,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def start(self):
|
|
46
|
+
"""Start health monitoring."""
|
|
47
|
+
try:
|
|
48
|
+
if self.running:
|
|
49
|
+
self.logger.warning("Health monitor already running")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
self.running = True
|
|
53
|
+
self.monitor_thread = threading.Thread(
|
|
54
|
+
target=self._monitor_loop, daemon=True
|
|
55
|
+
)
|
|
56
|
+
self.monitor_thread.start()
|
|
57
|
+
|
|
58
|
+
self.logger.info("Health monitor started")
|
|
59
|
+
|
|
60
|
+
except Exception as e:
|
|
61
|
+
self.logger.error(f"Error starting health monitor: {e}")
|
|
62
|
+
self.running = False
|
|
63
|
+
|
|
64
|
+
def stop(self):
|
|
65
|
+
"""Stop health monitoring."""
|
|
66
|
+
try:
|
|
67
|
+
self.running = False
|
|
68
|
+
|
|
69
|
+
if self.monitor_thread and self.monitor_thread.is_alive():
|
|
70
|
+
self.monitor_thread.join(timeout=5)
|
|
71
|
+
|
|
72
|
+
self.logger.info("Health monitor stopped")
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
self.logger.error(f"Error stopping health monitor: {e}")
|
|
76
|
+
|
|
77
|
+
def _monitor_loop(self):
|
|
78
|
+
"""Main monitoring loop."""
|
|
79
|
+
start_time = time.time()
|
|
80
|
+
|
|
81
|
+
while self.running:
|
|
82
|
+
try:
|
|
83
|
+
# Update metrics
|
|
84
|
+
self.metrics["uptime"] = time.time() - start_time
|
|
85
|
+
self.metrics["last_check"] = time.time()
|
|
86
|
+
self.metrics["service_responsive"] = self._check_service_health()
|
|
87
|
+
|
|
88
|
+
# Sleep before next check
|
|
89
|
+
time.sleep(30) # Check every 30 seconds
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
self.logger.error(f"Error in health monitoring loop: {e}")
|
|
93
|
+
self.metrics["error_count"] += 1
|
|
94
|
+
time.sleep(10) # Shorter sleep on error
|
|
95
|
+
|
|
96
|
+
def _check_service_health(self) -> bool:
|
|
97
|
+
"""Check if the service is responsive."""
|
|
98
|
+
try:
|
|
99
|
+
import socket
|
|
100
|
+
|
|
101
|
+
# Try to connect to the service port
|
|
102
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
103
|
+
sock.settimeout(5) # 5 second timeout
|
|
104
|
+
result = sock.connect_ex(("localhost", self.port))
|
|
105
|
+
sock.close()
|
|
106
|
+
|
|
107
|
+
return result == 0
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
self.logger.debug(f"Service health check failed: {e}")
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
def get_status(self) -> Dict:
|
|
114
|
+
"""Get current health status."""
|
|
115
|
+
try:
|
|
116
|
+
return {
|
|
117
|
+
"monitoring": self.running,
|
|
118
|
+
"healthy": self.metrics["service_responsive"],
|
|
119
|
+
"metrics": self.metrics.copy(),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
self.logger.error(f"Error getting health status: {e}")
|
|
124
|
+
return {"monitoring": False, "healthy": False, "error": str(e)}
|