- 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
179 lines
4.6 KiB
Go
179 lines
4.6 KiB
Go
// 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 (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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
|
|
traces []*Trace
|
|
max int
|
|
index int
|
|
running map[string]*Trace // Currently running traces by ID
|
|
}
|
|
|
|
// NewLogger creates a trace logger with given capacity.
|
|
func NewLogger(capacity int) *Logger {
|
|
return &Logger{
|
|
traces: make([]*Trace, capacity),
|
|
max: capacity,
|
|
running: make(map[string]*Trace),
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
|
|
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,
|
|
}
|
|
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 traces.
|
|
func (l *Logger) Recent(n int) []*Trace {
|
|
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([]*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.traces[idx] }
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Running returns currently active traces.
|
|
func (l *Logger) Running() []*Trace {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
|
|
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]) + "..."
|
|
}
|