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:
@@ -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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
|
internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
|
||||||
internalusage "github.com/router-for-me/CLIProxyAPI/v6/internal/usage"
|
internalusage "github.com/router-for-me/CLIProxyAPI/v6/internal/usage"
|
||||||
coreusage "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/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)
|
apiKey := strings.TrimSpace(record.APIKey)
|
||||||
requestID := strings.TrimSpace(internallogging.GetRequestID(ctx))
|
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{
|
tokens := internalusage.TokenStats{
|
||||||
InputTokens: record.Detail.InputTokens,
|
InputTokens: record.Detail.InputTokens,
|
||||||
@@ -106,40 +99,15 @@ type queuedUsageDetail struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func resolveSuccess(ctx context.Context) bool {
|
func resolveSuccess(ctx context.Context) bool {
|
||||||
if ctx == nil {
|
status := internallogging.GetResponseStatus(ctx)
|
||||||
return true
|
|
||||||
}
|
|
||||||
ginCtx, ok := ctx.Value("gin").(*gin.Context)
|
|
||||||
if !ok || ginCtx == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
status := ginCtx.Writer.Status()
|
|
||||||
if status == 0 {
|
if status == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return status < http.StatusBadRequest
|
return status < httpStatusBadRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveEndpoint(ctx context.Context) string {
|
func resolveEndpoint(ctx context.Context) string {
|
||||||
if ctx == nil {
|
return strings.TrimSpace(internallogging.GetEndpoint(ctx))
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const httpStatusBadRequest = 400
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ import (
|
|||||||
|
|
||||||
func TestUsageQueuePluginPayloadIncludesStableFieldsAndSuccess(t *testing.T) {
|
func TestUsageQueuePluginPayloadIncludesStableFieldsAndSuccess(t *testing.T) {
|
||||||
withEnabledQueue(t, func() {
|
withEnabledQueue(t, func() {
|
||||||
ginCtx := newTestGinContext(t, http.MethodPost, "/v1/chat/completions", http.StatusOK)
|
ctx := internallogging.WithRequestID(context.Background(), "ctx-request-id")
|
||||||
internallogging.SetGinRequestID(ginCtx, "gin-request-id-ignored")
|
ctx = internallogging.WithEndpoint(ctx, "POST /v1/chat/completions")
|
||||||
ctx := context.WithValue(internallogging.WithRequestID(context.Background(), "ctx-request-id"), "gin", ginCtx)
|
ctx = internallogging.WithResponseStatusHolder(ctx)
|
||||||
|
internallogging.SetResponseStatus(ctx, http.StatusOK)
|
||||||
|
|
||||||
plugin := &usageQueuePlugin{}
|
plugin := &usageQueuePlugin{}
|
||||||
plugin.HandleUsage(ctx, coreusage.Record{
|
plugin.HandleUsage(ctx, coreusage.Record{
|
||||||
@@ -49,9 +50,10 @@ func TestUsageQueuePluginPayloadIncludesStableFieldsAndSuccess(t *testing.T) {
|
|||||||
|
|
||||||
func TestUsageQueuePluginPayloadIncludesStableFieldsAndFailureAndGinRequestID(t *testing.T) {
|
func TestUsageQueuePluginPayloadIncludesStableFieldsAndFailureAndGinRequestID(t *testing.T) {
|
||||||
withEnabledQueue(t, func() {
|
withEnabledQueue(t, func() {
|
||||||
ginCtx := newTestGinContext(t, http.MethodGet, "/v1/responses", http.StatusInternalServerError)
|
ctx := internallogging.WithRequestID(context.Background(), "gin-request-id")
|
||||||
internallogging.SetGinRequestID(ginCtx, "gin-request-id")
|
ctx = internallogging.WithEndpoint(ctx, "GET /v1/responses")
|
||||||
ctx := context.WithValue(context.Background(), "gin", ginCtx)
|
ctx = internallogging.WithResponseStatusHolder(ctx)
|
||||||
|
internallogging.SetResponseStatus(ctx, http.StatusInternalServerError)
|
||||||
|
|
||||||
plugin := &usageQueuePlugin{}
|
plugin := &usageQueuePlugin{}
|
||||||
plugin.HandleUsage(ctx, coreusage.Record{
|
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()) {
|
func withEnabledQueue(t *testing.T, fn func()) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -127,6 +170,29 @@ func popSinglePayload(t *testing.T) map[string]json.RawMessage {
|
|||||||
return payload
|
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) {
|
func requireStringField(t *testing.T, payload map[string]json.RawMessage, key, want string) {
|
||||||
t.Helper()
|
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) {
|
func requireBoolField(t *testing.T, payload map[string]json.RawMessage, key string, want bool) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"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"
|
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 {
|
func resolveAPIIdentifier(ctx context.Context, record coreusage.Record) string {
|
||||||
if ctx != nil {
|
if ctx != nil {
|
||||||
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil {
|
if endpoint := strings.TrimSpace(internallogging.GetEndpoint(ctx)); endpoint != "" {
|
||||||
path := ginCtx.FullPath()
|
return endpoint
|
||||||
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 record.Provider != "" {
|
if record.Provider != "" {
|
||||||
@@ -425,14 +412,7 @@ func resolveAPIIdentifier(ctx context.Context, record coreusage.Record) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func resolveSuccess(ctx context.Context) bool {
|
func resolveSuccess(ctx context.Context) bool {
|
||||||
if ctx == nil {
|
status := internallogging.GetResponseStatus(ctx)
|
||||||
return true
|
|
||||||
}
|
|
||||||
ginCtx, ok := ctx.Value("gin").(*gin.Context)
|
|
||||||
if !ok || ginCtx == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
status := ginCtx.Writer.Status()
|
|
||||||
if status == 0 {
|
if status == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -375,11 +375,32 @@ func (h *BaseAPIHandler) GetContextWithCancel(handler interfaces.APIHandler, c *
|
|||||||
if requestCtx != nil && logging.GetRequestID(parentCtx) == "" {
|
if requestCtx != nil && logging.GetRequestID(parentCtx) == "" {
|
||||||
if requestID := logging.GetRequestID(requestCtx); requestID != "" {
|
if requestID := logging.GetRequestID(requestCtx); requestID != "" {
|
||||||
parentCtx = logging.WithRequestID(parentCtx, 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)
|
parentCtx = logging.WithRequestID(parentCtx, requestID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newCtx, cancel := context.WithCancel(parentCtx)
|
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
|
cancelCtx := newCtx
|
||||||
if requestCtx != nil && requestCtx != parentCtx {
|
if requestCtx != nil && requestCtx != parentCtx {
|
||||||
go func() {
|
go func() {
|
||||||
@@ -393,6 +414,9 @@ func (h *BaseAPIHandler) GetContextWithCancel(handler interfaces.APIHandler, c *
|
|||||||
newCtx = context.WithValue(newCtx, "gin", c)
|
newCtx = context.WithValue(newCtx, "gin", c)
|
||||||
newCtx = context.WithValue(newCtx, "handler", handler)
|
newCtx = context.WithValue(newCtx, "handler", handler)
|
||||||
return newCtx, func(params ...interface{}) {
|
return newCtx, func(params ...interface{}) {
|
||||||
|
if c != nil {
|
||||||
|
logging.SetResponseStatus(cancelCtx, c.Writer.Status())
|
||||||
|
}
|
||||||
if h.Cfg.RequestLog && len(params) == 1 {
|
if h.Cfg.RequestLog && len(params) == 1 {
|
||||||
if existing, exists := c.Get("API_RESPONSE"); exists {
|
if existing, exists := c.Get("API_RESPONSE"); exists {
|
||||||
if existingBytes, ok := existing.([]byte); ok && len(bytes.TrimSpace(existingBytes)) > 0 {
|
if existingBytes, ok := existing.([]byte); ok && len(bytes.TrimSpace(existingBytes)) > 0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user