@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user