mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-09 09:34:46 +08:00
167 lines
3.7 KiB
Go
167 lines
3.7 KiB
Go
|
|
package handler
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"strings"
|
||
|
|
"sync"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
||
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||
|
|
"github.com/gin-gonic/gin"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
opsModelKey = "ops_model"
|
||
|
|
opsStreamKey = "ops_stream"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
opsErrorLogWorkerCount = 10
|
||
|
|
opsErrorLogQueueSize = 256
|
||
|
|
opsErrorLogTimeout = 2 * time.Second
|
||
|
|
)
|
||
|
|
|
||
|
|
type opsErrorLogJob struct {
|
||
|
|
ops *service.OpsService
|
||
|
|
entry *service.OpsErrorLog
|
||
|
|
}
|
||
|
|
|
||
|
|
var (
|
||
|
|
opsErrorLogOnce sync.Once
|
||
|
|
opsErrorLogQueue chan opsErrorLogJob
|
||
|
|
)
|
||
|
|
|
||
|
|
func startOpsErrorLogWorkers() {
|
||
|
|
opsErrorLogQueue = make(chan opsErrorLogJob, opsErrorLogQueueSize)
|
||
|
|
for i := 0; i < opsErrorLogWorkerCount; i++ {
|
||
|
|
go func() {
|
||
|
|
for job := range opsErrorLogQueue {
|
||
|
|
if job.ops == nil || job.entry == nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
ctx, cancel := context.WithTimeout(context.Background(), opsErrorLogTimeout)
|
||
|
|
_ = job.ops.RecordError(ctx, job.entry)
|
||
|
|
cancel()
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func enqueueOpsErrorLog(ops *service.OpsService, entry *service.OpsErrorLog) {
|
||
|
|
if ops == nil || entry == nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
opsErrorLogOnce.Do(startOpsErrorLogWorkers)
|
||
|
|
|
||
|
|
select {
|
||
|
|
case opsErrorLogQueue <- opsErrorLogJob{ops: ops, entry: entry}:
|
||
|
|
default:
|
||
|
|
// Queue is full; drop to avoid blocking request handling.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func setOpsRequestContext(c *gin.Context, model string, stream bool) {
|
||
|
|
c.Set(opsModelKey, model)
|
||
|
|
c.Set(opsStreamKey, stream)
|
||
|
|
}
|
||
|
|
|
||
|
|
func recordOpsError(c *gin.Context, ops *service.OpsService, status int, errType, message, fallbackPlatform string) {
|
||
|
|
if ops == nil || c == nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
model, _ := c.Get(opsModelKey)
|
||
|
|
stream, _ := c.Get(opsStreamKey)
|
||
|
|
|
||
|
|
var modelName string
|
||
|
|
if m, ok := model.(string); ok {
|
||
|
|
modelName = m
|
||
|
|
}
|
||
|
|
streaming, _ := stream.(bool)
|
||
|
|
|
||
|
|
apiKey, _ := middleware2.GetAPIKeyFromContext(c)
|
||
|
|
|
||
|
|
logEntry := &service.OpsErrorLog{
|
||
|
|
Phase: classifyOpsPhase(errType, message),
|
||
|
|
Type: errType,
|
||
|
|
Severity: classifyOpsSeverity(errType, status),
|
||
|
|
StatusCode: status,
|
||
|
|
Platform: resolveOpsPlatform(apiKey, fallbackPlatform),
|
||
|
|
Model: modelName,
|
||
|
|
RequestID: c.Writer.Header().Get("x-request-id"),
|
||
|
|
Message: message,
|
||
|
|
ClientIP: c.ClientIP(),
|
||
|
|
RequestPath: func() string {
|
||
|
|
if c.Request != nil && c.Request.URL != nil {
|
||
|
|
return c.Request.URL.Path
|
||
|
|
}
|
||
|
|
return ""
|
||
|
|
}(),
|
||
|
|
Stream: streaming,
|
||
|
|
}
|
||
|
|
|
||
|
|
if apiKey != nil {
|
||
|
|
logEntry.APIKeyID = &apiKey.ID
|
||
|
|
if apiKey.User != nil {
|
||
|
|
logEntry.UserID = &apiKey.User.ID
|
||
|
|
}
|
||
|
|
if apiKey.GroupID != nil {
|
||
|
|
logEntry.GroupID = apiKey.GroupID
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
enqueueOpsErrorLog(ops, logEntry)
|
||
|
|
}
|
||
|
|
|
||
|
|
func resolveOpsPlatform(apiKey *service.APIKey, fallback string) string {
|
||
|
|
if apiKey != nil && apiKey.Group != nil && apiKey.Group.Platform != "" {
|
||
|
|
return apiKey.Group.Platform
|
||
|
|
}
|
||
|
|
return fallback
|
||
|
|
}
|
||
|
|
|
||
|
|
func classifyOpsPhase(errType, message string) string {
|
||
|
|
msg := strings.ToLower(message)
|
||
|
|
switch errType {
|
||
|
|
case "authentication_error":
|
||
|
|
return "auth"
|
||
|
|
case "billing_error", "subscription_error":
|
||
|
|
return "billing"
|
||
|
|
case "rate_limit_error":
|
||
|
|
if strings.Contains(msg, "concurrency") || strings.Contains(msg, "pending") {
|
||
|
|
return "concurrency"
|
||
|
|
}
|
||
|
|
return "upstream"
|
||
|
|
case "invalid_request_error":
|
||
|
|
return "response"
|
||
|
|
case "upstream_error", "overloaded_error":
|
||
|
|
return "upstream"
|
||
|
|
case "api_error":
|
||
|
|
if strings.Contains(msg, "no available accounts") {
|
||
|
|
return "scheduling"
|
||
|
|
}
|
||
|
|
return "internal"
|
||
|
|
default:
|
||
|
|
return "internal"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func classifyOpsSeverity(errType string, status int) string {
|
||
|
|
switch errType {
|
||
|
|
case "invalid_request_error", "authentication_error", "billing_error", "subscription_error":
|
||
|
|
return "P3"
|
||
|
|
}
|
||
|
|
if status >= 500 {
|
||
|
|
return "P1"
|
||
|
|
}
|
||
|
|
if status == 429 {
|
||
|
|
return "P1"
|
||
|
|
}
|
||
|
|
if status >= 400 {
|
||
|
|
return "P2"
|
||
|
|
}
|
||
|
|
return "P3"
|
||
|
|
}
|