mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-05-17 04:09:57 +08:00
refactor(logging): replace gin-specific context handling with generic context-based request metadata utilities
- Introduced reusable utilities in `requestmeta` to manage endpoint and response status in request contexts. - Refactored plugins and handlers to use context-based metadata, removing direct dependency on `gin`. - Updated tests to validate new context utilities and replaced `gin`-based context handling. Fixed: #3166
This commit is contained in:
62
internal/logging/requestmeta.go
Normal file
62
internal/logging/requestmeta.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type endpointKey struct{}
|
||||
type responseStatusKey struct{}
|
||||
|
||||
type responseStatusHolder struct {
|
||||
status atomic.Int32
|
||||
}
|
||||
|
||||
func WithEndpoint(ctx context.Context, endpoint string) context.Context {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
return context.WithValue(ctx, endpointKey{}, endpoint)
|
||||
}
|
||||
|
||||
func GetEndpoint(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
if endpoint, ok := ctx.Value(endpointKey{}).(string); ok {
|
||||
return endpoint
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func WithResponseStatusHolder(ctx context.Context) context.Context {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if holder, ok := ctx.Value(responseStatusKey{}).(*responseStatusHolder); ok && holder != nil {
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, responseStatusKey{}, &responseStatusHolder{})
|
||||
}
|
||||
|
||||
func SetResponseStatus(ctx context.Context, status int) {
|
||||
if ctx == nil || status <= 0 {
|
||||
return
|
||||
}
|
||||
holder, ok := ctx.Value(responseStatusKey{}).(*responseStatusHolder)
|
||||
if !ok || holder == nil {
|
||||
return
|
||||
}
|
||||
holder.status.Store(int32(status))
|
||||
}
|
||||
|
||||
func GetResponseStatus(ctx context.Context) int {
|
||||
if ctx == nil {
|
||||
return 0
|
||||
}
|
||||
holder, ok := ctx.Value(responseStatusKey{}).(*responseStatusHolder)
|
||||
if !ok || holder == nil {
|
||||
return 0
|
||||
}
|
||||
return int(holder.status.Load())
|
||||
}
|
||||
@@ -3,11 +3,9 @@ package redisqueue
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
|
||||
internalusage "github.com/router-for-me/CLIProxyAPI/v6/internal/usage"
|
||||
coreusage "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage"
|
||||
@@ -46,11 +44,6 @@ func (p *usageQueuePlugin) HandleUsage(ctx context.Context, record coreusage.Rec
|
||||
}
|
||||
apiKey := strings.TrimSpace(record.APIKey)
|
||||
requestID := strings.TrimSpace(internallogging.GetRequestID(ctx))
|
||||
if requestID == "" {
|
||||
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil {
|
||||
requestID = strings.TrimSpace(internallogging.GetGinRequestID(ginCtx))
|
||||
}
|
||||
}
|
||||
|
||||
tokens := internalusage.TokenStats{
|
||||
InputTokens: record.Detail.InputTokens,
|
||||
@@ -106,40 +99,15 @@ type queuedUsageDetail struct {
|
||||
}
|
||||
|
||||
func resolveSuccess(ctx context.Context) bool {
|
||||
if ctx == nil {
|
||||
return true
|
||||
}
|
||||
ginCtx, ok := ctx.Value("gin").(*gin.Context)
|
||||
if !ok || ginCtx == nil {
|
||||
return true
|
||||
}
|
||||
status := ginCtx.Writer.Status()
|
||||
status := internallogging.GetResponseStatus(ctx)
|
||||
if status == 0 {
|
||||
return true
|
||||
}
|
||||
return status < http.StatusBadRequest
|
||||
return status < httpStatusBadRequest
|
||||
}
|
||||
|
||||
func resolveEndpoint(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
ginCtx, ok := ctx.Value("gin").(*gin.Context)
|
||||
if !ok || ginCtx == nil || ginCtx.Request == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
path := strings.TrimSpace(ginCtx.FullPath())
|
||||
if path == "" && ginCtx.Request.URL != nil {
|
||||
path = strings.TrimSpace(ginCtx.Request.URL.Path)
|
||||
}
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
method := strings.TrimSpace(ginCtx.Request.Method)
|
||||
if method == "" {
|
||||
return path
|
||||
}
|
||||
return method + " " + path
|
||||
return strings.TrimSpace(internallogging.GetEndpoint(ctx))
|
||||
}
|
||||
|
||||
const httpStatusBadRequest = 400
|
||||
|
||||
@@ -16,9 +16,10 @@ import (
|
||||
|
||||
func TestUsageQueuePluginPayloadIncludesStableFieldsAndSuccess(t *testing.T) {
|
||||
withEnabledQueue(t, func() {
|
||||
ginCtx := newTestGinContext(t, http.MethodPost, "/v1/chat/completions", http.StatusOK)
|
||||
internallogging.SetGinRequestID(ginCtx, "gin-request-id-ignored")
|
||||
ctx := context.WithValue(internallogging.WithRequestID(context.Background(), "ctx-request-id"), "gin", ginCtx)
|
||||
ctx := internallogging.WithRequestID(context.Background(), "ctx-request-id")
|
||||
ctx = internallogging.WithEndpoint(ctx, "POST /v1/chat/completions")
|
||||
ctx = internallogging.WithResponseStatusHolder(ctx)
|
||||
internallogging.SetResponseStatus(ctx, http.StatusOK)
|
||||
|
||||
plugin := &usageQueuePlugin{}
|
||||
plugin.HandleUsage(ctx, coreusage.Record{
|
||||
@@ -49,9 +50,10 @@ func TestUsageQueuePluginPayloadIncludesStableFieldsAndSuccess(t *testing.T) {
|
||||
|
||||
func TestUsageQueuePluginPayloadIncludesStableFieldsAndFailureAndGinRequestID(t *testing.T) {
|
||||
withEnabledQueue(t, func() {
|
||||
ginCtx := newTestGinContext(t, http.MethodGet, "/v1/responses", http.StatusInternalServerError)
|
||||
internallogging.SetGinRequestID(ginCtx, "gin-request-id")
|
||||
ctx := context.WithValue(context.Background(), "gin", ginCtx)
|
||||
ctx := internallogging.WithRequestID(context.Background(), "gin-request-id")
|
||||
ctx = internallogging.WithEndpoint(ctx, "GET /v1/responses")
|
||||
ctx = internallogging.WithResponseStatusHolder(ctx)
|
||||
internallogging.SetResponseStatus(ctx, http.StatusInternalServerError)
|
||||
|
||||
plugin := &usageQueuePlugin{}
|
||||
plugin.HandleUsage(ctx, coreusage.Record{
|
||||
@@ -80,6 +82,47 @@ func TestUsageQueuePluginPayloadIncludesStableFieldsAndFailureAndGinRequestID(t
|
||||
})
|
||||
}
|
||||
|
||||
func TestUsageQueuePluginAsyncIgnoresRecycledGinContext(t *testing.T) {
|
||||
withEnabledQueue(t, func() {
|
||||
ginCtx := newTestGinContext(t, http.MethodPost, "/v1/chat/completions", http.StatusOK)
|
||||
ctx := context.WithValue(context.Background(), "gin", ginCtx)
|
||||
ctx = internallogging.WithRequestID(ctx, "ctx-request-id")
|
||||
ctx = internallogging.WithEndpoint(ctx, "POST /v1/chat/completions")
|
||||
ctx = internallogging.WithResponseStatusHolder(ctx)
|
||||
internallogging.SetResponseStatus(ctx, http.StatusInternalServerError)
|
||||
|
||||
mgr := coreusage.NewManager(16)
|
||||
defer mgr.Stop()
|
||||
|
||||
mgr.Register(pluginFunc(func(_ context.Context, _ coreusage.Record) {
|
||||
ginCtx.Request = httptest.NewRequest(http.MethodGet, "http://example.com/v1/responses", nil)
|
||||
ginCtx.Status(http.StatusOK)
|
||||
}))
|
||||
mgr.Register(&usageQueuePlugin{})
|
||||
|
||||
mgr.Publish(ctx, coreusage.Record{
|
||||
Provider: "openai",
|
||||
Model: "gpt-5.4",
|
||||
APIKey: "test-key",
|
||||
AuthIndex: "0",
|
||||
AuthType: "apikey",
|
||||
Source: "user@example.com",
|
||||
RequestedAt: time.Date(2026, 4, 25, 0, 0, 0, 0, time.UTC),
|
||||
Latency: 1500 * time.Millisecond,
|
||||
Detail: coreusage.Detail{
|
||||
InputTokens: 10,
|
||||
OutputTokens: 20,
|
||||
TotalTokens: 30,
|
||||
},
|
||||
})
|
||||
|
||||
payload := waitForSinglePayload(t, 2*time.Second)
|
||||
requireStringField(t, payload, "endpoint", "POST /v1/chat/completions")
|
||||
requireStringField(t, payload, "request_id", "ctx-request-id")
|
||||
requireBoolField(t, payload, "failed", true)
|
||||
})
|
||||
}
|
||||
|
||||
func withEnabledQueue(t *testing.T, fn func()) {
|
||||
t.Helper()
|
||||
|
||||
@@ -127,6 +170,29 @@ func popSinglePayload(t *testing.T) map[string]json.RawMessage {
|
||||
return payload
|
||||
}
|
||||
|
||||
func waitForSinglePayload(t *testing.T, timeout time.Duration) map[string]json.RawMessage {
|
||||
t.Helper()
|
||||
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
items := PopOldest(10)
|
||||
if len(items) == 0 {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
if len(items) != 1 {
|
||||
t.Fatalf("PopOldest() items = %d, want 1", len(items))
|
||||
}
|
||||
var payload map[string]json.RawMessage
|
||||
if err := json.Unmarshal(items[0], &payload); err != nil {
|
||||
t.Fatalf("unmarshal payload: %v", err)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
t.Fatalf("timeout waiting for queued payload")
|
||||
return nil
|
||||
}
|
||||
|
||||
func requireStringField(t *testing.T, payload map[string]json.RawMessage, key, want string) {
|
||||
t.Helper()
|
||||
|
||||
@@ -143,6 +209,12 @@ func requireStringField(t *testing.T, payload map[string]json.RawMessage, key, w
|
||||
}
|
||||
}
|
||||
|
||||
type pluginFunc func(context.Context, coreusage.Record)
|
||||
|
||||
func (fn pluginFunc) HandleUsage(ctx context.Context, record coreusage.Record) {
|
||||
fn(ctx, record)
|
||||
}
|
||||
|
||||
func requireBoolField(t *testing.T, payload map[string]json.RawMessage, key string, want bool) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
|
||||
coreusage "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage"
|
||||
)
|
||||
|
||||
@@ -401,21 +401,8 @@ func dedupKey(apiName, modelName string, detail RequestDetail) string {
|
||||
|
||||
func resolveAPIIdentifier(ctx context.Context, record coreusage.Record) string {
|
||||
if ctx != nil {
|
||||
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil {
|
||||
path := ginCtx.FullPath()
|
||||
if path == "" && ginCtx.Request != nil {
|
||||
path = ginCtx.Request.URL.Path
|
||||
}
|
||||
method := ""
|
||||
if ginCtx.Request != nil {
|
||||
method = ginCtx.Request.Method
|
||||
}
|
||||
if path != "" {
|
||||
if method != "" {
|
||||
return method + " " + path
|
||||
}
|
||||
return path
|
||||
}
|
||||
if endpoint := strings.TrimSpace(internallogging.GetEndpoint(ctx)); endpoint != "" {
|
||||
return endpoint
|
||||
}
|
||||
}
|
||||
if record.Provider != "" {
|
||||
@@ -425,14 +412,7 @@ func resolveAPIIdentifier(ctx context.Context, record coreusage.Record) string {
|
||||
}
|
||||
|
||||
func resolveSuccess(ctx context.Context) bool {
|
||||
if ctx == nil {
|
||||
return true
|
||||
}
|
||||
ginCtx, ok := ctx.Value("gin").(*gin.Context)
|
||||
if !ok || ginCtx == nil {
|
||||
return true
|
||||
}
|
||||
status := ginCtx.Writer.Status()
|
||||
status := internallogging.GetResponseStatus(ctx)
|
||||
if status == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -375,11 +375,32 @@ func (h *BaseAPIHandler) GetContextWithCancel(handler interfaces.APIHandler, c *
|
||||
if requestCtx != nil && logging.GetRequestID(parentCtx) == "" {
|
||||
if requestID := logging.GetRequestID(requestCtx); requestID != "" {
|
||||
parentCtx = logging.WithRequestID(parentCtx, requestID)
|
||||
} else if requestID := logging.GetGinRequestID(c); requestID != "" {
|
||||
} else if requestID = logging.GetGinRequestID(c); requestID != "" {
|
||||
parentCtx = logging.WithRequestID(parentCtx, requestID)
|
||||
}
|
||||
}
|
||||
newCtx, cancel := context.WithCancel(parentCtx)
|
||||
|
||||
endpoint := ""
|
||||
if c != nil && c.Request != nil {
|
||||
path := strings.TrimSpace(c.FullPath())
|
||||
if path == "" && c.Request.URL != nil {
|
||||
path = strings.TrimSpace(c.Request.URL.Path)
|
||||
}
|
||||
if path != "" {
|
||||
method := strings.TrimSpace(c.Request.Method)
|
||||
if method != "" {
|
||||
endpoint = method + " " + path
|
||||
} else {
|
||||
endpoint = path
|
||||
}
|
||||
}
|
||||
}
|
||||
if endpoint != "" {
|
||||
newCtx = logging.WithEndpoint(newCtx, endpoint)
|
||||
}
|
||||
newCtx = logging.WithResponseStatusHolder(newCtx)
|
||||
|
||||
cancelCtx := newCtx
|
||||
if requestCtx != nil && requestCtx != parentCtx {
|
||||
go func() {
|
||||
@@ -393,6 +414,9 @@ func (h *BaseAPIHandler) GetContextWithCancel(handler interfaces.APIHandler, c *
|
||||
newCtx = context.WithValue(newCtx, "gin", c)
|
||||
newCtx = context.WithValue(newCtx, "handler", handler)
|
||||
return newCtx, func(params ...interface{}) {
|
||||
if c != nil {
|
||||
logging.SetResponseStatus(cancelCtx, c.Writer.Status())
|
||||
}
|
||||
if h.Cfg.RequestLog && len(params) == 1 {
|
||||
if existing, exists := c.Get("API_RESPONSE"); exists {
|
||||
if existingBytes, ok := existing.([]byte); ok && len(bytes.TrimSpace(existingBytes)) > 0 {
|
||||
|
||||
Reference in New Issue
Block a user