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
@@ -0,0 +1,82 @@
|
||||
// Package trace provides a ring-buffer for full-chain tracing events.
|
||||
package trace
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Event represents a single event in a request trace chain.
|
||||
type Event struct {
|
||||
ID string `json:"id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Hop string `json:"hop"` // "msg_received" | "intent" | "llm_call" | "tool_call" | "response"
|
||||
Label string `json:"label"`
|
||||
Status string `json:"status"` // "success" | "error" | "pending"
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
Detail string `json:"detail"`
|
||||
Data map[string]any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// Logger is a ring-buffer that stores the last N trace events.
|
||||
type Logger struct {
|
||||
mu sync.RWMutex
|
||||
events []Event
|
||||
max int
|
||||
index int
|
||||
}
|
||||
|
||||
// NewLogger creates a trace logger with the given capacity.
|
||||
func NewLogger(capacity int) *Logger {
|
||||
return &Logger{events: make([]Event, capacity), max: capacity}
|
||||
}
|
||||
|
||||
// Record adds a trace event.
|
||||
func (l *Logger) Record(event Event) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if event.ID == "" {
|
||||
event.ID = time.Now().Format("150405") + "-" + event.Hop
|
||||
}
|
||||
l.events[l.index%l.max] = event
|
||||
l.index++
|
||||
}
|
||||
|
||||
// Recent returns the most recent N events.
|
||||
func (l *Logger) Recent(n int) []Event {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
|
||||
if n > l.max { n = l.max }
|
||||
count := l.index
|
||||
if count > l.max { count = l.max }
|
||||
if n > count { n = count }
|
||||
|
||||
result := make([]Event, n)
|
||||
for i := 0; i < n; i++ {
|
||||
idx := (l.index - 1 - i + l.max) % l.max
|
||||
if idx < l.max {
|
||||
result[n-1-i] = l.events[idx]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Hops returns unique hop names seen in the buffer.
|
||||
func (l *Logger) Hops() []string {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
|
||||
seen := make(map[string]bool)
|
||||
var result []string
|
||||
count := l.index
|
||||
if count > l.max { count = l.max }
|
||||
for i := 0; i < count; i++ {
|
||||
ev := l.events[i]
|
||||
if !seen[ev.Hop] && ev.Hop != "" {
|
||||
seen[ev.Hop] = true
|
||||
result = append(result, ev.Hop)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in new issue
Block a user