feat(trace): 深追踪(Span 生命周期/父子链/JSONL recorder,支持 llm/tool/state 等类别)
This commit is contained in:
1 parent
011d25a53d
commit
e33b005612
4 files changed
+661
No files matched your search
@@ -0,0 +1,174 @@
|
||||
package trace
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrSpanFinished is returned by Span.Record and Span.SetProp when called on a
|
||||
// span that has already been closed by Finish. A closed span is immutable.
|
||||
var ErrSpanFinished = errors.New("trace: span already finished")
|
||||
|
||||
// SpanKind classifies what a span represents so the M9 panel can group and color
|
||||
// nodes by stage of the run. The values mirror the span kinds listed in
|
||||
// DevDocs/design/04-运行时.md §9.
|
||||
type SpanKind string
|
||||
|
||||
const (
|
||||
// SpanKindLLM is a single LLM invocation.
|
||||
SpanKindLLM SpanKind = "llm_call"
|
||||
// SpanKindTool is a tool/function call.
|
||||
SpanKindTool SpanKind = "tool_call"
|
||||
// SpanKindOrchestration is an orchestration node, e.g. a plan step.
|
||||
SpanKindOrchestration SpanKind = "orchestration"
|
||||
// SpanKindStateTransition is a runtime state transition.
|
||||
SpanKindStateTransition SpanKind = "state_transition"
|
||||
// SpanKindSubagent is a spawned sub-agent.
|
||||
SpanKindSubagent SpanKind = "subagent"
|
||||
// SpanKindMemory is a memory read or write.
|
||||
SpanKindMemory SpanKind = "memory"
|
||||
// SpanKindGuardrail is a guardrail input/output check.
|
||||
SpanKindGuardrail SpanKind = "guardrail"
|
||||
)
|
||||
|
||||
// SpanStatus is the terminal outcome of a span.
|
||||
type SpanStatus string
|
||||
|
||||
const (
|
||||
// SpanStatusOK marks a successfully completed span.
|
||||
SpanStatusOK SpanStatus = "ok"
|
||||
// SpanStatusError marks a failed span.
|
||||
SpanStatusError SpanStatus = "error"
|
||||
)
|
||||
|
||||
// Event is a fine-grained record attached to a span, e.g. a token, retry or
|
||||
// decision. Events enrich the chain-of-thought view.
|
||||
type Event struct {
|
||||
// Time is when the event happened. Record sets it to now when left zero.
|
||||
Time time.Time `json:"time"`
|
||||
// Type is a short label identifying the event kind.
|
||||
Type string `json:"type"`
|
||||
// Props carries event-specific structured data.
|
||||
Props map[string]any `json:"props,omitempty"`
|
||||
}
|
||||
|
||||
// Span is a single traced unit of work, safe for concurrent use.
|
||||
//
|
||||
// A span is created by (*Tracer).StartSpan, which also returns a context that
|
||||
// carries the span as the current one. Runtime work then records detail with
|
||||
// Record and SetProp, and closes the span once with Finish. ID, ParentID, Name
|
||||
// and Kind are fixed at creation; Status, EndedAt and Duration are set when
|
||||
// Finish runs. Reading a span's fields is safe after Finish returns.
|
||||
type Span struct {
|
||||
mu sync.RWMutex
|
||||
tracer *Tracer
|
||||
|
||||
// ID is a span identifier unique across Tracers.
|
||||
ID string `json:"id"`
|
||||
// ParentID is the ID of the enclosing span, empty for a root span.
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
// Name is a human-readable label for the work.
|
||||
Name string `json:"name"`
|
||||
// Kind classifies the span.
|
||||
Kind SpanKind `json:"kind"`
|
||||
// Status is the terminal outcome, valid after Finish.
|
||||
Status SpanStatus `json:"status"`
|
||||
// StartedAt is when the span began.
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
// EndedAt is when Finish ran, zero until then.
|
||||
EndedAt time.Time `json:"ended_at,omitempty"`
|
||||
// Duration is EndedAt.Sub(StartedAt), zero until Finish.
|
||||
Duration time.Duration `json:"duration_ns"`
|
||||
// Props carries key/value metadata attached via SetProp.
|
||||
Props map[string]any `json:"props,omitempty"`
|
||||
// Events is the ordered list of events recorded via Record.
|
||||
Events []Event `json:"events,omitempty"`
|
||||
}
|
||||
|
||||
// Record appends an event to the span. A zero Timestamp is stamped with now.
|
||||
// Record returns ErrSpanFinished when the span has already been closed.
|
||||
func (s *Span) Record(evt Event) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if !s.EndedAt.IsZero() {
|
||||
return ErrSpanFinished
|
||||
}
|
||||
if evt.Time.IsZero() {
|
||||
evt.Time = time.Now()
|
||||
}
|
||||
s.Events = append(s.Events, evt)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetProp attaches a key/value property to the span for structured output.
|
||||
// SetProp returns ErrSpanFinished when the span has already been closed.
|
||||
func (s *Span) SetProp(key string, value any) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if !s.EndedAt.IsZero() {
|
||||
return ErrSpanFinished
|
||||
}
|
||||
s.Props[key] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
// Finish closes the span with the given status, stamping EndedAt and Duration
|
||||
// and handing the completed span to the owning Tracer's Writer. Finish is
|
||||
// idempotent: calls after the first are ignored.
|
||||
func (s *Span) Finish(status SpanStatus) {
|
||||
s.mu.Lock()
|
||||
if !s.EndedAt.IsZero() {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.Status = status
|
||||
s.EndedAt = time.Now()
|
||||
s.Duration = s.EndedAt.Sub(s.StartedAt)
|
||||
rec := s.recordLocked()
|
||||
s.mu.Unlock()
|
||||
|
||||
if s.tracer != nil {
|
||||
s.tracer.emit(rec)
|
||||
}
|
||||
}
|
||||
|
||||
// recordLocked snapshots the span as a SpanRecord. The caller must hold s.mu.
|
||||
func (s *Span) recordLocked() SpanRecord {
|
||||
rec := SpanRecord{
|
||||
ID: s.ID,
|
||||
ParentID: s.ParentID,
|
||||
Name: s.Name,
|
||||
Kind: s.Kind,
|
||||
Status: s.Status,
|
||||
StartedAt: s.StartedAt,
|
||||
EndedAt: s.EndedAt,
|
||||
Duration: s.Duration,
|
||||
}
|
||||
if len(s.Props) > 0 {
|
||||
rec.Props = make(map[string]any, len(s.Props))
|
||||
for k, v := range s.Props {
|
||||
rec.Props[k] = v
|
||||
}
|
||||
}
|
||||
if len(s.Events) > 0 {
|
||||
rec.Events = make([]Event, len(s.Events))
|
||||
copy(rec.Events, s.Events)
|
||||
}
|
||||
return rec
|
||||
}
|
||||
|
||||
// SpanRecord is an immutable, lock-free snapshot of a completed span, produced
|
||||
// by Finish and passed to a Writer for persistence. It is safe to copy.
|
||||
type SpanRecord struct {
|
||||
ID string `json:"id"`
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Kind SpanKind `json:"kind"`
|
||||
Status SpanStatus `json:"status"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
EndedAt time.Time `json:"ended_at,omitempty"`
|
||||
Duration time.Duration `json:"duration_ns"`
|
||||
Props map[string]any `json:"props,omitempty"`
|
||||
Events []Event `json:"events,omitempty"`
|
||||
}
|
||||
Reference in new issue
Block a user