feat(trace): waterfall visualization + persona YAML sync
- trace.Logger: Trace+Span model with 14+ hop tracking - trace.SpanTimer: convenience API for instrumentation - trace_adapter.go: serialize Trace+Span for frontend - Frontend TracePage: colored waterfall bars, expand/collapse, detail popup - persona.go: full v1 YAML parser (identity/traits/forbidden/sections) - cyrene_persona.yaml: v1 persona config copied to v2 - Orchestrator: TraceHook + MemoryExtractor injection points
This commit is contained in:
1 parent
47047c903c
commit
4e2e81f684
8 files changed
+1178
-198
No files matched your search
@@ -1,4 +1,6 @@
|
||||
// Package trace provides a ring-buffer for full-chain tracing events.
|
||||
// Package trace provides a full-chain request tracing system.
|
||||
// Each conversation request generates a Trace with multiple Spans
|
||||
// capturing every intermediate step from message receipt to response delivery.
|
||||
package trace
|
||||
|
||||
import (
|
||||
@@ -6,44 +8,101 @@ import (
|
||||
"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"`
|
||||
// Trace represents a single conversation request's full lifecycle.
|
||||
type Trace struct {
|
||||
ID string `json:"id"`
|
||||
UserMsg string `json:"user_msg"` // 用户说了什么(截取前80字)
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime time.Time `json:"end_time,omitempty"`
|
||||
Status string `json:"status"` // "running" | "success" | "error"
|
||||
Error string `json:"error,omitempty"`
|
||||
Spans []Span `json:"spans"`
|
||||
}
|
||||
|
||||
// Logger is a ring-buffer that stores the last N trace events.
|
||||
// Span represents a single step within a trace.
|
||||
type Span struct {
|
||||
Name string `json:"name"` // "intent_analysis" | "llm_call" | etc.
|
||||
Label string `json:"label"` // Display label
|
||||
StartMs int64 `json:"start_ms"` // Offset from trace start in ms
|
||||
Duration int64 `json:"duration_ms"`
|
||||
Status string `json:"status"` // "running" | "success" | "error"
|
||||
Detail string `json:"detail"` // Extra info (model name, token count, etc.)
|
||||
Children []Span `json:"children,omitempty"` // Sub-spans
|
||||
Data map[string]any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// Logger holds recent traces in a ring buffer.
|
||||
type Logger struct {
|
||||
mu sync.RWMutex
|
||||
events []Event
|
||||
max int
|
||||
index int
|
||||
mu sync.RWMutex
|
||||
traces []*Trace
|
||||
max int
|
||||
index int
|
||||
running map[string]*Trace // Currently running traces by ID
|
||||
}
|
||||
|
||||
// NewLogger creates a trace logger with the given capacity.
|
||||
// NewLogger creates a trace logger with given capacity.
|
||||
func NewLogger(capacity int) *Logger {
|
||||
return &Logger{events: make([]Event, capacity), max: capacity}
|
||||
return &Logger{
|
||||
traces: make([]*Trace, capacity),
|
||||
max: capacity,
|
||||
running: make(map[string]*Trace),
|
||||
}
|
||||
}
|
||||
|
||||
// Record adds a trace event.
|
||||
func (l *Logger) Record(event Event) {
|
||||
// StartTrace begins a new trace for an incoming user message.
|
||||
func (l *Logger) StartTrace(id, userMsg string) *Trace {
|
||||
t := &Trace{
|
||||
ID: id,
|
||||
UserMsg: truncateStr(userMsg, 80),
|
||||
StartTime: time.Now(),
|
||||
Status: "running",
|
||||
Spans: make([]Span, 0),
|
||||
}
|
||||
l.mu.Lock()
|
||||
l.running[id] = t
|
||||
l.mu.Unlock()
|
||||
return t
|
||||
}
|
||||
|
||||
// AddSpan adds a span to a running trace.
|
||||
func (l *Logger) AddSpan(traceID, name, label, status string, durationMs int64, detail string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if event.ID == "" {
|
||||
event.ID = time.Now().Format("150405") + "-" + event.Hop
|
||||
|
||||
t, ok := l.running[traceID]
|
||||
if !ok { return }
|
||||
|
||||
span := Span{
|
||||
Name: name,
|
||||
Label: label,
|
||||
StartMs: time.Since(t.StartTime).Milliseconds() - durationMs,
|
||||
Duration: durationMs,
|
||||
Status: status,
|
||||
Detail: detail,
|
||||
}
|
||||
l.events[l.index%l.max] = event
|
||||
t.Spans = append(t.Spans, span)
|
||||
}
|
||||
|
||||
// FinishTrace marks a trace as complete and stores it in the ring buffer.
|
||||
func (l *Logger) FinishTrace(id, status, errMsg string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
t, ok := l.running[id]
|
||||
if !ok { return }
|
||||
|
||||
t.EndTime = time.Now()
|
||||
t.Status = status
|
||||
if errMsg != "" { t.Error = errMsg }
|
||||
delete(l.running, id)
|
||||
|
||||
// Store in ring buffer
|
||||
l.traces[l.index%l.max] = t
|
||||
l.index++
|
||||
}
|
||||
|
||||
// Recent returns the most recent N events.
|
||||
func (l *Logger) Recent(n int) []Event {
|
||||
// Recent returns the most recent N traces.
|
||||
func (l *Logger) Recent(n int) []*Trace {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
|
||||
@@ -52,31 +111,68 @@ func (l *Logger) Recent(n int) []Event {
|
||||
if count > l.max { count = l.max }
|
||||
if n > count { n = count }
|
||||
|
||||
result := make([]Event, n)
|
||||
result := make([]*Trace, 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]
|
||||
}
|
||||
if idx < l.max { result[n-1-i] = l.traces[idx] }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Hops returns unique hop names seen in the buffer.
|
||||
func (l *Logger) Hops() []string {
|
||||
// Running returns currently active traces.
|
||||
func (l *Logger) Running() []*Trace {
|
||||
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)
|
||||
}
|
||||
result := make([]*Trace, 0, len(l.running))
|
||||
for _, t := range l.running {
|
||||
result = append(result, t)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Clear removes all traces.
|
||||
func (l *Logger) Clear() {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.traces = make([]*Trace, l.max)
|
||||
l.index = 0
|
||||
l.running = make(map[string]*Trace)
|
||||
}
|
||||
|
||||
// SpanTimer is a convenience helper for timing a span.
|
||||
type SpanTimer struct {
|
||||
logger *Logger
|
||||
traceID string
|
||||
name string
|
||||
label string
|
||||
start time.Time
|
||||
}
|
||||
|
||||
// StartSpan begins timing a span.
|
||||
func (l *Logger) StartSpan(traceID, name, label string) *SpanTimer {
|
||||
return &SpanTimer{
|
||||
logger: l,
|
||||
traceID: traceID,
|
||||
name: name,
|
||||
label: label,
|
||||
start: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Done finishes the span and adds it to the trace.
|
||||
func (st *SpanTimer) Done(status, detail string) {
|
||||
durationMs := time.Since(st.start).Milliseconds()
|
||||
st.logger.AddSpan(st.traceID, st.name, st.label, status, durationMs, detail)
|
||||
}
|
||||
|
||||
// DoneMs finishes with a pre-computed duration.
|
||||
func (st *SpanTimer) DoneMs(durationMs int64, status, detail string) {
|
||||
st.logger.AddSpan(st.traceID, st.name, st.label, status, durationMs, detail)
|
||||
}
|
||||
|
||||
func truncateStr(s string, maxLen int) string {
|
||||
runes := []rune(s)
|
||||
if len(runes) <= maxLen { return s }
|
||||
return string(runes[:maxLen]) + "..."
|
||||
}
|
||||
Reference in new issue
Block a user