Update Js for logs.html

This commit is contained in:
XOF
2025-11-24 20:47:12 +08:00
parent f2706d6fc8
commit e026d8f324
23 changed files with 1884 additions and 396 deletions

View File

@@ -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,

View File

@@ -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,
}
}

View File

@@ -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]

View File

@@ -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)

View File

@@ -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 {

View File

@@ -119,4 +119,4 @@ func extractBearerToken(c *gin.Context) string {
}
return strings.TrimSpace(authHeader[len(prefix):])
}
}

View File

@@ -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"`

View File

@@ -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 {

View File

@@ -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
}