| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- package main
- import (
- "context"
- "crypto/sha256"
- "database/sql"
- "encoding/hex"
- "encoding/json"
- "flag"
- "fmt"
- "log"
- "sort"
- "strings"
- "time"
- _ "github.com/Wei-Shaw/sub2api/ent/runtime"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/Wei-Shaw/sub2api/internal/repository"
- "github.com/lib/pq"
- )
- const classifierVersion = "ingress-reject-v1"
- type candidate struct {
- id int64
- statusCode int
- message string
- body string
- }
- func main() {
- beforeRaw := flag.String("before", "", "required RFC3339 cutoff; only older rows are considered")
- execute := flag.Bool("execute", false, "delete matched rows (default is dry-run)")
- batchSize := flag.Int("batch-size", 5000, "scan/delete batch size (1-5000)")
- flag.Parse()
- if *beforeRaw == "" {
- log.Fatal("--before is required")
- }
- before, err := time.Parse(time.RFC3339, *beforeRaw)
- if err != nil {
- log.Fatalf("invalid --before: %v", err)
- }
- if *batchSize < 1 || *batchSize > 5000 {
- log.Fatal("--batch-size must be between 1 and 5000")
- }
- cfg, err := config.LoadForBootstrap()
- if err != nil {
- log.Fatalf("load config: %v", err)
- }
- client, db, err := repository.InitEnt(cfg)
- if err != nil {
- log.Fatalf("initialize database: %v", err)
- }
- defer func() { _ = client.Close() }()
- ctx := context.Background()
- counts, scanned, matched, deleted, err := cleanup(ctx, db, before, *batchSize, *execute)
- if err != nil {
- log.Fatalf("cleanup failed: %v", err)
- }
- digest := sha256.Sum256([]byte(classifierVersion))
- mode := "dry-run"
- if *execute {
- mode = "execute"
- }
- fmt.Printf("mode=%s before=%s classifier=%s scanned=%d matched=%d deleted=%d\n",
- mode, before.UTC().Format(time.RFC3339), hex.EncodeToString(digest[:]), scanned, matched, deleted)
- reasons := make([]string, 0, len(counts))
- for reason := range counts {
- reasons = append(reasons, reason)
- }
- sort.Strings(reasons)
- for _, reason := range reasons {
- fmt.Printf("reason=%s count=%d\n", reason, counts[reason])
- }
- if *execute && deleted > 0 {
- fmt.Println("cleanup complete; schedule VACUUM (ANALYZE) ops_error_logs during normal maintenance")
- }
- }
- func cleanup(ctx context.Context, db *sql.DB, before time.Time, batchSize int, execute bool) (map[string]int64, int64, int64, int64, error) {
- counts := make(map[string]int64)
- var cursor, scanned, matched, deleted int64
- for {
- rows, err := db.QueryContext(ctx, `
- SELECT id, COALESCE(status_code, 0), COALESCE(error_message, ''), COALESCE(error_body, '')
- FROM ops_error_logs
- WHERE id > $1
- AND created_at < $2
- AND error_phase = 'auth'
- AND account_id IS NULL
- AND upstream_status_code IS NULL
- AND COALESCE(upstream_error_message, '') = ''
- AND COALESCE(upstream_error_detail, '') = ''
- ORDER BY id ASC
- LIMIT $3`, cursor, before, batchSize)
- if err != nil {
- return nil, scanned, matched, deleted, err
- }
- batch := make([]candidate, 0, batchSize)
- for rows.Next() {
- var item candidate
- if err := rows.Scan(&item.id, &item.statusCode, &item.message, &item.body); err != nil {
- _ = rows.Close()
- return nil, scanned, matched, deleted, err
- }
- batch = append(batch, item)
- cursor = item.id
- }
- if err := rows.Err(); err != nil {
- _ = rows.Close()
- return nil, scanned, matched, deleted, err
- }
- _ = rows.Close()
- if len(batch) == 0 {
- break
- }
- ids := make([]int64, 0, len(batch))
- for _, item := range batch {
- scanned++
- if reason, ok := historicalIngressRejectReason(item); ok {
- matched++
- counts[reason]++
- ids = append(ids, item.id)
- }
- }
- if execute && len(ids) > 0 {
- result, err := db.ExecContext(ctx,
- `DELETE FROM ops_error_logs WHERE id = ANY($1) AND created_at < $2`, pq.Array(ids), before)
- if err != nil {
- return nil, scanned, matched, deleted, err
- }
- n, err := result.RowsAffected()
- if err != nil {
- return nil, scanned, matched, deleted, err
- }
- deleted += n
- }
- }
- return counts, scanned, matched, deleted, nil
- }
- func historicalIngressRejectReason(item candidate) (string, bool) {
- code, message := parseErrorIdentity(item.body, item.message)
- switch code {
- case "API_KEY_REQUIRED":
- return "missing_key", true
- case "INVALID_API_KEY":
- return "invalid_key", true
- case "API_KEY_DISABLED":
- return "key_disabled", true
- case "USER_INACTIVE":
- return "user_inactive", true
- case "GROUP_DELETED":
- return "group_deleted", true
- case "GROUP_DISABLED":
- return "group_disabled", true
- case "GROUP_NOT_ALLOWED":
- return "group_forbidden", true
- case "ACCESS_DENIED":
- return "ip_acl_denied", true
- case "api_key_in_query_deprecated":
- return "query_key_deprecated", true
- }
- normalized := strings.TrimSpace(message)
- switch {
- case normalized == "API key is required":
- return "missing_key", true
- case normalized == "Invalid API key":
- return "invalid_key", true
- case normalized == "API key is disabled":
- return "key_disabled", true
- case normalized == "User account is not active":
- return "user_inactive", true
- case normalized == "API Key 所属分组已删除":
- return "group_deleted", true
- case normalized == "API Key 所属分组已停用":
- return "group_disabled", true
- case normalized == "API Key 所属专属分组不再允许当前用户使用":
- return "group_forbidden", true
- case normalized == "API Key is not assigned to any group and cannot be used. Please contact the administrator to assign it to a group.":
- return "group_unassigned", true
- case strings.HasPrefix(normalized, "Access denied. Your IP is "):
- return "ip_acl_denied", true
- case normalized == "Query parameter api_key is deprecated. Use Authorization header or key instead.":
- return "query_key_deprecated", true
- default:
- return "", false
- }
- }
- func parseErrorIdentity(body, fallbackMessage string) (string, string) {
- var payload struct {
- Code string `json:"code"`
- Message string `json:"message"`
- Error struct {
- Code json.RawMessage `json:"code"`
- Message string `json:"message"`
- } `json:"error"`
- }
- if err := json.Unmarshal([]byte(body), &payload); err != nil {
- return "", fallbackMessage
- }
- message := payload.Message
- if message == "" {
- message = payload.Error.Message
- }
- if message == "" {
- message = fallbackMessage
- }
- return strings.TrimSpace(payload.Code), message
- }
|