218fba559c
新增 sundynix-shared/otelx/slog.go:traceHandler 包装 slog.Handler,凡 ctx 有 活跃 span 的 slog.InfoContext(ctx,...) 自动注入 trace_id/span_id;SetupSlog(服务名) 装全局 JSON slog(service 标签 + LOG_LEVEL 控级)并设默认;TraceID(ctx) 辅助取 hex。 - 三个服务 main 启动调 otelx.SetupSlog。 - gateway 访问日志 Observe() 改用 slog.InfoContext(c.Request.Context(),...)(删旧 accessLogger)→ 每条 HTTP 日志带 trace_id。 - dispatcher orchestrator.Handle 的 received/done/error 改 ctx-aware slog → 任务 执行日志带 trace_id。 - otelx 单测 5 例(注入/无 span 不注入/With() 后仍生效/级别解析)。 - production_readiness.md 1.1:可观测性三件套(metrics+logs带trace_id+traces)闭环。 验证:dispatcher「task done」日志 trace_id 拿去 Jaeger /api/traces/<id> 命中同一条 11 span/3 服务链路;gateway 访问日志亦带同一 trace_id。四模块 build+vet+test 全绿。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
2.4 KiB
Go
93 lines
2.4 KiB
Go
package middleware
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"encoding/hex"
|
||
"log/slog"
|
||
"strconv"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/prometheus/client_golang/prometheus"
|
||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||
)
|
||
|
||
// CtxRequestID 是请求 ID 在 gin.Context 中的键。
|
||
const CtxRequestID = "request_id"
|
||
|
||
// ---- Prometheus 指标 ----
|
||
|
||
var (
|
||
httpRequests = promauto.NewCounterVec(prometheus.CounterOpts{
|
||
Name: "sundynix_http_requests_total",
|
||
Help: "HTTP 请求总数(按方法/路由模板/状态码)。",
|
||
}, []string{"method", "route", "status"})
|
||
|
||
httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||
Name: "sundynix_http_request_duration_seconds",
|
||
Help: "HTTP 请求耗时(秒)。",
|
||
Buckets: prometheus.DefBuckets,
|
||
}, []string{"method", "route"})
|
||
|
||
httpInFlight = promauto.NewGauge(prometheus.GaugeOpts{
|
||
Name: "sundynix_http_requests_in_flight",
|
||
Help: "当前处理中的 HTTP 请求数。",
|
||
})
|
||
)
|
||
|
||
// 访问日志走全局默认 slog(otelx.SetupSlog 安装,链路感知):用请求 ctx 打点即自动带 trace_id。
|
||
|
||
// RequestID 为每个请求生成/透传 X-Request-ID,写入上下文与响应头,供日志关联。
|
||
func RequestID() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
id := c.GetHeader("X-Request-ID")
|
||
if id == "" {
|
||
id = newRequestID()
|
||
}
|
||
c.Set(CtxRequestID, id)
|
||
c.Header("X-Request-ID", id)
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
// Observe 记录 Prometheus 指标 + 结构化访问日志。放在中间件链较前位置。
|
||
func Observe() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
start := time.Now()
|
||
httpInFlight.Inc()
|
||
c.Next()
|
||
httpInFlight.Dec()
|
||
|
||
route := c.FullPath() // 路由模板(/tasks/:id/...),避免按真实路径产生高基数
|
||
if route == "" {
|
||
route = "unmatched"
|
||
}
|
||
status := c.Writer.Status()
|
||
dur := time.Since(start)
|
||
method := c.Request.Method
|
||
|
||
httpRequests.WithLabelValues(method, route, strconv.Itoa(status)).Inc()
|
||
httpDuration.WithLabelValues(method, route).Observe(dur.Seconds())
|
||
|
||
uid, _ := c.Get(CtxUserID)
|
||
rid, _ := c.Get(CtxRequestID)
|
||
slog.InfoContext(c.Request.Context(), "http",
|
||
"request_id", rid,
|
||
"method", method,
|
||
"route", route,
|
||
"path", c.Request.URL.Path,
|
||
"status", status,
|
||
"latency_ms", dur.Milliseconds(),
|
||
"ip", c.ClientIP(),
|
||
"uid", uid,
|
||
"bytes", c.Writer.Size(),
|
||
)
|
||
}
|
||
}
|
||
|
||
func newRequestID() string {
|
||
var b [8]byte
|
||
_, _ = rand.Read(b[:])
|
||
return hex.EncodeToString(b[:])
|
||
}
|