mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
feat(api): add API key stats flush and summary endpoints
Introduce internal endpoint for flushing accumulated APIKey statistics from data plane to control plane database, updating both individual API keys and their parent provider groups with request counts and success/failure rates. Add admin endpoint to retrieve aggregated API key statistics summary across all provider groups, including total requests, success/failure counts, and calculated rates.
This commit is contained in:
@@ -29,6 +29,16 @@ type statsFlushEntry struct {
|
||||
LastAccessedAt int64 `json:"last_accessed_at"`
|
||||
}
|
||||
|
||||
type apiKeyStatsFlushRequest struct {
|
||||
Keys []apiKeyStatsFlushEntry `json:"keys"`
|
||||
}
|
||||
|
||||
type apiKeyStatsFlushEntry struct {
|
||||
APIKeyID uint `json:"api_key_id"`
|
||||
Requests int64 `json:"requests"`
|
||||
SuccessRequests int64 `json:"success_requests"`
|
||||
}
|
||||
|
||||
// FlushStats godoc
|
||||
// @Summary Flush key stats
|
||||
// @Description Internal endpoint for flushing accumulated key usage stats from DP to CP database
|
||||
@@ -105,3 +115,139 @@ func (h *InternalHandler) FlushStats(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"updated": updated})
|
||||
}
|
||||
|
||||
// FlushAPIKeyStats godoc
|
||||
// @Summary Flush API key stats
|
||||
// @Description Internal endpoint for flushing accumulated APIKey stats from DP to CP database
|
||||
// @Tags internal
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body apiKeyStatsFlushRequest true "Stats to flush"
|
||||
// @Success 200 {object} gin.H
|
||||
// @Failure 400 {object} gin.H
|
||||
// @Failure 500 {object} gin.H
|
||||
// @Router /internal/apikey-stats/flush [post]
|
||||
func (h *InternalHandler) FlushAPIKeyStats(c *gin.Context) {
|
||||
if h == nil || h.db == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
|
||||
return
|
||||
}
|
||||
|
||||
var req apiKeyStatsFlushRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
if len(req.Keys) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"updated": 0, "groups_updated": 0})
|
||||
return
|
||||
}
|
||||
|
||||
type apiKeyDelta struct {
|
||||
requests int64
|
||||
success int64
|
||||
}
|
||||
|
||||
deltas := make(map[uint]apiKeyDelta, len(req.Keys))
|
||||
for _, entry := range req.Keys {
|
||||
if entry.APIKeyID == 0 {
|
||||
continue
|
||||
}
|
||||
if entry.Requests < 0 || entry.SuccessRequests < 0 || entry.SuccessRequests > entry.Requests {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid stats payload"})
|
||||
return
|
||||
}
|
||||
if entry.Requests == 0 && entry.SuccessRequests == 0 {
|
||||
continue
|
||||
}
|
||||
delta := deltas[entry.APIKeyID]
|
||||
delta.requests += entry.Requests
|
||||
delta.success += entry.SuccessRequests
|
||||
deltas[entry.APIKeyID] = delta
|
||||
}
|
||||
|
||||
if len(deltas) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"updated": 0, "groups_updated": 0})
|
||||
return
|
||||
}
|
||||
|
||||
ids := make([]uint, 0, len(deltas))
|
||||
for id := range deltas {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
var apiKeys []model.APIKey
|
||||
if err := h.db.Model(&model.APIKey{}).Select("id, group_id").Where("id IN ?", ids).Find(&apiKeys).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load api keys", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
groupByKey := make(map[uint]uint, len(apiKeys))
|
||||
for _, key := range apiKeys {
|
||||
groupByKey[key.ID] = key.GroupID
|
||||
}
|
||||
|
||||
statsUpdates := func(requests, success int64) map[string]any {
|
||||
return map[string]any{
|
||||
"total_requests": gorm.Expr("total_requests + ?", requests),
|
||||
"success_requests": gorm.Expr("success_requests + ?", success),
|
||||
"failure_requests": gorm.Expr("(total_requests + ?) - (success_requests + ?)", requests, success),
|
||||
"success_rate": gorm.Expr(
|
||||
"CASE WHEN (total_requests + ?) > 0 THEN (success_requests + ?) * 1.0 / (total_requests + ?) ELSE 0 END",
|
||||
requests, success, requests,
|
||||
),
|
||||
"failure_rate": gorm.Expr(
|
||||
"CASE WHEN (total_requests + ?) > 0 THEN ((total_requests + ?) - (success_requests + ?)) * 1.0 / (total_requests + ?) ELSE 0 END",
|
||||
requests, requests, success, requests,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
updated := 0
|
||||
groupsUpdated := 0
|
||||
groupDeltas := make(map[uint]apiKeyDelta)
|
||||
|
||||
err := h.db.Transaction(func(tx *gorm.DB) error {
|
||||
for id, delta := range deltas {
|
||||
groupID, ok := groupByKey[id]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if delta.requests == 0 && delta.success == 0 {
|
||||
continue
|
||||
}
|
||||
res := tx.Model(&model.APIKey{}).Where("id = ?", id).Updates(statsUpdates(delta.requests, delta.success))
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected > 0 {
|
||||
updated++
|
||||
}
|
||||
if groupID > 0 {
|
||||
groupDelta := groupDeltas[groupID]
|
||||
groupDelta.requests += delta.requests
|
||||
groupDelta.success += delta.success
|
||||
groupDeltas[groupID] = groupDelta
|
||||
}
|
||||
}
|
||||
|
||||
for groupID, delta := range groupDeltas {
|
||||
if delta.requests == 0 && delta.success == 0 {
|
||||
continue
|
||||
}
|
||||
res := tx.Model(&model.ProviderGroup{}).Where("id = ?", groupID).Updates(statsUpdates(delta.requests, delta.success))
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected > 0 {
|
||||
groupsUpdated++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to flush api key stats", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"updated": updated, "groups_updated": groupsUpdated})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user