Skip to content

Commit 574f3df

Browse files
author
JiaDe
committed
feat: Monitor Runtime Events tab — real-time microVM lifecycle from CloudWatch
Backend: - New /api/v1/monitor/runtime-events endpoint - Queries CloudWatch Logs for AgentCore microVM events - Classifies: invocation, response, cold_start, release (SIGTERM), workspace assembly, S3 sync, Plan A injection, usage write, SSM mapping - Summary stats: invocations, cold starts, releases, active tenants Frontend: - New 'Runtime Events' tab in Monitor Center - 5 summary cards (invocations, cold starts, VM releases, active tenants, time range) - Real-time event timeline with color-coded types and icons - Auto-refresh every 15 seconds - Shows tenant_id, timestamps, event details - IT Admin can see: when microVMs start, when they release, which tenants are active, cold start vs warm ratio
1 parent e073de2 commit 574f3df

3 files changed

Lines changed: 172 additions & 1 deletion

File tree

enterprise/admin-console/server/main.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,6 +1465,98 @@ def get_session_detail(session_id: str):
14651465
return {"session": session, "conversation": conv, "quality": quality, "planE": plan_e}
14661466

14671467

1468+
@app.get("/api/v1/monitor/runtime-events")
1469+
def get_runtime_events(minutes: int = 30):
1470+
"""Query CloudWatch Logs for microVM lifecycle events (invocations, SIGTERM, assembly)."""
1471+
try:
1472+
import time as _time
1473+
cw = _boto3.client("logs", region_name="us-east-1")
1474+
start_time = int((_time.time() - minutes * 60) * 1000)
1475+
events = []
1476+
1477+
for log_group in _LOG_GROUPS:
1478+
try:
1479+
# Get all recent log events
1480+
resp = cw.filter_log_events(
1481+
logGroupName=log_group,
1482+
startTime=start_time,
1483+
limit=200,
1484+
interleaved=True,
1485+
)
1486+
for event in resp.get("events", []):
1487+
msg = event.get("message", "")
1488+
ts = event.get("timestamp", 0)
1489+
iso_ts = datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat()
1490+
1491+
# Classify event type
1492+
if "SIGTERM" in msg:
1493+
events.append({"type": "release", "message": "microVM released (SIGTERM)", "timestamp": iso_ts, "raw": msg.strip()[:200]})
1494+
elif "First invocation" in msg or "assembling workspace" in msg:
1495+
tenant = ""
1496+
if "tenant" in msg:
1497+
parts = msg.split("tenant")
1498+
if len(parts) > 1:
1499+
tenant = parts[1].strip().split(" ")[0].strip("= ")
1500+
events.append({"type": "cold_start", "message": f"Cold start — workspace assembly", "tenant": tenant, "timestamp": iso_ts, "raw": msg.strip()[:200]})
1501+
elif "Workspace ready" in msg or "Workspace assembled" in msg:
1502+
events.append({"type": "ready", "message": "Workspace ready", "timestamp": iso_ts, "raw": msg.strip()[:200]})
1503+
elif "Invocation tenant_id=" in msg:
1504+
tenant = msg.split("tenant_id=")[1].split(" ")[0] if "tenant_id=" in msg else ""
1505+
msg_len = msg.split("message_len=")[1].split(" ")[0] if "message_len=" in msg else "?"
1506+
events.append({"type": "invocation", "message": f"Agent invocation (msg_len={msg_len})", "tenant": tenant, "timestamp": iso_ts, "raw": msg.strip()[:200]})
1507+
elif "Response tenant_id=" in msg:
1508+
duration = ""
1509+
if "duration_ms=" in msg:
1510+
duration = msg.split("duration_ms=")[1].split(" ")[0]
1511+
model = ""
1512+
if "model=" in msg:
1513+
model = msg.split("model=")[1].split(" ")[0]
1514+
tokens = ""
1515+
if "tokens=" in msg:
1516+
tokens = msg.split("tokens=")[1].split(" ")[0]
1517+
events.append({"type": "response", "message": f"Response ({duration}ms, {tokens} tokens, {model})", "timestamp": iso_ts, "raw": msg.strip()[:200]})
1518+
elif "DynamoDB usage written" in msg:
1519+
events.append({"type": "usage", "message": "Usage written to DynamoDB", "timestamp": iso_ts, "raw": msg.strip()[:200]})
1520+
elif "Plan A" in msg:
1521+
events.append({"type": "plan_a", "message": "Plan A constraints injected", "timestamp": iso_ts, "raw": msg.strip()[:200]})
1522+
elif "S3 workspace synced" in msg or "watchdog" in msg.lower():
1523+
events.append({"type": "sync", "message": "S3 workspace sync", "timestamp": iso_ts, "raw": msg.strip()[:200]})
1524+
elif "SSM user-mapping" in msg:
1525+
events.append({"type": "mapping", "message": msg.strip()[:100], "timestamp": iso_ts, "raw": msg.strip()[:200]})
1526+
except _ClientError:
1527+
pass
1528+
1529+
# Sort by timestamp descending (newest first)
1530+
events.sort(key=lambda e: e["timestamp"], reverse=True)
1531+
1532+
# Summary stats
1533+
invocations = [e for e in events if e["type"] == "invocation"]
1534+
cold_starts = [e for e in events if e["type"] == "cold_start"]
1535+
releases = [e for e in events if e["type"] == "release"]
1536+
responses = [e for e in events if e["type"] == "response"]
1537+
1538+
# Unique active tenants
1539+
active_tenants = set()
1540+
for e in invocations:
1541+
t = e.get("tenant", "")
1542+
if t:
1543+
active_tenants.add(t)
1544+
1545+
return {
1546+
"events": events[:100], # cap at 100
1547+
"summary": {
1548+
"totalEvents": len(events),
1549+
"invocations": len(invocations),
1550+
"coldStarts": len(cold_starts),
1551+
"releases": len(releases),
1552+
"activeTenants": len(active_tenants),
1553+
"timeRangeMinutes": minutes,
1554+
},
1555+
}
1556+
except Exception as e:
1557+
return {"events": [], "summary": {"error": str(e)}}
1558+
1559+
14681560
@app.get("/api/v1/monitor/alerts")
14691561
def get_alert_rules():
14701562
"""Alert rules with real-time status evaluation against actual data."""

enterprise/admin-console/src/hooks/useApi.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,18 @@ export function useAlertRules() {
161161
});
162162
}
163163

164+
export interface RuntimeEvent {
165+
type: string; message: string; timestamp: string; tenant?: string; raw?: string;
166+
}
167+
168+
export function useRuntimeEvents(minutes: number = 30) {
169+
return useQuery<{ events: RuntimeEvent[]; summary: Record<string, number> }>({
170+
queryKey: ['runtime-events', minutes],
171+
queryFn: () => api.get(`/monitor/runtime-events?minutes=${minutes}`),
172+
refetchInterval: 15_000,
173+
});
174+
}
175+
164176
// === Audit ===
165177

166178
export function useAuditEntries(params?: { limit?: number; eventType?: string }) {

enterprise/admin-console/src/pages/Monitor/index.tsx

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Chart from 'react-apexcharts';
44
import type { ApexOptions } from 'apexcharts';
55
import { Bot, MessageSquare, Star, AlertTriangle, Shield, RefreshCw, Eye, Radio, Clock, Zap } from 'lucide-react';
66
import { Card, StatCard, Badge, Button, PageHeader, Table, StatusDot, Tabs } from '../../components/ui';
7-
import { useSessions, useAgents, useMonitorHealth, useAlertRules } from '../../hooks/useApi';
7+
import { useSessions, useAgents, useMonitorHealth, useAlertRules, useRuntimeEvents } from '../../hooks/useApi';
88
import { CHANNEL_LABELS } from '../../types';
99
import type { ChannelType } from '../../types';
1010
import SessionDetail from './SessionDetail';
@@ -28,6 +28,7 @@ export default function Monitor() {
2828
const navigate = useNavigate();
2929
const { data: healthData, refetch: refetchHealth } = useMonitorHealth();
3030
const { data: alertRules = [], refetch: refetchAlerts } = useAlertRules();
31+
const { data: runtimeData } = useRuntimeEvents(60);
3132
const [selectedSession, setSelectedSession] = useState<string | null>(null);
3233
const [activeTab, setActiveTab] = useState('sessions');
3334

@@ -107,6 +108,7 @@ export default function Monitor() {
107108
{ id: 'sessions', label: 'Live Sessions', count: sessions.length },
108109
{ id: 'health', label: 'Agent Health', count: health.length },
109110
{ id: 'alerts', label: 'Alert Rules', count: alertCount || undefined },
111+
{ id: 'runtime', label: 'Runtime Events' },
110112
]}
111113
activeTab={activeTab}
112114
onChange={setActiveTab}
@@ -244,6 +246,71 @@ export default function Monitor() {
244246
</div>
245247
</div>
246248
)}
249+
250+
{activeTab === 'runtime' && (
251+
<div>
252+
{/* Summary cards */}
253+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-5 mb-4">
254+
<div className="rounded-lg bg-dark-bg border border-dark-border p-3 text-center">
255+
<div className="text-lg font-bold text-primary">{runtimeData?.summary?.invocations || 0}</div>
256+
<div className="text-[10px] text-text-muted">Invocations</div>
257+
</div>
258+
<div className="rounded-lg bg-dark-bg border border-dark-border p-3 text-center">
259+
<div className="text-lg font-bold text-amber-400">{runtimeData?.summary?.coldStarts || 0}</div>
260+
<div className="text-[10px] text-text-muted">Cold Starts</div>
261+
</div>
262+
<div className="rounded-lg bg-dark-bg border border-dark-border p-3 text-center">
263+
<div className="text-lg font-bold text-red-400">{runtimeData?.summary?.releases || 0}</div>
264+
<div className="text-[10px] text-text-muted">VM Releases</div>
265+
</div>
266+
<div className="rounded-lg bg-dark-bg border border-dark-border p-3 text-center">
267+
<div className="text-lg font-bold text-green-400">{runtimeData?.summary?.activeTenants || 0}</div>
268+
<div className="text-[10px] text-text-muted">Active Tenants</div>
269+
</div>
270+
<div className="rounded-lg bg-dark-bg border border-dark-border p-3 text-center">
271+
<div className="text-lg font-bold text-text-muted">{runtimeData?.summary?.timeRangeMinutes || 60}m</div>
272+
<div className="text-[10px] text-text-muted">Time Range</div>
273+
</div>
274+
</div>
275+
276+
{/* Event timeline */}
277+
<p className="text-sm text-text-secondary mb-3">Real-time AgentCore microVM lifecycle events from CloudWatch Logs</p>
278+
<div className="space-y-1.5 max-h-[500px] overflow-y-auto">
279+
{(runtimeData?.events || []).map((e, i) => {
280+
const typeConfig: Record<string, { color: string; icon: string }> = {
281+
invocation: { color: 'text-primary', icon: '→' },
282+
response: { color: 'text-green-400', icon: '←' },
283+
cold_start: { color: 'text-amber-400', icon: '🔥' },
284+
release: { color: 'text-red-400', icon: '⏹' },
285+
ready: { color: 'text-green-400', icon: '✓' },
286+
sync: { color: 'text-cyan-400', icon: '↻' },
287+
plan_a: { color: 'text-orange-400', icon: '🛡' },
288+
usage: { color: 'text-blue-400', icon: '📊' },
289+
mapping: { color: 'text-purple-400', icon: '🔗' },
290+
};
291+
const cfg = typeConfig[e.type] || { color: 'text-text-muted', icon: '·' };
292+
return (
293+
<div key={i} className="flex items-start gap-2 rounded-lg bg-dark-bg/50 px-3 py-2 text-xs hover:bg-dark-bg transition-colors">
294+
<span className={`${cfg.color} font-mono w-4 shrink-0 text-center`}>{cfg.icon}</span>
295+
<span className="text-text-muted shrink-0 w-20 font-mono">{new Date(e.timestamp).toLocaleTimeString()}</span>
296+
<Badge color={e.type === 'invocation' ? 'primary' : e.type === 'response' ? 'success' : e.type === 'cold_start' ? 'warning' : e.type === 'release' ? 'danger' : 'default'}>
297+
{e.type}
298+
</Badge>
299+
<span className="text-text-secondary flex-1">{e.message}</span>
300+
{e.tenant && <code className="text-[10px] text-text-muted bg-dark-card px-1 rounded truncate max-w-[200px]">{e.tenant}</code>}
301+
</div>
302+
);
303+
})}
304+
{(!runtimeData?.events || runtimeData.events.length === 0) && (
305+
<div className="text-center py-8 text-text-muted">
306+
<Radio size={24} className="mx-auto mb-2 opacity-50" />
307+
<p className="text-sm">No runtime events in the last {runtimeData?.summary?.timeRangeMinutes || 60} minutes</p>
308+
<p className="text-xs mt-1">Events appear when agents are invoked via IM channels or Portal</p>
309+
</div>
310+
)}
311+
</div>
312+
</div>
313+
)}
247314
</div>
248315
</Card>
249316
</div>

0 commit comments

Comments
 (0)