Update Js for logs.html
This commit is contained in:
@@ -114,7 +114,7 @@ func (ch *GeminiChannel) ValidateKey(
|
||||
}
|
||||
|
||||
errorBody, _ := io.ReadAll(resp.Body)
|
||||
parsedMessage := CustomErrors.ParseUpstreamError(errorBody)
|
||||
parsedMessage, _ := CustomErrors.ParseUpstreamError(errorBody)
|
||||
|
||||
return &CustomErrors.APIError{
|
||||
HTTPStatus: resp.StatusCode,
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
type APIError struct {
|
||||
HTTPStatus int
|
||||
Code string
|
||||
Status string `json:"status,omitempty"`
|
||||
Message string
|
||||
}
|
||||
|
||||
@@ -61,11 +62,13 @@ func NewAPIError(base *APIError, message string) *APIError {
|
||||
}
|
||||
|
||||
// NewAPIErrorWithUpstream creates a new APIError specifically for wrapping raw upstream errors.
|
||||
func NewAPIErrorWithUpstream(statusCode int, code string, upstreamMessage string) *APIError {
|
||||
func NewAPIErrorWithUpstream(statusCode int, code string, bodyBytes []byte) *APIError {
|
||||
msg, status := ParseUpstreamError(bodyBytes)
|
||||
return &APIError{
|
||||
HTTPStatus: statusCode,
|
||||
Code: code,
|
||||
Message: upstreamMessage,
|
||||
Message: msg,
|
||||
Status: status,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package errors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -10,67 +9,37 @@ const (
|
||||
maxErrorBodyLength = 2048
|
||||
)
|
||||
|
||||
// standardErrorResponse matches formats like: {"error": {"message": "..."}}
|
||||
type standardErrorResponse struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
// vendorErrorResponse matches formats like: {"error_msg": "..."}
|
||||
type vendorErrorResponse struct {
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
}
|
||||
|
||||
// simpleErrorResponse matches formats like: {"error": "..."}
|
||||
type simpleErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// rootMessageErrorResponse matches formats like: {"message": "..."}
|
||||
type rootMessageErrorResponse struct {
|
||||
type upstreamErrorDetail struct {
|
||||
Message string `json:"message"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type upstreamErrorPayload struct {
|
||||
Error upstreamErrorDetail `json:"error"`
|
||||
}
|
||||
|
||||
// ParseUpstreamError attempts to parse a structured error message from an upstream response body
|
||||
func ParseUpstreamError(body []byte) string {
|
||||
// 1. Attempt to parse the standard OpenAI/Gemini format.
|
||||
var stdErr standardErrorResponse
|
||||
if err := json.Unmarshal(body, &stdErr); err == nil {
|
||||
if msg := strings.TrimSpace(stdErr.Error.Message); msg != "" {
|
||||
return truncateString(msg, maxErrorBodyLength)
|
||||
}
|
||||
func ParseUpstreamError(body []byte) (message string, status string) {
|
||||
if len(body) == 0 {
|
||||
return "Upstream returned an empty error body", ""
|
||||
}
|
||||
|
||||
// 2. Attempt to parse vendor-specific format (e.g., Baidu).
|
||||
var vendorErr vendorErrorResponse
|
||||
if err := json.Unmarshal(body, &vendorErr); err == nil {
|
||||
if msg := strings.TrimSpace(vendorErr.ErrorMsg); msg != "" {
|
||||
return truncateString(msg, maxErrorBodyLength)
|
||||
}
|
||||
// 优先级 1: 尝试解析 OpenAI 兼容接口返回的 `[{"error": {...}}]` 数组格式
|
||||
var arrayPayload []upstreamErrorPayload
|
||||
if err := json.Unmarshal(body, &arrayPayload); err == nil && len(arrayPayload) > 0 {
|
||||
detail := arrayPayload[0].Error
|
||||
return truncateString(detail.Message, maxErrorBodyLength), detail.Status
|
||||
}
|
||||
|
||||
// 3. Attempt to parse simple error format.
|
||||
var simpleErr simpleErrorResponse
|
||||
if err := json.Unmarshal(body, &simpleErr); err == nil {
|
||||
if msg := strings.TrimSpace(simpleErr.Error); msg != "" {
|
||||
return truncateString(msg, maxErrorBodyLength)
|
||||
}
|
||||
// 优先级 2: 尝试解析 Gemini 原生接口可能返回的 `{"error": {...}}` 单个对象格式
|
||||
var singlePayload upstreamErrorPayload
|
||||
if err := json.Unmarshal(body, &singlePayload); err == nil && singlePayload.Error.Message != "" {
|
||||
detail := singlePayload.Error
|
||||
return truncateString(detail.Message, maxErrorBodyLength), detail.Status
|
||||
}
|
||||
|
||||
// 4. Attempt to parse root-level message format.
|
||||
var rootMsgErr rootMessageErrorResponse
|
||||
if err := json.Unmarshal(body, &rootMsgErr); err == nil {
|
||||
if msg := strings.TrimSpace(rootMsgErr.Message); msg != "" {
|
||||
return truncateString(msg, maxErrorBodyLength)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Graceful Degradation: If all parsing fails, return the raw (but safe) body.
|
||||
return truncateString(string(body), maxErrorBodyLength)
|
||||
// 最终回退: 对于无法识别的 JSON 或纯文本错误
|
||||
return truncateString(string(body), maxErrorBodyLength), ""
|
||||
}
|
||||
|
||||
// truncateString ensures a string does not exceed a maximum length.
|
||||
// truncateString remains unchanged.
|
||||
func truncateString(s string, maxLength int) string {
|
||||
if len(s) > maxLength {
|
||||
return s[:maxLength]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Filename: internal/handlers/apikey_handler.go (最终决战版)
|
||||
// Filename: internal/handlers/apikey_handler.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
@@ -88,7 +88,6 @@ func (h *APIKeyHandler) AddMultipleKeysToGroup(c *gin.Context) {
|
||||
response.Error(c, errors.NewAPIError(errors.ErrInvalidJSON, err.Error()))
|
||||
return
|
||||
}
|
||||
// [修正] 将请求的 context 传递给 service 层
|
||||
taskStatus, err := h.keyImportService.StartAddKeysTask(c.Request.Context(), req.KeyGroupID, req.Keys, req.ValidateOnImport)
|
||||
if err != nil {
|
||||
response.Error(c, errors.NewAPIError(errors.ErrTaskInProgress, err.Error()))
|
||||
@@ -104,7 +103,6 @@ func (h *APIKeyHandler) UnlinkMultipleKeysFromGroup(c *gin.Context) {
|
||||
response.Error(c, errors.NewAPIError(errors.ErrInvalidJSON, err.Error()))
|
||||
return
|
||||
}
|
||||
// [修正] 将请求的 context 传递给 service 层
|
||||
taskStatus, err := h.keyImportService.StartUnlinkKeysTask(c.Request.Context(), req.KeyGroupID, req.Keys)
|
||||
if err != nil {
|
||||
response.Error(c, errors.NewAPIError(errors.ErrTaskInProgress, err.Error()))
|
||||
@@ -120,7 +118,6 @@ func (h *APIKeyHandler) HardDeleteMultipleKeys(c *gin.Context) {
|
||||
response.Error(c, errors.NewAPIError(errors.ErrInvalidJSON, err.Error()))
|
||||
return
|
||||
}
|
||||
// [修正] 将请求的 context 传递给 service 层
|
||||
taskStatus, err := h.keyImportService.StartHardDeleteKeysTask(c.Request.Context(), req.Keys)
|
||||
if err != nil {
|
||||
response.Error(c, errors.NewAPIError(errors.ErrTaskInProgress, err.Error()))
|
||||
@@ -136,7 +133,6 @@ func (h *APIKeyHandler) RestoreMultipleKeys(c *gin.Context) {
|
||||
response.Error(c, errors.NewAPIError(errors.ErrInvalidJSON, err.Error()))
|
||||
return
|
||||
}
|
||||
// [修正] 将请求的 context 传递给 service 层
|
||||
taskStatus, err := h.keyImportService.StartRestoreKeysTask(c.Request.Context(), req.Keys)
|
||||
if err != nil {
|
||||
response.Error(c, errors.NewAPIError(errors.ErrTaskInProgress, err.Error()))
|
||||
@@ -151,7 +147,6 @@ func (h *APIKeyHandler) TestMultipleKeys(c *gin.Context) {
|
||||
response.Error(c, errors.NewAPIError(errors.ErrInvalidJSON, err.Error()))
|
||||
return
|
||||
}
|
||||
// [修正] 将请求的 context 传递给 service 层
|
||||
taskStatus, err := h.keyValidationService.StartTestKeysTask(c.Request.Context(), req.KeyGroupID, req.Keys)
|
||||
if err != nil {
|
||||
response.Error(c, errors.NewAPIError(errors.ErrTaskInProgress, err.Error()))
|
||||
@@ -246,7 +241,6 @@ func (h *APIKeyHandler) TestKeysForGroup(c *gin.Context) {
|
||||
response.Error(c, errors.NewAPIError(errors.ErrInvalidJSON, err.Error()))
|
||||
return
|
||||
}
|
||||
// [修正] 将请求的 context 传递给 service 层
|
||||
taskStatus, err := h.keyValidationService.StartTestKeysTask(c.Request.Context(), uint(groupID), req.Keys)
|
||||
if err != nil {
|
||||
response.Error(c, errors.NewAPIError(errors.ErrTaskInProgress, err.Error()))
|
||||
@@ -366,7 +360,6 @@ func (h *APIKeyHandler) HandleBulkAction(c *gin.Context) {
|
||||
var apiErr *errors.APIError
|
||||
switch req.Action {
|
||||
case "revalidate":
|
||||
// [修正] 将请求的 context 传递给 service 层
|
||||
task, err = h.keyValidationService.StartTestKeysByFilterTask(c.Request.Context(), uint(groupID), req.Filter.Status)
|
||||
case "set_status":
|
||||
if req.NewStatus == "" {
|
||||
@@ -376,7 +369,6 @@ func (h *APIKeyHandler) HandleBulkAction(c *gin.Context) {
|
||||
targetStatus := models.APIKeyStatus(req.NewStatus)
|
||||
task, err = h.apiKeyService.StartUpdateStatusByFilterTask(c.Request.Context(), uint(groupID), req.Filter.Status, targetStatus)
|
||||
case "delete":
|
||||
// [修正] 将请求的 context 传递给 service 层
|
||||
task, err = h.keyImportService.StartUnlinkKeysByFilterTask(c.Request.Context(), uint(groupID), req.Filter.Status)
|
||||
default:
|
||||
apiErr = errors.NewAPIError(errors.ErrBadRequest, "Unsupported action: "+req.Action)
|
||||
|
||||
@@ -262,7 +262,7 @@ func (h *ProxyHandler) createModifyResponseFunc(attemptErr **errors.APIError, is
|
||||
|
||||
bodyBytes, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
*attemptErr = errors.NewAPIError(errors.ErrBadGateway, "Failed to read upstream response")
|
||||
*attemptErr = errors.NewAPIErrorWithUpstream(http.StatusBadGateway, "UPSTREAM_GATEWAY_ERROR", nil)
|
||||
resp.Body = io.NopCloser(bytes.NewReader([]byte{}))
|
||||
return nil
|
||||
}
|
||||
@@ -271,8 +271,7 @@ func (h *ProxyHandler) createModifyResponseFunc(attemptErr **errors.APIError, is
|
||||
*isSuccess = true
|
||||
*pTokens, *cTokens = extractUsage(bodyBytes)
|
||||
} else {
|
||||
parsedMsg := errors.ParseUpstreamError(bodyBytes)
|
||||
*attemptErr = errors.NewAPIErrorWithUpstream(resp.StatusCode, fmt.Sprintf("UPSTREAM_%d", resp.StatusCode), parsedMsg)
|
||||
*attemptErr = errors.NewAPIErrorWithUpstream(resp.StatusCode, fmt.Sprintf("UPSTREAM_%d", resp.StatusCode), bodyBytes)
|
||||
}
|
||||
resp.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
||||
return nil
|
||||
@@ -359,11 +358,15 @@ func (h *ProxyHandler) publishFinalLogEvent(c *gin.Context, startTime time.Time,
|
||||
if !isSuccess {
|
||||
errToLog := finalErr
|
||||
if errToLog == nil && rec != nil {
|
||||
errToLog = errors.NewAPIErrorWithUpstream(rec.Code, fmt.Sprintf("UPSTREAM_%d", rec.Code), "Request failed after all retries.")
|
||||
errToLog = errors.NewAPIErrorWithUpstream(rec.Code, fmt.Sprintf("UPSTREAM_%d", rec.Code), rec.Body.Bytes())
|
||||
}
|
||||
if errToLog != nil {
|
||||
if errToLog.Code == "" && errToLog.HTTPStatus >= 400 {
|
||||
errToLog.Code = fmt.Sprintf("UPSTREAM_%d", errToLog.HTTPStatus)
|
||||
}
|
||||
event.Error = errToLog
|
||||
event.RequestLog.ErrorCode, event.RequestLog.ErrorMessage = errToLog.Code, errToLog.Message
|
||||
event.RequestLog.Status = errToLog.Status
|
||||
}
|
||||
}
|
||||
eventData, err := json.Marshal(event)
|
||||
@@ -385,6 +388,7 @@ func (h *ProxyHandler) publishRetryLogEvent(c *gin.Context, startTime time.Time,
|
||||
if attemptErr != nil {
|
||||
retryEvent.Error = attemptErr
|
||||
retryEvent.RequestLog.ErrorCode, retryEvent.RequestLog.ErrorMessage = attemptErr.Code, attemptErr.Message
|
||||
retryEvent.RequestLog.Status = attemptErr.Status
|
||||
}
|
||||
eventData, err := json.Marshal(retryEvent)
|
||||
if err != nil {
|
||||
|
||||
@@ -119,4 +119,4 @@ func extractBearerToken(c *gin.Context) string {
|
||||
}
|
||||
|
||||
return strings.TrimSpace(authHeader[len(prefix):])
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,7 @@ type RequestLog struct {
|
||||
LatencyMs int
|
||||
IsSuccess bool
|
||||
StatusCode int
|
||||
Status string `gorm:"type:varchar(100);index"`
|
||||
ModelName string `gorm:"type:varchar(100);index"`
|
||||
GroupID *uint `gorm:"index"`
|
||||
KeyID *uint `gorm:"index"`
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
type AuthTokenRepository interface {
|
||||
GetAllTokensWithGroups() ([]*models.AuthToken, error)
|
||||
BatchUpdateTokens(updates []*models.TokenUpdateRequest) error
|
||||
GetTokenByHashedValue(tokenHash string) (*models.AuthToken, error) // <-- Add this line
|
||||
SeedAdminToken(encryptedToken, tokenHash string) error // <-- And this line for the seeder
|
||||
GetTokenByHashedValue(tokenHash string) (*models.AuthToken, error)
|
||||
SeedAdminToken(encryptedToken, tokenHash string) error
|
||||
}
|
||||
|
||||
type gormAuthTokenRepository struct {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"gemini-balancer/internal/models"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
@@ -28,13 +29,16 @@ func (s *LogService) Record(ctx context.Context, log *models.RequestLog) error {
|
||||
|
||||
// LogQueryParams 解耦 Gin,使用结构体传参
|
||||
type LogQueryParams struct {
|
||||
Page int
|
||||
PageSize int
|
||||
ModelName string
|
||||
IsSuccess *bool // 使用指针区分"未设置"和"false"
|
||||
StatusCode *int
|
||||
KeyID *uint64
|
||||
GroupID *uint64
|
||||
Page int
|
||||
PageSize int
|
||||
ModelName string
|
||||
IsSuccess *bool // 使用指针区分"未设置"和"false"
|
||||
StatusCode *int
|
||||
KeyIDs []string
|
||||
GroupIDs []string
|
||||
Q string
|
||||
ErrorCodes []string
|
||||
StatusCodes []string
|
||||
}
|
||||
|
||||
func (s *LogService) GetLogs(ctx context.Context, params LogQueryParams) ([]models.RequestLog, int64, error) {
|
||||
@@ -52,17 +56,12 @@ func (s *LogService) GetLogs(ctx context.Context, params LogQueryParams) ([]mode
|
||||
// 构建基础查询
|
||||
query := s.db.WithContext(ctx).Model(&models.RequestLog{})
|
||||
query = s.applyFilters(query, params)
|
||||
|
||||
// 计算总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to count logs: %w", err)
|
||||
}
|
||||
|
||||
if total == 0 {
|
||||
return []models.RequestLog{}, 0, nil
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (params.Page - 1) * params.PageSize
|
||||
if err := query.Order("request_time DESC").
|
||||
Limit(params.PageSize).
|
||||
@@ -70,7 +69,6 @@ func (s *LogService) GetLogs(ctx context.Context, params LogQueryParams) ([]mode
|
||||
Find(&logs).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to query logs: %w", err)
|
||||
}
|
||||
|
||||
return logs, total, nil
|
||||
}
|
||||
|
||||
@@ -84,11 +82,24 @@ func (s *LogService) applyFilters(query *gorm.DB, params LogQueryParams) *gorm.D
|
||||
if params.StatusCode != nil {
|
||||
query = query.Where("status_code = ?", *params.StatusCode)
|
||||
}
|
||||
if params.KeyID != nil {
|
||||
query = query.Where("key_id = ?", *params.KeyID)
|
||||
if len(params.KeyIDs) > 0 {
|
||||
query = query.Where("key_id IN (?)", params.KeyIDs)
|
||||
}
|
||||
if params.GroupID != nil {
|
||||
query = query.Where("group_id = ?", *params.GroupID)
|
||||
if len(params.GroupIDs) > 0 {
|
||||
query = query.Where("group_id IN (?)", params.GroupIDs)
|
||||
}
|
||||
if len(params.ErrorCodes) > 0 {
|
||||
query = query.Where("error_code IN (?)", params.ErrorCodes)
|
||||
}
|
||||
if len(params.StatusCodes) > 0 {
|
||||
query = query.Where("status_code IN (?)", params.StatusCodes)
|
||||
}
|
||||
if params.Q != "" {
|
||||
searchQuery := "%" + params.Q + "%"
|
||||
query = query.Where(
|
||||
"model_name LIKE ? OR error_code LIKE ? OR error_message LIKE ? OR CAST(status_code AS CHAR) LIKE ?",
|
||||
searchQuery, searchQuery, searchQuery, searchQuery,
|
||||
)
|
||||
}
|
||||
return query
|
||||
}
|
||||
@@ -132,21 +143,22 @@ func ParseLogQueryParams(queryParams map[string]string) (LogQueryParams, error)
|
||||
}
|
||||
}
|
||||
|
||||
if keyIDStr, ok := queryParams["key_id"]; ok {
|
||||
if keyID, err := strconv.ParseUint(keyIDStr, 10, 64); err == nil {
|
||||
params.KeyID = &keyID
|
||||
} else {
|
||||
return params, fmt.Errorf("invalid key_id parameter: %s", keyIDStr)
|
||||
}
|
||||
if keyIDsStr, ok := queryParams["key_ids"]; ok {
|
||||
params.KeyIDs = strings.Split(keyIDsStr, ",")
|
||||
}
|
||||
|
||||
if groupIDStr, ok := queryParams["group_id"]; ok {
|
||||
if groupID, err := strconv.ParseUint(groupIDStr, 10, 64); err == nil {
|
||||
params.GroupID = &groupID
|
||||
} else {
|
||||
return params, fmt.Errorf("invalid group_id parameter: %s", groupIDStr)
|
||||
}
|
||||
if groupIDsStr, ok := queryParams["group_ids"]; ok {
|
||||
params.GroupIDs = strings.Split(groupIDsStr, ",")
|
||||
}
|
||||
|
||||
if errorCodesStr, ok := queryParams["error_codes"]; ok {
|
||||
params.ErrorCodes = strings.Split(errorCodesStr, ",")
|
||||
}
|
||||
if statusCodesStr, ok := queryParams["status_codes"]; ok {
|
||||
params.StatusCodes = strings.Split(statusCodesStr, ",")
|
||||
}
|
||||
if q, ok := queryParams["q"]; ok {
|
||||
params.Q = q
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user