- 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
83 lines
2.0 KiB
Go
83 lines
2.0 KiB
Go
// 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
|
|
}
|