gitlab-labkit 1.20.1 → 1.21.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/.copier-answers.yml +2 -1
- data/.gitignore +1 -0
- data/.gitlab-ci.yml +10 -0
- data/README.md +11 -0
- data/docker-compose.yml +10 -0
- data/exe/labkit-logging +3 -1
- data/lib/labkit/rate_limit/README.md +329 -0
- data/lib/labkit/rate_limit/rule.rb +2 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 43d394fdecdc0275e1ddab3a28ae7824f907e21637c4d06b3cc183a801df9377
|
|
4
|
+
data.tar.gz: 18632e75c51ec22c6b78079c59476f736b3b3b298c7988fbf4a49c9505c4d05e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d56453b6af992b51da987bec68fd990fefc808b68be96702c3b4274ae609a773139b39bb5609032ed9148a7427976bd64ea0566fa0c8354582e4d4d4135d481e
|
|
7
|
+
data.tar.gz: f8d71107b36351983b8a1da70b9a43624b2eb73e000b37fd45cad03606749c84a2da0282d1e02cd7f5a1b687e2e030717fc1770f2671d05175a7cd4d9eb9bad1
|
data/.copier-answers.yml
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
# See the project for instructions on how to update the project
|
|
4
4
|
#
|
|
5
5
|
# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
6
|
-
_commit: v1.
|
|
6
|
+
_commit: v1.48.0
|
|
7
7
|
_src_path: https://gitlab.com/gitlab-com/gl-infra/common-template-copier.git
|
|
8
8
|
ee_licensed: false
|
|
9
|
+
gitlab_namespace: gitlab-org/ruby/gems
|
|
9
10
|
golang: false
|
|
10
11
|
helm: false
|
|
11
12
|
initial_codeowners: '@reprazent @andrewn @mkaeppler @ayufan'
|
data/.gitignore
CHANGED
data/.gitlab-ci.yml
CHANGED
|
@@ -27,6 +27,16 @@ include:
|
|
|
27
27
|
|
|
28
28
|
- component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/danger@v3.24
|
|
29
29
|
|
|
30
|
+
# Attach a redis service to the rspec job from common-ci-tasks/ruby-build.
|
|
31
|
+
# GitLab merges keys when a local job has the same name as an included one,
|
|
32
|
+
# so this augments the base job (and is inherited by anything that extends it).
|
|
33
|
+
rspec:
|
|
34
|
+
services:
|
|
35
|
+
- name: redis:7-alpine
|
|
36
|
+
alias: redis
|
|
37
|
+
variables:
|
|
38
|
+
LABKIT_TEST_REDIS_URL: redis://redis
|
|
39
|
+
|
|
30
40
|
ruby-versions:
|
|
31
41
|
extends: rspec
|
|
32
42
|
image: ${CI_REGISTRY}/gitlab-com/gl-infra/common-ci-tasks-images/ruby:${RUBY_VERSION}
|
data/README.md
CHANGED
|
@@ -24,6 +24,7 @@ LabKit-Ruby provides functionality in a number of areas:
|
|
|
24
24
|
1. `Labkit::FIPS` for checking for FIPS mode and using FIPS-compliant algorithms.
|
|
25
25
|
1. `Labkit::Logging` for sanitizing log messages.
|
|
26
26
|
1. `Labkit::Metrics` for metrics. More on the [README](./lib/labkit/metrics/README.md).
|
|
27
|
+
1. `Labkit::RateLimit` for rules-based, Redis-backed rate limiting. More on the [README](./lib/labkit/rate_limit/README.md).
|
|
27
28
|
1. `Labkit::RSpec` for RSpec matchers to test Labkit functionality (requires selective loading). More on the [README](./lib/labkit/rspec/README.md).
|
|
28
29
|
1. `Labkit::Tracing` for handling and propagating distributed traces.
|
|
29
30
|
|
|
@@ -43,6 +44,16 @@ $ # Run tests, linters
|
|
|
43
44
|
$ bundle exec rake verify
|
|
44
45
|
```
|
|
45
46
|
|
|
47
|
+
Some specs require a real Redis instance. When you run the suite locally,
|
|
48
|
+
it will automatically start one via `docker compose up -d redis` (see
|
|
49
|
+
`docker-compose.yml`) and tear it down again when the test process exits.
|
|
50
|
+
Redis is exposed on `localhost:6390` so it does not collide with a local
|
|
51
|
+
GDK/Caproni Redis on the default port.
|
|
52
|
+
|
|
53
|
+
To opt out of autostart (e.g. you've started Redis some other way), set
|
|
54
|
+
`LABKIT_TEST_REDIS_URL` to a reachable instance, or
|
|
55
|
+
`LABKIT_TEST_REDIS_NO_AUTOSTART=1` to fail loudly instead of spawning.
|
|
56
|
+
|
|
46
57
|
Please also review the [development section of the LabKit (go) README](https://gitlab.com/gitlab-org/labkit#developing-labkit) for details of the LabKit architectural philosophy.
|
|
47
58
|
|
|
48
59
|
To work on some of the scripts we use for releasing a new version,
|
data/docker-compose.yml
ADDED
data/exe/labkit-logging
CHANGED
|
@@ -176,7 +176,9 @@ module Labkit
|
|
|
176
176
|
http.read_timeout = 30
|
|
177
177
|
req = Net::HTTP::Get.new(uri.request_uri)
|
|
178
178
|
req['PRIVATE-TOKEN'] = token
|
|
179
|
-
http.request(req)
|
|
179
|
+
resp = http.request(req)
|
|
180
|
+
abort "Unauthorized: check that your GitLab token is valid and has access to this project" if resp.is_a?(Net::HTTPUnauthorized) || resp.is_a?(Net::HTTPForbidden)
|
|
181
|
+
resp
|
|
180
182
|
end
|
|
181
183
|
|
|
182
184
|
def token
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# Labkit::RateLimit
|
|
2
|
+
|
|
3
|
+
`Labkit::RateLimit` is a rules-based rate limiter backed by Redis counters. It
|
|
4
|
+
maintains a fixed-window counter per `(call-site, rule, characteristics)` tuple
|
|
5
|
+
and decides whether each request is within the configured limit.
|
|
6
|
+
|
|
7
|
+
The module is intentionally small: a `Limiter` is configured at boot with an
|
|
8
|
+
ordered list of `Rule`s, and every request calls `Limiter#check(identifier)` to
|
|
9
|
+
get back a `Result` describing what the caller should do.
|
|
10
|
+
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
```mermaid
|
|
14
|
+
flowchart LR
|
|
15
|
+
App[Application code] -->|"check(identifier)"| Limiter
|
|
16
|
+
Limiter -->|delegates| Evaluator
|
|
17
|
+
Evaluator -->|iterates ordered| Rules[Rule list]
|
|
18
|
+
Evaluator <-->|INCR / TTL / EXPIRE| Redis[(Redis)]
|
|
19
|
+
Evaluator -->|emits| Metrics[Prometheus metrics]
|
|
20
|
+
Evaluator -->|returns| Result
|
|
21
|
+
Result --> App
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
A `Limiter` is configured once per call site and holds an `Evaluator` plus the
|
|
25
|
+
compiled `Rule` list. Every `check` call delegates to the same `Evaluator`,
|
|
26
|
+
which iterates the rules in declaration order, talks to Redis, emits metrics,
|
|
27
|
+
and builds a `Result`.
|
|
28
|
+
|
|
29
|
+
## Configuration
|
|
30
|
+
|
|
31
|
+
`Labkit::RateLimit.configure` sets a global Redis connection pool and logger
|
|
32
|
+
that are reused across all `Limiter` instances unless a per-Limiter override is
|
|
33
|
+
supplied:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
Labkit::RateLimit.configure do |c|
|
|
37
|
+
c.redis = ConnectionPool.new(size: 5) { Redis.new(url: ENV["REDIS_URL"]) }
|
|
38
|
+
c.logger = Labkit::Logging::JsonLogger.new($stdout)
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The `redis` value must respond to `.with { |conn| ... }` and yield a connection
|
|
43
|
+
that supports `incr`, `ttl`, `get`, `expire`, and `pipelined`. A
|
|
44
|
+
`ConnectionPool` of `Redis` clients is the typical choice.
|
|
45
|
+
|
|
46
|
+
The logger is used only for warnings (invalid rule names, fail-open errors,
|
|
47
|
+
duplicate rule names in production). It defaults to a JSON logger writing to
|
|
48
|
+
`$stdout`.
|
|
49
|
+
|
|
50
|
+
## Defining a Limiter
|
|
51
|
+
|
|
52
|
+
A `Limiter` is the unit of configuration for one call site (e.g. "rack
|
|
53
|
+
requests", "graphql mutations", "ai actions"). Construct it once and reuse it:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
RACK_LIMITER = Labkit::RateLimit::Limiter.new(
|
|
57
|
+
name: "rack_request",
|
|
58
|
+
rules: [
|
|
59
|
+
Labkit::RateLimit::Rule.new(
|
|
60
|
+
name: "api_user",
|
|
61
|
+
limit: 600,
|
|
62
|
+
period: 60,
|
|
63
|
+
characteristics: [:user],
|
|
64
|
+
match: { endpoint: { re: '\A/api/' } }
|
|
65
|
+
),
|
|
66
|
+
Labkit::RateLimit::Rule.new(
|
|
67
|
+
name: "unauthenticated",
|
|
68
|
+
limit: 60,
|
|
69
|
+
period: 60,
|
|
70
|
+
characteristics: [:ip],
|
|
71
|
+
match: { user: nil }
|
|
72
|
+
)
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- `name` must match `/\A[a-z0-9_]+\z/`. It is used as the first segment of
|
|
78
|
+
every Redis counter key for this limiter, so renaming a `Limiter` abandons
|
|
79
|
+
any in-flight counters.
|
|
80
|
+
- `rules` is an ordered array of `Rule` objects. The first rule whose `match`
|
|
81
|
+
hash is satisfied wins (with the exception of `:log` rules — see [Actions](#actions)).
|
|
82
|
+
- `redis` and `logger` are optional; they fall back to the global
|
|
83
|
+
`Labkit::RateLimit.config` values.
|
|
84
|
+
|
|
85
|
+
A `Labkit::RateLimit.check(name:, identifier:, rules:, ...)` convenience method
|
|
86
|
+
exists for one-off cases that cannot cache a `Limiter` instance, but it
|
|
87
|
+
allocates a fresh `Limiter` on every call and is not the recommended path.
|
|
88
|
+
|
|
89
|
+
## Checking a request
|
|
90
|
+
|
|
91
|
+
`Limiter#check(identifier)` increments the counter for the matched rule and
|
|
92
|
+
returns a `Result`. `identifier` is either an `Identifier` or a plain `Hash`
|
|
93
|
+
of caller attributes:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
result = RACK_LIMITER.check(
|
|
97
|
+
user: current_user&.id,
|
|
98
|
+
ip: request.ip,
|
|
99
|
+
endpoint: request.path
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if result.exceeded? && result.action == :block
|
|
103
|
+
response.headers.merge!(result.to_response_headers)
|
|
104
|
+
render plain: "Too Many Requests", status: 429
|
|
105
|
+
return
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The `endpoint` key is treated specially: the query string is stripped at
|
|
110
|
+
`Identifier` construction time so URLs that vary only by query parameter share
|
|
111
|
+
the same counter.
|
|
112
|
+
|
|
113
|
+
### Peeking without incrementing
|
|
114
|
+
|
|
115
|
+
`Limiter#peek(identifier)` returns the same `Result` shape but does not
|
|
116
|
+
mutate Redis. It is useful when one code path should account for the request
|
|
117
|
+
(`check`) and another should gate a side-effect on whether the caller is
|
|
118
|
+
already over-limit. `peek` skips `:log` rules — their state is unobservable
|
|
119
|
+
without incrementing.
|
|
120
|
+
|
|
121
|
+
## Identifier
|
|
122
|
+
|
|
123
|
+
`Identifier` is a small value object wrapping a hash of caller attributes.
|
|
124
|
+
You can pass a `Hash` to `check`/`peek` and `Limiter` will wrap it for you, or
|
|
125
|
+
construct one explicitly:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
id = Labkit::RateLimit::Identifier.new(
|
|
129
|
+
user: 42,
|
|
130
|
+
ip: "1.2.3.4",
|
|
131
|
+
endpoint: "/api/v4/projects/1?per_page=20" # becomes "/api/v4/projects/1"
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Keys can be symbols or strings — they are normalised to symbols on the way
|
|
136
|
+
in.
|
|
137
|
+
|
|
138
|
+
## Rule
|
|
139
|
+
|
|
140
|
+
A `Rule` is a `Data.define` value object with the following fields:
|
|
141
|
+
|
|
142
|
+
| field | meaning |
|
|
143
|
+
|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
144
|
+
| `name` | Stable identifier used in Redis keys and metric labels. Must match `/\A[a-z0-9_]+\z/`, max 64 chars. Renaming a rule abandons its in-flight counters. |
|
|
145
|
+
| `match` | Hash of identifier key/value predicates that must **all** be satisfied for the rule to apply. Empty hash matches anything. See [Matchers](#matchers). |
|
|
146
|
+
| `limit` | Integer request threshold per `period`. May be a callable resolved on every check. |
|
|
147
|
+
| `period` | Window length in seconds. May be a callable resolved on every check. |
|
|
148
|
+
| `action` | What the result reports when the limit is exceeded. One of `:block`, `:log`, `:allow`. Default `:block`. See [Actions](#actions). |
|
|
149
|
+
| `characteristics` | Array of identifier keys whose values are folded into the Redis counter key. Each unique combination gets its own counter. |
|
|
150
|
+
|
|
151
|
+
Making `limit` or `period` callable is the supported pattern for
|
|
152
|
+
runtime-tunable thresholds (e.g. feature flags or database-backed settings):
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
Labkit::RateLimit::Rule.new(
|
|
156
|
+
name: "api_user",
|
|
157
|
+
limit: -> { Settings.rate_limit_api_user_per_minute },
|
|
158
|
+
period: 60,
|
|
159
|
+
characteristics: [:user]
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Matchers
|
|
164
|
+
|
|
165
|
+
A `match` hash gates whether a rule applies. Each value is normalised through
|
|
166
|
+
`Matcher.build`:
|
|
167
|
+
|
|
168
|
+
| input shape | matcher | example |
|
|
169
|
+
|-------------------|----------|--------------------------------------------------------|
|
|
170
|
+
| plain value | `eq` | `match: { user: nil }`, `match: { method: "POST" }` |
|
|
171
|
+
| `Regexp` | `re` | `match: { endpoint: %r{\A/api/} }` |
|
|
172
|
+
| `{ eq: <value> }` | `eq` | `match: { method: { eq: "POST" } }` (YAML-friendly) |
|
|
173
|
+
| `{ re: <source> }`| `re` | `match: { endpoint: { re: '\A/api/' } }` (YAML-friendly) |
|
|
174
|
+
|
|
175
|
+
`re` coerces the identifier value via `#to_s` before matching, so it can be
|
|
176
|
+
used against non-String values (e.g. matching a 503 status against `{ re: '^5' }`).
|
|
177
|
+
|
|
178
|
+
Glob and prefix matchers are intentionally out of scope.
|
|
179
|
+
|
|
180
|
+
### Evaluation flow
|
|
181
|
+
|
|
182
|
+
`check` walks the rule list in order. The first **terminating** rule wins;
|
|
183
|
+
`:log` rules count but do not terminate, so they cannot disable a following
|
|
184
|
+
`:block` rule. A pure `:log`-only path still emits one `rule="unmatched"`
|
|
185
|
+
metric increment because no terminating rule fired.
|
|
186
|
+
|
|
187
|
+
```mermaid
|
|
188
|
+
flowchart TD
|
|
189
|
+
Start([check identifier]) --> Iter{Next rule?}
|
|
190
|
+
Iter -->|yes| Match{rule.match<br/>all satisfied?}
|
|
191
|
+
Match -->|no| Iter
|
|
192
|
+
Match -->|yes| Eval["INCR Redis counter<br/>(see Redis sequence below)"]
|
|
193
|
+
Eval --> Build[Build Result<br/>resolve limit/period]
|
|
194
|
+
Build --> Emit[Emit calls_total + limit/period gauges]
|
|
195
|
+
Emit --> Act{rule.action}
|
|
196
|
+
Act -->|":log<br/>(non-terminating)"| Iter
|
|
197
|
+
Act -->|:block or :allow| Return([Return Result])
|
|
198
|
+
Iter -->|no more rules| Unmatched[Emit calls_total<br/>rule=unmatched, action=allow]
|
|
199
|
+
Unmatched --> ReturnUnmatched([Return matched=false<br/>action=:allow])
|
|
200
|
+
Eval -. StandardError .-> Error[Emit errors_total<br/>log warn]
|
|
201
|
+
Error --> ReturnErr([Return error=true<br/>action=:allow])
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Actions
|
|
205
|
+
|
|
206
|
+
The rule's `action` controls how the `Result` reports an over-limit hit. The
|
|
207
|
+
counter is always incremented when a rule matches, regardless of `action`:
|
|
208
|
+
|
|
209
|
+
- `:block` — when exceeded, `Result#action` is `:block`. Caller should reject
|
|
210
|
+
the request (e.g. with HTTP 429). When under the limit, action is `:allow`.
|
|
211
|
+
- `:log` — **non-terminating**. The rule counts the request and records
|
|
212
|
+
metrics, but evaluation continues to the next rule. This is the mechanism
|
|
213
|
+
for shadow rules during rollout: stack a `:log` rule and a `:block` rule
|
|
214
|
+
together and the `:log` rule cannot disable the `:block` rule. Note that a
|
|
215
|
+
pure `:log`-only check still emits one `rule="unmatched"` metric entry
|
|
216
|
+
because no terminating rule fired.
|
|
217
|
+
- `:allow` — when exceeded, `Result#action` is `:allow` (rather than
|
|
218
|
+
`:block`). Useful for "always allow this caller even if they're over the
|
|
219
|
+
limit" cases while still observing them via metrics. Evaluation terminates
|
|
220
|
+
on the first match.
|
|
221
|
+
|
|
222
|
+
### Redis keys
|
|
223
|
+
|
|
224
|
+
Each matched check writes a key shaped:
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
labkit:rl:<limiter_name>:<rule_name>:<char>:<value>[:<char>:<value>...]
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Characteristic values longer than 200 bytes are replaced with a SHA-256
|
|
231
|
+
hexdigest to bound key length. Missing or empty characteristic values are
|
|
232
|
+
encoded as `_unknown_`. The TTL is set on the first write of each window
|
|
233
|
+
(`count == 1`) and is not extended on subsequent INCRs, so the window is a
|
|
234
|
+
true fixed window starting at the first request, not a sliding window.
|
|
235
|
+
|
|
236
|
+
```mermaid
|
|
237
|
+
sequenceDiagram
|
|
238
|
+
autonumber
|
|
239
|
+
participant E as Evaluator
|
|
240
|
+
participant P as Connection pool
|
|
241
|
+
participant R as Redis
|
|
242
|
+
|
|
243
|
+
E->>P: pool.with { |conn| ... }
|
|
244
|
+
P-->>E: conn
|
|
245
|
+
E->>R: PIPELINE { INCR key, TTL key }
|
|
246
|
+
R-->>E: [count, ttl]
|
|
247
|
+
alt count == 1 (first write of window)
|
|
248
|
+
E->>R: EXPIRE key period
|
|
249
|
+
R-->>E: 1
|
|
250
|
+
Note over E: ttl returned is -1 here;<br/>build_result falls back to<br/>resolved_period for reset_at.
|
|
251
|
+
else count > 1
|
|
252
|
+
Note over E: TTL is not extended:<br/>fixed window from first write.
|
|
253
|
+
end
|
|
254
|
+
E-->>P: release conn
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`peek` follows the same shape but uses `GET` instead of `INCR` and never
|
|
258
|
+
issues `EXPIRE`. A missing key (`GET → nil`, `TTL → -2`) is reported as
|
|
259
|
+
`count = 0` and the window is treated as not-yet-started.
|
|
260
|
+
|
|
261
|
+
## Result
|
|
262
|
+
|
|
263
|
+
`Result` carries the decision back to the caller:
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
result.matched? # => true if some rule matched
|
|
267
|
+
result.exceeded? # => true if the matched rule's counter > limit
|
|
268
|
+
result.action # => :block | :log | :allow
|
|
269
|
+
result.rule # => the matched Rule, or nil
|
|
270
|
+
result.error? # => true if Redis failed (see Fail-open)
|
|
271
|
+
result.info # => Result::Info or nil
|
|
272
|
+
result.to_response_headers
|
|
273
|
+
# => { "RateLimit-Limit" => "...", "RateLimit-Remaining" => "...", "RateLimit-Reset" => "<unix-ts>" }
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`Result::Info` holds the per-window counter snapshot:
|
|
277
|
+
|
|
278
|
+
| field | meaning |
|
|
279
|
+
|-------------------|------------------------------------------------------------------|
|
|
280
|
+
| `resolved_limit` | The evaluated `Integer` limit for this check. |
|
|
281
|
+
| `resolved_period` | The evaluated `Integer` period in seconds for this check. |
|
|
282
|
+
| `count` | Raw INCR value; useful for utilization-ratio metrics. |
|
|
283
|
+
| `remaining` | `[resolved_limit - count, 0].max`. |
|
|
284
|
+
| `reset_at` | Best-effort UTC `Time` when the window resets (advisory only). |
|
|
285
|
+
|
|
286
|
+
`to_response_headers` returns `{}` for an unmatched or error result, so it is
|
|
287
|
+
safe to merge unconditionally.
|
|
288
|
+
|
|
289
|
+
## Fail-open
|
|
290
|
+
|
|
291
|
+
The evaluator wraps `check` and `peek` in a broad rescue. Any `StandardError`
|
|
292
|
+
(Redis connection failure, timeout, OOM in user-supplied callables, …) is
|
|
293
|
+
logged at WARN with `message: "rate_limit_error"` and returned as a
|
|
294
|
+
`Result(matched: false, error: true, action: :allow)`. The
|
|
295
|
+
`gitlab_labkit_rate_limiter_errors_total` counter is incremented. The caller
|
|
296
|
+
should treat the request as allowed.
|
|
297
|
+
|
|
298
|
+
## Metrics
|
|
299
|
+
|
|
300
|
+
`Labkit::RateLimit::Metrics` emits the following Prometheus metrics through
|
|
301
|
+
`Labkit::Metrics::Client`:
|
|
302
|
+
|
|
303
|
+
| metric | type | labels | meaning |
|
|
304
|
+
|-------------------------------------------------|---------|-------------------------------------|----------------------------------------------------------------------|
|
|
305
|
+
| `gitlab_labkit_rate_limiter_calls_total` | counter | `rate_limiter`, `rule`, `action` | One increment per terminating decision; also incremented per matched `:log` rule. `action` is one of `"allow"`, `"block"`, `"log"`. `rule="unmatched", action="allow"` when no rule terminated. |
|
|
306
|
+
| `gitlab_labkit_rate_limiter_errors_total` | counter | `rate_limiter` | Fail-open events (any `StandardError` in the labkit path). |
|
|
307
|
+
| `gitlab_labkit_rate_limiter_limit` | gauge | `rate_limiter`, `rule` | Resolved limit at the last check (useful when `limit:` is callable). |
|
|
308
|
+
| `gitlab_labkit_rate_limiter_period_seconds` | gauge | `rate_limiter`, `rule` | Resolved period at the last check. |
|
|
309
|
+
|
|
310
|
+
Because `:log` rules do not terminate, a single `check` call can emit
|
|
311
|
+
**multiple** `calls_total` increments: one per `:log` rule that matched, plus
|
|
312
|
+
one for the terminating decision (or `rule="unmatched"` if no terminating
|
|
313
|
+
rule fired).
|
|
314
|
+
|
|
315
|
+
## Dev/test vs production guards
|
|
316
|
+
|
|
317
|
+
`Limiter.new` and `Rule.new` validate names and configuration. In
|
|
318
|
+
`Labkit.dev_or_test?` mode (`RAILS_ENV` set to `development` or `test`), they
|
|
319
|
+
raise `ArgumentError` on:
|
|
320
|
+
|
|
321
|
+
- invalid limiter or rule names
|
|
322
|
+
- duplicate rule names within a single `Limiter`
|
|
323
|
+
- unknown `action` values
|
|
324
|
+
- rule names longer than 64 characters
|
|
325
|
+
|
|
326
|
+
In production, the same conditions are downgraded: invalid names are
|
|
327
|
+
sanitised (and the original/sanitised pair is logged), duplicate rule names
|
|
328
|
+
are dropped (first occurrence wins), and a warning is logged. This keeps a
|
|
329
|
+
misconfiguration from taking the application down at boot.
|
|
@@ -14,7 +14,8 @@ module Labkit
|
|
|
14
14
|
# period - window in seconds; may be a callable (resolved per check)
|
|
15
15
|
# action - :block (enforce), :log (count and log only, do not block,
|
|
16
16
|
# evaluation continues to subsequent rules), or :allow
|
|
17
|
-
# (
|
|
17
|
+
# (count but always permit; terminates evaluation on match
|
|
18
|
+
# regardless of whether the limit was exceeded)
|
|
18
19
|
# characteristics - identifier keys used to build the compound Redis counter key
|
|
19
20
|
#
|
|
20
21
|
# +name+ must be a lowercase alphanumeric-and-underscore string of at most 64
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gitlab-labkit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.21.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Newdigate
|
|
@@ -547,6 +547,7 @@ files:
|
|
|
547
547
|
- config/user_experience_slis/testing_sample.yml
|
|
548
548
|
- doc/FIELD_STANDARDIZATION.md
|
|
549
549
|
- doc/architecture/decisions/001_field_standardization_dynamic_runtime_linting.md
|
|
550
|
+
- docker-compose.yml
|
|
550
551
|
- exe/labkit-logging
|
|
551
552
|
- gitlab-labkit.gemspec
|
|
552
553
|
- lib/gitlab-labkit.rb
|
|
@@ -599,6 +600,7 @@ files:
|
|
|
599
600
|
- lib/labkit/middleware/sidekiq/user_experience_sli/server.rb
|
|
600
601
|
- lib/labkit/net_http_publisher.rb
|
|
601
602
|
- lib/labkit/rate_limit.rb
|
|
603
|
+
- lib/labkit/rate_limit/README.md
|
|
602
604
|
- lib/labkit/rate_limit/configuration.rb
|
|
603
605
|
- lib/labkit/rate_limit/evaluator.rb
|
|
604
606
|
- lib/labkit/rate_limit/identifier.rb
|