Files
sundynix-agentix/sundynix-gateway/internal/middleware/observability.go
T
Blizzard 218fba559c feat(observability): slog 日志带 trace_id,与链路双向互跳
新增 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>
2026-06-24 12:41:33 +08:00

93 lines
2.4 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 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 请求数。",
})
)
// 访问日志走全局默认 slogotelx.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[:])
}