package main import ( "fmt" "os" "sync" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace" "git.yeij.top/AskaEth/Cyrene_v2/internal/services/gateway" ) // newAppTraceWriter 构建组合根的 trace writer:JSONL stderr 记录器(每个 span 的 // 本地结构化日志),外加为 gateway 可观测性出口预留的位置 —— 后者只能在 gateway // 存在之后注册(见 wireGatewayTraceSink)。 func newAppTraceWriter() *traceFanout { return newTraceFanout(trace.NewJSONRecorder(os.Stderr)) } // wireGatewayTraceSink 把 gateway 的可观测性出口注册到应用的 trace 扇出上。它由 // wireFrontend 调用 —— 那是 gateway 首次存在的时刻 —— 因为 gateway 构建在发出 span // 的循环之后。没有它,管理面板的 trace 环形缓冲区永远不会被喂数据, // GET /api/v1/admin/observability/trace 会永远返回空列表(design/19 §2)。在没有 // tracer 或 gateway 的启动中是空操作。 func wireGatewayTraceSink(a *app, gw *gateway.Server) { if a.traceWriter == nil || gw == nil { return } a.traceWriter.Add(gatewaySpanSink{srv: gw}) a.log.Info("observability trace sink wired") } // traceFanout 是组合根的 trace.Writer。它把每个已完成的 span 扇出到启动时安装的 // 出口(JSONL stderr 记录器),以及启动流程中稍后注册的出口:gateway 的可观测性 // 缓冲区要等 wireFrontend 构建好 gateway 才存在,而发出 span 的循环在 wireRuntime // 中更早就构建了。 // // 它可安全并发使用 —— 循环会从多个 goroutine 结束 span(按步、按工具调用、子代理)。 type traceFanout struct { mu sync.RWMutex writers []trace.Writer } // newTraceFanout 在给定 writers 之上构建扇出。nil writer 会被丢弃。 func newTraceFanout(writers ...trace.Writer) *traceFanout { f := &traceFanout{} for _, w := range writers { f.Add(w) } return f } // Add 再安装一个出口。nil writer 被忽略。 func (f *traceFanout) Add(w trace.Writer) { if w == nil { return } f.mu.Lock() defer f.mu.Unlock() f.writers = append(f.writers, w) } // WriteSpan 把 rec 写给每个出口并返回第一个错误。即使失败也仍会尝试所有出口, // 因此单个坏掉的出口无法把 span 从其他出口那里藏起来;tracer 只记录返回的错误 // (trace.Tracer.emit)。 func (f *traceFanout) WriteSpan(rec trace.SpanRecord) error { f.mu.RLock() defer f.mu.RUnlock() var firstErr error for _, w := range f.writers { if w == nil { continue } if err := w.WriteSpan(rec); err != nil && firstErr == nil { firstErr = fmt.Errorf("trace fanout: %w", err) } } return firstErr } // wireGatewayLogBuffer 把组合根的日志环形缓冲区交给 gateway,使 // GET /api/v1/admin/observability/logs 能提供最近的后端日志尾部。logger 构建在 // gateway 之前(其记录必须从第一行日志起就被捕获),因此缓冲区在这里注入 —— // gateway 首次存在的时刻 —— 与 wireGatewayTraceSink 对 span 的做法一致。在没有 // gateway 的启动中是空操作。 func wireGatewayLogBuffer(a *app, buf *logger.RingBuffer) { if a == nil || a.gw == nil || buf == nil { return } a.gw.SetLogBuffer(buf) a.log.Info("observability log buffer wired", "capacity", buf.Len()) } // gatewaySpanSink 通过 *gateway.Server 的 ObserveSpan 入口把它适配成 trace.Writer, // 使发出的 span 落在管理面板的 trace 环形缓冲区**并且**计入 LLM/子代理指标计数器 // (ObserveSpan 既追加到缓冲区,也观测指标;缓冲区正是 // GET /api/v1/admin/observability/trace 所提供的内容)。 type gatewaySpanSink struct{ srv *gateway.Server } // WriteSpan 把 rec 喂给 gateway。server 缺失时是空操作。 func (g gatewaySpanSink) WriteSpan(rec trace.SpanRecord) error { if g.srv == nil { return nil } g.srv.ObserveSpan(rec) return nil }