Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
package requestid
import (
"strings"
"github.com/gin-gonic/gin"
)
func New(opts ...Option) gin.HandlerFunc {
var cfg config = config{
generator: DefaultFenerator,
}
for _, opt := range opts {
opt(&cfg)
}
return func(c *gin.Context) {
rid := strings.TrimSpace(c.GetHeader(HeaderKey))
clientProvided := rid != "" && len(rid) <= 128
if !clientProvided {
rid = cfg.generator()
}
c.Request.Header.Set(HeaderKey, rid)
c.Header(HeaderKey, rid)
c.Set(ContextKey, rid)
c.Set(ClientProvidedContextKey, clientProvided)
}
}
// FromClient 仅返回客户端显式提供且长度合规的请求ID。
func FromClient(c *gin.Context) (string, bool) {
if c == nil {
return "", false
}
provided, ok := c.Get(ClientProvidedContextKey)
if !ok || provided != true {
return "", false
}
return c.GetHeader(HeaderKey), true
}
+82
View File
@@ -0,0 +1,82 @@
package requestid
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
)
func TestMiddlewareClientProvidedRequestID(t *testing.T) {
gin.SetMode(gin.TestMode)
var (
gotID string
gotProvided bool
)
router := gin.New()
router.Use(New(WithGenerator(func() string { return "generated-id" })))
router.GET("/", func(c *gin.Context) {
gotID, gotProvided = FromClient(c)
c.Status(http.StatusNoContent)
})
request := httptest.NewRequest(http.MethodGet, "/", nil)
request.Header.Set(HeaderKey, " client-id ")
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
if gotID != "client-id" || !gotProvided {
t.Fatalf("FromClient() = (%q, %v), want (client-id, true)", gotID, gotProvided)
}
if got := response.Header().Get(HeaderKey); got != "client-id" {
t.Fatalf("response %s = %q, want client-id", HeaderKey, got)
}
}
func TestMiddlewareGeneratedRequestIDIsTracingOnly(t *testing.T) {
gin.SetMode(gin.TestMode)
tests := []struct {
name string
requestID string
}{
{name: "missing"},
{name: "blank", requestID: " "},
{name: "too long", requestID: strings.Repeat("x", 129)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var (
gotID string
gotProvided bool
)
router := gin.New()
router.Use(New(WithGenerator(func() string { return "generated-id" })))
router.GET("/", func(c *gin.Context) {
gotID, gotProvided = FromClient(c)
c.Status(http.StatusNoContent)
})
request := httptest.NewRequest(http.MethodGet, "/", nil)
if tt.requestID != "" {
request.Header.Set(HeaderKey, tt.requestID)
}
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
if gotID != "" || gotProvided {
t.Fatalf("FromClient() = (%q, %v), want empty and false", gotID, gotProvided)
}
if got := request.Header.Get(HeaderKey); got != "generated-id" {
t.Fatalf("request %s = %q, want generated-id", HeaderKey, got)
}
if got := response.Header().Get(HeaderKey); got != "generated-id" {
t.Fatalf("response %s = %q, want generated-id", HeaderKey, got)
}
})
}
}
+29
View File
@@ -0,0 +1,29 @@
package requestid
import uuid "github.com/satori/go.uuid"
const (
HeaderKey = "X-Request-ID"
ContextKey = "Ctx-Request-ID"
// ClientProvidedContextKey 标记请求ID是否由客户端显式提供;
// 服务端自动生成的ID仅用于链路追踪,不应自动开启业务幂等缓存。
ClientProvidedContextKey = "Ctx-Request-ID-Client-Provided"
)
type RequestIDGenerator func() string
type config struct {
generator RequestIDGenerator
}
type Option func(*config)
func WithGenerator(g RequestIDGenerator) Option {
return func(c *config) {
c.generator = g
}
}
var DefaultFenerator = func() string {
return uuid.NewV4().String()
}