// 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 }