ops_ingress_reject.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. package service
  2. import (
  3. "context"
  4. "log"
  5. "strings"
  6. "sync"
  7. "sync/atomic"
  8. "time"
  9. )
  10. const (
  11. ingressRejectShardCount = 16
  12. ingressRejectMaxEntries = 8192
  13. ingressRejectMaxPendingBatches = 4
  14. ingressRejectBucketSize = time.Minute
  15. ingressRejectFlushInterval = 5 * time.Second
  16. ingressRejectFlushTimeout = 5 * time.Second
  17. )
  18. type OpsIngressRejectAggregate struct {
  19. ID int64 `json:"id"`
  20. BucketStart time.Time `json:"bucket_start"`
  21. RejectReason string `json:"reject_reason"`
  22. RouteFamily string `json:"route_family"`
  23. Protocol string `json:"protocol"`
  24. ClientIP string `json:"client_ip"`
  25. UserID *int64 `json:"user_id,omitempty"`
  26. APIKeyID *int64 `json:"api_key_id,omitempty"`
  27. RequestCount int64 `json:"request_count"`
  28. FirstSeen time.Time `json:"first_seen"`
  29. LastSeen time.Time `json:"last_seen"`
  30. }
  31. type OpsIngressRejectFilter struct {
  32. StartTime *time.Time
  33. EndTime *time.Time
  34. RejectReason string
  35. RouteFamily string
  36. Protocol string
  37. ClientIP string
  38. UserID *int64
  39. APIKeyID *int64
  40. Page int
  41. PageSize int
  42. }
  43. type OpsIngressRejectList struct {
  44. Items []*OpsIngressRejectAggregate `json:"items"`
  45. Total int `json:"total"`
  46. Page int `json:"page"`
  47. PageSize int `json:"page_size"`
  48. }
  49. type OpsIngressRejectHealth struct {
  50. Cardinality int64 `json:"cardinality"`
  51. Capacity int `json:"capacity"`
  52. PendingBatches int `json:"pending_batches"`
  53. PendingRows int `json:"pending_rows"`
  54. Overflowed uint64 `json:"overflowed_count"`
  55. Dropped uint64 `json:"dropped_count"`
  56. Flushed uint64 `json:"flushed_request_count"`
  57. FlushFailures uint64 `json:"flush_failure_count"`
  58. Accepting bool `json:"accepting"`
  59. LastError string `json:"last_error,omitempty"`
  60. }
  61. type OpsIngressRejectRepository interface {
  62. BatchUpsertIngressRejects(ctx context.Context, items []*OpsIngressRejectAggregate) error
  63. ListIngressRejects(ctx context.Context, filter *OpsIngressRejectFilter) (*OpsIngressRejectList, error)
  64. }
  65. type ingressRejectKey struct {
  66. reason string
  67. routeFamily string
  68. protocol string
  69. clientIP string
  70. userID int64
  71. apiKeyID int64
  72. }
  73. type ingressRejectShard struct {
  74. mu sync.Mutex
  75. items map[ingressRejectKey]*OpsIngressRejectAggregate
  76. }
  77. type OpsIngressRejectAggregator struct {
  78. repo OpsIngressRejectRepository
  79. shards [ingressRejectShardCount]ingressRejectShard
  80. recordMu sync.RWMutex
  81. snapshotMu sync.Mutex
  82. bucket atomic.Int64
  83. cardinality atomic.Int64
  84. overflowMu sync.Mutex
  85. overflow *OpsIngressRejectAggregate
  86. pendingMu sync.Mutex
  87. pending [][]*OpsIngressRejectAggregate
  88. ctx context.Context
  89. cancel context.CancelFunc
  90. wg sync.WaitGroup
  91. flushCh chan struct{}
  92. started atomic.Bool
  93. accepting atomic.Bool
  94. stopOnce sync.Once
  95. overflowed atomic.Uint64
  96. dropped atomic.Uint64
  97. flushed atomic.Uint64
  98. flushFailures atomic.Uint64
  99. lastError atomic.Value
  100. }
  101. func NewOpsIngressRejectAggregator(repo OpsIngressRejectRepository) *OpsIngressRejectAggregator {
  102. ctx, cancel := context.WithCancel(context.Background())
  103. a := &OpsIngressRejectAggregator{
  104. repo: repo, ctx: ctx, cancel: cancel, flushCh: make(chan struct{}, 1),
  105. }
  106. for i := range a.shards {
  107. a.shards[i].items = make(map[ingressRejectKey]*OpsIngressRejectAggregate)
  108. }
  109. a.lastError.Store("")
  110. return a
  111. }
  112. func (a *OpsIngressRejectAggregator) Start() {
  113. if a == nil || a.repo == nil || !a.started.CompareAndSwap(false, true) {
  114. return
  115. }
  116. a.accepting.Store(true)
  117. a.wg.Add(1)
  118. go func() {
  119. defer a.wg.Done()
  120. ticker := time.NewTicker(ingressRejectFlushInterval)
  121. defer ticker.Stop()
  122. for {
  123. select {
  124. case <-a.ctx.Done():
  125. return
  126. case <-ticker.C:
  127. a.snapshotAndEnqueue(false)
  128. a.flushPending()
  129. case <-a.flushCh:
  130. a.flushPending()
  131. }
  132. }
  133. }()
  134. }
  135. func (a *OpsIngressRejectAggregator) Stop() {
  136. if a == nil {
  137. return
  138. }
  139. a.stopOnce.Do(func() {
  140. a.accepting.Store(false)
  141. a.recordMu.Lock()
  142. a.cancel()
  143. a.recordMu.Unlock()
  144. a.wg.Wait()
  145. a.snapshotAndEnqueue(false)
  146. a.flushPending()
  147. })
  148. }
  149. func (a *OpsIngressRejectAggregator) RecordIngressReject(reason, routeFamily, protocol, clientIP string, userID, apiKeyID int64) {
  150. if a == nil || a.repo == nil || !a.accepting.Load() {
  151. return
  152. }
  153. now := time.Now().UTC()
  154. bucket := now.Truncate(ingressRejectBucketSize)
  155. key := ingressRejectKey{
  156. reason: boundedDimension(reason, "unknown"), routeFamily: boundedDimension(routeFamily, "other"),
  157. protocol: boundedDimension(protocol, "other"), clientIP: boundedDimension(clientIP, "0.0.0.0"),
  158. userID: userID, apiKeyID: apiKeyID,
  159. }
  160. for {
  161. a.ensureBucket(bucket)
  162. a.recordMu.RLock()
  163. if !a.accepting.Load() {
  164. a.recordMu.RUnlock()
  165. return
  166. }
  167. bucketUnix := a.bucket.Load()
  168. if bucketUnix != bucket.Unix() {
  169. a.recordMu.RUnlock()
  170. continue
  171. }
  172. shard := &a.shards[ingressRejectHash(key)%ingressRejectShardCount]
  173. shard.mu.Lock()
  174. if item := shard.items[key]; item != nil {
  175. item.RequestCount++
  176. item.LastSeen = now
  177. shard.mu.Unlock()
  178. a.recordMu.RUnlock()
  179. return
  180. }
  181. if a.reserveDimension() {
  182. shard.items[key] = aggregateFromKey(key, bucket, now)
  183. shard.mu.Unlock()
  184. a.recordMu.RUnlock()
  185. return
  186. }
  187. shard.mu.Unlock()
  188. a.recordOverflow(bucket, now)
  189. a.recordMu.RUnlock()
  190. return
  191. }
  192. }
  193. func (a *OpsIngressRejectAggregator) ensureBucket(bucket time.Time) {
  194. if a.bucket.Load() == bucket.Unix() {
  195. return
  196. }
  197. a.snapshotMu.Lock()
  198. defer a.snapshotMu.Unlock()
  199. a.recordMu.Lock()
  200. defer a.recordMu.Unlock()
  201. if a.bucket.Load() == bucket.Unix() {
  202. return
  203. }
  204. items := a.snapshotLocked(true)
  205. a.enqueue(items)
  206. a.bucket.Store(bucket.Unix())
  207. a.cardinality.Store(0)
  208. select {
  209. case a.flushCh <- struct{}{}:
  210. default:
  211. }
  212. }
  213. func (a *OpsIngressRejectAggregator) reserveDimension() bool {
  214. for {
  215. current := a.cardinality.Load()
  216. if current >= ingressRejectMaxEntries-1 {
  217. return false
  218. }
  219. if a.cardinality.CompareAndSwap(current, current+1) {
  220. return true
  221. }
  222. }
  223. }
  224. func (a *OpsIngressRejectAggregator) recordOverflow(bucket, now time.Time) {
  225. a.overflowed.Add(1)
  226. a.overflowMu.Lock()
  227. defer a.overflowMu.Unlock()
  228. if a.overflow == nil || !a.overflow.BucketStart.Equal(bucket) {
  229. a.overflow = &OpsIngressRejectAggregate{
  230. BucketStart: bucket, RejectReason: "other", RouteFamily: "other", Protocol: "other",
  231. ClientIP: "0.0.0.0", RequestCount: 1, FirstSeen: now, LastSeen: now,
  232. }
  233. return
  234. }
  235. a.overflow.RequestCount++
  236. a.overflow.LastSeen = now
  237. }
  238. func (a *OpsIngressRejectAggregator) snapshotAndEnqueue(reset bool) {
  239. if a == nil || a.repo == nil {
  240. return
  241. }
  242. a.snapshotMu.Lock()
  243. items := a.snapshotLocked(reset)
  244. a.snapshotMu.Unlock()
  245. a.enqueue(items)
  246. }
  247. // snapshotLocked captures counter deltas. When reset is false the dimension keys
  248. // remain resident for the whole minute, so periodic flushes cannot reset the budget.
  249. func (a *OpsIngressRejectAggregator) snapshotLocked(reset bool) []*OpsIngressRejectAggregate {
  250. items := make([]*OpsIngressRejectAggregate, 0)
  251. for i := range a.shards {
  252. shard := &a.shards[i]
  253. shard.mu.Lock()
  254. for key, item := range shard.items {
  255. if item.RequestCount > 0 {
  256. copyItem := *item
  257. items = append(items, &copyItem)
  258. item.RequestCount = 0
  259. }
  260. if reset {
  261. delete(shard.items, key)
  262. }
  263. }
  264. shard.mu.Unlock()
  265. }
  266. a.overflowMu.Lock()
  267. if a.overflow != nil && a.overflow.RequestCount > 0 {
  268. copyItem := *a.overflow
  269. items = append(items, &copyItem)
  270. a.overflow.RequestCount = 0
  271. }
  272. if reset {
  273. a.overflow = nil
  274. }
  275. a.overflowMu.Unlock()
  276. return items
  277. }
  278. func (a *OpsIngressRejectAggregator) enqueue(items []*OpsIngressRejectAggregate) {
  279. if len(items) == 0 {
  280. return
  281. }
  282. a.pendingMu.Lock()
  283. defer a.pendingMu.Unlock()
  284. if len(a.pending) >= ingressRejectMaxPendingBatches {
  285. a.dropped.Add(batchRequestCount(items))
  286. return
  287. }
  288. a.pending = append(a.pending, items)
  289. }
  290. func (a *OpsIngressRejectAggregator) flushPending() {
  291. for {
  292. a.pendingMu.Lock()
  293. if len(a.pending) == 0 {
  294. a.pendingMu.Unlock()
  295. return
  296. }
  297. batch := a.pending[0]
  298. a.pendingMu.Unlock()
  299. ctx, cancel := context.WithTimeout(context.Background(), ingressRejectFlushTimeout)
  300. err := a.repo.BatchUpsertIngressRejects(ctx, batch)
  301. cancel()
  302. if err != nil {
  303. a.flushFailures.Add(1)
  304. a.lastError.Store(err.Error())
  305. log.Printf("[IngressRejectAggregator] flush failed: %v", err)
  306. return
  307. }
  308. a.pendingMu.Lock()
  309. if len(a.pending) > 0 {
  310. a.pending = a.pending[1:]
  311. }
  312. a.pendingMu.Unlock()
  313. a.flushed.Add(batchRequestCount(batch))
  314. a.lastError.Store("")
  315. }
  316. }
  317. func batchRequestCount(items []*OpsIngressRejectAggregate) uint64 {
  318. var count uint64
  319. for _, item := range items {
  320. if item != nil && item.RequestCount > 0 {
  321. count += uint64(item.RequestCount)
  322. }
  323. }
  324. return count
  325. }
  326. func (a *OpsIngressRejectAggregator) Health() OpsIngressRejectHealth {
  327. h := OpsIngressRejectHealth{Capacity: ingressRejectMaxEntries}
  328. if a == nil {
  329. return h
  330. }
  331. h.Cardinality = a.cardinality.Load()
  332. a.overflowMu.Lock()
  333. if a.overflow != nil {
  334. h.Cardinality++
  335. }
  336. a.overflowMu.Unlock()
  337. a.pendingMu.Lock()
  338. h.PendingBatches = len(a.pending)
  339. for _, batch := range a.pending {
  340. h.PendingRows += len(batch)
  341. }
  342. a.pendingMu.Unlock()
  343. h.Overflowed = a.overflowed.Load()
  344. h.Dropped = a.dropped.Load()
  345. h.Flushed = a.flushed.Load()
  346. h.FlushFailures = a.flushFailures.Load()
  347. h.Accepting = a.accepting.Load()
  348. if v := a.lastError.Load(); v != nil {
  349. h.LastError, _ = v.(string)
  350. }
  351. return h
  352. }
  353. func boundedDimension(value, fallback string) string {
  354. value = strings.TrimSpace(value)
  355. if value == "" {
  356. return fallback
  357. }
  358. if len(value) > 64 {
  359. return value[:64]
  360. }
  361. return value
  362. }
  363. func ingressRejectHash(k ingressRejectKey) int {
  364. h := uint64(1469598103934665603)
  365. for _, value := range []string{k.reason, k.routeFamily, k.protocol, k.clientIP} {
  366. for i := 0; i < len(value); i++ {
  367. h ^= uint64(value[i])
  368. h *= 1099511628211
  369. }
  370. }
  371. h ^= uint64(k.userID)
  372. h *= 1099511628211
  373. h ^= uint64(k.apiKeyID)
  374. return int(h & 0x7fffffff)
  375. }
  376. func aggregateFromKey(k ingressRejectKey, bucket, now time.Time) *OpsIngressRejectAggregate {
  377. item := &OpsIngressRejectAggregate{
  378. BucketStart: bucket, RejectReason: k.reason, RouteFamily: k.routeFamily, Protocol: k.protocol,
  379. ClientIP: k.clientIP, RequestCount: 1, FirstSeen: now, LastSeen: now,
  380. }
  381. if k.userID > 0 {
  382. value := k.userID
  383. item.UserID = &value
  384. }
  385. if k.apiKeyID > 0 {
  386. value := k.apiKeyID
  387. item.APIKeyID = &value
  388. }
  389. return item
  390. }
  391. func (s *OpsService) SetIngressRejectAggregator(a *OpsIngressRejectAggregator) {
  392. if s != nil {
  393. s.ingressRejectAggregator = a
  394. }
  395. }
  396. func (s *OpsService) RecordIngressReject(reason, routeFamily, protocol, clientIP string, userID, apiKeyID int64) {
  397. if s != nil && s.ingressRejectAggregator != nil {
  398. s.ingressRejectAggregator.RecordIngressReject(reason, routeFamily, protocol, clientIP, userID, apiKeyID)
  399. }
  400. }
  401. func (s *OpsService) GetIngressRejectHealth() OpsIngressRejectHealth {
  402. if s == nil || s.ingressRejectAggregator == nil {
  403. return OpsIngressRejectHealth{Capacity: ingressRejectMaxEntries}
  404. }
  405. return s.ingressRejectAggregator.Health()
  406. }
  407. func (s *OpsService) ListIngressRejects(ctx context.Context, filter *OpsIngressRejectFilter) (*OpsIngressRejectList, error) {
  408. if err := s.RequireMonitoringEnabled(ctx); err != nil {
  409. return nil, err
  410. }
  411. repo, ok := s.opsRepo.(OpsIngressRejectRepository)
  412. if !ok {
  413. return &OpsIngressRejectList{Items: []*OpsIngressRejectAggregate{}, Page: 1, PageSize: 50}, nil
  414. }
  415. return repo.ListIngressRejects(ctx, filter)
  416. }