- 四组并行完成:main/wire*.go、wire_capacity|channels|runtime、channels|persona_*|emotion|observability、 voice|plugins|stream|skills|vision|subagent_tool|extractor|mcp|sessionread_tool|terminal_tool。 - 机械校验(git diff -U0 去 // 行与空行)仅剩 wire_frontend.go:84 一处行尾注释:代码前缀 「\ta.gw = gw 」经 cat -A 与 HEAD 逐字节比对一致(allow 清单已声明)。 - 验收:隔离 worktree(HEAD + 本批 patch)go build/vet 绿、全树 58 包 ok、 linux/amd64 + darwin/arm64 交叉编译绿;gofmt -l 干净。 - 备注:整树两次红均为负载敏感 flake(backthink / orchestration 的 timeout 类用例单独 -count=20 全过)。
108 lines
3.9 KiB
Go
108 lines
3.9 KiB
Go
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
|
||
}
|