gitlab-labkit 4.6.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitlab/CODEOWNERS +1 -1
- data/CODEOWNERS +1 -1
- data/lib/labkit/rate_limit/README.md +14 -21
- data/lib/labkit/rate_limit/evaluator.rb +20 -33
- data/lib/labkit/rate_limit/metrics.rb +5 -17
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d2bc5199fdb961286c36547f3c359b9de03bb8dc8c6c260c2074c177d24fc79
|
|
4
|
+
data.tar.gz: '09d7ddd1b8e64f242ca95c63fce1e7d1a29a8d4d44a3908be2aad99e8df57207'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 366832f3f6b6a1f76f2c4638c4303c3ad4c1c9065a876c9bb32a049fb3c94f214804f43ded0f5a30a65b87f3a1c9595e1cf05c85152a1eb9c6d8eed7228a1ddc
|
|
7
|
+
data.tar.gz: 45f429a193fa57913cc61b5fda09356abb2135eb1f596c8d6eb4c0982fe2d4a4012551a5dedd2e2576b27da6017a7632f00cd24991352c15be7716a72abd4957
|
data/.gitlab/CODEOWNERS
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
* @reprazent @andrewn @mkaeppler @ayufan @hmerscher @
|
|
1
|
+
* @reprazent @andrewn @mkaeppler @ayufan @hmerscher @sankalp_gl @hardikgala @nindurkar @ashs2
|
data/CODEOWNERS
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
# CODEOWNERS is used to lookup assignees for
|
|
2
2
|
# Renovate Bot dependency change Merge Requests.
|
|
3
3
|
# https://docs.renovatebot.com/configuration-options/#assigneesfromcodeowners
|
|
4
|
-
* @reprazent @andrewn @mkaeppler @ayufan @hmerscher @
|
|
4
|
+
* @reprazent @andrewn @mkaeppler @ayufan @hmerscher @e_forbes @M_Alvarez @mwoolf @sankalp_gl @hardikgala @nindurkar @ashs2
|
|
@@ -121,6 +121,8 @@ different question than `check`. A matched `:skip` rule terminates `peek`
|
|
|
121
121
|
the same way it terminates `check`: matched, `:allow`, no Redis read, no
|
|
122
122
|
`info`.
|
|
123
123
|
|
|
124
|
+
A peek is counted by its own `peeks_total` counter. See [Metrics](#metrics).
|
|
125
|
+
|
|
124
126
|
### Clearing state
|
|
125
127
|
|
|
126
128
|
`Limiter#clear(identifier)` deletes this limiter's counters for one
|
|
@@ -267,7 +269,7 @@ flowchart TD
|
|
|
267
269
|
Return --> Check[Emit checks_total<br/>action, matched, error]
|
|
268
270
|
SkipReturn --> Check
|
|
269
271
|
Return2 --> Check
|
|
270
|
-
Eval -. StandardError .-> Error[Emit
|
|
272
|
+
Eval -. StandardError .-> Error[Emit checks_total error=true<br/>log warn]
|
|
271
273
|
Error --> ReturnErr([Return error=true<br/>action=:allow])
|
|
272
274
|
```
|
|
273
275
|
|
|
@@ -290,12 +292,6 @@ fail-open also emits nothing here — it was never evaluated; that check is
|
|
|
290
292
|
visible via `checks_total{error="true"}` and the
|
|
291
293
|
`rate_limit_missing_count_distinct` log, which carries the rule name.
|
|
292
294
|
|
|
293
|
-
During the transition the deprecated `calls_total` counter is additionally
|
|
294
|
-
emitted at every point the diagram emits `rule_evaluations_total` (with its
|
|
295
|
-
historical per-rule semantics, including the `rule="unmatched"` placeholder
|
|
296
|
-
after the loop). It is not shown above to keep the diagram legible; see the
|
|
297
|
-
Metrics table below.
|
|
298
|
-
|
|
299
295
|
### Actions
|
|
300
296
|
|
|
301
297
|
The rule's `action` describes what the rule does; the result's `action`
|
|
@@ -361,7 +357,7 @@ window, and it stops counting while that ban holds:
|
|
|
361
357
|
written with `SET EX`. A callable is checked again each time it resolves, and
|
|
362
358
|
a value under a second raises rather than reaching Redis. Being an error, it
|
|
363
359
|
fails open like any other, so a rule whose `ban_for` cannot resolve stops
|
|
364
|
-
blocking entirely: watch `
|
|
360
|
+
blocking entirely: watch `checks_total{error="true"}` after changing one.
|
|
365
361
|
|
|
366
362
|
Take care combining `ban_for` with `cost:`. One expensive call can cross the
|
|
367
363
|
limit on its own, and with a ban attached that costs the caller the whole ban
|
|
@@ -453,11 +449,13 @@ the most constraining one (see [Evaluation flow](#evaluation-flow)):
|
|
|
453
449
|
|
|
454
450
|
```ruby
|
|
455
451
|
result.matched? # => true if some rule matched
|
|
452
|
+
result.skipped? # => true if a matched :skip rule bypassed the check
|
|
456
453
|
result.exceeded? # => true if the reported rule's counter > limit
|
|
457
454
|
result.action # => :block | :allow — what the caller should do
|
|
458
455
|
result.block? # => result.action == :block
|
|
459
456
|
result.rule # => the reported Rule, or nil
|
|
460
457
|
result.error? # => true if Redis failed (see Fail-open)
|
|
458
|
+
result.degraded? # => true if a matched count_distinct rule was skipped
|
|
461
459
|
result.info # => Result::Info or nil
|
|
462
460
|
result.evaluations # => every counted Result::Evaluation, in rule order
|
|
463
461
|
result.to_response_headers
|
|
@@ -493,12 +491,13 @@ of checks that encountered an error is `checks_total{error="true"}` over
|
|
|
493
491
|
`count_distinct` key — that check completes (`action` and `matched` describe
|
|
494
492
|
its outcome as usual), so `error="true"` is not exclusively fail-open traffic.
|
|
495
493
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
`
|
|
501
|
-
|
|
494
|
+
A failed-open `peek` emits `peeks_total{error="true"}`, so the fraction of
|
|
495
|
+
peeks that encountered an error is `peeks_total{error="true"}` over
|
|
496
|
+
`peeks_total`. A `peek` emits no `checks_total`.
|
|
497
|
+
|
|
498
|
+
`clear` emits no counter at all. A failure there is benign — the counters it
|
|
499
|
+
could not delete expire on their own — so it is only logged, with the limiter
|
|
500
|
+
name in the `name` field.
|
|
502
501
|
|
|
503
502
|
Metric emission itself is best-effort: a failure in the metrics stack never
|
|
504
503
|
alters the verdict or breaks fail-open, and is logged at WARN with
|
|
@@ -514,8 +513,7 @@ flooding).
|
|
|
514
513
|
|-----------------------------------------------------|---------|----------------------------------------------|----------------------------------------------------------------------|
|
|
515
514
|
| `gitlab_labkit_rate_limiter_checks_total` | counter | `rate_limiter`, `action`, `matched`, `error` | Exactly one increment per `check` call, including fail-open. `action` is what the caller should do (`"allow"` or `"block"`); `matched` and `error` are `"true"`/`"false"`. |
|
|
516
515
|
| `gitlab_labkit_rate_limiter_rule_evaluations_total` | counter | `rate_limiter`, `rule`, `action`, `result` | One increment per evaluated rule (plus one per matched `:skip` rule). `action` is the configured rule action (`"limit"`, `"log"`, `"skip"`); `result` is what the evaluation decided (`"allow"`, `"block"`, `"log"`, `"skip"`, `"banned"` — see the Actions table). |
|
|
517
|
-
| `
|
|
518
|
-
| `gitlab_labkit_rate_limiter_errors_total` | counter | `rate_limiter` | **Deprecated** — use `checks_total{error="true"}`. Fail-open events (any `StandardError` in the labkit path); still the only error metric for `peek`. |
|
|
516
|
+
| `gitlab_labkit_rate_limiter_peeks_total` | counter | `rate_limiter`, `error` | Exactly one increment per `peek` call, including fail-open. `error` is `"true"`/`"false"`. |
|
|
519
517
|
| `gitlab_labkit_rate_limiter_limit` | gauge | `rate_limiter`, `rule` | Resolved limit at the last check (useful when `limit:` is callable). |
|
|
520
518
|
| `gitlab_labkit_rate_limiter_period_seconds` | gauge | `rate_limiter`, `rule` | Resolved period at the last check. |
|
|
521
519
|
|
|
@@ -524,11 +522,6 @@ the request rate through a limiter — no exclusions or dedup needed. A single
|
|
|
524
522
|
`check` call emits **one** `checks_total` increment and as many
|
|
525
523
|
`rule_evaluations_total` increments as rules it evaluated (possibly zero).
|
|
526
524
|
|
|
527
|
-
**Transition:** the deprecated `calls_total` and `errors_total` counters keep
|
|
528
|
-
emitting exactly as before this split, so existing dashboards and alerts stay
|
|
529
|
-
correct while consumers migrate to the new counters. Both are removed in a
|
|
530
|
-
follow-up major release once nothing consumes them.
|
|
531
|
-
|
|
532
525
|
## Dev/test vs production guards
|
|
533
526
|
|
|
534
527
|
`Limiter.new` and `Rule.new` validate names and configuration. In
|
|
@@ -124,7 +124,6 @@ module Labkit
|
|
|
124
124
|
rescue StandardError => e
|
|
125
125
|
# Intentionally broad: fail-open applies to any unexpected error (network,
|
|
126
126
|
# timeout, OOM) not only Redis protocol errors.
|
|
127
|
-
report_error_metrics
|
|
128
127
|
log_error(e, identifier, cursor.rule)
|
|
129
128
|
result = Result.error
|
|
130
129
|
ensure
|
|
@@ -138,11 +137,12 @@ module Labkit
|
|
|
138
137
|
# extended. A missing Redis key is treated as count=0 (matched, not exceeded).
|
|
139
138
|
def peek(identifier, rule_context: nil)
|
|
140
139
|
cursor = RuleCursor.new
|
|
141
|
-
peek_rules(identifier, rule_context, cursor)
|
|
140
|
+
result = peek_rules(identifier, rule_context, cursor)
|
|
142
141
|
rescue StandardError => e
|
|
143
|
-
report_error_metrics
|
|
144
142
|
log_error(e, identifier, cursor.rule)
|
|
145
|
-
Result.error
|
|
143
|
+
result = Result.error
|
|
144
|
+
ensure
|
|
145
|
+
report_peek_metrics(result) if result
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
# Deletes this limiter's counters, and any bans, for one identifier.
|
|
@@ -175,7 +175,6 @@ module Labkit
|
|
|
175
175
|
|
|
176
176
|
removed
|
|
177
177
|
rescue StandardError => e
|
|
178
|
-
report_error_metrics
|
|
179
178
|
log_error(e, identifier, nil)
|
|
180
179
|
removed
|
|
181
180
|
end
|
|
@@ -203,14 +202,14 @@ module Labkit
|
|
|
203
202
|
# most-constraining evaluation (ranking lives in Result::Evaluation#<=>).
|
|
204
203
|
# cost is therefore debited from every matching rule, not just the first.
|
|
205
204
|
#
|
|
206
|
-
# Metrics: each evaluated rule emits rule_evaluations_total
|
|
207
|
-
#
|
|
208
|
-
#
|
|
205
|
+
# Metrics: each evaluated rule emits rule_evaluations_total; the per-check
|
|
206
|
+
# checks_total is emitted once in #check. Full contract in the README's
|
|
207
|
+
# Metrics section.
|
|
209
208
|
#
|
|
210
209
|
# SET-mode rules (rule.count_distinct set) that match but whose identifier
|
|
211
|
-
# is missing the count_distinct key fail open + log +
|
|
212
|
-
#
|
|
213
|
-
#
|
|
210
|
+
# is missing the count_distinct key fail open + log + flag the Result, and
|
|
211
|
+
# the loop continues to the next rule (the rule is treated as not
|
|
212
|
+
# applicable rather than aborting the whole evaluation).
|
|
214
213
|
#
|
|
215
214
|
# Error handling stays whole-check (see #check): a raise part-way through
|
|
216
215
|
# discards the results of the rules already evaluated, even though their
|
|
@@ -234,7 +233,6 @@ module Labkit
|
|
|
234
233
|
|
|
235
234
|
if rule.count_distinct && missing_count_distinct_value?(rule, identifier)
|
|
236
235
|
log_missing_count_distinct(rule, identifier)
|
|
237
|
-
report_error_metrics
|
|
238
236
|
result.degraded!
|
|
239
237
|
next
|
|
240
238
|
end
|
|
@@ -248,7 +246,6 @@ module Labkit
|
|
|
248
246
|
# The loop is done, so anything raised from here on belongs to no rule.
|
|
249
247
|
cursor.rule = nil
|
|
250
248
|
|
|
251
|
-
report_unmatched_metrics unless result.matched?
|
|
252
249
|
result
|
|
253
250
|
rescue StandardError => e # binds e for the ensure
|
|
254
251
|
raise
|
|
@@ -393,11 +390,10 @@ module Labkit
|
|
|
393
390
|
Result::Evaluation.new(rule: rule, exceeded: count > resolved_limit, info: info)
|
|
394
391
|
end
|
|
395
392
|
|
|
396
|
-
# The
|
|
397
|
-
#
|
|
398
|
-
#
|
|
399
|
-
#
|
|
400
|
-
# the slot and cannot collide with a characteristic value.
|
|
393
|
+
# The braces are the cluster hash tag: a counter and its ban must share a
|
|
394
|
+
# slot because BAN_SCRIPT touches both in one call. The rule name stays
|
|
395
|
+
# inside it so one identifier's keys spread across nodes instead of piling
|
|
396
|
+
# onto one. +suffix+ sits outside, so it cannot change the slot.
|
|
401
397
|
def build_redis_key(rule, identifier, suffix = nil)
|
|
402
398
|
key = "#{REDIS_KEY_PREFIX}:{#{@name}:#{rule.name}"
|
|
403
399
|
rule.characteristics.each do |char|
|
|
@@ -566,12 +562,6 @@ module Labkit
|
|
|
566
562
|
rule_labels.merge(action: evaluation.rule.action.to_s, result: evaluation_result(evaluation)),
|
|
567
563
|
logger: @logger
|
|
568
564
|
)
|
|
569
|
-
# Deprecated dual emission - remove together with Metrics.calls_total.
|
|
570
|
-
Metrics.safe_increment(
|
|
571
|
-
:calls_total,
|
|
572
|
-
rule_labels.merge(action: (evaluation.exceeded? ? evaluation.rule.action : :allow).to_s),
|
|
573
|
-
logger: @logger
|
|
574
|
-
)
|
|
575
565
|
Metrics.safe_set(:limit_gauge, rule_labels, evaluation.info.resolved_limit, logger: @logger)
|
|
576
566
|
Metrics.safe_set(:period_gauge, rule_labels, evaluation.info.resolved_period, logger: @logger)
|
|
577
567
|
end
|
|
@@ -595,13 +585,6 @@ module Labkit
|
|
|
595
585
|
{ rate_limiter: @name, rule: rule.name, action: "skip", result: "skip" },
|
|
596
586
|
logger: @logger
|
|
597
587
|
)
|
|
598
|
-
# Deprecated dual emission - remove together with Metrics.calls_total.
|
|
599
|
-
Metrics.safe_increment(:calls_total, { rate_limiter: @name, rule: rule.name, action: "skip" }, logger: @logger)
|
|
600
|
-
end
|
|
601
|
-
|
|
602
|
-
# Deprecated dual emission - remove together with Metrics.calls_total.
|
|
603
|
-
def report_unmatched_metrics
|
|
604
|
-
Metrics.safe_increment(:calls_total, { rate_limiter: @name, rule: "unmatched", action: "allow" }, logger: @logger)
|
|
605
588
|
end
|
|
606
589
|
|
|
607
590
|
def report_check_metrics(result)
|
|
@@ -617,8 +600,12 @@ module Labkit
|
|
|
617
600
|
)
|
|
618
601
|
end
|
|
619
602
|
|
|
620
|
-
def
|
|
621
|
-
Metrics.safe_increment(
|
|
603
|
+
def report_peek_metrics(result)
|
|
604
|
+
Metrics.safe_increment(
|
|
605
|
+
:peeks_total,
|
|
606
|
+
{ rate_limiter: @name, error: (result.error? || result.degraded?).to_s },
|
|
607
|
+
logger: @logger
|
|
608
|
+
)
|
|
622
609
|
end
|
|
623
610
|
end
|
|
624
611
|
end
|
|
@@ -52,14 +52,12 @@ module Labkit
|
|
|
52
52
|
)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
# matched) until consumers migrate; will get removed in a follow-up release.
|
|
58
|
-
def calls_total
|
|
55
|
+
# Emitted exactly once per #peek call, including calls that fail open.
|
|
56
|
+
def peeks_total
|
|
59
57
|
Labkit::Metrics::Client.counter(
|
|
60
|
-
:
|
|
61
|
-
'Total number of
|
|
62
|
-
{ rate_limiter: nil,
|
|
58
|
+
:gitlab_labkit_rate_limiter_peeks_total,
|
|
59
|
+
'Total number of rate limit peeks',
|
|
60
|
+
{ rate_limiter: nil, error: nil }
|
|
63
61
|
)
|
|
64
62
|
end
|
|
65
63
|
|
|
@@ -73,16 +71,6 @@ module Labkit
|
|
|
73
71
|
)
|
|
74
72
|
end
|
|
75
73
|
|
|
76
|
-
# Deprecated: superseded by checks_total{error="true"}. Still emitted
|
|
77
|
-
# because it remains the only error metric for #peek.
|
|
78
|
-
def errors_total
|
|
79
|
-
Labkit::Metrics::Client.counter(
|
|
80
|
-
:gitlab_labkit_rate_limiter_errors_total,
|
|
81
|
-
'Total number of rate limit check errors',
|
|
82
|
-
{ rate_limiter: nil }
|
|
83
|
-
)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
74
|
def limit_gauge
|
|
87
75
|
Labkit::Metrics::Client.gauge(
|
|
88
76
|
:gitlab_labkit_rate_limiter_limit,
|