Files
Cyrene_v2/internal/services/gateway/middleware.go
T
Aska e0a543cc37
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
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 空)。
2026-09-06 21:02:46 +08:00

116 lines
3.5 KiB
Go

package gateway
import (
"bufio"
"fmt"
"net"
"net/http"
"time"
apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors"
)
// cors sets the Access-Control-* headers for origins in the configured allow
// list and short-circuits OPTIONS preflight requests. An empty origin (same-origin
// or non-browser client) is passed through untouched.
func (s *Server) cors(next http.Handler) http.Handler {
allowed, allowAll := s.allowedOrigins()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if origin := r.Header.Get("Origin"); origin != "" && (allowAll || allowed[origin]) {
if allowAll {
w.Header().Set("Access-Control-Allow-Origin", "*")
} else {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
w.Header().Set("Vary", "Origin")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
w.Header().Set("Access-Control-Max-Age", "600")
}
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// allowedOrigins resolves the configured origin list into a lookup set plus an
// allow-all flag for the "*" wildcard.
func (s *Server) allowedOrigins() (map[string]bool, bool) {
list := s.cfg.AllowedOrigins
if len(list) == 0 {
return map[string]bool{}, false
}
allowAll := false
out := make(map[string]bool, len(list))
for _, o := range list {
if o == "*" {
allowAll = true
continue
}
out[o] = true
}
return out, allowAll
}
// recover converts a panic in a downstream handler into a 500 {error:...}
// response instead of crashing the process, and logs the panic.
func (s *Server) recover(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
s.log.Error("panic recovered", "panic", rec, "method", r.Method, "path", r.URL.Path)
writeError(w, apperr.ErrInternal)
}
}()
next.ServeHTTP(w, r)
})
}
// logging records one structured line per request (method, path, status, dur).
func (s *Server) logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rec, r)
durMs := time.Since(start).Milliseconds()
s.recordMetric(r.Method, r.URL.Path, rec.status, durMs)
s.log.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", rec.status,
"duration_ms", durMs,
)
})
}
// statusRecorder captures the written status so the logging middleware can report
// it. It forwards the optional Hijacker/Flusher interfaces so the WebSocket
// upgrader (which requires http.Hijacker) and streaming handlers still work.
type statusRecorder struct {
http.ResponseWriter
status int
}
// WriteHeader records the status before delegating.
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
// Hijack forwards to the underlying writer when it supports connection hijacking.
func (r *statusRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := r.ResponseWriter.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, fmt.Errorf("gateway: hijack unsupported by %T", r.ResponseWriter)
}
// Flush forwards to the underlying writer when it supports flushing.
func (r *statusRecorder) Flush() {
if f, ok := r.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}