Merge pull request #486 from s-Joshua-s/feat/usage-filter-by-apikey
feat(gateway): filter /v1/usage stats by API Key instead of UserID
This commit is contained in:
@@ -616,10 +616,10 @@ func (h *GatewayHandler) Usage(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best-effort: 获取用量统计,失败不影响基础响应
|
// Best-effort: 获取用量统计(按当前 API Key 过滤),失败不影响基础响应
|
||||||
var usageData gin.H
|
var usageData gin.H
|
||||||
if h.usageService != nil {
|
if h.usageService != nil {
|
||||||
dashStats, err := h.usageService.GetUserDashboardStats(c.Request.Context(), subject.UserID)
|
dashStats, err := h.usageService.GetAPIKeyDashboardStats(c.Request.Context(), apiKey.ID)
|
||||||
if err == nil && dashStats != nil {
|
if err == nil && dashStats != nil {
|
||||||
usageData = gin.H{
|
usageData = gin.H{
|
||||||
"today": gin.H{
|
"today": gin.H{
|
||||||
|
|||||||
@@ -1125,6 +1125,107 @@ func (r *usageLogRepository) GetUserDashboardStats(ctx context.Context, userID i
|
|||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getPerformanceStatsByAPIKey 获取指定 API Key 的 RPM 和 TPM(近5分钟平均值)
|
||||||
|
func (r *usageLogRepository) getPerformanceStatsByAPIKey(ctx context.Context, apiKeyID int64) (rpm, tpm int64, err error) {
|
||||||
|
fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as request_count,
|
||||||
|
COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as token_count
|
||||||
|
FROM usage_logs
|
||||||
|
WHERE created_at >= $1 AND api_key_id = $2`
|
||||||
|
args := []any{fiveMinutesAgo, apiKeyID}
|
||||||
|
|
||||||
|
var requestCount int64
|
||||||
|
var tokenCount int64
|
||||||
|
if err := scanSingleRow(ctx, r.sql, query, args, &requestCount, &tokenCount); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return requestCount / 5, tokenCount / 5, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAPIKeyDashboardStats 获取指定 API Key 的仪表盘统计(按 api_key_id 过滤)
|
||||||
|
func (r *usageLogRepository) GetAPIKeyDashboardStats(ctx context.Context, apiKeyID int64) (*UserDashboardStats, error) {
|
||||||
|
stats := &UserDashboardStats{}
|
||||||
|
today := timezone.Today()
|
||||||
|
|
||||||
|
// API Key 维度不需要统计 key 数量,设为 1
|
||||||
|
stats.TotalAPIKeys = 1
|
||||||
|
stats.ActiveAPIKeys = 1
|
||||||
|
|
||||||
|
// 累计 Token 统计
|
||||||
|
totalStatsQuery := `
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_requests,
|
||||||
|
COALESCE(SUM(input_tokens), 0) as total_input_tokens,
|
||||||
|
COALESCE(SUM(output_tokens), 0) as total_output_tokens,
|
||||||
|
COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
|
||||||
|
COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens,
|
||||||
|
COALESCE(SUM(total_cost), 0) as total_cost,
|
||||||
|
COALESCE(SUM(actual_cost), 0) as total_actual_cost,
|
||||||
|
COALESCE(AVG(duration_ms), 0) as avg_duration_ms
|
||||||
|
FROM usage_logs
|
||||||
|
WHERE api_key_id = $1
|
||||||
|
`
|
||||||
|
if err := scanSingleRow(
|
||||||
|
ctx,
|
||||||
|
r.sql,
|
||||||
|
totalStatsQuery,
|
||||||
|
[]any{apiKeyID},
|
||||||
|
&stats.TotalRequests,
|
||||||
|
&stats.TotalInputTokens,
|
||||||
|
&stats.TotalOutputTokens,
|
||||||
|
&stats.TotalCacheCreationTokens,
|
||||||
|
&stats.TotalCacheReadTokens,
|
||||||
|
&stats.TotalCost,
|
||||||
|
&stats.TotalActualCost,
|
||||||
|
&stats.AverageDurationMs,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheCreationTokens + stats.TotalCacheReadTokens
|
||||||
|
|
||||||
|
// 今日 Token 统计
|
||||||
|
todayStatsQuery := `
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as today_requests,
|
||||||
|
COALESCE(SUM(input_tokens), 0) as today_input_tokens,
|
||||||
|
COALESCE(SUM(output_tokens), 0) as today_output_tokens,
|
||||||
|
COALESCE(SUM(cache_creation_tokens), 0) as today_cache_creation_tokens,
|
||||||
|
COALESCE(SUM(cache_read_tokens), 0) as today_cache_read_tokens,
|
||||||
|
COALESCE(SUM(total_cost), 0) as today_cost,
|
||||||
|
COALESCE(SUM(actual_cost), 0) as today_actual_cost
|
||||||
|
FROM usage_logs
|
||||||
|
WHERE api_key_id = $1 AND created_at >= $2
|
||||||
|
`
|
||||||
|
if err := scanSingleRow(
|
||||||
|
ctx,
|
||||||
|
r.sql,
|
||||||
|
todayStatsQuery,
|
||||||
|
[]any{apiKeyID, today},
|
||||||
|
&stats.TodayRequests,
|
||||||
|
&stats.TodayInputTokens,
|
||||||
|
&stats.TodayOutputTokens,
|
||||||
|
&stats.TodayCacheCreationTokens,
|
||||||
|
&stats.TodayCacheReadTokens,
|
||||||
|
&stats.TodayCost,
|
||||||
|
&stats.TodayActualCost,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stats.TodayTokens = stats.TodayInputTokens + stats.TodayOutputTokens + stats.TodayCacheCreationTokens + stats.TodayCacheReadTokens
|
||||||
|
|
||||||
|
// 性能指标:RPM 和 TPM(最近5分钟,按 API Key 过滤)
|
||||||
|
rpm, tpm, err := r.getPerformanceStatsByAPIKey(ctx, apiKeyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stats.Rpm = rpm
|
||||||
|
stats.Tpm = tpm
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserUsageTrendByUserID 获取指定用户的使用趋势
|
// GetUserUsageTrendByUserID 获取指定用户的使用趋势
|
||||||
func (r *usageLogRepository) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) (results []TrendDataPoint, err error) {
|
func (r *usageLogRepository) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) (results []TrendDataPoint, err error) {
|
||||||
dateFormat := "YYYY-MM-DD"
|
dateFormat := "YYYY-MM-DD"
|
||||||
|
|||||||
@@ -1610,6 +1610,10 @@ func (r *stubUsageLogRepo) GetUserDashboardStats(ctx context.Context, userID int
|
|||||||
return nil, errors.New("not implemented")
|
return nil, errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *stubUsageLogRepo) GetAPIKeyDashboardStats(ctx context.Context, apiKeyID int64) (*usagestats.UserDashboardStats, error) {
|
||||||
|
return nil, errors.New("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
func (r *stubUsageLogRepo) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]usagestats.TrendDataPoint, error) {
|
func (r *stubUsageLogRepo) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]usagestats.TrendDataPoint, error) {
|
||||||
return nil, errors.New("not implemented")
|
return nil, errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type UsageLogRepository interface {
|
|||||||
|
|
||||||
// User dashboard stats
|
// User dashboard stats
|
||||||
GetUserDashboardStats(ctx context.Context, userID int64) (*usagestats.UserDashboardStats, error)
|
GetUserDashboardStats(ctx context.Context, userID int64) (*usagestats.UserDashboardStats, error)
|
||||||
|
GetAPIKeyDashboardStats(ctx context.Context, apiKeyID int64) (*usagestats.UserDashboardStats, error)
|
||||||
GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]usagestats.TrendDataPoint, error)
|
GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]usagestats.TrendDataPoint, error)
|
||||||
GetUserModelStats(ctx context.Context, userID int64, startTime, endTime time.Time) ([]usagestats.ModelStat, error)
|
GetUserModelStats(ctx context.Context, userID int64, startTime, endTime time.Time) ([]usagestats.ModelStat, error)
|
||||||
|
|
||||||
|
|||||||
@@ -288,6 +288,15 @@ func (s *UsageService) GetUserDashboardStats(ctx context.Context, userID int64)
|
|||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAPIKeyDashboardStats returns dashboard summary stats filtered by API Key.
|
||||||
|
func (s *UsageService) GetAPIKeyDashboardStats(ctx context.Context, apiKeyID int64) (*usagestats.UserDashboardStats, error) {
|
||||||
|
stats, err := s.usageRepo.GetAPIKeyDashboardStats(ctx, apiKeyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get api key dashboard stats: %w", err)
|
||||||
|
}
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserUsageTrendByUserID returns per-user usage trend.
|
// GetUserUsageTrendByUserID returns per-user usage trend.
|
||||||
func (s *UsageService) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]usagestats.TrendDataPoint, error) {
|
func (s *UsageService) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]usagestats.TrendDataPoint, error) {
|
||||||
trend, err := s.usageRepo.GetUserUsageTrendByUserID(ctx, userID, startTime, endTime, granularity)
|
trend, err := s.usageRepo.GetUserUsageTrendByUserID(ctx, userID, startTime, endTime, granularity)
|
||||||
|
|||||||
Reference in New Issue
Block a user