usage_log_repo_stats_integration_test.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. //go:build integration
  2. package repository
  3. import (
  4. "context"
  5. "strings"
  6. "testing"
  7. "time"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/stretchr/testify/require"
  11. )
  12. func TestUsageLog_UpstreamModelMismatchFilterAndPartialIndex(t *testing.T) {
  13. ctx := context.Background()
  14. tx := testEntTx(t)
  15. client := tx.Client()
  16. repo := newUsageLogRepositoryWithSQL(client, tx)
  17. user := mustCreateUser(t, client, &service.User{Email: "model-audit@test.com"})
  18. apiKey := mustCreateApiKey(t, client, &service.APIKey{UserID: user.ID, Key: "sk-model-audit", Name: "model-audit"})
  19. account := mustCreateAccount(t, client, &service.Account{Name: "model-audit-account"})
  20. now := time.Now().UTC()
  21. responseModel := "gpt-5.4"
  22. for _, mismatch := range []bool{true, false} {
  23. mismatchValue := mismatch
  24. _, err := repo.Create(ctx, &service.UsageLog{
  25. UserID: user.ID, APIKeyID: apiKey.ID, AccountID: account.ID,
  26. Model: "gpt-5.5", InputTokens: 1, OutputTokens: 1,
  27. UpstreamResponseModel: &responseModel, UpstreamModelMismatch: &mismatchValue,
  28. CreatedAt: now,
  29. })
  30. require.NoError(t, err)
  31. }
  32. start := now.Add(-time.Hour)
  33. end := now.Add(time.Hour)
  34. trueValue := true
  35. stats, err := repo.GetStatsWithFilters(ctx, usagestats.UsageLogFilters{
  36. UserID: user.ID, StartTime: &start, EndTime: &end, UpstreamModelMismatch: &trueValue,
  37. })
  38. require.NoError(t, err)
  39. require.Equal(t, int64(1), stats.TotalRequests)
  40. require.Equal(t, []usagestats.EndpointStat{{
  41. Endpoint: "unknown", Requests: 1, TotalTokens: 2,
  42. }}, stats.Endpoints)
  43. require.Equal(t, []usagestats.EndpointStat{{
  44. Endpoint: "unknown", Requests: 1, TotalTokens: 2,
  45. }}, stats.UpstreamEndpoints)
  46. require.Equal(t, []usagestats.EndpointStat{{
  47. Endpoint: "unknown -> unknown", Requests: 1, TotalTokens: 2,
  48. }}, stats.EndpointPaths)
  49. trend, err := repo.GetUsageTrendWithUsageFilters(ctx, start, end, "hour", usagestats.UsageLogFilters{
  50. UserID: user.ID, UpstreamModelMismatch: &trueValue,
  51. })
  52. require.NoError(t, err)
  53. require.Len(t, trend, 1)
  54. require.Equal(t, int64(1), trend[0].Requests)
  55. _, err = tx.ExecContext(ctx, "SET LOCAL enable_seqscan = off")
  56. require.NoError(t, err)
  57. assertPlanUsesIndex := func(query, indexName string, args ...any) {
  58. rows, queryErr := tx.QueryContext(ctx, query, args...)
  59. require.NoError(t, queryErr)
  60. var planLines []string
  61. for rows.Next() {
  62. var line string
  63. require.NoError(t, rows.Scan(&line))
  64. planLines = append(planLines, line)
  65. }
  66. require.NoError(t, rows.Err())
  67. require.NoError(t, rows.Close())
  68. require.Contains(t, strings.Join(planLines, "\n"), indexName)
  69. }
  70. assertPlanUsesIndex(`
  71. EXPLAIN (COSTS OFF)
  72. SELECT id
  73. FROM usage_logs
  74. WHERE upstream_model_mismatch IS TRUE
  75. ORDER BY created_at DESC, id DESC
  76. LIMIT 100
  77. `, usageLogsUpstreamModelMismatchIndex)
  78. assertPlanUsesIndex(`
  79. EXPLAIN (COSTS OFF)
  80. SELECT id
  81. FROM usage_logs
  82. WHERE COALESCE(NULLIF(TRIM(requested_model), ''), model) = $1
  83. AND created_at >= $2 AND created_at < $3
  84. ORDER BY created_at DESC, id DESC
  85. LIMIT 100
  86. `, usageLogsEffectiveRequestedModelIndex, "gpt-5.5", start, end)
  87. assertPlanUsesIndex(`
  88. EXPLAIN (COSTS OFF)
  89. SELECT id
  90. FROM usage_logs
  91. WHERE COALESCE(NULLIF(TRIM(upstream_model), ''), model) = $1
  92. AND created_at >= $2 AND created_at < $3
  93. ORDER BY created_at DESC, id DESC
  94. LIMIT 100
  95. `, usageLogsEffectiveUpstreamModelIndex, "gpt-5.5", start, end)
  96. }
  97. func TestUsageLog_GetStatsWithFilters_AggregatesAndEndpoints(t *testing.T) {
  98. ctx := context.Background()
  99. tx := testEntTx(t)
  100. client := tx.Client()
  101. repo := newUsageLogRepositoryWithSQL(client, tx)
  102. user := mustCreateUser(t, client, &service.User{Email: "stats@test.com"})
  103. apiKey := mustCreateApiKey(t, client, &service.APIKey{UserID: user.ID, Key: "sk-stats-1", Name: "k"})
  104. account := mustCreateAccount(t, client, &service.Account{Name: "acc-stats"})
  105. now := time.Now().UTC()
  106. inboundEndpoint := "/v1/messages"
  107. upstreamEndpoint := "/v1/responses"
  108. for i := 0; i < 3; i++ {
  109. _, err := repo.Create(ctx, &service.UsageLog{
  110. UserID: user.ID, APIKeyID: apiKey.ID, AccountID: account.ID,
  111. Model: "claude-3", InputTokens: 2, OutputTokens: 3,
  112. CacheCreationTokens: 4, CacheReadTokens: 5,
  113. TotalCost: 0.5, ActualCost: 0.4, CreatedAt: now,
  114. InboundEndpoint: &inboundEndpoint, UpstreamEndpoint: &upstreamEndpoint,
  115. })
  116. require.NoError(t, err)
  117. }
  118. start := now.Add(-1 * time.Hour)
  119. end := now.Add(1 * time.Hour)
  120. // 按本测试创建的 user 维度过滤:集成库为共享实例,其它用 testEntClient 的兄弟测试会留下
  121. // 已提交的 usage_log 行(含零 token 的失败请求),不限定 user 会把它们计入 TotalRequests。
  122. stats, err := repo.GetStatsWithFilters(ctx, usagestats.UsageLogFilters{UserID: user.ID, StartTime: &start, EndTime: &end})
  123. require.NoError(t, err)
  124. require.Equal(t, int64(3), stats.TotalRequests)
  125. require.Equal(t, int64(6), stats.TotalInputTokens)
  126. require.Equal(t, int64(9), stats.TotalOutputTokens)
  127. require.Equal(t, int64(27), stats.TotalCacheTokens)
  128. require.Equal(t, int64(12), stats.TotalCacheCreationTokens)
  129. require.Equal(t, int64(15), stats.TotalCacheReadTokens)
  130. require.InDelta(t, 1.2, stats.TotalActualCost, 1e-9)
  131. require.NotEmpty(t, stats.Endpoints)
  132. require.NotEmpty(t, stats.UpstreamEndpoints)
  133. require.NotEmpty(t, stats.EndpointPaths)
  134. }