spend_guard.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. package service
  2. import (
  3. "context"
  4. "database/sql"
  5. "encoding/json"
  6. "errors"
  7. "log"
  8. "log/slog"
  9. "strings"
  10. "sync"
  11. "time"
  12. )
  13. // SettingKeySpendGuardSettings 烧钱防护配置(JSON,见 SpendGuardSettings)。
  14. const SettingKeySpendGuardSettings = "spend_guard_settings"
  15. // SpendGuardSettings 烧钱防护阈值:窗口内某 key 的 token 速率或错误率
  16. // 超阈即冻结该 key(status=disabled),仅管理员手动解冻。
  17. type SpendGuardSettings struct {
  18. Enabled bool `json:"enabled"`
  19. WindowMinutes int `json:"window_minutes"`
  20. TokensPerMinute float64 `json:"tokens_per_minute"`
  21. MinRequests int `json:"min_requests"`
  22. MaxErrorRate float64 `json:"max_error_rate"`
  23. IntervalSeconds int `json:"interval_seconds"`
  24. }
  25. // DefaultSpendGuardSettings 默认:5 分钟窗口,速率 ≥200 万 token/分钟或
  26. // 错误率 ≥80%(≥5 请求)冻结。默认关闭,由管理员显式启用。
  27. func DefaultSpendGuardSettings() SpendGuardSettings {
  28. return SpendGuardSettings{
  29. Enabled: false, WindowMinutes: 5, TokensPerMinute: 2000000,
  30. MinRequests: 5, MaxErrorRate: 0.8, IntervalSeconds: 60,
  31. }
  32. }
  33. // SpendGuardOffender 烧钱嫌疑 key 行(管理端展示)。
  34. type SpendGuardOffender struct {
  35. APIKeyID int64 `json:"api_key_id"`
  36. Name string `json:"name"`
  37. UserID int64 `json:"user_id"`
  38. Status string `json:"status"`
  39. Requests int64 `json:"requests"`
  40. Tokens int64 `json:"tokens"`
  41. TokensPerMin float64 `json:"tokens_per_min"`
  42. Errors int64 `json:"errors"`
  43. ErrRate float64 `json:"err_rate"`
  44. Frozen bool `json:"frozen"`
  45. }
  46. // SpendGuardEvent 冻结/解冻事件(内存保留近 100 条)。
  47. type SpendGuardEvent struct {
  48. At time.Time `json:"at"`
  49. APIKeyID int64 `json:"api_key_id"`
  50. Name string `json:"name"`
  51. Action string `json:"action"` // frozen / unfrozen
  52. Reason string `json:"reason"`
  53. }
  54. // SpendGuardKeyStore 冻结需要的 key 读写接口(APIKeyService 已实现)。
  55. type SpendGuardKeyStore interface {
  56. GetByID(ctx context.Context, id int64) (*APIKey, error)
  57. Update(ctx context.Context, id int64, userID int64, req UpdateAPIKeyRequest) (*APIKey, error)
  58. }
  59. // SpendGuardService 烧钱防护:窗口 token 速率 / 错误率超阈自动冻结 key。
  60. type SpendGuardService struct {
  61. db *sql.DB
  62. keys SpendGuardKeyStore
  63. settings SettingRepository
  64. mu sync.RWMutex
  65. frozen map[int64]time.Time
  66. events []SpendGuardEvent
  67. stopCh chan struct{}
  68. stopOnce sync.Once
  69. wg sync.WaitGroup
  70. }
  71. // NewSpendGuardService 创建烧钱防护服务(不启动后台循环)。
  72. func NewSpendGuardService(db *sql.DB, keys SpendGuardKeyStore, settingRepo SettingRepository) *SpendGuardService {
  73. return &SpendGuardService{
  74. db: db, keys: keys, settings: settingRepo,
  75. frozen: make(map[int64]time.Time), stopCh: make(chan struct{}),
  76. }
  77. }
  78. // ProvideSpendGuardService 创建并启动烧钱防护服务(wire 用)。
  79. func ProvideSpendGuardService(db *sql.DB, apiKeyService *APIKeyService, settingRepo SettingRepository) *SpendGuardService {
  80. svc := NewSpendGuardService(db, apiKeyService, settingRepo)
  81. svc.Start()
  82. return svc
  83. }
  84. // Start 启动周期评估。
  85. func (s *SpendGuardService) Start() {
  86. if s == nil || s.db == nil || s.keys == nil {
  87. return
  88. }
  89. s.wg.Add(1)
  90. go func() {
  91. defer s.wg.Done()
  92. ticker := time.NewTicker(time.Duration(s.currentSettings().IntervalSeconds) * time.Second)
  93. defer ticker.Stop()
  94. s.runOnce()
  95. for {
  96. select {
  97. case <-ticker.C:
  98. s.runOnce()
  99. case <-s.stopCh:
  100. return
  101. }
  102. }
  103. }()
  104. }
  105. // Stop 停止后台循环。
  106. func (s *SpendGuardService) Stop() {
  107. if s == nil {
  108. return
  109. }
  110. s.stopOnce.Do(func() { close(s.stopCh) })
  111. s.wg.Wait()
  112. }
  113. func (s *SpendGuardService) currentSettings() SpendGuardSettings {
  114. cfg := DefaultSpendGuardSettings()
  115. if s == nil || s.settings == nil {
  116. return cfg
  117. }
  118. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  119. defer cancel()
  120. raw, err := s.settings.GetValue(ctx, SettingKeySpendGuardSettings)
  121. if err != nil || strings.TrimSpace(raw) == "" {
  122. return cfg
  123. }
  124. var parsed SpendGuardSettings
  125. if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
  126. slog.Warn("spend_guard.bad_settings", "error", err)
  127. return cfg
  128. }
  129. return normalizeSpendGuardSettings(parsed)
  130. }
  131. func normalizeSpendGuardSettings(in SpendGuardSettings) SpendGuardSettings {
  132. def := DefaultSpendGuardSettings()
  133. if in.WindowMinutes <= 0 {
  134. in.WindowMinutes = def.WindowMinutes
  135. }
  136. if in.TokensPerMinute < 0 {
  137. in.TokensPerMinute = def.TokensPerMinute
  138. }
  139. if in.MinRequests <= 0 {
  140. in.MinRequests = def.MinRequests
  141. }
  142. if in.MaxErrorRate <= 0 || in.MaxErrorRate > 1 {
  143. in.MaxErrorRate = def.MaxErrorRate
  144. }
  145. if in.IntervalSeconds < 10 {
  146. in.IntervalSeconds = def.IntervalSeconds
  147. }
  148. return in
  149. }
  150. // GetSettings 读取当前配置。
  151. func (s *SpendGuardService) GetSettings(_ context.Context) SpendGuardSettings {
  152. if s == nil {
  153. return DefaultSpendGuardSettings()
  154. }
  155. return s.currentSettings()
  156. }
  157. // UpdateSettings 保存配置。
  158. func (s *SpendGuardService) UpdateSettings(ctx context.Context, in SpendGuardSettings) (SpendGuardSettings, error) {
  159. if s == nil || s.settings == nil {
  160. return DefaultSpendGuardSettings(), nil
  161. }
  162. norm := normalizeSpendGuardSettings(in)
  163. raw, err := json.Marshal(norm)
  164. if err != nil {
  165. return norm, err
  166. }
  167. if err := s.settings.Set(ctx, SettingKeySpendGuardSettings, string(raw)); err != nil {
  168. return norm, err
  169. }
  170. return norm, nil
  171. }
  172. // Events 返回近期冻结/解冻事件。
  173. func (s *SpendGuardService) Events() []SpendGuardEvent {
  174. if s == nil {
  175. return nil
  176. }
  177. s.mu.RLock()
  178. defer s.mu.RUnlock()
  179. out := make([]SpendGuardEvent, len(s.events))
  180. copy(out, s.events)
  181. return out
  182. }
  183. func (s *SpendGuardService) pushEvent(ev SpendGuardEvent) {
  184. s.mu.Lock()
  185. defer s.mu.Unlock()
  186. s.events = append([]SpendGuardEvent{ev}, s.events...)
  187. if len(s.events) > 100 {
  188. s.events = s.events[:100]
  189. }
  190. }
  191. type spendGuardRow struct {
  192. keyID int64
  193. name string
  194. userID int64
  195. status string
  196. reqs int64
  197. tokens int64
  198. errs int64
  199. }
  200. // Offenders 查询窗口内各 key 的用量与错误(管理端列表与评估共用)。
  201. func (s *SpendGuardService) Offenders(ctx context.Context, windowMinutes int) ([]SpendGuardOffender, error) {
  202. if s == nil || s.db == nil {
  203. return nil, nil
  204. }
  205. if windowMinutes <= 0 {
  206. windowMinutes = 5
  207. }
  208. // usage 与 errors 双源 UNION:纯报错(零成功行)的 key 也必须可见,
  209. // 否则错误率冻结对最坏情况失效。
  210. q := `
  211. SELECT ak.id, COALESCE(ak.name, ''), COALESCE(ak.user_id, 0), COALESCE(ak.status, ''),
  212. COALESCE(SUM(t.reqs), 0), COALESCE(SUM(t.toks), 0), COALESCE(SUM(t.errs), 0)
  213. FROM (
  214. SELECT api_key_id AS key_id, COUNT(*) AS reqs,
  215. SUM(input_tokens + output_tokens + cache_read_tokens + cache_creation_tokens) AS toks,
  216. 0 AS errs
  217. FROM usage_logs
  218. WHERE created_at >= NOW() - ($1 || ' minutes')::interval
  219. AND api_key_id IS NOT NULL AND api_key_id > 0
  220. GROUP BY api_key_id
  221. UNION ALL
  222. SELECT api_key_id AS key_id, 0 AS reqs, 0 AS toks, COUNT(*) AS errs
  223. FROM ops_error_logs
  224. WHERE created_at >= NOW() - ($1 || ' minutes')::interval
  225. AND COALESCE(status_code, 0) >= 400
  226. AND NOT COALESCE(is_business_limited, FALSE)
  227. AND api_key_id IS NOT NULL
  228. GROUP BY api_key_id
  229. ) t
  230. JOIN api_keys ak ON ak.id = t.key_id
  231. GROUP BY ak.id, ak.name, ak.user_id, ak.status
  232. ORDER BY SUM(t.toks) DESC, SUM(t.errs) DESC
  233. LIMIT 500`
  234. rows, err := s.db.QueryContext(ctx, q, windowMinutes)
  235. if err != nil {
  236. return nil, err
  237. }
  238. defer func() { _ = rows.Close() }()
  239. var out []SpendGuardOffender
  240. for rows.Next() {
  241. var r spendGuardRow
  242. if err := rows.Scan(&r.keyID, &r.name, &r.userID, &r.status, &r.reqs, &r.tokens, &r.errs); err != nil {
  243. return nil, err
  244. }
  245. o := SpendGuardOffender{
  246. APIKeyID: r.keyID, Name: r.name, UserID: r.userID, Status: r.status,
  247. Requests: r.reqs, Tokens: r.tokens, Errors: r.errs,
  248. TokensPerMin: float64(r.tokens) / float64(windowMinutes),
  249. }
  250. if total := r.reqs + r.errs; total > 0 {
  251. o.ErrRate = float64(r.errs) / float64(total)
  252. }
  253. s.mu.RLock()
  254. _, o.Frozen = s.frozen[r.keyID]
  255. s.mu.RUnlock()
  256. out = append(out, o)
  257. }
  258. return out, rows.Err()
  259. }
  260. func spendGuardBreach(o SpendGuardOffender, cfg SpendGuardSettings) (bool, string) {
  261. if o.Requests+o.Errors < int64(cfg.MinRequests) {
  262. return false, ""
  263. }
  264. if cfg.TokensPerMinute > 0 && o.TokensPerMin >= cfg.TokensPerMinute {
  265. return true, "token velocity"
  266. }
  267. if o.Errors > 0 && o.ErrRate >= cfg.MaxErrorRate {
  268. return true, "error rate"
  269. }
  270. return false, ""
  271. }
  272. func (s *SpendGuardService) runOnce() {
  273. cfg := s.currentSettings()
  274. if !cfg.Enabled {
  275. return
  276. }
  277. ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
  278. defer cancel()
  279. offenders, err := s.Offenders(ctx, cfg.WindowMinutes)
  280. if err != nil {
  281. log.Printf("[SpendGuard] offenders query failed: %v", err)
  282. return
  283. }
  284. now := time.Now()
  285. for _, o := range offenders {
  286. ok, reason := spendGuardBreach(o, cfg)
  287. if !ok {
  288. continue
  289. }
  290. s.mu.RLock()
  291. _, already := s.frozen[o.APIKeyID]
  292. s.mu.RUnlock()
  293. if already || o.Status != StatusAPIKeyActive {
  294. continue
  295. }
  296. key, err := s.keys.GetByID(ctx, o.APIKeyID)
  297. if err != nil || key == nil || key.Status != StatusAPIKeyActive {
  298. continue
  299. }
  300. disabled := StatusAPIKeyDisabled
  301. if _, err := s.keys.Update(ctx, o.APIKeyID, key.UserID, UpdateAPIKeyRequest{Status: &disabled}); err != nil {
  302. log.Printf("[SpendGuard] freeze key %d failed: %v", o.APIKeyID, err)
  303. continue
  304. }
  305. s.mu.Lock()
  306. s.frozen[o.APIKeyID] = now
  307. s.mu.Unlock()
  308. slog.Warn("spend_guard.key_frozen", "api_key_id", o.APIKeyID, "reason", reason,
  309. "tokens_per_min", o.TokensPerMin, "err_rate", o.ErrRate)
  310. s.pushEvent(SpendGuardEvent{At: now, APIKeyID: o.APIKeyID, Name: o.Name,
  311. Action: "frozen", Reason: reason})
  312. }
  313. }
  314. // Unfreeze 手动解冻 key(恢复 active 并清除跟踪)。
  315. // 仅当 key 当前为 disabled 才写回,避免覆盖 quota_exhausted/expired 等中间态。
  316. func (s *SpendGuardService) Unfreeze(ctx context.Context, id int64) error {
  317. if s == nil || s.keys == nil {
  318. return nil
  319. }
  320. key, err := s.keys.GetByID(ctx, id)
  321. if err != nil {
  322. return err
  323. }
  324. if key == nil {
  325. return errors.New("api key not found")
  326. }
  327. if key.Status != StatusAPIKeyDisabled {
  328. s.mu.Lock()
  329. delete(s.frozen, id)
  330. s.mu.Unlock()
  331. return nil
  332. }
  333. active := StatusAPIKeyActive
  334. if _, err := s.keys.Update(ctx, id, key.UserID, UpdateAPIKeyRequest{Status: &active}); err != nil {
  335. return err
  336. }
  337. s.mu.Lock()
  338. delete(s.frozen, id)
  339. s.mu.Unlock()
  340. s.pushEvent(SpendGuardEvent{At: time.Now(), APIKeyID: id, Name: key.Name,
  341. Action: "unfrozen", Reason: "manual"})
  342. return nil
  343. }