114 lines
3.5 KiB
Go
114 lines
3.5 KiB
Go
// Package trace provides structured deep tracing for the Cyrene runtime.
|
|
//
|
|
// It records work as a tree of spans — one span per LLM call, tool call,
|
|
// orchestration node, state transition, sub-agent, memory access or guardrail —
|
|
// so the run can be rendered later as a flow graph and a chain-of-thought view
|
|
// (DevDocs/design/04-运行时.md §9). This package is a leaf: it depends only on
|
|
// the standard library and log/slog, and never imports the runtime packages
|
|
// (loop/agent) that consume it.
|
|
//
|
|
// Usage: create a Tracer over a Writer, then StartSpan a root and nested spans.
|
|
// StartSpan threads a parent/child chain through the context it returns, so
|
|
// runtime code only needs to pass that context along. A span is closed with
|
|
// Finish(status); finished spans are handed to the Writer (here JSONRecorder)
|
|
// for structured local logging.
|
|
package trace
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"log/slog"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// spanKey is the unexported context key that carries the current *Span.
|
|
type spanKey struct{}
|
|
|
|
// Tracer creates, chains and completes spans. It is safe for concurrent use.
|
|
//
|
|
// A nil Writer is tolerated: spans are still created and chained, but nothing
|
|
// is emitted on Finish. Construct with New.
|
|
type Tracer struct {
|
|
mu sync.RWMutex
|
|
w Writer
|
|
seq uint64
|
|
idPrefix string
|
|
}
|
|
|
|
// New builds a Tracer that emits completed spans to w. Pass a Writer such as a
|
|
// *JSONRecorder to persist structured logs; pass nil to trace without emitting.
|
|
func New(w Writer) *Tracer {
|
|
return &Tracer{w: w, idPrefix: randPrefix()}
|
|
}
|
|
|
|
// StartSpan begins a span named name of the given kind and returns it alongside
|
|
// a context that carries the span as the current one. If ctx already carries a
|
|
// span, the new span is chained as its child (its ParentID is set), enabling
|
|
// nested tracing; a nil ctx is treated as context.Background().
|
|
func (t *Tracer) StartSpan(ctx context.Context, name string, kind SpanKind) (context.Context, *Span) {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
parent := SpanFromContext(ctx)
|
|
span := t.newSpan(name, kind, parent)
|
|
return context.WithValue(ctx, spanKey{}, span), span
|
|
}
|
|
|
|
// SpanFromContext returns the span carried by ctx, or nil when ctx holds none
|
|
// (including a nil ctx).
|
|
func SpanFromContext(ctx context.Context) *Span {
|
|
if ctx == nil {
|
|
return nil
|
|
}
|
|
sp, _ := ctx.Value(spanKey{}).(*Span)
|
|
return sp
|
|
}
|
|
|
|
// newSpan allocates a span, assigns it a unique ID, links ParentID and records
|
|
// its start time.
|
|
func (t *Tracer) newSpan(name string, kind SpanKind, parent *Span) *Span {
|
|
t.mu.Lock()
|
|
t.seq++
|
|
id := fmt.Sprintf("%s-%d", t.idPrefix, t.seq)
|
|
t.mu.Unlock()
|
|
|
|
sp := &Span{
|
|
tracer: t,
|
|
ID: id,
|
|
Name: name,
|
|
Kind: kind,
|
|
StartedAt: time.Now(),
|
|
Props: make(map[string]any),
|
|
}
|
|
if parent != nil {
|
|
sp.ParentID = parent.ID
|
|
}
|
|
return sp
|
|
}
|
|
|
|
// emit hands a finished span to the writer. Write failures are logged and never
|
|
// propagated, since Finish cannot return an error. It is a no-op for a nil
|
|
// writer.
|
|
func (t *Tracer) emit(rec SpanRecord) {
|
|
if t.w == nil {
|
|
return
|
|
}
|
|
if err := t.w.WriteSpan(rec); err != nil {
|
|
slog.Error("trace: write span failed", "span_id", rec.ID, "error", err)
|
|
}
|
|
}
|
|
|
|
// randPrefix returns a short unique prefix used in span IDs so spans generated
|
|
// by different Tracers do not collide. It falls back to "tr" if the system
|
|
// entropy source is unavailable.
|
|
func randPrefix() string {
|
|
var b [8]byte
|
|
if _, err := rand.Read(b[:]); err != nil {
|
|
return "tr"
|
|
}
|
|
return hex.EncodeToString(b[:])
|
|
}
|