channel_monitor_v2_repo.go 59 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580
  1. package repository
  2. import (
  3. "context"
  4. "database/sql"
  5. "encoding/json"
  6. "fmt"
  7. "sort"
  8. "strings"
  9. "time"
  10. "github.com/Wei-Shaw/sub2api/internal/service"
  11. "github.com/lib/pq"
  12. )
  13. type channelMonitorV2Repository struct{ db *sql.DB }
  14. func NewChannelMonitorV2Repository(db *sql.DB) service.ChannelMonitorV2Repository {
  15. return &channelMonitorV2Repository{db: db}
  16. }
  17. func (r *channelMonitorV2Repository) GetConfig(ctx context.Context) (*service.ChannelMonitorV2Config, error) {
  18. var cfg service.ChannelMonitorV2Config
  19. var platforms, thresholds []byte
  20. err := r.db.QueryRowContext(ctx, `
  21. SELECT version, enabled, refresh_interval_seconds, platforms, group_ids,
  22. COALESCE(ignored_error_categories, '{}'),
  23. COALESCE(health_thresholds, '{}'::jsonb),
  24. updated_at, updated_by
  25. FROM channel_monitor_v2_config WHERE id = 1`).Scan(
  26. &cfg.Version, &cfg.Enabled, &cfg.RefreshIntervalSeconds, &platforms,
  27. pq.Array(&cfg.GroupIDs), pq.Array(&cfg.IgnoredErrorCategories),
  28. &thresholds,
  29. &cfg.UpdatedAt, &cfg.UpdatedBy,
  30. )
  31. if err != nil {
  32. return nil, fmt.Errorf("get channel monitor v2 config: %w", err)
  33. }
  34. if err := json.Unmarshal(platforms, &cfg.Platforms); err != nil {
  35. return nil, fmt.Errorf("decode channel monitor v2 platforms: %w", err)
  36. }
  37. if cfg.IgnoredErrorCategories == nil {
  38. cfg.IgnoredErrorCategories = []string{}
  39. }
  40. cfg.HealthThresholds = service.DefaultChannelMonitorV2HealthThresholds()
  41. if len(thresholds) > 0 {
  42. _ = json.Unmarshal(thresholds, &cfg.HealthThresholds)
  43. }
  44. cfg.HealthThresholds = service.NormalizeChannelMonitorV2HealthThresholds(cfg.HealthThresholds)
  45. return &cfg, nil
  46. }
  47. func (r *channelMonitorV2Repository) UpdateConfig(ctx context.Context, cfg service.ChannelMonitorV2Config, expectedVersion int) (*service.ChannelMonitorV2Config, error) {
  48. platforms, err := json.Marshal(cfg.Platforms)
  49. if err != nil {
  50. return nil, err
  51. }
  52. if cfg.IgnoredErrorCategories == nil {
  53. cfg.IgnoredErrorCategories = []string{}
  54. }
  55. cfg.HealthThresholds = service.NormalizeChannelMonitorV2HealthThresholds(cfg.HealthThresholds)
  56. thresholds, err := json.Marshal(cfg.HealthThresholds)
  57. if err != nil {
  58. return nil, err
  59. }
  60. var updated service.ChannelMonitorV2Config
  61. var raw, rawThresholds []byte
  62. err = r.db.QueryRowContext(ctx, `
  63. UPDATE channel_monitor_v2_config
  64. SET version = version + 1, enabled = $1, refresh_interval_seconds = $2,
  65. platforms = $3, group_ids = $4, ignored_error_categories = $5,
  66. health_thresholds = $6, updated_by = $7, updated_at = NOW()
  67. WHERE id = 1 AND version = $8
  68. RETURNING version, enabled, refresh_interval_seconds, platforms, group_ids,
  69. COALESCE(ignored_error_categories, '{}'),
  70. COALESCE(health_thresholds, '{}'::jsonb),
  71. updated_at, updated_by`,
  72. cfg.Enabled, cfg.RefreshIntervalSeconds, platforms, pq.Array(cfg.GroupIDs),
  73. pq.Array(cfg.IgnoredErrorCategories), thresholds, cfg.UpdatedBy, expectedVersion,
  74. ).Scan(&updated.Version, &updated.Enabled, &updated.RefreshIntervalSeconds, &raw,
  75. pq.Array(&updated.GroupIDs), pq.Array(&updated.IgnoredErrorCategories),
  76. &rawThresholds,
  77. &updated.UpdatedAt, &updated.UpdatedBy)
  78. if err == sql.ErrNoRows {
  79. return nil, service.ErrChannelMonitorV2ConfigConflict
  80. }
  81. if err != nil {
  82. return nil, fmt.Errorf("update channel monitor v2 config: %w", err)
  83. }
  84. if err := json.Unmarshal(raw, &updated.Platforms); err != nil {
  85. return nil, err
  86. }
  87. if updated.IgnoredErrorCategories == nil {
  88. updated.IgnoredErrorCategories = []string{}
  89. }
  90. updated.HealthThresholds = service.DefaultChannelMonitorV2HealthThresholds()
  91. _ = json.Unmarshal(rawThresholds, &updated.HealthThresholds)
  92. updated.HealthThresholds = service.NormalizeChannelMonitorV2HealthThresholds(updated.HealthThresholds)
  93. return &updated, nil
  94. }
  95. type channelMonitorV2Fact struct {
  96. BucketStart, Platform, GroupName, Model string
  97. GroupID int64
  98. Success, Errors, UpstreamAffected, UpstreamAttempts int64
  99. Input, Output, CacheCreation, CacheRead int64
  100. TTFTSum, TTFTCount, DurationSum, DurationCount int64
  101. }
  102. type channelMonitorV2Histogram struct {
  103. BucketStart, Platform, Model, Metric string
  104. GroupID, UserID int64
  105. UpperBound int64
  106. Count int64
  107. }
  108. func (r *channelMonitorV2Repository) GetDimensions(ctx context.Context, filter service.ChannelMonitorV2Filter, cfg service.ChannelMonitorV2Config) (*service.ChannelMonitorV2Dimensions, error) {
  109. if channelMonitorV2RestrictedGroupScopeEmpty(filter, cfg) {
  110. return &service.ChannelMonitorV2Dimensions{
  111. Platforms: []service.ChannelMonitorV2Dimension{},
  112. Groups: []service.ChannelMonitorV2GroupDimension{},
  113. Models: []service.ChannelMonitorV2Dimension{},
  114. }, nil
  115. }
  116. coverage, err := r.loadCoverage(ctx, filter)
  117. if err != nil {
  118. return nil, err
  119. }
  120. // Catalog dimensions must stay independent of the currently selected
  121. // platform/group/model multi-select filters so pickers keep their full option set.
  122. // Time/coverage still come from the request filter; config scope (enabled platforms,
  123. // group allow-list, display-model collapse) still applies.
  124. catalogFilter := channelMonitorV2CatalogFilter(channelMonitorV2CommonCoverageFilter(filter, *coverage))
  125. where, args, _ := channelMonitorV2WhereWithRollup(catalogFilter, cfg, "m")
  126. query := `SELECT m.platform, COALESCE(g.name, ''), lower(COALESCE(NULLIF(TRIM(g.platform), ''), 'unknown')), m.group_id, m.model,
  127. SUM(m.success_requests + m.error_requests)
  128. FROM ` + channelMonitorV2MetricsTable(catalogFilter) + ` m
  129. LEFT JOIN groups g ON g.id = NULLIF(m.group_id, 0) ` + where + `
  130. GROUP BY m.platform, g.name, g.platform, m.group_id, m.model`
  131. rows, err := r.db.QueryContext(ctx, query, args...)
  132. if err != nil {
  133. return nil, err
  134. }
  135. defer func() { _ = rows.Close() }()
  136. platformCounts := map[string]int64{}
  137. type modelValue struct {
  138. platform string
  139. count int64
  140. }
  141. // Model dimensions are platform-scoped; identical names can represent
  142. // different upstream models and must not share counts.
  143. modelCounts := map[string]modelValue{}
  144. type groupValue struct {
  145. name string
  146. platform string
  147. count int64
  148. }
  149. groupCounts := map[int64]groupValue{}
  150. for _, platform := range channelMonitorV2EnabledPlatforms(cfg) {
  151. platformCounts[platform] += 0
  152. for _, p := range cfg.Platforms {
  153. if p.Platform != platform || len(p.Models) == 0 {
  154. continue
  155. }
  156. for _, model := range p.Models {
  157. modelCounts[platform+"\x00"+model] = modelValue{platform: platform, count: 0}
  158. }
  159. }
  160. }
  161. groupInfo, err := r.loadChannelMonitorV2GroupInfo(ctx, configuredChannelMonitorV2GroupIDs(catalogFilter, cfg))
  162. if err != nil {
  163. return nil, err
  164. }
  165. for groupID, info := range groupInfo {
  166. groupCounts[groupID] = groupValue{name: info.name, platform: info.platform, count: 0}
  167. }
  168. for rows.Next() {
  169. var platform, groupName, groupPlatform, model string
  170. var groupID, count int64
  171. if err := rows.Scan(&platform, &groupName, &groupPlatform, &groupID, &model, &count); err != nil {
  172. return nil, err
  173. }
  174. platformCounts[platform] += count
  175. displayModel := channelMonitorV2DisplayModel(cfg, platform, model)
  176. modelKey := platform + "\x00" + displayModel
  177. currentModel := modelCounts[modelKey]
  178. currentModel.count += count
  179. if currentModel.platform == "" {
  180. currentModel.platform = platform
  181. }
  182. modelCounts[modelKey] = currentModel
  183. if groupID > 0 {
  184. current := groupCounts[groupID]
  185. current.name = groupName
  186. current.platform = groupPlatform
  187. current.count += count
  188. groupCounts[groupID] = current
  189. }
  190. }
  191. result := &service.ChannelMonitorV2Dimensions{
  192. Platforms: []service.ChannelMonitorV2Dimension{},
  193. Groups: []service.ChannelMonitorV2GroupDimension{},
  194. Models: []service.ChannelMonitorV2Dimension{},
  195. }
  196. for value, count := range platformCounts {
  197. result.Platforms = append(result.Platforms, service.ChannelMonitorV2Dimension{Value: value, Label: value, RequestCount: count})
  198. }
  199. for value, meta := range modelCounts {
  200. result.Models = append(result.Models, service.ChannelMonitorV2Dimension{Value: value, Label: channelMonitorV2ModelLabel(value), Platform: meta.platform, RequestCount: meta.count})
  201. }
  202. for id, value := range groupCounts {
  203. result.Groups = append(result.Groups, service.ChannelMonitorV2GroupDimension{ID: id, Name: value.name, Platform: value.platform, RequestCount: value.count})
  204. }
  205. sort.Slice(result.Platforms, func(i, j int) bool { return result.Platforms[i].RequestCount > result.Platforms[j].RequestCount })
  206. sort.Slice(result.Models, func(i, j int) bool { return result.Models[i].RequestCount > result.Models[j].RequestCount })
  207. sort.Slice(result.Groups, func(i, j int) bool { return result.Groups[i].RequestCount > result.Groups[j].RequestCount })
  208. return result, rows.Err()
  209. }
  210. func (r *channelMonitorV2Repository) GetSnapshot(ctx context.Context, filter service.ChannelMonitorV2Filter, cfg service.ChannelMonitorV2Config, admin bool) (*service.ChannelMonitorV2Snapshot, error) {
  211. if channelMonitorV2RestrictedGroupScopeEmpty(filter, cfg) {
  212. return &service.ChannelMonitorV2Snapshot{Trend: []service.ChannelMonitorV2TrendPoint{}}, nil
  213. }
  214. coverage, err := r.loadCoverage(ctx, filter)
  215. if err != nil {
  216. return nil, err
  217. }
  218. effectiveFilter := channelMonitorV2CommonCoverageFilter(filter, *coverage)
  219. facts, err := r.loadFacts(ctx, effectiveFilter, cfg, true)
  220. if err != nil {
  221. return nil, err
  222. }
  223. histograms, err := r.loadHistograms(ctx, effectiveFilter, cfg, 0, true)
  224. if err != nil {
  225. return nil, err
  226. }
  227. byBucket := map[string]*metricAccumulator{}
  228. total := newMetricAccumulator()
  229. for _, fact := range facts {
  230. if !channelMonitorV2ModelSelected(filter, cfg, fact.Platform, fact.Model) {
  231. continue
  232. }
  233. bucket := fact.BucketStart
  234. acc := byBucket[bucket]
  235. if acc == nil {
  236. acc = newMetricAccumulator()
  237. byBucket[bucket] = acc
  238. }
  239. acc.addFact(fact)
  240. total.addFact(fact)
  241. }
  242. for _, hist := range histograms {
  243. if !channelMonitorV2ModelSelected(filter, cfg, hist.Platform, hist.Model) {
  244. continue
  245. }
  246. if acc := byBucket[hist.BucketStart]; acc != nil {
  247. acc.addHistogram(hist)
  248. }
  249. total.addHistogram(hist)
  250. }
  251. minutes := channelMonitorV2CoveredMinutes(filter, *coverage)
  252. // Category-level ignored errors adjust error_rate without dropping volume.
  253. ignoredByBucket, ignoredTotal, err := r.loadIgnoredErrorCounts(ctx, effectiveFilter, cfg)
  254. if err != nil {
  255. return nil, fmt.Errorf("load ignored error counts: %w", err)
  256. }
  257. metrics := total.metric(minutes, admin)
  258. applyIgnoredErrors(&metrics, ignoredTotal)
  259. if !admin {
  260. cfg.UpdatedBy = nil
  261. }
  262. result := &service.ChannelMonitorV2Snapshot{Config: cfg, Coverage: *coverage, Metrics: metrics, Health: service.ChannelMonitorV2HealthForWithThresholds(metrics, cfg.HealthThresholds), Trend: []service.ChannelMonitorV2TrendPoint{}}
  263. keys := make([]string, 0, len(byBucket))
  264. for key := range byBucket {
  265. keys = append(keys, key)
  266. }
  267. sort.Strings(keys)
  268. for _, key := range keys {
  269. bucket, _ := time.Parse(time.RFC3339Nano, key)
  270. m := byBucket[key].metric(filter.Bucket.Minutes(), admin)
  271. applyIgnoredErrors(&m, ignoredByBucket[key])
  272. result.Trend = append(result.Trend, service.ChannelMonitorV2TrendPoint{BucketStart: bucket, Metrics: m, Health: service.ChannelMonitorV2HealthForWithThresholds(m, cfg.HealthThresholds)})
  273. }
  274. return result, nil
  275. }
  276. func (r *channelMonitorV2Repository) GetModels(ctx context.Context, filter service.ChannelMonitorV2Filter, cfg service.ChannelMonitorV2Config, admin bool) (*service.ChannelMonitorV2List[service.ChannelMonitorV2ModelRow], error) {
  277. if channelMonitorV2RestrictedGroupScopeEmpty(filter, cfg) {
  278. return &service.ChannelMonitorV2List[service.ChannelMonitorV2ModelRow]{Items: []service.ChannelMonitorV2ModelRow{}}, nil
  279. }
  280. coverage, err := r.loadCoverage(ctx, filter)
  281. if err != nil {
  282. return nil, err
  283. }
  284. effectiveFilter := channelMonitorV2CommonCoverageFilter(filter, *coverage)
  285. facts, err := r.loadFacts(ctx, effectiveFilter, cfg, false)
  286. if err != nil {
  287. return nil, err
  288. }
  289. hist, err := r.loadHistograms(ctx, effectiveFilter, cfg, 0, false)
  290. if err != nil {
  291. return nil, err
  292. }
  293. accs := map[string]*metricAccumulator{}
  294. for _, platform := range channelMonitorV2EnabledPlatforms(cfg) {
  295. if len(filter.Platforms) > 0 && !containsString(filter.Platforms, platform) {
  296. continue
  297. }
  298. models := configuredChannelMonitorV2Models(cfg, platform, filter)
  299. for _, model := range models {
  300. if model == "" {
  301. continue
  302. }
  303. key := platform + "\x00" + model
  304. if accs[key] == nil {
  305. accs[key] = newMetricAccumulator()
  306. }
  307. }
  308. }
  309. for _, fact := range facts {
  310. if !channelMonitorV2ModelSelected(filter, cfg, fact.Platform, fact.Model) {
  311. continue
  312. }
  313. model := channelMonitorV2DisplayModel(cfg, fact.Platform, fact.Model)
  314. key := fact.Platform + "\x00" + model
  315. if accs[key] == nil {
  316. accs[key] = newMetricAccumulator()
  317. }
  318. accs[key].addFact(fact)
  319. }
  320. for _, h := range hist {
  321. if !channelMonitorV2ModelSelected(filter, cfg, h.Platform, h.Model) {
  322. continue
  323. }
  324. key := h.Platform + "\x00" + channelMonitorV2DisplayModel(cfg, h.Platform, h.Model)
  325. if accs[key] != nil {
  326. accs[key].addHistogram(h)
  327. }
  328. }
  329. // Per platform+model ignored counts for error_rate adjustment.
  330. ignoredByPM, _, err := r.loadIgnoredErrorCountsByPlatformModel(ctx, effectiveFilter, cfg)
  331. if err != nil {
  332. return nil, fmt.Errorf("load ignored error counts by platform/model: %w", err)
  333. }
  334. items := make([]service.ChannelMonitorV2ModelRow, 0, len(accs))
  335. minutes := channelMonitorV2CoveredMinutes(filter, *coverage)
  336. for key, acc := range accs {
  337. parts := strings.SplitN(key, "\x00", 2)
  338. metrics := acc.metric(minutes, admin)
  339. applyIgnoredErrors(&metrics, ignoredByPM[key])
  340. items = append(items, service.ChannelMonitorV2ModelRow{Platform: parts[0], Model: parts[1], Metrics: metrics, Health: service.ChannelMonitorV2HealthForWithThresholds(metrics, cfg.HealthThresholds)})
  341. }
  342. sort.Slice(items, func(i, j int) bool { return items[i].Metrics.RequestCount > items[j].Metrics.RequestCount })
  343. return &service.ChannelMonitorV2List[service.ChannelMonitorV2ModelRow]{Coverage: *coverage, Items: items}, nil
  344. }
  345. type channelMonitorV2MatrixKey struct {
  346. platform, model string
  347. groupID int64
  348. }
  349. type channelMonitorV2MatrixAccumulator struct {
  350. groupName string
  351. total *metricAccumulator
  352. buckets map[string]*metricAccumulator
  353. }
  354. func (r *channelMonitorV2Repository) GetMatrix(ctx context.Context, filter service.ChannelMonitorV2Filter, cfg service.ChannelMonitorV2Config, groupBy service.ChannelMonitorV2GroupBy, admin bool) (*service.ChannelMonitorV2Matrix, error) {
  355. if channelMonitorV2RestrictedGroupScopeEmpty(filter, cfg) {
  356. return &service.ChannelMonitorV2Matrix{GroupBy: groupBy, Items: []service.ChannelMonitorV2MatrixRow{}}, nil
  357. }
  358. coverage, err := r.loadCoverage(ctx, filter)
  359. if err != nil {
  360. return nil, err
  361. }
  362. effectiveFilter := channelMonitorV2CommonCoverageFilter(filter, *coverage)
  363. facts, err := r.loadFacts(ctx, effectiveFilter, cfg, true)
  364. if err != nil {
  365. return nil, err
  366. }
  367. histograms, err := r.loadHistograms(ctx, effectiveFilter, cfg, 0, true)
  368. if err != nil {
  369. return nil, err
  370. }
  371. seedGroupIDs := configuredChannelMonitorV2GroupIDs(filter, cfg)
  372. // Empty config group list means all groups — load active groups so matrix seed
  373. // can materialize real platform/group rows (not bare platform placeholders).
  374. if len(seedGroupIDs) == 0 && !filter.RestrictGroups &&
  375. (groupBy == service.ChannelMonitorV2GroupByPlatformGroup || groupBy == service.ChannelMonitorV2GroupByPlatformGroupModel) {
  376. allIDs, loadErr := r.listActiveGroupIDs(ctx)
  377. if loadErr != nil {
  378. return nil, loadErr
  379. }
  380. seedGroupIDs = allIDs
  381. }
  382. groupInfo, err := r.loadChannelMonitorV2GroupInfo(ctx, seedGroupIDs)
  383. if err != nil {
  384. return nil, err
  385. }
  386. accs := seedChannelMonitorV2MatrixAccumulators(filter, cfg, groupBy, groupInfo)
  387. for _, fact := range facts {
  388. if !channelMonitorV2ModelSelected(filter, cfg, fact.Platform, fact.Model) {
  389. continue
  390. }
  391. // platform_group dimensions require a real group; skip group_id=0 traffic rows.
  392. if (groupBy == service.ChannelMonitorV2GroupByPlatformGroup || groupBy == service.ChannelMonitorV2GroupByPlatformGroupModel) &&
  393. fact.GroupID <= 0 {
  394. continue
  395. }
  396. key := channelMonitorV2MatrixDimensionKey(groupBy, cfg, fact.Platform, fact.GroupID, fact.Model)
  397. acc := accs[key]
  398. if acc == nil {
  399. acc = &channelMonitorV2MatrixAccumulator{total: newMetricAccumulator(), buckets: make(map[string]*metricAccumulator)}
  400. accs[key] = acc
  401. }
  402. if key.groupID > 0 {
  403. acc.groupName = fact.GroupName
  404. }
  405. bucket := acc.buckets[fact.BucketStart]
  406. if bucket == nil {
  407. bucket = newMetricAccumulator()
  408. acc.buckets[fact.BucketStart] = bucket
  409. }
  410. acc.total.addFact(fact)
  411. bucket.addFact(fact)
  412. }
  413. for _, histogram := range histograms {
  414. if !channelMonitorV2ModelSelected(filter, cfg, histogram.Platform, histogram.Model) {
  415. continue
  416. }
  417. if (groupBy == service.ChannelMonitorV2GroupByPlatformGroup || groupBy == service.ChannelMonitorV2GroupByPlatformGroupModel) &&
  418. histogram.GroupID <= 0 {
  419. continue
  420. }
  421. key := channelMonitorV2MatrixDimensionKey(groupBy, cfg, histogram.Platform, histogram.GroupID, histogram.Model)
  422. acc := accs[key]
  423. if acc == nil {
  424. continue
  425. }
  426. acc.total.addHistogram(histogram)
  427. if bucket := acc.buckets[histogram.BucketStart]; bucket != nil {
  428. bucket.addHistogram(histogram)
  429. }
  430. }
  431. result := &service.ChannelMonitorV2Matrix{GroupBy: groupBy, Coverage: *coverage, Items: make([]service.ChannelMonitorV2MatrixRow, 0, len(accs))}
  432. minutes := channelMonitorV2CoveredMinutes(filter, *coverage)
  433. ignoredByDimBucket, ignoredByDim, err := r.loadIgnoredErrorCountsByMatrixKey(ctx, effectiveFilter, cfg, groupBy)
  434. if err != nil {
  435. return nil, fmt.Errorf("load ignored error counts by matrix key: %w", err)
  436. }
  437. for key, acc := range accs {
  438. // platform_group views only emit rows with a real group_id.
  439. if (groupBy == service.ChannelMonitorV2GroupByPlatformGroup || groupBy == service.ChannelMonitorV2GroupByPlatformGroupModel) &&
  440. key.groupID <= 0 {
  441. continue
  442. }
  443. metrics := acc.total.metric(minutes, admin)
  444. applyIgnoredErrors(&metrics, ignoredByDim[key])
  445. row := service.ChannelMonitorV2MatrixRow{Platform: key.platform, GroupName: acc.groupName, Model: key.model, Metrics: metrics, Health: service.ChannelMonitorV2HealthForWithThresholds(metrics, cfg.HealthThresholds), Buckets: []service.ChannelMonitorV2TrendPoint{}}
  446. if key.groupID > 0 {
  447. groupID := key.groupID
  448. row.GroupID = &groupID
  449. }
  450. bucketKeys := make([]string, 0, len(acc.buckets))
  451. for bucket := range acc.buckets {
  452. bucketKeys = append(bucketKeys, bucket)
  453. }
  454. sort.Strings(bucketKeys)
  455. for _, bucketKey := range bucketKeys {
  456. bucketStart, parseErr := time.Parse(time.RFC3339Nano, bucketKey)
  457. if parseErr != nil {
  458. continue
  459. }
  460. bucketMetrics := acc.buckets[bucketKey].metric(filter.Bucket.Minutes(), admin)
  461. applyIgnoredErrors(&bucketMetrics, ignoredByDimBucket[key][bucketKey])
  462. row.Buckets = append(row.Buckets, service.ChannelMonitorV2TrendPoint{BucketStart: bucketStart, Metrics: bucketMetrics, Health: service.ChannelMonitorV2HealthForWithThresholds(bucketMetrics, cfg.HealthThresholds)})
  463. }
  464. result.Items = append(result.Items, row)
  465. }
  466. sort.Slice(result.Items, func(i, j int) bool {
  467. a, b := result.Items[i], result.Items[j]
  468. if a.Platform != b.Platform {
  469. return a.Platform < b.Platform
  470. }
  471. if a.GroupName != b.GroupName {
  472. return a.GroupName < b.GroupName
  473. }
  474. if a.GroupID != nil && b.GroupID != nil && *a.GroupID != *b.GroupID {
  475. return *a.GroupID < *b.GroupID
  476. }
  477. return a.Model < b.Model
  478. })
  479. return result, nil
  480. }
  481. func channelMonitorV2MatrixDimensionKey(groupBy service.ChannelMonitorV2GroupBy, cfg service.ChannelMonitorV2Config, platform string, groupID int64, model string) channelMonitorV2MatrixKey {
  482. key := channelMonitorV2MatrixKey{platform: platform}
  483. if groupBy == service.ChannelMonitorV2GroupByPlatformGroup || groupBy == service.ChannelMonitorV2GroupByPlatformGroupModel {
  484. key.groupID = groupID
  485. }
  486. if groupBy == service.ChannelMonitorV2GroupByPlatformModel || groupBy == service.ChannelMonitorV2GroupByPlatformGroupModel {
  487. key.model = channelMonitorV2DisplayModel(cfg, platform, model)
  488. }
  489. return key
  490. }
  491. func seedChannelMonitorV2MatrixAccumulators(filter service.ChannelMonitorV2Filter, cfg service.ChannelMonitorV2Config, groupBy service.ChannelMonitorV2GroupBy, groupInfo map[int64]channelMonitorV2GroupInfo) map[channelMonitorV2MatrixKey]*channelMonitorV2MatrixAccumulator {
  492. accs := map[channelMonitorV2MatrixKey]*channelMonitorV2MatrixAccumulator{}
  493. platforms := channelMonitorV2EnabledPlatforms(cfg)
  494. if len(filter.Platforms) > 0 {
  495. platforms = intersectStrings(platforms, filter.Platforms)
  496. }
  497. // Platform-only groupBy seeds groupID=0. When group is part of the dimension
  498. // (platform_group / platform_group_model), never seed a bare platform row —
  499. // only real group IDs (from config or discovered groupInfo).
  500. needsGroup := groupBy == service.ChannelMonitorV2GroupByPlatformGroup || groupBy == service.ChannelMonitorV2GroupByPlatformGroupModel
  501. groupIDs := []int64{0}
  502. if needsGroup {
  503. groupIDs = configuredChannelMonitorV2GroupIDs(filter, cfg)
  504. if len(groupIDs) == 0 && !filter.RestrictGroups {
  505. // Empty config group list means "all groups": use discovered active groups.
  506. for id := range groupInfo {
  507. groupIDs = append(groupIDs, id)
  508. }
  509. sort.Slice(groupIDs, func(i, j int) bool { return groupIDs[i] < groupIDs[j] })
  510. }
  511. // Still empty → seed nothing; rows come only from fact traffic.
  512. if len(groupIDs) == 0 {
  513. return accs
  514. }
  515. }
  516. for _, platform := range platforms {
  517. models := []string{""}
  518. if groupBy == service.ChannelMonitorV2GroupByPlatformModel || groupBy == service.ChannelMonitorV2GroupByPlatformGroupModel {
  519. models = configuredChannelMonitorV2Models(cfg, platform, filter)
  520. if len(models) == 0 {
  521. models = []string{""}
  522. }
  523. }
  524. for _, groupID := range groupIDs {
  525. info := groupInfo[groupID]
  526. if needsGroup {
  527. if groupID <= 0 {
  528. continue
  529. }
  530. if info.platform == "" || info.platform != platform {
  531. continue
  532. }
  533. }
  534. for _, model := range models {
  535. key := channelMonitorV2MatrixKey{platform: platform}
  536. if needsGroup {
  537. key.groupID = groupID
  538. }
  539. if groupBy == service.ChannelMonitorV2GroupByPlatformModel || groupBy == service.ChannelMonitorV2GroupByPlatformGroupModel {
  540. key.model = model
  541. }
  542. if accs[key] == nil {
  543. accs[key] = &channelMonitorV2MatrixAccumulator{groupName: info.name, total: newMetricAccumulator(), buckets: make(map[string]*metricAccumulator)}
  544. }
  545. }
  546. }
  547. }
  548. return accs
  549. }
  550. func configuredChannelMonitorV2Models(cfg service.ChannelMonitorV2Config, platform string, filter service.ChannelMonitorV2Filter) []string {
  551. models := []string{}
  552. for _, p := range cfg.Platforms {
  553. if p.Platform != platform {
  554. continue
  555. }
  556. models = append(models, p.Models...)
  557. break
  558. }
  559. if len(filter.Models) > 0 {
  560. if len(models) == 0 {
  561. models = append(models, filter.Models...)
  562. } else {
  563. models = intersectStrings(models, filter.Models)
  564. }
  565. }
  566. return models
  567. }
  568. // channelMonitorV2CatalogFilter clears multi-select dimensions so catalog endpoints
  569. // return the full configured option set for the time window. Metrics queries must
  570. // keep using the original filter.
  571. func channelMonitorV2CatalogFilter(filter service.ChannelMonitorV2Filter) service.ChannelMonitorV2Filter {
  572. catalog := filter
  573. catalog.Platforms = nil
  574. catalog.GroupIDs = nil
  575. catalog.Models = nil
  576. return catalog
  577. }
  578. func configuredChannelMonitorV2GroupIDs(filter service.ChannelMonitorV2Filter, cfg service.ChannelMonitorV2Config) []int64 {
  579. groups, empty := channelMonitorV2ScopedGroupIDs(filter, cfg)
  580. if empty {
  581. return []int64{}
  582. }
  583. return groups
  584. }
  585. func channelMonitorV2ScopedGroupIDs(filter service.ChannelMonitorV2Filter, cfg service.ChannelMonitorV2Config) ([]int64, bool) {
  586. groups := append([]int64(nil), cfg.GroupIDs...)
  587. if filter.RestrictGroups {
  588. if len(groups) > 0 {
  589. groups = intersectInt64(groups, filter.AllowedGroupIDs)
  590. } else {
  591. groups = append([]int64(nil), filter.AllowedGroupIDs...)
  592. }
  593. if len(groups) == 0 {
  594. return nil, true
  595. }
  596. }
  597. if len(filter.GroupIDs) > 0 {
  598. if len(groups) > 0 {
  599. groups = intersectInt64(groups, filter.GroupIDs)
  600. } else {
  601. groups = append([]int64(nil), filter.GroupIDs...)
  602. }
  603. if len(groups) == 0 {
  604. return nil, true
  605. }
  606. }
  607. return groups, false
  608. }
  609. func channelMonitorV2RestrictedGroupScopeEmpty(filter service.ChannelMonitorV2Filter, cfg service.ChannelMonitorV2Config) bool {
  610. if !filter.RestrictGroups {
  611. return false
  612. }
  613. _, empty := channelMonitorV2ScopedGroupIDs(filter, cfg)
  614. return empty
  615. }
  616. type channelMonitorV2GroupInfo struct {
  617. name string
  618. platform string
  619. }
  620. func (r *channelMonitorV2Repository) listActiveGroupIDs(ctx context.Context) ([]int64, error) {
  621. rows, err := r.db.QueryContext(ctx, `SELECT id FROM groups WHERE deleted_at IS NULL AND status = 'active' ORDER BY id`)
  622. if err != nil {
  623. return nil, err
  624. }
  625. defer func() { _ = rows.Close() }()
  626. ids := []int64{}
  627. for rows.Next() {
  628. var id int64
  629. if err := rows.Scan(&id); err != nil {
  630. return nil, err
  631. }
  632. ids = append(ids, id)
  633. }
  634. return ids, rows.Err()
  635. }
  636. func (r *channelMonitorV2Repository) loadChannelMonitorV2GroupInfo(ctx context.Context, groupIDs []int64) (map[int64]channelMonitorV2GroupInfo, error) {
  637. out := map[int64]channelMonitorV2GroupInfo{}
  638. if len(groupIDs) == 0 {
  639. return out, nil
  640. }
  641. rows, err := r.db.QueryContext(ctx, `SELECT id, COALESCE(name, ''), lower(COALESCE(NULLIF(TRIM(platform), ''), 'unknown')) FROM groups WHERE id = ANY($1) AND deleted_at IS NULL AND status = 'active'`, pq.Array(groupIDs))
  642. if err != nil {
  643. return nil, err
  644. }
  645. defer func() { _ = rows.Close() }()
  646. for rows.Next() {
  647. var id int64
  648. var name, platform string
  649. if err := rows.Scan(&id, &name, &platform); err != nil {
  650. return nil, err
  651. }
  652. out[id] = channelMonitorV2GroupInfo{name: name, platform: platform}
  653. }
  654. return out, rows.Err()
  655. }
  656. func (r *channelMonitorV2Repository) GetErrors(ctx context.Context, filter service.ChannelMonitorV2Filter, cfg service.ChannelMonitorV2Config, includeAdmin bool) (*service.ChannelMonitorV2List[service.ChannelMonitorV2ErrorRow], error) {
  657. coverage, err := r.loadCoverage(ctx, filter)
  658. if err != nil {
  659. return nil, err
  660. }
  661. filter = channelMonitorV2CommonCoverageFilter(filter, *coverage)
  662. where, args, _ := channelMonitorV2WhereWithRollup(filter, cfg, "e")
  663. rows, err := r.db.QueryContext(ctx, `SELECT e.platform,e.model,e.error_category,SUM(e.error_requests) FROM `+channelMonitorV2ErrorMetricsTable(filter)+` e `+where+` AND e.taxonomy_version = `+fmt.Sprint(service.ChannelMonitorV2TaxonomyVersion)+` GROUP BY e.platform,e.model,e.error_category`, args...)
  664. if err != nil {
  665. return nil, err
  666. }
  667. defer func() { _ = rows.Close() }()
  668. counts := map[string]int64{}
  669. var total int64
  670. ignoredSet := service.ChannelMonitorV2IgnoredCategorySet(cfg)
  671. for rows.Next() {
  672. var platform, model, category string
  673. var count int64
  674. if err := rows.Scan(&platform, &model, &category, &count); err != nil {
  675. return nil, err
  676. }
  677. if !channelMonitorV2ModelSelected(filter, cfg, platform, model) {
  678. continue
  679. }
  680. if category == "" {
  681. category = "other"
  682. }
  683. counts[category] += count
  684. total += count
  685. }
  686. items := make([]service.ChannelMonitorV2ErrorRow, 0, len(counts))
  687. // Upstream message samples are admin-only: skip the raw ops_error_logs scan
  688. // for user-facing callers (privacy + avoids multi-million-row online scans).
  689. var details map[string][]service.ChannelMonitorV2ErrorDetail
  690. if includeAdmin {
  691. loaded, detailErr := r.loadErrorDetails(ctx, filter, cfg)
  692. if detailErr != nil {
  693. return nil, detailErr
  694. }
  695. details = loaded
  696. }
  697. for category, count := range counts {
  698. rate := 0.0
  699. if total > 0 {
  700. rate = float64(count) / float64(total)
  701. }
  702. _, ignored := ignoredSet[category]
  703. row := service.ChannelMonitorV2ErrorRow{Category: category, Count: count, Rate: rate, Ignored: ignored}
  704. if includeAdmin {
  705. row.Details = details[category]
  706. }
  707. items = append(items, row)
  708. }
  709. // Stable: counted categories first (by count), then ignored (grey in UI).
  710. sort.Slice(items, func(i, j int) bool {
  711. if items[i].Ignored != items[j].Ignored {
  712. return !items[i].Ignored && items[j].Ignored
  713. }
  714. return items[i].Count > items[j].Count
  715. })
  716. return &service.ChannelMonitorV2List[service.ChannelMonitorV2ErrorRow]{Coverage: *coverage, Items: items}, rows.Err()
  717. }
  718. func (r *channelMonitorV2Repository) loadErrorDetails(ctx context.Context, filter service.ChannelMonitorV2Filter, cfg service.ChannelMonitorV2Config) (map[string][]service.ChannelMonitorV2ErrorDetail, error) {
  719. conditions := []string{
  720. "current_error.created_at >= $1",
  721. "current_error.created_at < $2",
  722. "NOT current_error.is_count_tokens",
  723. "(COALESCE(current_error.status_code, 0) >= 400 OR current_error.error_type = 'cyber_policy')",
  724. `(NULLIF(current_error.request_id, '') IS NULL OR NOT EXISTS (
  725. SELECT 1 FROM ops_error_logs newer
  726. WHERE newer.request_id = current_error.request_id
  727. AND NOT newer.is_count_tokens
  728. AND (COALESCE(newer.status_code, 0) >= 400 OR newer.error_type = 'cyber_policy')
  729. AND (newer.created_at, newer.id) > (current_error.created_at, current_error.id)
  730. ))`,
  731. }
  732. args := []any{filter.Start, filter.End}
  733. platforms := channelMonitorV2EnabledPlatforms(cfg)
  734. if len(filter.Platforms) > 0 {
  735. platforms = intersectStrings(platforms, filter.Platforms)
  736. }
  737. if len(platforms) > 0 {
  738. args = append(args, pq.Array(platforms))
  739. conditions = append(conditions, fmt.Sprintf("lower(COALESCE(NULLIF(TRIM(current_error.platform), ''), 'unknown')) = ANY($%d)", len(args)))
  740. } else {
  741. conditions = append(conditions, "FALSE")
  742. }
  743. groups, groupScopeEmpty := channelMonitorV2ScopedGroupIDs(filter, cfg)
  744. if groupScopeEmpty {
  745. conditions = append(conditions, "FALSE")
  746. } else if len(groups) > 0 {
  747. args = append(args, pq.Array(groups))
  748. conditions = append(conditions, fmt.Sprintf("COALESCE(current_error.group_id, 0) = ANY($%d)", len(args)))
  749. }
  750. query := `SELECT
  751. lower(COALESCE(NULLIF(TRIM(current_error.platform), ''), 'unknown')) AS platform,
  752. COALESCE(current_error.group_id, 0) AS group_id,
  753. COALESCE(NULLIF(TRIM(current_error.requested_model), ''), NULLIF(TRIM(current_error.model), ''), 'unknown') AS model,
  754. COALESCE(current_error.error_type, '') AS error_type,
  755. COALESCE(current_error.error_owner, '') AS error_owner,
  756. COALESCE(current_error.error_source, '') AS error_source,
  757. COALESCE(current_error.status_code, 0) AS status_code,
  758. COALESCE(current_error.upstream_status_code, 0) AS upstream_status_code,
  759. LEFT(COALESCE(NULLIF(current_error.upstream_error_message, ''), NULLIF(current_error.error_message, ''), NULLIF(current_error.upstream_error_detail, ''), NULLIF(current_error.error_body, ''), current_error.error_type, ''), 600) AS message,
  760. COUNT(*) AS count
  761. FROM ops_error_logs current_error
  762. WHERE ` + strings.Join(conditions, " AND ") + `
  763. GROUP BY 1,2,3,4,5,6,7,8,9
  764. ORDER BY count DESC
  765. LIMIT 400`
  766. rows, err := r.db.QueryContext(ctx, query, args...)
  767. if err != nil {
  768. return nil, err
  769. }
  770. defer func() { _ = rows.Close() }()
  771. out := map[string][]service.ChannelMonitorV2ErrorDetail{}
  772. for rows.Next() {
  773. var platform, model, errorType, owner, source, message string
  774. var groupID int64
  775. var statusCode, upstreamStatusCode int
  776. var count int64
  777. if err := rows.Scan(&platform, &groupID, &model, &errorType, &owner, &source, &statusCode, &upstreamStatusCode, &message, &count); err != nil {
  778. return nil, err
  779. }
  780. if !channelMonitorV2ModelSelected(filter, cfg, platform, model) {
  781. continue
  782. }
  783. category := service.ClassifyChannelMonitorV2Error(service.ChannelMonitorV2ErrorInput{
  784. ErrorType: errorType,
  785. ErrorOwner: owner,
  786. ErrorSource: source,
  787. StatusCode: statusCode,
  788. UpstreamStatusCode: upstreamStatusCode,
  789. Message: message,
  790. })
  791. if len(out[category]) >= 5 {
  792. continue
  793. }
  794. out[category] = append(out[category], service.ChannelMonitorV2ErrorDetail{
  795. Platform: platform,
  796. Model: channelMonitorV2DisplayModel(cfg, platform, model),
  797. ErrorType: errorType,
  798. StatusCode: statusCode,
  799. UpstreamStatusCode: upstreamStatusCode,
  800. Message: sanitizeChannelMonitorV2ErrorDetail(message),
  801. Count: count,
  802. })
  803. }
  804. return out, rows.Err()
  805. }
  806. func sanitizeChannelMonitorV2ErrorDetail(message string) string {
  807. message = strings.Join(strings.Fields(message), " ")
  808. replacements := []string{"Bearer ", "bearer ", "sk-", "sk-proj-"}
  809. for _, marker := range replacements {
  810. if idx := strings.Index(message, marker); idx >= 0 {
  811. end := idx + len(marker)
  812. for end < len(message) && message[end] != ' ' && message[end] != ',' && message[end] != '"' {
  813. end++
  814. }
  815. message = message[:idx] + marker + "****" + message[end:]
  816. }
  817. }
  818. if len(message) > 240 {
  819. message = message[:240] + "…"
  820. }
  821. return message
  822. }
  823. func (r *channelMonitorV2Repository) GetUsers(ctx context.Context, filter service.ChannelMonitorV2Filter, cfg service.ChannelMonitorV2Config, admin bool) (*service.ChannelMonitorV2List[service.ChannelMonitorV2UserRow], error) {
  824. coverage, err := r.loadCoverage(ctx, filter)
  825. if err != nil {
  826. return nil, err
  827. }
  828. effectiveFilter := channelMonitorV2CommonCoverageFilter(filter, *coverage)
  829. filter = effectiveFilter
  830. where, args, _ := channelMonitorV2WhereWithRollup(filter, cfg, "m")
  831. query := `SELECT m.user_id,COALESCE(u.email,''),COALESCE(u.username,''),m.platform,m.model,
  832. SUM(m.success_requests),SUM(m.error_requests),SUM(m.input_tokens),SUM(m.output_tokens),SUM(m.cache_creation_tokens),SUM(m.cache_read_tokens),SUM(m.ttft_sum_ms),SUM(m.ttft_count),SUM(m.duration_sum_ms),SUM(m.duration_count)
  833. FROM ` + channelMonitorV2UserMetricsTable(filter) + ` m LEFT JOIN users u ON u.id=m.user_id ` + where + ` GROUP BY m.user_id,u.email,u.username,m.platform,m.model`
  834. rows, err := r.db.QueryContext(ctx, query, args...)
  835. if err != nil {
  836. return nil, err
  837. }
  838. defer func() { _ = rows.Close() }()
  839. type userMeta struct{ email, username string }
  840. meta := map[int64]userMeta{}
  841. accs := map[int64]*metricAccumulator{}
  842. for rows.Next() {
  843. var uid int64
  844. var email, username string
  845. var f channelMonitorV2Fact
  846. if err := rows.Scan(&uid, &email, &username, &f.Platform, &f.Model, &f.Success, &f.Errors, &f.Input, &f.Output, &f.CacheCreation, &f.CacheRead, &f.TTFTSum, &f.TTFTCount, &f.DurationSum, &f.DurationCount); err != nil {
  847. return nil, err
  848. }
  849. if !channelMonitorV2ModelSelected(filter, cfg, f.Platform, f.Model) {
  850. continue
  851. }
  852. if accs[uid] == nil {
  853. accs[uid] = newMetricAccumulator()
  854. }
  855. accs[uid].addFact(f)
  856. meta[uid] = userMeta{email, username}
  857. }
  858. histograms, err := r.loadHistograms(ctx, filter, cfg, -1, false)
  859. if err != nil {
  860. return nil, err
  861. }
  862. for _, histogram := range histograms {
  863. if !channelMonitorV2ModelSelected(filter, cfg, histogram.Platform, histogram.Model) {
  864. continue
  865. }
  866. if acc := accs[histogram.UserID]; acc != nil {
  867. acc.addHistogram(histogram)
  868. }
  869. }
  870. // Error category facts are not per-user. Approximate ignored-error impact by
  871. // applying the window-level ignored/error ratio to each user's error count so
  872. // user-rank rates stay consistent with overview scoring.
  873. _, ignoredTotal, err := r.loadIgnoredErrorCounts(ctx, effectiveFilter, cfg)
  874. if err != nil {
  875. return nil, fmt.Errorf("load ignored error counts for users: %w", err)
  876. }
  877. var windowErrors int64
  878. for _, acc := range accs {
  879. windowErrors += acc.errors
  880. }
  881. ignoreRatio := 0.0
  882. if windowErrors > 0 && ignoredTotal > 0 {
  883. ignoreRatio = float64(ignoredTotal) / float64(windowErrors)
  884. if ignoreRatio > 1 {
  885. ignoreRatio = 1
  886. }
  887. }
  888. items := make([]service.ChannelMonitorV2UserRow, 0, len(accs))
  889. minutes := channelMonitorV2CoveredMinutes(filter, *coverage)
  890. for uid, acc := range accs {
  891. id := uid
  892. m := meta[uid]
  893. label := m.username
  894. if label == "" {
  895. label = m.email
  896. }
  897. metrics := acc.metric(minutes, false)
  898. if ignoreRatio > 0 && metrics.ErrorRequests > 0 {
  899. approxIgnored := int64(float64(metrics.ErrorRequests)*ignoreRatio + 0.5)
  900. applyIgnoredErrors(&metrics, approxIgnored)
  901. }
  902. // Recompute health after rate adjustment.
  903. // (metric() does not attach health; callers that need it recompute.)
  904. items = append(items, service.ChannelMonitorV2UserRow{UserID: &id, Email: m.email, Username: m.username, DisplayLabel: label, CanDrilldown: admin, Metrics: metrics})
  905. }
  906. sort.Slice(items, func(i, j int) bool { return items[i].Metrics.RequestCount > items[j].Metrics.RequestCount })
  907. return &service.ChannelMonitorV2List[service.ChannelMonitorV2UserRow]{Coverage: *coverage, Items: items}, rows.Err()
  908. }
  909. func (r *channelMonitorV2Repository) loadFacts(ctx context.Context, filter service.ChannelMonitorV2Filter, cfg service.ChannelMonitorV2Config, byBucket bool) ([]channelMonitorV2Fact, error) {
  910. where, args, bucketSeconds := channelMonitorV2WhereWithRollup(filter, cfg, "m")
  911. bucketExpr := "MIN(m.bucket_start)"
  912. group := "m.platform,m.group_id,g.name,m.model"
  913. if byBucket {
  914. if bucketSeconds > 0 {
  915. bucketExpr = "m.bucket_start"
  916. group = bucketExpr + "," + group
  917. } else {
  918. args = append([]any{fmt.Sprintf("%d seconds", int(filter.Bucket.Seconds()))}, args...)
  919. where = shiftSQLPlaceholders(where, 1)
  920. bucketExpr = "date_bin($1::interval,m.bucket_start,TIMESTAMPTZ '1970-01-01')"
  921. group = bucketExpr + "," + group
  922. }
  923. }
  924. query := `SELECT ` + bucketExpr + `,m.platform,m.group_id,COALESCE(g.name,''),m.model,SUM(m.success_requests),SUM(m.error_requests),SUM(m.upstream_affected_requests),SUM(m.upstream_attempt_count),SUM(m.input_tokens),SUM(m.output_tokens),SUM(m.cache_creation_tokens),SUM(m.cache_read_tokens),SUM(m.ttft_sum_ms),SUM(m.ttft_count),SUM(m.duration_sum_ms),SUM(m.duration_count) FROM ` + channelMonitorV2MetricsTable(filter) + ` m LEFT JOIN groups g ON g.id=NULLIF(m.group_id,0) ` + where + ` GROUP BY ` + group
  925. rows, err := r.db.QueryContext(ctx, query, args...)
  926. if err != nil {
  927. return nil, err
  928. }
  929. defer func() { _ = rows.Close() }()
  930. facts := []channelMonitorV2Fact{}
  931. for rows.Next() {
  932. var bucket time.Time
  933. var f channelMonitorV2Fact
  934. if err := rows.Scan(&bucket, &f.Platform, &f.GroupID, &f.GroupName, &f.Model, &f.Success, &f.Errors, &f.UpstreamAffected, &f.UpstreamAttempts, &f.Input, &f.Output, &f.CacheCreation, &f.CacheRead, &f.TTFTSum, &f.TTFTCount, &f.DurationSum, &f.DurationCount); err != nil {
  935. return nil, err
  936. }
  937. f.BucketStart = bucket.UTC().Format(time.RFC3339Nano)
  938. facts = append(facts, f)
  939. }
  940. return facts, rows.Err()
  941. }
  942. func (r *channelMonitorV2Repository) loadHistograms(ctx context.Context, filter service.ChannelMonitorV2Filter, cfg service.ChannelMonitorV2Config, userID int64, byBucket bool) ([]channelMonitorV2Histogram, error) {
  943. where, args, bucketSeconds := channelMonitorV2WhereWithRollup(filter, cfg, "h")
  944. if userID < 0 {
  945. where += " AND h.user_id > 0"
  946. } else {
  947. args = append(args, userID)
  948. where += fmt.Sprintf(" AND h.user_id = $%d", len(args))
  949. }
  950. bucketExpr := "MIN(h.bucket_start)"
  951. group := "h.platform,h.group_id,h.model,h.user_id,h.metric,h.upper_bound_ms"
  952. if byBucket {
  953. if bucketSeconds > 0 {
  954. bucketExpr = "h.bucket_start"
  955. group = bucketExpr + "," + group
  956. } else {
  957. oldArgs := args
  958. args = []any{fmt.Sprintf("%d seconds", int(filter.Bucket.Seconds()))}
  959. args = append(args, oldArgs...)
  960. where = shiftSQLPlaceholders(where, 1)
  961. bucketExpr = "date_bin($1::interval,h.bucket_start,TIMESTAMPTZ '1970-01-01')"
  962. group = bucketExpr + "," + group
  963. }
  964. }
  965. rows, err := r.db.QueryContext(ctx, `SELECT `+bucketExpr+`,h.platform,h.group_id,h.model,h.user_id,h.metric,h.upper_bound_ms,SUM(h.sample_count) FROM `+channelMonitorV2HistogramTable(filter)+` h `+where+` GROUP BY `+group, args...)
  966. if err != nil {
  967. return nil, err
  968. }
  969. defer func() { _ = rows.Close() }()
  970. out := []channelMonitorV2Histogram{}
  971. for rows.Next() {
  972. var bucket time.Time
  973. var h channelMonitorV2Histogram
  974. if err := rows.Scan(&bucket, &h.Platform, &h.GroupID, &h.Model, &h.UserID, &h.Metric, &h.UpperBound, &h.Count); err != nil {
  975. return nil, err
  976. }
  977. h.BucketStart = bucket.UTC().Format(time.RFC3339Nano)
  978. out = append(out, h)
  979. }
  980. return out, rows.Err()
  981. }
  982. func (r *channelMonitorV2Repository) GetAggregationWatermark(ctx context.Context) (*service.ChannelMonitorV2AggregationWatermark, error) {
  983. var usageStart, errorStart, dataThrough, computed, backfill sql.NullTime
  984. err := r.db.QueryRowContext(ctx, `
  985. SELECT usage_coverage_start, error_coverage_start, data_through, last_successful_at, backfill_cursor
  986. FROM channel_monitor_v2_watermarks WHERE id = 1`).Scan(&usageStart, &errorStart, &dataThrough, &computed, &backfill)
  987. if err == sql.ErrNoRows {
  988. return &service.ChannelMonitorV2AggregationWatermark{}, nil
  989. }
  990. if err != nil {
  991. return nil, err
  992. }
  993. out := &service.ChannelMonitorV2AggregationWatermark{HasData: dataThrough.Valid}
  994. if usageStart.Valid {
  995. out.UsageCoverageStart = usageStart.Time.UTC()
  996. }
  997. if errorStart.Valid {
  998. out.ErrorCoverageStart = errorStart.Time.UTC()
  999. }
  1000. if dataThrough.Valid {
  1001. out.DataThrough = dataThrough.Time.UTC()
  1002. }
  1003. if computed.Valid {
  1004. out.LastSuccessfulAt = computed.Time.UTC()
  1005. }
  1006. if backfill.Valid {
  1007. out.BackfillCursor = backfill.Time.UTC()
  1008. }
  1009. return out, nil
  1010. }
  1011. func (r *channelMonitorV2Repository) loadCoverage(ctx context.Context, filter service.ChannelMonitorV2Filter) (*service.ChannelMonitorV2Coverage, error) {
  1012. wm, err := r.GetAggregationWatermark(ctx)
  1013. if err != nil {
  1014. return nil, err
  1015. }
  1016. if wm == nil {
  1017. wm = &service.ChannelMonitorV2AggregationWatermark{}
  1018. }
  1019. bootstrap := service.ChannelMonitorV2BootstrapProgress(time.Now().UTC(), wm.BackfillCursor, wm.HasData)
  1020. if !wm.HasData || wm.DataThrough.IsZero() || wm.LastSuccessfulAt.IsZero() {
  1021. return &service.ChannelMonitorV2Coverage{
  1022. RequestedStart: filter.Start,
  1023. RequestedEnd: filter.End,
  1024. CoverageStart: filter.End,
  1025. DataThrough: filter.Start,
  1026. ComputedAt: time.Time{},
  1027. AggregationLagSeconds: 0,
  1028. CoverageComplete: false,
  1029. BucketSeconds: int(filter.Bucket.Seconds()),
  1030. Bootstrap: bootstrap,
  1031. }, nil
  1032. }
  1033. coverageStart := filter.Start
  1034. if !wm.UsageCoverageStart.IsZero() && wm.UsageCoverageStart.After(coverageStart) {
  1035. coverageStart = wm.UsageCoverageStart
  1036. }
  1037. if !wm.ErrorCoverageStart.IsZero() && wm.ErrorCoverageStart.After(coverageStart) {
  1038. coverageStart = wm.ErrorCoverageStart
  1039. }
  1040. through := filter.End
  1041. if !wm.DataThrough.IsZero() && wm.DataThrough.Before(through) {
  1042. through = wm.DataThrough
  1043. }
  1044. computedAt := wm.LastSuccessfulAt
  1045. lag := int64(0)
  1046. if !through.IsZero() {
  1047. lag = int64(time.Since(through).Seconds())
  1048. if lag < 0 {
  1049. lag = 0
  1050. }
  1051. }
  1052. // Complete = history depth for this range is filled (backfill reached
  1053. // filter.Start). Do not require data_through >= filter.End: ParseFilter
  1054. // aligns End to the next whole bucket (often in the future), so a healthy
  1055. // minute-level lag would otherwise always show "partial historical coverage".
  1056. return &service.ChannelMonitorV2Coverage{
  1057. RequestedStart: filter.Start,
  1058. RequestedEnd: filter.End,
  1059. CoverageStart: coverageStart,
  1060. DataThrough: through,
  1061. ComputedAt: computedAt,
  1062. AggregationLagSeconds: lag,
  1063. CoverageComplete: channelMonitorV2HistoryCoverageComplete(coverageStart, filter.Start),
  1064. BucketSeconds: int(filter.Bucket.Seconds()),
  1065. Bootstrap: bootstrap,
  1066. }, nil
  1067. }
  1068. // channelMonitorV2HistoryCoverageComplete is true when aggregated history
  1069. // reaches the requested window start. Trailing freshness is reported via
  1070. // data_through / aggregation_lag_seconds, not coverage_complete.
  1071. func channelMonitorV2HistoryCoverageComplete(coverageStart, filterStart time.Time) bool {
  1072. if coverageStart.IsZero() || filterStart.IsZero() {
  1073. return false
  1074. }
  1075. return !coverageStart.After(filterStart)
  1076. }
  1077. func channelMonitorV2FixedBucketSeconds(filter service.ChannelMonitorV2Filter) int {
  1078. seconds := int(filter.Bucket.Seconds())
  1079. switch seconds {
  1080. case 300, 3600, 43200, 86400:
  1081. return seconds
  1082. default:
  1083. return 0
  1084. }
  1085. }
  1086. func channelMonitorV2MetricsTable(filter service.ChannelMonitorV2Filter) string {
  1087. if channelMonitorV2FixedBucketSeconds(filter) > 0 {
  1088. return "channel_monitor_v2_metrics_rollup"
  1089. }
  1090. return "channel_monitor_v2_metrics_1m"
  1091. }
  1092. func channelMonitorV2UserMetricsTable(filter service.ChannelMonitorV2Filter) string {
  1093. if channelMonitorV2FixedBucketSeconds(filter) > 0 {
  1094. return "channel_monitor_v2_user_metrics_rollup"
  1095. }
  1096. return "channel_monitor_v2_user_metrics_1m"
  1097. }
  1098. func channelMonitorV2ErrorMetricsTable(filter service.ChannelMonitorV2Filter) string {
  1099. if channelMonitorV2FixedBucketSeconds(filter) > 0 {
  1100. return "channel_monitor_v2_error_metrics_rollup"
  1101. }
  1102. return "channel_monitor_v2_error_metrics_1m"
  1103. }
  1104. func channelMonitorV2HistogramTable(filter service.ChannelMonitorV2Filter) string {
  1105. if channelMonitorV2FixedBucketSeconds(filter) > 0 {
  1106. return "channel_monitor_v2_latency_histograms_rollup"
  1107. }
  1108. return "channel_monitor_v2_latency_histograms_1m"
  1109. }
  1110. func channelMonitorV2WhereWithRollup(filter service.ChannelMonitorV2Filter, cfg service.ChannelMonitorV2Config, alias string) (string, []any, int) {
  1111. where, args := channelMonitorV2Where(filter, cfg, alias)
  1112. bucketSeconds := channelMonitorV2FixedBucketSeconds(filter)
  1113. if bucketSeconds > 0 {
  1114. args = append(args, bucketSeconds)
  1115. where += fmt.Sprintf(" AND %s.bucket_seconds = $%d", alias, len(args))
  1116. }
  1117. return where, args, bucketSeconds
  1118. }
  1119. func channelMonitorV2Where(filter service.ChannelMonitorV2Filter, cfg service.ChannelMonitorV2Config, alias string) (string, []any) {
  1120. conditions := []string{alias + ".bucket_start >= $1", alias + ".bucket_start < $2"}
  1121. args := []any{filter.Start, filter.End}
  1122. platforms := channelMonitorV2EnabledPlatforms(cfg)
  1123. if len(filter.Platforms) > 0 {
  1124. platforms = intersectStrings(platforms, filter.Platforms)
  1125. }
  1126. if len(platforms) > 0 {
  1127. args = append(args, pq.Array(platforms))
  1128. conditions = append(conditions, fmt.Sprintf("%s.platform = ANY($%d)", alias, len(args)))
  1129. } else {
  1130. conditions = append(conditions, "FALSE")
  1131. }
  1132. groups, groupScopeEmpty := channelMonitorV2ScopedGroupIDs(filter, cfg)
  1133. if groupScopeEmpty {
  1134. conditions = append(conditions, "FALSE")
  1135. } else if len(groups) > 0 {
  1136. args = append(args, pq.Array(groups))
  1137. conditions = append(conditions, fmt.Sprintf("%s.group_id = ANY($%d)", alias, len(args)))
  1138. }
  1139. return "WHERE " + strings.Join(conditions, " AND "), args
  1140. }
  1141. func channelMonitorV2EnabledPlatforms(cfg service.ChannelMonitorV2Config) []string {
  1142. out := []string{}
  1143. for _, p := range cfg.Platforms {
  1144. if p.Enabled {
  1145. out = append(out, p.Platform)
  1146. }
  1147. }
  1148. return out
  1149. }
  1150. // channelMonitorV2DisplayModel maps a raw model name for presentation.
  1151. // Semantics (parallel to empty group_ids = all groups):
  1152. // - platform not in config / disabled → keep raw model (still collected)
  1153. // - models list empty → show the real model name (no collapsing)
  1154. // - models list non-empty → selected keep identity; everything else → __other__
  1155. func channelMonitorV2DisplayModel(cfg service.ChannelMonitorV2Config, platform, model string) string {
  1156. model = strings.TrimSpace(model)
  1157. if model == "" {
  1158. return service.ChannelMonitorV2OtherModel
  1159. }
  1160. for _, p := range cfg.Platforms {
  1161. if p.Platform != platform {
  1162. continue
  1163. }
  1164. // Empty allow-list: surface every real model instead of dumping into __other__.
  1165. // Operators opt into the named + __other__ split only by listing models.
  1166. if len(p.Models) == 0 {
  1167. return model
  1168. }
  1169. for _, selected := range p.Models {
  1170. if selected == model {
  1171. return model
  1172. }
  1173. }
  1174. return service.ChannelMonitorV2OtherModel
  1175. }
  1176. // Platform not configured: still show the real model so traffic is visible.
  1177. return model
  1178. }
  1179. func channelMonitorV2ModelSelected(filter service.ChannelMonitorV2Filter, cfg service.ChannelMonitorV2Config, platform, model string) bool {
  1180. if len(filter.Models) == 0 {
  1181. return true
  1182. }
  1183. display := channelMonitorV2DisplayModel(cfg, platform, model)
  1184. for _, selected := range filter.Models {
  1185. if selected == display {
  1186. return true
  1187. }
  1188. }
  1189. return false
  1190. }
  1191. func channelMonitorV2ModelLabel(model string) string {
  1192. if model == service.ChannelMonitorV2OtherModel {
  1193. return "Other models"
  1194. }
  1195. return model
  1196. }
  1197. func channelMonitorV2CoveredMinutes(filter service.ChannelMonitorV2Filter, coverage service.ChannelMonitorV2Coverage) float64 {
  1198. start := filter.Start
  1199. if coverage.CoverageStart.After(start) {
  1200. start = coverage.CoverageStart
  1201. }
  1202. end := filter.End
  1203. if !coverage.DataThrough.IsZero() && coverage.DataThrough.Before(end) {
  1204. end = coverage.DataThrough
  1205. }
  1206. if !start.Before(end) {
  1207. return 1
  1208. }
  1209. return end.Sub(start).Minutes()
  1210. }
  1211. // Usage and error sources can have different retention windows. Composite
  1212. // health metrics must use their common interval; otherwise error rates mix
  1213. // unlike periods and RPM/TPM use a denominator shorter than their numerator.
  1214. func channelMonitorV2CommonCoverageFilter(filter service.ChannelMonitorV2Filter, coverage service.ChannelMonitorV2Coverage) service.ChannelMonitorV2Filter {
  1215. if coverage.CoverageStart.After(filter.Start) {
  1216. filter.Start = coverage.CoverageStart
  1217. }
  1218. if !coverage.DataThrough.IsZero() && coverage.DataThrough.Before(filter.End) {
  1219. filter.End = coverage.DataThrough
  1220. }
  1221. return filter
  1222. }
  1223. func intersectStrings(a, b []string) []string {
  1224. set := map[string]struct{}{}
  1225. for _, v := range b {
  1226. set[v] = struct{}{}
  1227. }
  1228. out := []string{}
  1229. for _, v := range a {
  1230. if _, ok := set[v]; ok {
  1231. out = append(out, v)
  1232. }
  1233. }
  1234. return out
  1235. }
  1236. func containsString(values []string, target string) bool {
  1237. for _, value := range values {
  1238. if value == target {
  1239. return true
  1240. }
  1241. }
  1242. return false
  1243. }
  1244. func intersectInt64(a, b []int64) []int64 {
  1245. set := map[int64]struct{}{}
  1246. for _, v := range b {
  1247. set[v] = struct{}{}
  1248. }
  1249. out := []int64{}
  1250. for _, v := range a {
  1251. if _, ok := set[v]; ok {
  1252. out = append(out, v)
  1253. }
  1254. }
  1255. return out
  1256. }
  1257. // shiftSQLPlaceholders shifts existing positional placeholders by offset.
  1258. func shiftSQLPlaceholders(query string, offset int) string {
  1259. for i := 64; i >= 1; i-- {
  1260. query = strings.ReplaceAll(query, fmt.Sprintf("$%d", i), fmt.Sprintf("$%d", i+offset))
  1261. }
  1262. return query
  1263. }
  1264. type metricAccumulator struct {
  1265. success, errors, upstreamAffected, upstreamAttempts, input, output, cacheCreation, cacheRead, ttftSum, ttftCount, durationSum, durationCount int64
  1266. hist map[string]map[int64]int64
  1267. }
  1268. func newMetricAccumulator() *metricAccumulator {
  1269. return &metricAccumulator{hist: map[string]map[int64]int64{"ttft": {}, "duration": {}}}
  1270. }
  1271. func (a *metricAccumulator) addFact(f channelMonitorV2Fact) {
  1272. a.success += f.Success
  1273. a.errors += f.Errors
  1274. a.upstreamAffected += f.UpstreamAffected
  1275. a.upstreamAttempts += f.UpstreamAttempts
  1276. a.input += f.Input
  1277. a.output += f.Output
  1278. a.cacheCreation += f.CacheCreation
  1279. a.cacheRead += f.CacheRead
  1280. a.ttftSum += f.TTFTSum
  1281. a.ttftCount += f.TTFTCount
  1282. a.durationSum += f.DurationSum
  1283. a.durationCount += f.DurationCount
  1284. }
  1285. func (a *metricAccumulator) addHistogram(h channelMonitorV2Histogram) {
  1286. if a.hist[h.Metric] == nil {
  1287. a.hist[h.Metric] = map[int64]int64{}
  1288. }
  1289. a.hist[h.Metric][h.UpperBound] += h.Count
  1290. }
  1291. func (a *metricAccumulator) metric(minutes float64, admin bool) service.ChannelMonitorV2Metric {
  1292. requests := a.success + a.errors
  1293. tokens := a.input + a.output + a.cacheCreation + a.cacheRead
  1294. denom := a.input + a.cacheCreation + a.cacheRead
  1295. if minutes <= 0 {
  1296. minutes = 1
  1297. }
  1298. m := service.ChannelMonitorV2Metric{SuccessRequests: a.success, ErrorRequests: a.errors, RequestCount: requests, InputTokens: a.input, OutputTokens: a.output, CacheCreationTokens: a.cacheCreation, CacheReadTokens: a.cacheRead, TokenCount: tokens, RPM: float64(requests) / minutes, TPM: float64(tokens) / minutes, CacheRateNumerator: a.cacheRead, CacheRateDenominator: denom, TTFT: latencyMetric(a.ttftSum, a.ttftCount, a.hist["ttft"]), Duration: latencyMetric(a.durationSum, a.durationCount, a.hist["duration"])}
  1299. if requests > 0 {
  1300. m.ErrorRate = float64(a.errors) / float64(requests)
  1301. m.SuccessRate = float64(a.success) / float64(requests)
  1302. }
  1303. if denom > 0 {
  1304. m.CacheRate = float64(a.cacheRead) / float64(denom)
  1305. }
  1306. if admin {
  1307. affected, attempts := a.upstreamAffected, a.upstreamAttempts
  1308. m.UpstreamAffectedRequests = &affected
  1309. m.UpstreamAttemptCount = &attempts
  1310. }
  1311. return m
  1312. }
  1313. func latencyMetric(sum, count int64, hist map[int64]int64) service.ChannelMonitorV2Latency {
  1314. result := service.ChannelMonitorV2Latency{SampleCount: count}
  1315. if count > 0 {
  1316. avg := float64(sum) / float64(count)
  1317. result.AvgMs = &avg
  1318. }
  1319. result.P50Ms = histPercentile(hist, 0.50)
  1320. result.P90Ms = histPercentile(hist, 0.90)
  1321. result.P95Ms = histPercentile(hist, 0.95)
  1322. return result
  1323. }
  1324. func histPercentile(hist map[int64]int64, p float64) *int64 {
  1325. var total int64
  1326. keys := make([]int64, 0, len(hist))
  1327. for bound, count := range hist {
  1328. keys = append(keys, bound)
  1329. total += count
  1330. }
  1331. if total == 0 {
  1332. return nil
  1333. }
  1334. sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
  1335. target := int64(float64(total)*p + 0.999999)
  1336. var cumulative int64
  1337. for _, bound := range keys {
  1338. cumulative += hist[bound]
  1339. if cumulative >= target {
  1340. v := bound
  1341. return &v
  1342. }
  1343. }
  1344. v := keys[len(keys)-1]
  1345. return &v
  1346. }
  1347. // applyIgnoredErrors rewrites ErrorRate so ignored categories do not inflate the
  1348. // scored error rate used by health. Absolute ErrorRequests / RequestCount stay
  1349. // for volume/RPM. SuccessRate remains true success/request (ignored errors are
  1350. // not treated as successes).
  1351. func applyIgnoredErrors(m *service.ChannelMonitorV2Metric, ignoredCount int64) {
  1352. if m == nil || ignoredCount <= 0 || m.RequestCount <= 0 {
  1353. return
  1354. }
  1355. if ignoredCount > m.ErrorRequests {
  1356. ignoredCount = m.ErrorRequests
  1357. }
  1358. countedErrors := m.ErrorRequests - ignoredCount
  1359. if countedErrors < 0 {
  1360. countedErrors = 0
  1361. }
  1362. m.ErrorRate = float64(countedErrors) / float64(m.RequestCount)
  1363. // Keep SuccessRate = SuccessRequests / RequestCount when absolute success is
  1364. // known; fall back to residual only if SuccessRequests was never populated.
  1365. if m.SuccessRequests > 0 || m.ErrorRequests >= m.RequestCount {
  1366. m.SuccessRate = float64(m.SuccessRequests) / float64(m.RequestCount)
  1367. }
  1368. if m.SuccessRate < 0 {
  1369. m.SuccessRate = 0
  1370. }
  1371. if m.SuccessRate > 1 {
  1372. m.SuccessRate = 1
  1373. }
  1374. }
  1375. // loadIgnoredErrorCounts returns per-bucket and total ignored error request counts
  1376. // for categories listed in cfg.IgnoredErrorCategories. Buckets use the same
  1377. // date_bin alignment as loadFacts when filter.Bucket is set.
  1378. func (r *channelMonitorV2Repository) loadIgnoredErrorCounts(
  1379. ctx context.Context,
  1380. filter service.ChannelMonitorV2Filter,
  1381. cfg service.ChannelMonitorV2Config,
  1382. ) (byBucket map[string]int64, total int64, err error) {
  1383. byBucket = map[string]int64{}
  1384. if len(cfg.IgnoredErrorCategories) == 0 {
  1385. return byBucket, 0, nil
  1386. }
  1387. where, args, bucketSeconds := channelMonitorV2WhereWithRollup(filter, cfg, "e")
  1388. bucketExpr := "e.bucket_start"
  1389. groupBy := "e.bucket_start, e.platform, e.model"
  1390. if filter.Bucket > 0 && bucketSeconds == 0 {
  1391. args = append([]any{fmt.Sprintf("%d seconds", int(filter.Bucket.Seconds()))}, args...)
  1392. where = shiftSQLPlaceholders(where, 1)
  1393. bucketExpr = "date_bin($1::interval,e.bucket_start,TIMESTAMPTZ '1970-01-01')"
  1394. groupBy = bucketExpr + ", e.platform, e.model"
  1395. }
  1396. args = append(args, pq.Array(cfg.IgnoredErrorCategories), service.ChannelMonitorV2TaxonomyVersion)
  1397. catIdx := len(args) - 1
  1398. taxIdx := len(args)
  1399. query := fmt.Sprintf(
  1400. `SELECT %s, e.platform, e.model, SUM(e.error_requests)
  1401. FROM `+channelMonitorV2ErrorMetricsTable(filter)+` e %s
  1402. AND e.error_category = ANY($%d) AND e.taxonomy_version = $%d
  1403. GROUP BY %s`,
  1404. bucketExpr, where, catIdx, taxIdx, groupBy,
  1405. )
  1406. rows, err := r.db.QueryContext(ctx, query, args...)
  1407. if err != nil {
  1408. return byBucket, 0, err
  1409. }
  1410. defer func() { _ = rows.Close() }()
  1411. for rows.Next() {
  1412. var bucket time.Time
  1413. var platform, model string
  1414. var count int64
  1415. if err := rows.Scan(&bucket, &platform, &model, &count); err != nil {
  1416. return byBucket, 0, err
  1417. }
  1418. if !channelMonitorV2ModelSelected(filter, cfg, platform, model) {
  1419. continue
  1420. }
  1421. key := bucket.UTC().Format(time.RFC3339Nano)
  1422. byBucket[key] += count
  1423. total += count
  1424. }
  1425. return byBucket, total, rows.Err()
  1426. }
  1427. // loadIgnoredErrorCountsByPlatformModel keys are platform + "\x00" + display model.
  1428. func (r *channelMonitorV2Repository) loadIgnoredErrorCountsByPlatformModel(
  1429. ctx context.Context,
  1430. filter service.ChannelMonitorV2Filter,
  1431. cfg service.ChannelMonitorV2Config,
  1432. ) (byPM map[string]int64, total int64, err error) {
  1433. byPM = map[string]int64{}
  1434. if len(cfg.IgnoredErrorCategories) == 0 {
  1435. return byPM, 0, nil
  1436. }
  1437. where, args, _ := channelMonitorV2WhereWithRollup(filter, cfg, "e")
  1438. args = append(args, pq.Array(cfg.IgnoredErrorCategories), service.ChannelMonitorV2TaxonomyVersion)
  1439. catIdx := len(args) - 1
  1440. taxIdx := len(args)
  1441. query := fmt.Sprintf(
  1442. `SELECT e.platform, e.model, SUM(e.error_requests)
  1443. FROM `+channelMonitorV2ErrorMetricsTable(filter)+` e %s
  1444. AND e.error_category = ANY($%d) AND e.taxonomy_version = $%d
  1445. GROUP BY e.platform, e.model`,
  1446. where, catIdx, taxIdx,
  1447. )
  1448. rows, err := r.db.QueryContext(ctx, query, args...)
  1449. if err != nil {
  1450. return byPM, 0, err
  1451. }
  1452. defer func() { _ = rows.Close() }()
  1453. for rows.Next() {
  1454. var platform, model string
  1455. var count int64
  1456. if err := rows.Scan(&platform, &model, &count); err != nil {
  1457. return byPM, 0, err
  1458. }
  1459. if !channelMonitorV2ModelSelected(filter, cfg, platform, model) {
  1460. continue
  1461. }
  1462. display := channelMonitorV2DisplayModel(cfg, platform, model)
  1463. byPM[platform+"\x00"+display] += count
  1464. total += count
  1465. }
  1466. return byPM, total, rows.Err()
  1467. }
  1468. // loadIgnoredErrorCountsByMatrixKey returns ignored counts keyed by matrix dimension
  1469. // and by (dimension, bucket). Uses the same date_bin alignment as GetMatrix facts.
  1470. func (r *channelMonitorV2Repository) loadIgnoredErrorCountsByMatrixKey(
  1471. ctx context.Context,
  1472. filter service.ChannelMonitorV2Filter,
  1473. cfg service.ChannelMonitorV2Config,
  1474. groupBy service.ChannelMonitorV2GroupBy,
  1475. ) (byDimBucket map[channelMonitorV2MatrixKey]map[string]int64, byDim map[channelMonitorV2MatrixKey]int64, err error) {
  1476. byDimBucket = map[channelMonitorV2MatrixKey]map[string]int64{}
  1477. byDim = map[channelMonitorV2MatrixKey]int64{}
  1478. if len(cfg.IgnoredErrorCategories) == 0 {
  1479. return byDimBucket, byDim, nil
  1480. }
  1481. where, args, bucketSeconds := channelMonitorV2WhereWithRollup(filter, cfg, "e")
  1482. bucketExpr := "e.bucket_start"
  1483. groupSQL := "e.bucket_start, e.platform, e.group_id, e.model"
  1484. if filter.Bucket > 0 && bucketSeconds == 0 {
  1485. args = append([]any{fmt.Sprintf("%d seconds", int(filter.Bucket.Seconds()))}, args...)
  1486. where = shiftSQLPlaceholders(where, 1)
  1487. bucketExpr = "date_bin($1::interval,e.bucket_start,TIMESTAMPTZ '1970-01-01')"
  1488. groupSQL = bucketExpr + ", e.platform, e.group_id, e.model"
  1489. }
  1490. args = append(args, pq.Array(cfg.IgnoredErrorCategories), service.ChannelMonitorV2TaxonomyVersion)
  1491. catIdx := len(args) - 1
  1492. taxIdx := len(args)
  1493. query := fmt.Sprintf(
  1494. `SELECT %s, e.platform, e.group_id, e.model, SUM(e.error_requests)
  1495. FROM `+channelMonitorV2ErrorMetricsTable(filter)+` e %s
  1496. AND e.error_category = ANY($%d) AND e.taxonomy_version = $%d
  1497. GROUP BY %s`,
  1498. bucketExpr, where, catIdx, taxIdx, groupSQL,
  1499. )
  1500. rows, err := r.db.QueryContext(ctx, query, args...)
  1501. if err != nil {
  1502. return byDimBucket, byDim, err
  1503. }
  1504. defer func() { _ = rows.Close() }()
  1505. for rows.Next() {
  1506. var bucket time.Time
  1507. var platform, model string
  1508. var groupID, count int64
  1509. if err := rows.Scan(&bucket, &platform, &groupID, &model, &count); err != nil {
  1510. return byDimBucket, byDim, err
  1511. }
  1512. if !channelMonitorV2ModelSelected(filter, cfg, platform, model) {
  1513. continue
  1514. }
  1515. bucketKey := bucket.UTC().Format(time.RFC3339Nano)
  1516. key := channelMonitorV2MatrixDimensionKey(groupBy, cfg, platform, groupID, model)
  1517. byDim[key] += count
  1518. if byDimBucket[key] == nil {
  1519. byDimBucket[key] = map[string]int64{}
  1520. }
  1521. byDimBucket[key][bucketKey] += count
  1522. }
  1523. return byDimBucket, byDim, rows.Err()
  1524. }