- observability.go/observability_metrics.go/observability_trace.go/observability_audit.go:
拆分保证 ≤500行/函数≤80。metrics 并发安全 MetricsCollector(方法/路径/状态计数、平均/最大延迟、
近1s QPS窗口);trace 并发安全 RingBuffer(默认512, newest-first)+traceSink+MultiTraceWriter;
audit AuditStore 接口+内存回退+SQLite 持久化(NewSQLiteAuditStore 参数化 SQL+audit_logs 表)。
- server.go:Deps.AuditStore + Server.obs + 3 路由 GET /admin/observability/{metrics,trace,audit};
ObserveSpan/TraceSink 注入点;middleware logging recordMetric;audit 落库(仅 slog→Append)。
- 前端 Obs.tsx:metrics 接 4 张真实 StatCard+请求分布、trace 接 span 列表(展开 props/events)、
audit 从后端拉(前端内存审计作回退);api.ts/types.ts 加 obs 端点/类型(AdminObservabilityMetrics/
AdminTraceSpan/AuditRecord.id 放宽)。成本指标不伪造(保留'—'+注明)。
- wire.go 收口:Deps.AuditStore 注入 gateway.NewSQLiteAuditStore(ctx, a.db)(审计真落库),
未注入回退内存。
- Docs/api-client.md §2/v0.6。
实机验证:三端点 200,metrics 返回 totalRequests:4/qps:4/perPath/statusCounts:{200:4}。
go build/vet/test 全绿 + 前端 tsc。trace 数据源对接(gw.TraceSink→loop tracer)因 gw/tracer
构造顺序留作可选后置(端点已就绪, 未对接 span 空)。
138 lines
3.8 KiB
Go
138 lines
3.8 KiB
Go
package gateway
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
|
|
)
|
|
|
|
// defaultTraceBufCap is how many completed spans the ring buffer holds before the
|
|
// oldest is evicted. Compile-time constant.
|
|
const defaultTraceBufCap = 512
|
|
|
|
// AdminTraceSpan is the camelCase wire shape of one traced span. It decouples the
|
|
// frontend from the trace package's snake_case 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 is a concurrency-safe bounded buffer of completed trace spans. The
|
|
// oldest span is evicted when the buffer is full, so the admin panel always sees
|
|
// the most recent work without unbounded growth.
|
|
type RingBuffer struct {
|
|
mu sync.RWMutex
|
|
buf []trace.SpanRecord
|
|
next int
|
|
full bool
|
|
cap int
|
|
}
|
|
|
|
// newRingBuffer builds a ring buffer holding up to cap spans (default when cap<=0).
|
|
func newRingBuffer(cap int) *RingBuffer {
|
|
if cap <= 0 {
|
|
cap = defaultTraceBufCap
|
|
}
|
|
return &RingBuffer{buf: make([]trace.SpanRecord, cap), cap: cap}
|
|
}
|
|
|
|
// Append stores rec, evicting the oldest when the buffer is full.
|
|
func (r *RingBuffer) Append(rec trace.SpanRecord) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.buf[r.next] = rec
|
|
r.next = (r.next + 1) % r.cap
|
|
if r.next == 0 {
|
|
r.full = true
|
|
}
|
|
}
|
|
|
|
// Snapshot returns up to n most-recent spans, newest first. A non-positive n
|
|
// selects defaultObsLimit; n is clamped to 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])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// traceSink adapts a *RingBuffer to trace.Writer so a completed span is appended
|
|
// to the gateway's trace buffer.
|
|
type traceSink struct{ b *RingBuffer }
|
|
|
|
// WriteSpan appends rec to the ring buffer.
|
|
func (s traceSink) WriteSpan(rec trace.SpanRecord) error {
|
|
if s.b == nil {
|
|
return nil
|
|
}
|
|
s.b.Append(rec)
|
|
return nil
|
|
}
|
|
|
|
// MultiTraceWriter fans a completed span out to several trace.Writers, so a tracer
|
|
// can record to both the gateway's ring buffer and a JSONL file in one pass. A nil
|
|
// Writer is skipped. It is the composition-root helper the main session uses to
|
|
// route the loop tracer's output into the gateway without dropping stderr.
|
|
type MultiTraceWriter struct {
|
|
// Writers are the sinks a span is written to. A nil Writer is skipped.
|
|
Writers []trace.Writer
|
|
}
|
|
|
|
// WriteSpan writes rec to every configured 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 maps a trace.SpanRecord to the camelCase admin wire shape.
|
|
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
|
|
}
|