Files
Cyrene_v2/backend/services/ai-core/trace/logger.go
T
Aska 4e2e81f684 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
2026-08-12 18:52:39 +08:00

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]) + "..."
}