* feat(subscriptions): refine credential trend insights * fix(classic): accept health identity fields (#682) * fix(ui): standardize English unit abbreviations (#683) * feat(subscriptions): refine credential trend insights * fix(subscriptions): address trend review findings
98 lines
3.8 KiB
Go
98 lines
3.8 KiB
Go
package requestlog
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"gpt-load/internal/platform/epochms"
|
|
)
|
|
|
|
// ResolveUsageTimeBucket 根据精确时间跨度选择趋势桶,不依赖前端快捷范围。
|
|
func ResolveUsageTimeBucket(fromMS, toMS int64) (UsageGranularity, int64, error) {
|
|
const maxSafeMilliseconds int64 = 1<<53 - 1
|
|
if fromMS < 0 || toMS <= fromMS || toMS > maxSafeMilliseconds {
|
|
return "", 0, fmt.Errorf("query usage: invalid or unsafe time range")
|
|
}
|
|
spanMS := toMS - fromMS
|
|
hour, day := epochms.MillisecondsPerHour, epochms.MillisecondsPerDay
|
|
switch {
|
|
case spanMS <= 6*hour:
|
|
return UsageGranularityMinute, UsageFiveMinuteBucketMS, nil
|
|
case spanMS <= day:
|
|
return UsageGranularityHour, hour, nil
|
|
case spanMS <= 3*day:
|
|
return UsageGranularityHour, 3 * hour, nil
|
|
case spanMS <= 7*day:
|
|
return UsageGranularityHour, 6 * hour, nil
|
|
case spanMS <= 15*day:
|
|
return UsageGranularityHour, 12 * hour, nil
|
|
default:
|
|
// 向上取整时不先加除数,超长区间仍保持安全整数运算。
|
|
return UsageGranularityDay, ((spanMS-1)/(30*day) + 1) * day, nil
|
|
}
|
|
}
|
|
|
|
// usageWindowScope 先按时间拆分来源,再合并行集供总览、趋势和分布使用。
|
|
// 数据缺失不会改变分段,也不会把首尾小时的整桶统计带入精确区间。
|
|
func usageWindowScope(db *gorm.DB, input UsageQuery, groupIDs ...uint) *gorm.DB {
|
|
hour := epochms.MillisecondsPerHour
|
|
if input.ToMS-input.FromMS <= hour {
|
|
return usageRequestLogScope(db, input, groupIDs...)
|
|
}
|
|
fullFromMS := input.FromMS
|
|
if remainder := fullFromMS % hour; remainder != 0 {
|
|
fullFromMS += hour - remainder
|
|
}
|
|
fullToMS := input.ToMS - input.ToMS%hour
|
|
full := input
|
|
full.FromMS, full.ToMS = fullFromMS, fullToMS
|
|
// 数值运算固定参数类型,避免 PostgreSQL 把 SELECT 中的独立参数推断为 text。
|
|
parts := []any{usageStatScope(db, full, groupIDs...).Select(usageWindowColumns+", ? + 0 AS bucket_alignment_ms", hour)}
|
|
unionSQL := "?"
|
|
for _, boundary := range [][2]int64{{input.FromMS, fullFromMS}, {fullToMS, input.ToMS}} {
|
|
if boundary[0] >= boundary[1] {
|
|
continue
|
|
}
|
|
part := input
|
|
part.FromMS, part.ToMS = boundary[0], boundary[1]
|
|
parts = append(parts, usageRequestLogScope(db, part, groupIDs...).Select(usageWindowColumns+", bucket_alignment_ms"))
|
|
unionSQL += " UNION ALL ?"
|
|
}
|
|
return db.Session(&gorm.Session{NewDB: true}).Table("("+unionSQL+") AS usage_rows", parts...)
|
|
}
|
|
|
|
func queryUsageWindowSeries(scope *gorm.DB, input UsageQuery, widthMS int64) ([]UsageSeriesPoint, error) {
|
|
var rows []struct {
|
|
BucketStartMS int64 `gorm:"column:series_bucket_ms"`
|
|
UsageAggregate
|
|
}
|
|
// 直接按最终自然桶聚合,年度等长区间不会把每个小时传回进程再合并。
|
|
if err := scope.Select("bucket_start_ms - bucket_start_ms % ? AS series_bucket_ms, "+usageAggregateSelect, widthMS).
|
|
Group("series_bucket_ms").Order("series_bucket_ms ASC").
|
|
Scan(&rows).Error; err != nil {
|
|
return nil, fmt.Errorf("query usage window series: %w", err)
|
|
}
|
|
series := make([]UsageSeriesPoint, 0, len(rows))
|
|
for _, row := range rows {
|
|
if err := validateUsageAggregate(row.UsageAggregate); err != nil {
|
|
return nil, fmt.Errorf("validate usage window series: %w", err)
|
|
}
|
|
endMS := input.ToMS
|
|
if input.ToMS-row.BucketStartMS > widthMS {
|
|
endMS = row.BucketStartMS + widthMS
|
|
}
|
|
series = append(series, UsageSeriesPoint{
|
|
BucketStartMS: max(row.BucketStartMS, input.FromMS), BucketEndMS: endMS,
|
|
UsageAggregate: row.UsageAggregate,
|
|
})
|
|
}
|
|
return series, nil
|
|
}
|
|
|
|
const usageWindowColumns = `bucket_start_ms, group_id, access_key_id, model,
|
|
request_count, success_count, failure_count,
|
|
uncached_input_tokens, cache_read_tokens, cache_write_5m_tokens, cache_write_1h_tokens,
|
|
cache_write_unknown_tokens, output_tokens, estimated_cost_nano_usd,
|
|
usage_missing_count, partial_count, unpriced_request_count, pricing_partial_count`
|