feat(api): add internal alerts reporting endpoint with deduplication

Add ReportAlerts endpoint for Data Plane to report alerts to Control Plane
with fingerprint-based deduplication using a 5-minute cooldown period.

Changes:
- Add POST /internal/alerts/report endpoint with validation
- Add Fingerprint field to Alert model for deduplication
- Extend GetAPIKeyStatsSummary with optional time range filtering
  using since/until query parameters to query from log records
This commit is contained in:
zenfun
2025-12-31 14:18:09 +08:00
parent 71f7578c7b
commit bfba16bbd4
4 changed files with 208 additions and 1 deletions

View File

@@ -2,6 +2,8 @@ package api
import (
"net/http"
"strconv"
"time"
"github.com/ez-api/ez-api/internal/model"
"github.com/gin-gonic/gin"
@@ -21,6 +23,8 @@ type APIKeyStatsSummaryResponse struct {
// @Tags admin
// @Produce json
// @Security AdminAuth
// @Param since query int false "Start time (unix seconds)"
// @Param until query int false "End time (unix seconds)"
// @Success 200 {object} APIKeyStatsSummaryResponse
// @Failure 500 {object} gin.H
// @Router /admin/apikey-stats/summary [get]
@@ -30,6 +34,28 @@ func (h *AdminHandler) GetAPIKeyStatsSummary(c *gin.Context) {
return
}
// Parse optional time range parameters
var sinceTime, untilTime *time.Time
if sinceStr := c.Query("since"); sinceStr != "" {
if ts, err := strconv.ParseInt(sinceStr, 10, 64); err == nil {
t := time.Unix(ts, 0).UTC()
sinceTime = &t
}
}
if untilStr := c.Query("until"); untilStr != "" {
if ts, err := strconv.ParseInt(untilStr, 10, 64); err == nil {
t := time.Unix(ts, 0).UTC()
untilTime = &t
}
}
// If time range specified, query from LogRecord table
if sinceTime != nil || untilTime != nil {
h.getAPIKeyStatsFromLogs(c, sinceTime, untilTime)
return
}
// Default: use aggregated stats from APIKey table (all-time)
var totals struct {
TotalRequests int64 `json:"total_requests"`
SuccessRequests int64 `json:"success_requests"`
@@ -65,3 +91,53 @@ func (h *AdminHandler) GetAPIKeyStatsSummary(c *gin.Context) {
FailureRate: failureRate,
})
}
// getAPIKeyStatsFromLogs calculates stats from log records for a specific time range
func (h *AdminHandler) getAPIKeyStatsFromLogs(c *gin.Context, sinceTime, untilTime *time.Time) {
q := h.logBaseQuery()
if sinceTime != nil {
q = q.Where("created_at >= ?", *sinceTime)
}
if untilTime != nil {
q = q.Where("created_at <= ?", *untilTime)
}
// Only count requests that went through a provider (provider_id > 0)
q = q.Where("provider_id > 0")
var result struct {
TotalRequests int64
SuccessRequests int64
}
if err := q.Select(`
COUNT(*) as total_requests,
SUM(CASE WHEN status_code >= 200 AND status_code < 400 THEN 1 ELSE 0 END) as success_requests
`).Scan(&result).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to aggregate api key stats from logs", "details": err.Error()})
return
}
total := result.TotalRequests
success := result.SuccessRequests
failure := total - success
if failure < 0 {
failure = 0
}
var successRate float64
var failureRate float64
if total > 0 {
successRate = float64(success) / float64(total)
failureRate = float64(failure) / float64(total)
}
c.JSON(http.StatusOK, APIKeyStatsSummaryResponse{
TotalRequests: total,
SuccessRequests: success,
FailureRequests: failure,
SuccessRate: successRate,
FailureRate: failureRate,
})
}