feat(admin): add full-chain trace and performance monitoring
- Backend: trace.Logger ring-buffer for request chain events - Backend: perf.Monitor with runtime.MemStats sampling every 10s - Backend: /api/v1/admin/trace and /api/v1/admin/perf endpoints - Frontend: TracePage — timeline with hop icons and status dots - Frontend: PerformancePage — memory/GC bar charts + live stats - Admin nav expanded to 11 items
This commit is contained in:
1 parent
8420691438
commit
e8b5679cf4
8 files changed
+442
No files matched your search
@@ -12,6 +12,8 @@ import LLMCallsPage from './pages/admin/LLMCallsPage'
|
||||
import SystemPage from './pages/admin/SystemPage'
|
||||
import ModelsPage from './pages/admin/ModelsPage'
|
||||
import BridgePage from './pages/admin/BridgePage'
|
||||
import TracePage from './pages/admin/TracePage'
|
||||
import PerformancePage from './pages/admin/PerformancePage'
|
||||
import LoginPage from './pages/LoginPage'
|
||||
|
||||
export default function App() {
|
||||
@@ -42,6 +44,8 @@ export default function App() {
|
||||
<Route path="llm-calls" element={<LLMCallsPage />} />
|
||||
<Route path="models" element={<ModelsPage />} />
|
||||
<Route path="bridge" element={<BridgePage />} />
|
||||
<Route path="trace" element={<TracePage />} />
|
||||
<Route path="perf" element={<PerformancePage />} />
|
||||
<Route path="system" element={<SystemPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -8,6 +8,8 @@ const navItems = [
|
||||
{ to: '/admin/plugins', label: '插件管理', icon: '◇' },
|
||||
{ to: '/admin/memory', label: '记忆管理', icon: '♡' },
|
||||
{ to: '/admin/bridge', label: '平台桥接', icon: '↗' },
|
||||
{ to: '/admin/trace', label: '链路追踪', icon: '▸' },
|
||||
{ to: '/admin/perf', label: '性能监控', icon: '▴' },
|
||||
{ to: '/admin/llm-calls', label: 'LLM 日志', icon: '🧠' },
|
||||
{ to: '/admin/system', label: '系统信息', icon: '⊞' },
|
||||
]
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getToken } from '../../api/client'
|
||||
|
||||
export default function PerformancePage() {
|
||||
const [latest, setLatest] = useState<any>({})
|
||||
const [history, setHistory] = useState<any[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
fetchPerf()
|
||||
const interval = setInterval(fetchPerf, 10000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const fetchPerf = async () => {
|
||||
try {
|
||||
const token = getToken()
|
||||
const res = await fetch('/api/v1/admin/perf', { headers: { Authorization: `Bearer ${token}` } })
|
||||
const data = await res.json()
|
||||
setLatest(data.latest || {})
|
||||
setHistory(data.history || [])
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const maxAlloc = Math.max(10, ...history.map((h: any) => h.alloc_mb || 0))
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<h2 className="text-lg font-semibold">性能监控</h2>
|
||||
|
||||
{/* Current stats */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<Stat label="内存分配" value={`${(latest.alloc_mb||0).toFixed(1)} MB`} sub="Heap Alloc" />
|
||||
<Stat label="系统内存" value={`${(latest.sys_mb||0).toFixed(1)} MB`} sub="Sys" />
|
||||
<Stat label="GC 次数" value={latest.num_gc || 0} sub="NumGC" />
|
||||
<Stat label="Goroutines" value={latest.goroutines || 0} sub="并发" />
|
||||
</div>
|
||||
|
||||
{/* Memory chart */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-white/60 mb-3">内存使用趋势 (最近 {history.length} 次采样)</h3>
|
||||
<div className="bg-[#000] border border-[#404040] rounded-[18px] p-4">
|
||||
<div className="flex items-end gap-1 h-32">
|
||||
{history.slice(-60).map((h: any, i: number) => {
|
||||
const pct = maxAlloc > 0 ? ((h.alloc_mb || 0) / maxAlloc) * 100 : 0
|
||||
return (
|
||||
<div key={i}
|
||||
className="flex-1 bg-[#277AF7]/60 hover:bg-[#277AF7] rounded-t-sm transition-colors"
|
||||
style={{ height: `${Math.max(2, pct)}%` }}
|
||||
title={`${(h.alloc_mb||0).toFixed(1)}MB`}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="flex justify-between mt-2 text-xs text-white/30">
|
||||
<span>{history[0]?.timestamp?.slice(11, 19) || '--'}</span>
|
||||
<span>{history[history.length-1]?.timestamp?.slice(11, 19) || '--'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GC chart */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-white/60 mb-3">GC 次数趋势</h3>
|
||||
<div className="bg-[#000] border border-[#404040] rounded-[18px] p-4">
|
||||
<div className="flex items-end gap-1 h-24">
|
||||
{history.slice(-60).map((h: any, i: number) => {
|
||||
const maxGC = Math.max(1, ...history.map((x: any) => x.num_gc || 0))
|
||||
const pct = ((h.num_gc || 0) / maxGC) * 100
|
||||
return (
|
||||
<div key={i}
|
||||
className="flex-1 bg-[#4788FF]/40 hover:bg-[#4788FF] rounded-t-sm transition-colors"
|
||||
style={{ height: `${Math.max(2, pct)}%` }}
|
||||
title={`GC: ${h.num_gc}`}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Stat({ label, value, sub }: { label:string; value:string|number; sub:string }) {
|
||||
return (
|
||||
<div className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3">
|
||||
<p className="text-xs text-white/40 mb-1">{label}</p>
|
||||
<p className="text-lg font-bold text-[#F2F2F2]">{value}</p>
|
||||
<p className="text-xs text-white/20 mt-0.5">{sub}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getToken } from '../../api/client'
|
||||
|
||||
const hopIcons: Record<string, string> = {
|
||||
msg_received: '📥', msg_sent: '📤',
|
||||
intent: '🎯', llm_call: '🧠', tool_call: '🔧',
|
||||
response: '💬', session: '👤', subsession: '⚡',
|
||||
}
|
||||
|
||||
export default function TracePage() {
|
||||
const [events, setEvents] = useState<any[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
fetchTrace()
|
||||
const interval = setInterval(fetchTrace, 5000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const fetchTrace = async () => {
|
||||
try {
|
||||
const token = getToken()
|
||||
const res = await fetch('/api/v1/admin/trace', { headers: { Authorization: `Bearer ${token}` } })
|
||||
const data = await res.json()
|
||||
setEvents(data.events || [])
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<h2 className="text-lg font-semibold">全链路追踪</h2>
|
||||
<p className="text-xs text-white/40">最近 {events.length} 条追踪事件</p>
|
||||
|
||||
{events.length === 0 ? (
|
||||
<div className="p-8 text-center text-white/30 text-sm">暂无追踪数据 — 发送消息后刷新</div>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{events.map((ev, i) => (
|
||||
<div key={ev.id || i}
|
||||
className={`bg-[#000] border rounded-[14px] px-4 py-3 flex items-center gap-3 ${
|
||||
ev.status === 'error' ? 'border-[#F12522]/30' : 'border-[#404040]'
|
||||
}`}
|
||||
>
|
||||
{/* Hop icon */}
|
||||
<span className="text-lg shrink-0">{hopIcons[ev.hop] || '●'}</span>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-[#F2F2F2]">{ev.label || ev.hop}</span>
|
||||
{ev.duration_ms > 0 && (
|
||||
<span className="text-xs text-white/30">{ev.duration_ms}ms</span>
|
||||
)}
|
||||
</div>
|
||||
{ev.detail && (
|
||||
<p className="text-xs text-white/30 truncate mt-0.5">{ev.detail}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
<span className={`text-xs font-mono text-white/30`}>
|
||||
{ev.timestamp ? ev.timestamp.slice(11, 19) : ''}
|
||||
</span>
|
||||
<span className={`w-2 h-2 rounded-full ${
|
||||
ev.status === 'success' ? 'bg-[#4788FF]' :
|
||||
ev.status === 'error' ? 'bg-[#F12522]' :
|
||||
ev.status === 'pending' ? 'bg-yellow-500' : 'bg-[#404040]'
|
||||
}`} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user