-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrate_limiter.go
More file actions
210 lines (174 loc) · 5.48 KB
/
Copy pathrate_limiter.go
File metadata and controls
210 lines (174 loc) · 5.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package sentry_transport
import (
"strconv"
"strings"
"sync"
"time"
"go.uber.org/zap"
)
// RateLimiter handles Sentry rate limiting based on response headers
type RateLimiter struct {
mu sync.RWMutex
rateLimits map[string]time.Time // category -> disabled until time
logger *zap.Logger
}
// NewRateLimiter creates a new rate limiter instance
func NewRateLimiter(logger *zap.Logger) *RateLimiter {
return &RateLimiter{
rateLimits: make(map[string]time.Time),
logger: logger,
}
}
// IsRateLimited checks if the given event type is currently rate limited
func (rl *RateLimiter) IsRateLimited(eventType string) bool {
rl.mu.RLock()
defer rl.mu.RUnlock()
now := time.Now()
// Check specific category rate limit
if disabledUntil, exists := rl.rateLimits[eventType]; exists && disabledUntil.After(now) {
return true
}
// Check global rate limit
if disabledUntil, exists := rl.rateLimits["all"]; exists && disabledUntil.After(now) {
return true
}
return false
}
// GetDisabledUntil returns the time until which the event type is disabled
func (rl *RateLimiter) GetDisabledUntil(eventType string) time.Time {
rl.mu.RLock()
defer rl.mu.RUnlock()
now := time.Now()
var maxDisabledUntil time.Time
// Check specific category
if disabledUntil, exists := rl.rateLimits[eventType]; exists && disabledUntil.After(now) {
maxDisabledUntil = disabledUntil
}
// Check global rate limit
if disabledUntil, exists := rl.rateLimits["all"]; exists && disabledUntil.After(now) {
if disabledUntil.After(maxDisabledUntil) {
maxDisabledUntil = disabledUntil
}
}
return maxDisabledUntil
}
// HandleRateLimitHeaders processes Sentry rate limit headers
func (rl *RateLimiter) HandleRateLimitHeaders(headers map[string][]string) {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
// Handle X-Sentry-Rate-Limits header
if rateLimits, exists := headers["X-Sentry-Rate-Limits"]; exists && len(rateLimits) > 0 {
rl.parseRateLimitHeader(rateLimits[0], now)
return
}
// Handle Retry-After header as fallback
if retryAfter, exists := headers["Retry-After"]; exists && len(retryAfter) > 0 {
rl.parseRetryAfterHeader(retryAfter[0], now)
}
}
// parseRateLimitHeader parses the X-Sentry-Rate-Limits header
// Format: "retry_after:categories:scope:reason_code:namespaces"
func (rl *RateLimiter) parseRateLimitHeader(header string, now time.Time) {
for _, limit := range strings.Split(header, ",") {
limit = strings.TrimSpace(limit)
parts := strings.Split(limit, ":")
if len(parts) < 2 {
continue
}
// Parse retry_after (first part)
retryAfterSeconds, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil {
rl.logger.Warn("Failed to parse retry_after from rate limit header", zap.String("value", parts[0]))
retryAfterSeconds = 60 // Default fallback
}
retryAfter := now.Add(time.Duration(retryAfterSeconds) * time.Second)
// Parse categories (second part)
categoriesStr := strings.TrimSpace(parts[1])
if categoriesStr == "" {
categoriesStr = "all"
}
categories := strings.Split(categoriesStr, ";")
for _, category := range categories {
category = strings.TrimSpace(category)
if category == "" {
category = "all"
}
// Convert event type to data category
category = rl.normalizeCategory(category)
rl.rateLimits[category] = retryAfter
rl.logger.Warn("Rate limit applied",
zap.String("category", category),
zap.Time("disabled_until", retryAfter),
zap.Int("retry_after_seconds", retryAfterSeconds))
}
}
}
// parseRetryAfterHeader parses the Retry-After header
func (rl *RateLimiter) parseRetryAfterHeader(header string, now time.Time) {
header = strings.TrimSpace(header)
// Try to parse as seconds
if seconds, err := strconv.Atoi(header); err == nil {
retryAfter := now.Add(time.Duration(seconds) * time.Second)
rl.rateLimits["all"] = retryAfter
rl.logger.Warn("Global rate limit applied via Retry-After header",
zap.Time("disabled_until", retryAfter),
zap.Int("retry_after_seconds", seconds))
return
}
// Try to parse as HTTP date
if retryTime, err := time.Parse(time.RFC1123, header); err == nil && retryTime.After(now) {
rl.rateLimits["all"] = retryTime
rl.logger.Warn("Global rate limit applied via Retry-After header",
zap.Time("disabled_until", retryTime))
return
}
// Default fallback
retryAfter := now.Add(60 * time.Second)
rl.rateLimits["all"] = retryAfter
rl.logger.Warn("Failed to parse Retry-After header, using default",
zap.String("header", header),
zap.Time("disabled_until", retryAfter))
}
// normalizeCategory converts event types to Sentry data categories
func (rl *RateLimiter) normalizeCategory(category string) string {
switch category {
case "event":
return "error"
case "log":
return "log_item"
case "transaction":
return "transaction"
case "session":
return "session"
case "attachment":
return "attachment"
case "profile":
return "profile"
case "replay":
return "replay"
default:
return category
}
}
// CleanupExpired removes expired rate limits
func (rl *RateLimiter) CleanupExpired() {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
for category, disabledUntil := range rl.rateLimits {
if !disabledUntil.After(now) {
delete(rl.rateLimits, category)
}
}
}
// GetStatus returns current rate limit status
func (rl *RateLimiter) GetStatus() map[string]time.Time {
rl.mu.RLock()
defer rl.mu.RUnlock()
status := make(map[string]time.Time)
for category, disabledUntil := range rl.rateLimits {
status[category] = disabledUntil
}
return status
}