Files
Cyrene_v2/internal/services/gateway/observability_trace.go
T
AskaEth 0e1f163ef9
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(gateway,admin-web): 可观测面从轮询改为 WS 帧推送(裁决 05.4.3 + 16.4.3-WS)
- 新增三个下行帧(纯新增,既有帧名/形状零改动):observability_log / observability_trace /
  observability_metrics,各带 ≤1s 节拍、无新数据不发帧、单帧上限(日志 200 / span 100)
- 帧壳 {type,data} 不带 sessionId;**不进 frameReachesAll 白名单** ⇒ fail-closed 只发 owner+admin
  (变异验证两次:加进白名单 → viewer 用例必红)
- 推送开启条件=owner/admin 凭据「读过一次」对应 REST(引导数据仍由该次读取返回),
  连接全断即退订并停循环(无常驻 goroutine);既有 REST 端点路径/RBAC/形状完全不变
- 前端:删除 logs/metrics/trace 三处 useAutoReload 轮询;新增 observabilityStream(引导+去重合并+窗口)
  与 observabilityViews(三个面板 + WS 徽标),长列表统一走已落地的 VirtualList
- 复核:build/vet 绿 + gateway 全包测试绿(含真 bus→真 /ws/chat 三角色帧断言)+ `-race`;
  前端 tsc 0 错 + vite build 成功
- 待登记契约:api-client 需新增「可观测实时推送」一节
2026-10-04 13:58:03 +08:00

181 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gateway
import (
"sync"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
)
// defaultTraceBufCap 是环形缓冲在逐出最旧 span 之前持有的已
// 完成 span 数。编译期常量。
const defaultTraceBufCap = 512
// AdminTraceSpan 是一个已追踪 span 的驼峰式线上结构。它把
// 前端与 trace 包蛇形命名的 SpanRecord 解耦。
type AdminTraceSpan struct {
ID string `json:"id"`
ParentID string `json:"parentId,omitempty"`
Name string `json:"name"`
Kind string `json:"kind"`
Status string `json:"status"`
StartedAt string `json:"startedAt"`
EndedAt string `json:"endedAt,omitempty"`
DurationNs int64 `json:"durationNs"`
Props map[string]any `json:"props,omitempty"`
Events []trace.Event `json:"events,omitempty"`
}
// RingBuffer 是已完成的 trace span 的并发安全有界缓冲。缓冲
// 满时逐出最旧 span,因此管理面板总能看到
// 最近的工作而不会无界增长。
type RingBuffer struct {
mu sync.RWMutex
buf []traceEntry
next int
full bool
cap int
// seq 是最后写入 span 的序号(单调递增,从 1 起)。
// 增量推送(observability_trace 帧)用它做游标,见 SnapshotSince。
seq uint64
}
// traceEntry 是环形缓冲内的一个存储格。序号随写入单调递增,因此
// 下游可以「只取比游标新的」而不是靠时间戳去猜。
type traceEntry struct {
seq uint64
rec trace.SpanRecord
}
// newRingBuffer 构建至多容纳 cap 个 span 的环形缓冲(cap<=0 时用默认值)。
func newRingBuffer(cap int) *RingBuffer {
if cap <= 0 {
cap = defaultTraceBufCap
}
return &RingBuffer{buf: make([]traceEntry, cap), cap: cap}
}
// Append 存储 rec,缓冲满时逐出最旧的。
func (r *RingBuffer) Append(rec trace.SpanRecord) {
r.mu.Lock()
defer r.mu.Unlock()
r.seq++
r.buf[r.next] = traceEntry{seq: r.seq, rec: rec}
r.next = (r.next + 1) % r.cap
if r.next == 0 {
r.full = true
}
}
// Seq 返回最后写入 span 的序号(还没写过任何 span 时为 0)。
func (r *RingBuffer) Seq() uint64 {
r.mu.RLock()
defer r.mu.RUnlock()
return r.seq
}
// Snapshot 返回至多 n 条最近 span,最新优先。非正的 n
// 选用 defaultObsLimit;n 被夹取到 maxObsLimit。
func (r *RingBuffer) Snapshot(n int) []trace.SpanRecord {
if n <= 0 {
n = defaultObsLimit
}
if n > maxObsLimit {
n = maxObsLimit
}
r.mu.RLock()
defer r.mu.RUnlock()
count := r.next
if r.full {
count = r.cap
}
if count > n {
count = n
}
out := make([]trace.SpanRecord, 0, count)
for i := 0; i < count; i++ {
idx := (r.next - 1 - i + r.cap*2) % r.cap
out = append(out, r.buf[idx].rec)
}
return out
}
// SnapshotSince 返回序号大于 since 的至多 n 条 span,最新优先(与 Snapshot
// 同序),供增量推送使用。n 非正时只用 defaultObsLimit 兜底;调用方按
// 「帧载荷有上限」的约束传入自己的上限。
func (r *RingBuffer) SnapshotSince(since uint64, n int) []traceEntry {
if n <= 0 {
n = defaultObsLimit
}
r.mu.RLock()
defer r.mu.RUnlock()
count := r.next
if r.full {
count = r.cap
}
out := make([]traceEntry, 0, min(count, n))
for i := 0; i < count && len(out) < n; i++ {
idx := (r.next - 1 - i + r.cap*2) % r.cap
e := r.buf[idx]
if e.seq <= since {
break
}
out = append(out, e)
}
return out
}
// traceSink 把 *RingBuffer 适配为 trace.Writer,使已完成的 span 被
// 追加进网关的 trace 缓冲。
type traceSink struct{ b *RingBuffer }
// WriteSpan 把 rec 追加进环形缓冲。
func (s traceSink) WriteSpan(rec trace.SpanRecord) error {
if s.b == nil {
return nil
}
s.b.Append(rec)
return nil
}
// MultiTraceWriter 把已完成的 span 扇出到多个 trace.Writer,使一个
// tracer 能一趟同时记录到网关的环形缓冲与一个 JSONL 文件。nil 的
// Writer 被跳过。它是主会话用来把 loop tracer 的输出引入
// 网关而不丢掉 stderr 的组合根辅助件。
type MultiTraceWriter struct {
// Writers 是 span 被写入的汇聚点。nil 的 Writer 被跳过。
Writers []trace.Writer
}
// WriteSpan 把 rec 写入每一个已配置的 Writer。
func (m MultiTraceWriter) WriteSpan(rec trace.SpanRecord) error {
for _, w := range m.Writers {
if w == nil {
continue
}
if err := w.WriteSpan(rec); err != nil {
return err
}
}
return nil
}
// adminSpanOf 把 trace.SpanRecord 映射为驼峰式管理线上结构。
func adminSpanOf(rec trace.SpanRecord) AdminTraceSpan {
sp := AdminTraceSpan{
ID: rec.ID,
ParentID: rec.ParentID,
Name: rec.Name,
Kind: string(rec.Kind),
Status: string(rec.Status),
StartedAt: rec.StartedAt.UTC().Format(time.RFC3339),
DurationNs: rec.Duration.Nanoseconds(),
Props: rec.Props,
Events: rec.Events,
}
if !rec.EndedAt.IsZero() {
sp.EndedAt = rec.EndedAt.UTC().Format(time.RFC3339)
}
return sp
}