feat: 建立可观测性、容量与恢复验证基线

This commit is contained in:
weijuesen
2026-08-14 14:54:56 +08:00
parent 126c215a6b
commit 1e6a5dbfb8
16 changed files with 398 additions and 3 deletions
+10
View File
@@ -2,6 +2,7 @@ package middleware
import (
"log/slog"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -17,13 +18,22 @@ func Logger() gin.HandlerFunc {
latency := time.Since(start)
status := c.Writer.Status()
query := RedactSensitiveQuery(c.Request.URL.RawQuery)
auth := c.GetHeader("Authorization")
authPrefix := ""
if auth != "" {
authPrefix = strings.SplitN(auth, " ", 2)[0]
}
slog.Info("请求",
"requestId", RequestID(c),
"method", c.Request.Method,
"path", path,
"query", query,
"status", status,
"latency", latency.String(),
"ip", c.ClientIP(),
"authType", authPrefix,
)
}
}
@@ -0,0 +1,65 @@
package middleware
import (
"crypto/rand"
"encoding/hex"
"net/url"
"strings"
"time"
"github.com/gin-gonic/gin"
)
const RequestIDHeader = "X-Request-ID"
// RequestID 返回当前请求 ID。
func RequestID(c *gin.Context) string {
value, _ := c.Get("requestId")
if id, ok := value.(string); ok {
return id
}
return ""
}
// RequestIDMiddleware 生成或透传 X-Request-ID。
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.GetHeader(RequestIDHeader)
if id == "" {
id = randomRequestID()
}
c.Set("requestId", id)
c.Header(RequestIDHeader, id)
c.Next()
}
}
func randomRequestID() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "req-" + hex.EncodeToString([]byte(time.Now().Format(time.RFC3339Nano)))
}
return hex.EncodeToString(b)
}
// RedactSensitiveQuery 脱敏 URL query 中的 token/password/secret。
func RedactSensitiveQuery(rawQuery string) string {
if rawQuery == "" {
return ""
}
values, err := url.ParseQuery(rawQuery)
if err != nil {
return "[invalid-query]"
}
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
for _, key := range keys {
lower := strings.ToLower(key)
if strings.Contains(lower, "token") || strings.Contains(lower, "password") || strings.Contains(lower, "secret") || strings.Contains(lower, "authorization") {
values.Set(key, "[redacted]")
}
}
return values.Encode()
}
@@ -0,0 +1,42 @@
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func TestRequestIDMiddlewarePassesThrough(t *testing.T) {
gin.SetMode(gin.TestMode)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
c.Request.Header.Set(RequestIDHeader, "request-1")
RequestIDMiddleware()(c)
if rec.Header().Get(RequestIDHeader) != "request-1" {
t.Fatalf("header = %s", rec.Header().Get(RequestIDHeader))
}
if RequestID(c) != "request-1" {
t.Fatalf("request id = %s", RequestID(c))
}
}
func TestRequestIDMiddlewareGenerates(t *testing.T) {
gin.SetMode(gin.TestMode)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
RequestIDMiddleware()(c)
if RequestID(c) == "" {
t.Fatal("should generate request id")
}
}
func TestRedactSensitiveQuery(t *testing.T) {
got := RedactSensitiveQuery("access_token=abc&password=secret&roomId=1")
if got != "access_token=%5Bredacted%5D&password=%5Bredacted%5D&roomId=1" {
t.Fatalf("redacted query = %s", got)
}
}