feat(observability): 管理面板可观测性接真实数据——metrics/trace/audit 三端点 + 审计持久化
- 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 空)。
This commit is contained in:
1 parent
76134ed47e
commit
e0a543cc37
12 files changed
+803
-22
No files matched your search
@@ -0,0 +1,137 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user