Fix loglist
This commit is contained in:
@@ -7,7 +7,9 @@ import (
|
||||
"gemini-balancer/internal/response"
|
||||
"gemini-balancer/internal/service"
|
||||
"gemini-balancer/internal/task"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -160,6 +162,29 @@ func (h *APIKeyHandler) ListAPIKeys(c *gin.Context) {
|
||||
response.Error(c, errors.NewAPIError(errors.ErrBadRequest, err.Error()))
|
||||
return
|
||||
}
|
||||
if params.IDs != "" {
|
||||
idStrs := strings.Split(params.IDs, ",")
|
||||
ids := make([]uint, 0, len(idStrs))
|
||||
for _, s := range idStrs {
|
||||
id, err := strconv.ParseUint(s, 10, 64)
|
||||
if err == nil {
|
||||
ids = append(ids, uint(id))
|
||||
}
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
keys, err := h.apiKeyService.GetKeysByIds(ids)
|
||||
if err != nil {
|
||||
response.Error(c, &errors.APIError{
|
||||
HTTPStatus: http.StatusInternalServerError,
|
||||
Code: "DATA_FETCH_ERROR",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
response.Success(c, keys)
|
||||
return
|
||||
}
|
||||
}
|
||||
if params.Page <= 0 {
|
||||
params.Page = 1
|
||||
}
|
||||
|
||||
@@ -3,14 +3,13 @@ package handlers
|
||||
|
||||
import (
|
||||
"gemini-balancer/internal/errors"
|
||||
"gemini-balancer/internal/models"
|
||||
"gemini-balancer/internal/response"
|
||||
"gemini-balancer/internal/service"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// LogHandler 负责处理与日志相关的HTTP请求
|
||||
type LogHandler struct {
|
||||
logService *service.LogService
|
||||
}
|
||||
@@ -20,14 +19,22 @@ func NewLogHandler(logService *service.LogService) *LogHandler {
|
||||
}
|
||||
|
||||
func (h *LogHandler) GetLogs(c *gin.Context) {
|
||||
// 直接将Gin的上下文传递给Service层,让Service自己去解析查询参数
|
||||
logs, err := h.logService.GetLogs(c)
|
||||
// 调用新的服务函数,接收日志列表和总数
|
||||
logs, total, err := h.logService.GetLogs(c)
|
||||
if err != nil {
|
||||
response.Error(c, errors.ErrDatabase)
|
||||
return
|
||||
}
|
||||
if logs == nil {
|
||||
logs = []models.RequestLog{}
|
||||
}
|
||||
response.Success(c, logs)
|
||||
|
||||
// 解析分页参数用于响应体
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
// 使用标准的分页响应结构
|
||||
response.Success(c, gin.H{
|
||||
"items": logs,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -136,34 +136,48 @@ func (h *ProxyHandler) serveTransparentProxy(c *gin.Context, requestBody []byte,
|
||||
var finalPromptTokens, finalCompletionTokens int
|
||||
var actualRetries int = 0
|
||||
defer func() {
|
||||
// 如果一次尝试都未成功(例如,在第一次获取资源时就失败),则不记录日志
|
||||
if lastUsedResources == nil {
|
||||
h.logger.WithField("id", correlationID).Warn("No resources were used, skipping final log event.")
|
||||
return
|
||||
}
|
||||
finalEvent := h.createLogEvent(c, startTime, correlationID, modelName, lastUsedResources, models.LogTypeFinal, isPreciseRouting)
|
||||
finalEvent.LatencyMs = int(time.Since(startTime).Milliseconds())
|
||||
finalEvent.IsSuccess = isSuccess
|
||||
finalEvent.Retries = actualRetries
|
||||
|
||||
finalEvent.RequestLog.LatencyMs = int(time.Since(startTime).Milliseconds())
|
||||
finalEvent.RequestLog.IsSuccess = isSuccess
|
||||
finalEvent.RequestLog.Retries = actualRetries
|
||||
if isSuccess {
|
||||
finalEvent.PromptTokens = finalPromptTokens
|
||||
finalEvent.CompletionTokens = finalCompletionTokens
|
||||
finalEvent.RequestLog.PromptTokens = finalPromptTokens
|
||||
finalEvent.RequestLog.CompletionTokens = finalCompletionTokens
|
||||
}
|
||||
|
||||
// 确保即使在成功的情况下,如果recorder存在,也记录最终的状态码
|
||||
if finalRecorder != nil {
|
||||
finalEvent.StatusCode = finalRecorder.Code
|
||||
finalEvent.RequestLog.StatusCode = finalRecorder.Code
|
||||
}
|
||||
if !isSuccess {
|
||||
// 将 finalProxyErr 的信息填充到 RequestLog 中
|
||||
if finalProxyErr != nil {
|
||||
finalEvent.Error = finalProxyErr
|
||||
finalEvent.ErrorCode = finalProxyErr.Code
|
||||
finalEvent.ErrorMessage = finalProxyErr.Message
|
||||
finalEvent.Error = finalProxyErr // Error 字段用于事件传递,不会被序列化到数据库
|
||||
finalEvent.RequestLog.ErrorCode = finalProxyErr.Code
|
||||
finalEvent.RequestLog.ErrorMessage = finalProxyErr.Message
|
||||
} else if finalRecorder != nil {
|
||||
apiErr := errors.NewAPIErrorWithUpstream(finalRecorder.Code, "PROXY_ERROR", "Request failed after all retries.")
|
||||
// 降级处理:如果 finalProxyErr 为空但 recorder 存在且失败
|
||||
apiErr := errors.NewAPIErrorWithUpstream(finalRecorder.Code, fmt.Sprintf("UPSTREAM_%d", finalRecorder.Code), "Request failed after all retries.")
|
||||
finalEvent.Error = apiErr
|
||||
finalEvent.ErrorCode = apiErr.Code
|
||||
finalEvent.ErrorMessage = apiErr.Message
|
||||
finalEvent.RequestLog.ErrorCode = apiErr.Code
|
||||
finalEvent.RequestLog.ErrorMessage = apiErr.Message
|
||||
}
|
||||
}
|
||||
eventData, _ := json.Marshal(finalEvent)
|
||||
_ = h.store.Publish(models.TopicRequestFinished, eventData)
|
||||
// 将完整的事件发布
|
||||
eventData, err := json.Marshal(finalEvent)
|
||||
if err != nil {
|
||||
h.logger.WithField("id", correlationID).WithError(err).Error("Failed to marshal final log event.")
|
||||
return
|
||||
}
|
||||
if err := h.store.Publish(models.TopicRequestFinished, eventData); err != nil {
|
||||
h.logger.WithField("id", correlationID).WithError(err).Error("Failed to publish final log event.")
|
||||
}
|
||||
}()
|
||||
var maxRetries int
|
||||
if isPreciseRouting {
|
||||
@@ -417,18 +431,24 @@ func (h *ProxyHandler) createLogEvent(c *gin.Context, startTime time.Time, corrI
|
||||
}
|
||||
if authTokenValue, exists := c.Get("authToken"); exists {
|
||||
if authToken, ok := authTokenValue.(*models.AuthToken); ok {
|
||||
event.AuthTokenID = &authToken.ID
|
||||
event.RequestLog.AuthTokenID = &authToken.ID
|
||||
}
|
||||
}
|
||||
if res != nil {
|
||||
event.KeyID = res.APIKey.ID
|
||||
event.GroupID = res.KeyGroup.ID
|
||||
// [核心修正] 填充到内嵌的 RequestLog 结构体中
|
||||
if res.APIKey != nil {
|
||||
event.RequestLog.KeyID = &res.APIKey.ID
|
||||
}
|
||||
if res.KeyGroup != nil {
|
||||
event.RequestLog.GroupID = &res.KeyGroup.ID
|
||||
}
|
||||
if res.UpstreamEndpoint != nil {
|
||||
event.UpstreamID = &res.UpstreamEndpoint.ID
|
||||
event.RequestLog.UpstreamID = &res.UpstreamEndpoint.ID
|
||||
// UpstreamURL 是事件传递字段,不是数据库字段,所以在这里赋值是正确的
|
||||
event.UpstreamURL = &res.UpstreamEndpoint.URL
|
||||
}
|
||||
if res.ProxyConfig != nil {
|
||||
event.ProxyID = &res.ProxyConfig.ID
|
||||
event.RequestLog.ProxyID = &res.ProxyConfig.ID
|
||||
}
|
||||
}
|
||||
return event
|
||||
|
||||
@@ -57,6 +57,7 @@ type APIKeyQueryParams struct {
|
||||
PageSize int `form:"limit"`
|
||||
Status string `form:"status"`
|
||||
Keyword string `form:"keyword"`
|
||||
IDs string `form:"ids"`
|
||||
}
|
||||
|
||||
// APIKeyDetails is a DTO that combines APIKey info with its contextual status from the mapping.
|
||||
|
||||
@@ -17,15 +17,10 @@ const (
|
||||
|
||||
type RequestFinishedEvent struct {
|
||||
RequestLog
|
||||
KeyID uint
|
||||
GroupID uint
|
||||
IsSuccess bool
|
||||
StatusCode int
|
||||
Error *errors.APIError
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
UpstreamID *uint `json:"upstream_id"`
|
||||
UpstreamURL *string `json:"upstream_url,omitempty"`
|
||||
IsPreciseRouting bool `json:"is_precise_routing"`
|
||||
Error *errors.APIError `json:"error,omitempty"` // Error 结构体不存入数据库,仅供事件传递
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
UpstreamURL *string `json:"upstream_url,omitempty"`
|
||||
IsPreciseRouting bool `json:"is_precise_routing"`
|
||||
}
|
||||
|
||||
type KeyStatusChangedEvent struct {
|
||||
|
||||
@@ -14,6 +14,7 @@ type MasterAPIKeyStatus string
|
||||
type PollingStrategy string
|
||||
type FileProcessingState string
|
||||
type LogType string
|
||||
type ProtocolType string
|
||||
|
||||
const (
|
||||
// --- 运营状态 (在中间表中使用) ---
|
||||
@@ -35,8 +36,12 @@ const (
|
||||
FileActive FileProcessingState = "ACTIVE"
|
||||
FileFailed FileProcessingState = "FAILED"
|
||||
|
||||
LogTypeFinal LogType = "FINAL" // Represents the final outcome of a request, including all retries.
|
||||
LogTypeRetry LogType = "RETRY" // Represents a single, failed attempt that triggered a retry.
|
||||
LogTypeFinal LogType = "FINAL" // Represents the final outcome of a request, including all retries.
|
||||
LogTypeRetry LogType = "RETRY" // Represents a single, failed attempt that triggered a retry.
|
||||
LogTypeValidation LogType = "VALIDATION"
|
||||
|
||||
ProtocolOpenAI ProtocolType = "openai"
|
||||
ProtocolGemini ProtocolType = "gemini"
|
||||
)
|
||||
|
||||
// ========= 核心数据库模型 =========
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -81,13 +82,20 @@ func (r *gormKeyRepository) SelectOneActiveKey(group *models.KeyGroup) (*models.
|
||||
|
||||
// SelectOneActiveKeyFromBasePool 为智能聚合模式设计的全新轮询器。
|
||||
func (r *gormKeyRepository) SelectOneActiveKeyFromBasePool(pool *BasePool) (*models.APIKey, *models.KeyGroup, error) {
|
||||
protocol := "default"
|
||||
if pool.Protocol != "" {
|
||||
protocol = string(pool.Protocol)
|
||||
}
|
||||
// 生成唯一的池ID,确保不同请求组合的轮询状态相互隔离
|
||||
poolID := generatePoolID(pool.CandidateGroups)
|
||||
log := r.logger.WithField("pool_id", poolID)
|
||||
poolID := generatePoolID(pool.CandidateGroups, protocol)
|
||||
log := r.logger.WithField("pool_id", poolID).WithField("protocol", protocol)
|
||||
|
||||
if err := r.ensureBasePoolCacheExists(pool, poolID); err != nil {
|
||||
log.WithError(err).Error("Failed to ensure BasePool cache exists.")
|
||||
return nil, nil, err
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, nil, fmt.Errorf("unexpected error while ensuring base pool cache: %w", err)
|
||||
}
|
||||
|
||||
var keyIDStr string
|
||||
@@ -145,25 +153,40 @@ func (r *gormKeyRepository) SelectOneActiveKeyFromBasePool(pool *BasePool) (*mod
|
||||
|
||||
// ensureBasePoolCacheExists 动态创建 BasePool 的 Redis 结构
|
||||
func (r *gormKeyRepository) ensureBasePoolCacheExists(pool *BasePool, poolID string) error {
|
||||
// 使用 LIST 键作为存在性检查的标志
|
||||
listKey := fmt.Sprintf(BasePoolSequential, poolID)
|
||||
exists, err := r.store.Exists(listKey)
|
||||
if err != nil {
|
||||
r.logger.WithError(err).Errorf("Failed to check existence of basepool key: %s", listKey)
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
|
||||
val, err := r.store.LIndex(listKey, 0)
|
||||
if err == nil && val == EmptyPoolPlaceholder {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if val == EmptyPoolPlaceholder {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
lockKey := fmt.Sprintf("lock:basepool:%s", poolID)
|
||||
acquired, err := r.store.SetNX(lockKey, []byte("1"), 10*time.Second)
|
||||
if err != nil {
|
||||
r.logger.WithError(err).Errorf("Failed to acquire distributed lock for basepool build: %s", lockKey)
|
||||
return err
|
||||
}
|
||||
if !acquired {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return r.ensureBasePoolCacheExists(pool, poolID)
|
||||
}
|
||||
defer r.store.Del(lockKey)
|
||||
if exists, _ := r.store.Exists(listKey); exists {
|
||||
return nil
|
||||
}
|
||||
r.logger.Infof("BasePool cache for pool_id '%s' not found. Building now...", poolID)
|
||||
|
||||
var allActiveKeyIDs []string
|
||||
lruMembers := make(map[string]float64)
|
||||
|
||||
for _, group := range pool.CandidateGroups {
|
||||
activeKeySetKey := fmt.Sprintf(KeyGroup, group.ID)
|
||||
groupKeyIDs, err := r.store.SMembers(activeKeySetKey)
|
||||
@@ -171,17 +194,21 @@ func (r *gormKeyRepository) ensureBasePoolCacheExists(pool *BasePool, poolID str
|
||||
r.logger.WithError(err).Warnf("Failed to get active keys for group %d during BasePool build", group.ID)
|
||||
continue
|
||||
}
|
||||
allActiveKeyIDs = append(allActiveKeyIDs, groupKeyIDs...)
|
||||
|
||||
for _, keyIDStr := range groupKeyIDs {
|
||||
keyID, _ := strconv.ParseUint(keyIDStr, 10, 64)
|
||||
|
||||
_, mapping, err := r.getKeyDetailsFromCache(uint(keyID), group.ID)
|
||||
if err == nil && mapping != nil {
|
||||
var score float64
|
||||
if mapping.LastUsedAt != nil {
|
||||
score = float64(mapping.LastUsedAt.UnixMilli())
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) || strings.Contains(err.Error(), "failed to get") {
|
||||
r.logger.WithError(err).Warnf("Cache inconsistency detected for KeyID %s in GroupID %d. Skipping.", keyIDStr, group.ID)
|
||||
continue
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
lruMembers[keyIDStr] = score
|
||||
}
|
||||
allActiveKeyIDs = append(allActiveKeyIDs, keyIDStr)
|
||||
if mapping != nil && mapping.LastUsedAt != nil {
|
||||
lruMembers[keyIDStr] = float64(mapping.LastUsedAt.UnixMilli())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,23 +221,16 @@ func (r *gormKeyRepository) ensureBasePoolCacheExists(pool *BasePool, poolID str
|
||||
}
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
// 使用管道填充所有轮询结构
|
||||
pipe := r.store.Pipeline()
|
||||
// 1. 顺序
|
||||
pipe.LPush(fmt.Sprintf(BasePoolSequential, poolID), toInterfaceSlice(allActiveKeyIDs)...)
|
||||
// 2. 随机
|
||||
pipe.SAdd(fmt.Sprintf(BasePoolRandomMain, poolID), toInterfaceSlice(allActiveKeyIDs)...)
|
||||
|
||||
// 设置合理的过期时间,例如5分钟,以防止孤儿数据
|
||||
pipe.Expire(fmt.Sprintf(BasePoolSequential, poolID), CacheTTL)
|
||||
pipe.Expire(fmt.Sprintf(BasePoolRandomMain, poolID), CacheTTL)
|
||||
pipe.Expire(fmt.Sprintf(BasePoolRandomCooldown, poolID), CacheTTL)
|
||||
pipe.Expire(fmt.Sprintf(BasePoolLRU, poolID), CacheTTL)
|
||||
|
||||
if err := pipe.Exec(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(lruMembers) > 0 {
|
||||
r.store.ZAdd(fmt.Sprintf(BasePoolLRU, poolID), lruMembers)
|
||||
}
|
||||
@@ -226,7 +246,7 @@ func (r *gormKeyRepository) updateKeyUsageTimestampForPool(poolID string, keyID
|
||||
}
|
||||
|
||||
// generatePoolID 根据候选组ID列表生成一个稳定的、唯一的字符串ID
|
||||
func generatePoolID(groups []*models.KeyGroup) string {
|
||||
func generatePoolID(groups []*models.KeyGroup, protocol string) string {
|
||||
ids := make([]int, len(groups))
|
||||
for i, g := range groups {
|
||||
ids[i] = int(g.ID)
|
||||
@@ -234,7 +254,7 @@ func generatePoolID(groups []*models.KeyGroup) string {
|
||||
sort.Ints(ids)
|
||||
|
||||
h := sha1.New()
|
||||
io.WriteString(h, fmt.Sprintf("%v", ids))
|
||||
io.WriteString(h, fmt.Sprintf("protocol:%s;groups:%v", protocol, ids))
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
type BasePool struct {
|
||||
CandidateGroups []*models.KeyGroup
|
||||
PollingStrategy models.PollingStrategy
|
||||
Protocol models.ProtocolType
|
||||
}
|
||||
|
||||
type KeyRepository interface {
|
||||
|
||||
@@ -84,26 +84,24 @@ func (s *AnalyticsService) eventListener() {
|
||||
}
|
||||
|
||||
func (s *AnalyticsService) handleAnalyticsEvent(event *models.RequestFinishedEvent) {
|
||||
if event.GroupID == 0 {
|
||||
if event.RequestLog.GroupID == nil {
|
||||
return
|
||||
}
|
||||
key := fmt.Sprintf("analytics:hourly:%s", time.Now().UTC().Format("2006-01-02T15"))
|
||||
fieldPrefix := fmt.Sprintf("%d:%s", event.GroupID, event.ModelName)
|
||||
|
||||
fieldPrefix := fmt.Sprintf("%d:%s", *event.RequestLog.GroupID, event.RequestLog.ModelName)
|
||||
pipe := s.store.Pipeline()
|
||||
pipe.HIncrBy(key, fieldPrefix+":requests", 1)
|
||||
if event.IsSuccess {
|
||||
if event.RequestLog.IsSuccess {
|
||||
pipe.HIncrBy(key, fieldPrefix+":success", 1)
|
||||
}
|
||||
if event.PromptTokens > 0 {
|
||||
pipe.HIncrBy(key, fieldPrefix+":prompt", int64(event.PromptTokens))
|
||||
if event.RequestLog.PromptTokens > 0 {
|
||||
pipe.HIncrBy(key, fieldPrefix+":prompt", int64(event.RequestLog.PromptTokens))
|
||||
}
|
||||
if event.CompletionTokens > 0 {
|
||||
pipe.HIncrBy(key, fieldPrefix+":completion", int64(event.CompletionTokens))
|
||||
if event.RequestLog.CompletionTokens > 0 {
|
||||
pipe.HIncrBy(key, fieldPrefix+":completion", int64(event.RequestLog.CompletionTokens))
|
||||
}
|
||||
|
||||
if err := pipe.Exec(); err != nil {
|
||||
s.logger.Warnf("[%s] Failed to record analytics event to store for group %d: %v", event.CorrelationID, event.GroupID, err)
|
||||
s.logger.Warnf("[%s] Failed to record analytics event to store for group %d: %v", event.CorrelationID, *event.RequestLog.GroupID, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -174,49 +174,40 @@ func (s *APIKeyService) Stop() {
|
||||
}
|
||||
|
||||
func (s *APIKeyService) handleKeyUsageEvent(event *models.RequestFinishedEvent) {
|
||||
if event.KeyID == 0 || event.GroupID == 0 {
|
||||
if event.RequestLog.KeyID == nil || event.RequestLog.GroupID == nil {
|
||||
return
|
||||
}
|
||||
// Handle success case: key recovery and timestamp update.
|
||||
if event.IsSuccess {
|
||||
mapping, err := s.keyRepo.GetMapping(event.GroupID, event.KeyID)
|
||||
if event.RequestLog.IsSuccess {
|
||||
mapping, err := s.keyRepo.GetMapping(*event.RequestLog.GroupID, *event.RequestLog.KeyID)
|
||||
if err != nil {
|
||||
// Log if mapping is not found, but don't proceed.
|
||||
s.logger.Warnf("[%s] Could not find mapping for G:%d K:%d on successful request: %v", event.CorrelationID, event.GroupID, event.KeyID, err)
|
||||
s.logger.Warnf("[%s] Could not find mapping for G:%d K:%d on successful request: %v", event.CorrelationID, *event.RequestLog.GroupID, *event.RequestLog.KeyID, err)
|
||||
return
|
||||
}
|
||||
|
||||
needsUpdate := false
|
||||
statusChanged := false
|
||||
oldStatus := mapping.Status
|
||||
|
||||
// If status was not active, it's a recovery.
|
||||
if mapping.Status != models.StatusActive {
|
||||
mapping.Status = models.StatusActive
|
||||
mapping.ConsecutiveErrorCount = 0
|
||||
mapping.LastError = ""
|
||||
needsUpdate = true
|
||||
statusChanged = true
|
||||
}
|
||||
// Always update LastUsedAt timestamp.
|
||||
|
||||
now := time.Now()
|
||||
mapping.LastUsedAt = &now
|
||||
needsUpdate = true
|
||||
|
||||
if needsUpdate {
|
||||
if err := s.keyRepo.UpdateMapping(mapping); err != nil {
|
||||
s.logger.Errorf("[%s] Failed to update mapping for G:%d K:%d after successful request: %v", event.CorrelationID, event.GroupID, event.KeyID, err)
|
||||
} else if oldStatus != models.StatusActive {
|
||||
// Only publish event if status actually changed.
|
||||
go s.publishStatusChangeEvent(event.GroupID, event.KeyID, oldStatus, models.StatusActive, "key_recovered_after_use")
|
||||
}
|
||||
if err := s.keyRepo.UpdateMapping(mapping); err != nil {
|
||||
s.logger.Errorf("[%s] Failed to update mapping for G:%d K:%d after successful request: %v", event.CorrelationID, *event.RequestLog.GroupID, *event.RequestLog.KeyID, err)
|
||||
return
|
||||
}
|
||||
if statusChanged {
|
||||
go s.publishStatusChangeEvent(*event.RequestLog.GroupID, *event.RequestLog.KeyID, oldStatus, models.StatusActive, "key_recovered_after_use")
|
||||
}
|
||||
return
|
||||
}
|
||||
// Handle failure case: delegate to the centralized judgment function.
|
||||
if event.Error != nil {
|
||||
s.judgeKeyErrors(
|
||||
event.CorrelationID,
|
||||
event.GroupID,
|
||||
event.KeyID,
|
||||
*event.RequestLog.GroupID,
|
||||
*event.RequestLog.KeyID,
|
||||
event.Error,
|
||||
event.IsPreciseRouting,
|
||||
)
|
||||
@@ -354,6 +345,10 @@ func (s *APIKeyService) ListAPIKeys(params *models.APIKeyQueryParams) (*Paginate
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *APIKeyService) GetKeysByIds(ids []uint) ([]models.APIKey, error) {
|
||||
return s.keyRepo.GetKeysByIDs(ids)
|
||||
}
|
||||
|
||||
func (s *APIKeyService) UpdateAPIKey(key *models.APIKey) error {
|
||||
go func() {
|
||||
var oldKey models.APIKey
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Filename: internal/service/db_log_writer_service.go (全新文件)
|
||||
// Filename: internal/service/db_log_writer_service.go
|
||||
|
||||
package service
|
||||
|
||||
|
||||
@@ -164,12 +164,15 @@ func (s *KeyValidationService) runTestKeysTask(taskID string, resourceID string,
|
||||
|
||||
var currentResult models.KeyTestResult
|
||||
event := models.RequestFinishedEvent{
|
||||
GroupID: groupID,
|
||||
KeyID: apiKeyModel.ID,
|
||||
RequestLog: models.RequestLog{
|
||||
// GroupID 和 KeyID 在 RequestLog 模型中是指针,需要取地址
|
||||
GroupID: &groupID,
|
||||
KeyID: &apiKeyModel.ID,
|
||||
},
|
||||
}
|
||||
if validationErr == nil {
|
||||
currentResult = models.KeyTestResult{Key: apiKeyModel.APIKey, Status: "valid", Message: "Validation successful."}
|
||||
event.IsSuccess = true
|
||||
event.RequestLog.IsSuccess = true
|
||||
} else {
|
||||
var apiErr *CustomErrors.APIError
|
||||
if CustomErrors.As(validationErr, &apiErr) {
|
||||
@@ -179,7 +182,7 @@ func (s *KeyValidationService) runTestKeysTask(taskID string, resourceID string,
|
||||
currentResult = models.KeyTestResult{Key: apiKeyModel.APIKey, Status: "error", Message: "Validation check failed: " + validationErr.Error()}
|
||||
event.Error = &CustomErrors.APIError{Message: validationErr.Error()}
|
||||
}
|
||||
event.IsSuccess = false
|
||||
event.RequestLog.IsSuccess = false
|
||||
}
|
||||
eventData, _ := json.Marshal(event)
|
||||
if err := s.store.Publish(models.TopicRequestFinished, eventData); err != nil {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Filename: internal/service/log_service.go
|
||||
package service
|
||||
|
||||
import (
|
||||
@@ -16,28 +17,35 @@ func NewLogService(db *gorm.DB) *LogService {
|
||||
return &LogService{db: db}
|
||||
}
|
||||
|
||||
// Record 记录一条日志到数据库 (TODO 暂时保留简单实现,后续再重构为异步)
|
||||
func (s *LogService) Record(log *models.RequestLog) error {
|
||||
return s.db.Create(log).Error
|
||||
}
|
||||
|
||||
func (s *LogService) GetLogs(c *gin.Context) ([]models.RequestLog, error) {
|
||||
func (s *LogService) GetLogs(c *gin.Context) ([]models.RequestLog, int64, error) {
|
||||
var logs []models.RequestLog
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&models.RequestLog{}).Scopes(s.filtersScope(c)).Order("request_time desc")
|
||||
query := s.db.Model(&models.RequestLog{}).Scopes(s.filtersScope(c))
|
||||
|
||||
// 简单的分页 ( TODO 后续可以做得更复杂)
|
||||
// 先计算总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if total == 0 {
|
||||
return []models.RequestLog{}, 0, nil
|
||||
}
|
||||
|
||||
// 再执行分页查询
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
// 执行查询
|
||||
err := query.Limit(pageSize).Offset(offset).Find(&logs).Error
|
||||
err := query.Order("request_time desc").Limit(pageSize).Offset(offset).Find(&logs).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
return logs, total, nil
|
||||
}
|
||||
|
||||
func (s *LogService) filtersScope(c *gin.Context) func(db *gorm.DB) *gorm.DB {
|
||||
@@ -60,6 +68,11 @@ func (s *LogService) filtersScope(c *gin.Context) func(db *gorm.DB) *gorm.DB {
|
||||
db = db.Where("key_id = ?", keyID)
|
||||
}
|
||||
}
|
||||
if groupIDStr := c.Query("group_id"); groupIDStr != "" {
|
||||
if groupID, err := strconv.ParseUint(groupIDStr, 10, 64); err == nil {
|
||||
db = db.Where("group_id = ?", groupID)
|
||||
}
|
||||
}
|
||||
return db
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user