computeSourceTotals rolls up by_source / by_key / grand_total. userID/apiKeyID are optional filters. includeLegacy controls whether the legacy bucket is exposed (admin-only).
(db *gorm.DB, userID, apiKeyID string, since time.Time, includeLegacy bool)
| 296 | // userID/apiKeyID are optional filters. includeLegacy controls whether the |
| 297 | // legacy bucket is exposed (admin-only). |
| 298 | func computeSourceTotals(db *gorm.DB, userID, apiKeyID string, since time.Time, includeLegacy bool) SourceTotals { |
| 299 | totals := SourceTotals{BySource: map[string]TotalsEntry{}} |
| 300 | |
| 301 | bySourceQ := db.Model(&UsageRecord{}). |
| 302 | Select("source, SUM(total_tokens) as tokens, COUNT(*) as requests"). |
| 303 | Group("source") |
| 304 | bySourceQ = applyFilters(bySourceQ, userID, apiKeyID, since, includeLegacy) |
| 305 | |
| 306 | var bySourceRows []struct { |
| 307 | Source string |
| 308 | Tokens int64 |
| 309 | Requests int64 |
| 310 | } |
| 311 | if err := bySourceQ.Scan(&bySourceRows).Error; err != nil { |
| 312 | xlog.Warn("computeSourceTotals: by-source Scan failed", "error", err) |
| 313 | return totals |
| 314 | } |
| 315 | for _, r := range bySourceRows { |
| 316 | totals.BySource[r.Source] = TotalsEntry{Tokens: r.Tokens, Requests: r.Requests} |
| 317 | totals.GrandTotal.Tokens += r.Tokens |
| 318 | totals.GrandTotal.Requests += r.Requests |
| 319 | } |
| 320 | |
| 321 | byKeyQ := db.Model(&UsageRecord{}). |
| 322 | Select("COALESCE(api_key_id, '') as api_key_id, api_key_name, "+ |
| 323 | "user_id, user_name, "+ |
| 324 | "SUM(total_tokens) as tokens, COUNT(*) as requests, MAX(created_at) as last_used"). |
| 325 | Where("api_key_id IS NOT NULL AND api_key_id <> ''"). |
| 326 | Group("api_key_id, api_key_name, user_id, user_name"). |
| 327 | Order("tokens DESC"). |
| 328 | Limit(maxKeyTotals) |
| 329 | byKeyQ = applyFilters(byKeyQ, userID, apiKeyID, since, includeLegacy) |
| 330 | |
| 331 | // Iterate Rows() manually because MAX(created_at) is returned as a string by |
| 332 | // the SQLite driver, and Go's database/sql refuses to scan that into |
| 333 | // *time.Time. Postgres returns a proper timestamp. We accept both shapes |
| 334 | // via a Rows.Scan into a string column, then parse uniformly. |
| 335 | rows, err := byKeyQ.Rows() |
| 336 | if err != nil { |
| 337 | xlog.Warn("computeSourceTotals: by-key Rows() failed", "error", err) |
| 338 | } else { |
| 339 | defer func() { _ = rows.Close() }() |
| 340 | out := make([]KeyTotal, 0) |
| 341 | for rows.Next() { |
| 342 | var ( |
| 343 | apiKeyID, apiKeyName, userIDCol, userName, lastUsedRaw string |
| 344 | tokens, requests int64 |
| 345 | ) |
| 346 | if scanErr := rows.Scan(&apiKeyID, &apiKeyName, &userIDCol, &userName, &tokens, &requests, &lastUsedRaw); scanErr != nil { |
| 347 | continue |
| 348 | } |
| 349 | out = append(out, KeyTotal{ |
| 350 | APIKeyID: apiKeyID, |
| 351 | APIKeyName: apiKeyName, |
| 352 | UserID: userIDCol, |
| 353 | UserName: userName, |
| 354 | Tokens: tokens, |
| 355 | Requests: requests, |
no test coverage detected