gitlab-labkit 2.6.1 → 2.8.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/.editorconfig +4 -0
- data/.gitlab/duo/agent-config.yml +33 -0
- data/.gitlab-ci-asdf-versions.yml +1 -1
- data/.gitlab-ci.yml +3 -3
- data/.mise.toml +6 -0
- data/.pre-commit-config.yaml +1 -1
- data/.tool-versions +1 -1
- data/LICENSE +1 -1
- data/README.md +1 -0
- data/lib/labkit/fields.rb +1 -1
- data/lib/labkit/rate_limit/README.md +17 -5
- data/lib/labkit/rate_limit/evaluator.rb +29 -0
- data/lib/labkit/rate_limit/result.rb +3 -1
- data/lib/labkit/rate_limit/rule.rb +6 -3
- data/scripts/prepare-dev-env.sh +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a0038ac3c9b17772bb517d4e2fc9e61ca26d05647e3de991c4899dfc867f4d88
|
|
4
|
+
data.tar.gz: 041f4b6ee0f7257738b0bb7663adcbca06c9f520152cd7f7b8397b31ed280d7c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ee2a000a091c0eb8433911f6a0e17d7921c1477204ed9f5e5fb54a3b8d625c8361b7653134cb5231c7b78e4fdc6df74c1b3c9d92424b1a17533f35257b913115
|
|
7
|
+
data.tar.gz: dd0a54427b6a2181bfad09efda0536ca4c0b703e2ee8182214a0ce02079f7ee207423f4e7f7d4f40f564a98d2e2d6eaeac8b4e312cf288450c2c0d8e6d146c4f
|
data/.copier-answers.yml
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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.60.0
|
|
7
7
|
_src_path: https://gitlab.com/gitlab-com/gl-infra/common-template-copier.git
|
|
8
8
|
ee_licensed: false
|
|
9
9
|
gitlab_namespace: gitlab-org/ruby/gems
|
|
@@ -16,5 +16,6 @@ project_name: labkit-ruby
|
|
|
16
16
|
release_platform: false
|
|
17
17
|
ruby: true
|
|
18
18
|
rubygem: true
|
|
19
|
+
rust: false
|
|
19
20
|
terraform: false
|
|
20
21
|
truncated_tags: true
|
data/.editorconfig
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# GitLab Duo Agent Platform execution config.
|
|
2
|
+
# https://docs.gitlab.com/user/duo_agent_platform/flows/execution/
|
|
3
|
+
#
|
|
4
|
+
# Runs flows for this project inside our own duo-workflow-sandbox image (which
|
|
5
|
+
# ships mise), pre-installs the repo's mise toolset, and caches it between runs.
|
|
6
|
+
# NOTE: this file is only read from the default branch.
|
|
7
|
+
image: registry.gitlab.com/gitlab-com/gl-infra/common-ci-tasks-images/duo-workflow-sandbox:latest
|
|
8
|
+
|
|
9
|
+
setup_script:
|
|
10
|
+
# Keep mise's data + cache inside the project dir so the `cache:` block below
|
|
11
|
+
# can capture them (caching only reliably covers paths under the project dir).
|
|
12
|
+
- export MISE_DATA_DIR="${CI_PROJECT_DIR}/.cache/mise/data"
|
|
13
|
+
- export MISE_CACHE_DIR="${CI_PROJECT_DIR}/.cache/mise/cache"
|
|
14
|
+
- mise trust --yes
|
|
15
|
+
- mise install --yes
|
|
16
|
+
|
|
17
|
+
cache:
|
|
18
|
+
# Re-use installed tools as long as the version pins are unchanged.
|
|
19
|
+
key:
|
|
20
|
+
files:
|
|
21
|
+
- .tool-versions
|
|
22
|
+
- .mise.toml
|
|
23
|
+
paths:
|
|
24
|
+
- .cache/mise/data
|
|
25
|
+
- .cache/mise/cache
|
|
26
|
+
|
|
27
|
+
network_policy:
|
|
28
|
+
# Allow the Sandbox Runtime's recommended allowlist (GitLab, package
|
|
29
|
+
# registries, etc.) so flows can reach the services they normally need.
|
|
30
|
+
include_recommended_allowed: true
|
|
31
|
+
# Additional domains flows are allowed to reach.
|
|
32
|
+
allowed_domains:
|
|
33
|
+
- api.osv.dev
|
data/.gitlab-ci.yml
CHANGED
|
@@ -19,13 +19,13 @@ include:
|
|
|
19
19
|
# It includes standard checks, gitlab-scanners, validations and release processes
|
|
20
20
|
# common to all projects using this template library.
|
|
21
21
|
# see https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/blob/main/templates/standard.md
|
|
22
|
-
- component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/standard-build@v4.
|
|
22
|
+
- component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/standard-build@v4.22
|
|
23
23
|
|
|
24
24
|
# Runs rspec tests and rubocop on the project
|
|
25
25
|
# see https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/blob/main/templates/ruby.md
|
|
26
|
-
- component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/ruby-build@v4.
|
|
26
|
+
- component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/ruby-build@v4.22
|
|
27
27
|
|
|
28
|
-
- component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/danger@v4.
|
|
28
|
+
- component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/danger@v4.22
|
|
29
29
|
|
|
30
30
|
# Attach a redis service to the rspec job from common-ci-tasks/ruby-build.
|
|
31
31
|
# GitLab merges keys when a local job has the same name as an included one,
|
data/.mise.toml
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
[settings]
|
|
2
2
|
legacy_version_file = false
|
|
3
|
+
# Install precompiled Ruby instead of building from source. The from-source
|
|
4
|
+
# build compiles a vendored OpenSSL (ruby-build's `needs_openssl` check fails
|
|
5
|
+
# because CI images ship the OpenSSL runtime but no dev headers/pkg-config),
|
|
6
|
+
# and that compile is slow and fails in CI. Precompiled Ruby bundles its own
|
|
7
|
+
# OpenSSL and sidesteps it. This becomes mise's default in 2026.8.0.
|
|
8
|
+
ruby.compile = false
|
|
3
9
|
|
|
4
10
|
[tools]
|
|
5
11
|
# Please continue to use `.tool-versions` to specify tool versions, rather than adding them directly here.
|
data/.pre-commit-config.yaml
CHANGED
|
@@ -25,7 +25,7 @@ repos:
|
|
|
25
25
|
# Documentation available at
|
|
26
26
|
# https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/blob/main/docs/pre-commit.md
|
|
27
27
|
- repo: https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks
|
|
28
|
-
rev: v4.
|
|
28
|
+
rev: v4.22 # renovate:managed
|
|
29
29
|
|
|
30
30
|
hooks:
|
|
31
31
|
- id: shellcheck # Run shellcheck for changed Shell files
|
data/.tool-versions
CHANGED
data/LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -21,6 +21,7 @@ LabKit-Ruby provides functionality in a number of areas:
|
|
|
21
21
|
1. `Labkit::Context` used for providing context information to log messages.
|
|
22
22
|
1. `Labkit::Correlation` for accessing the correlation id. (Generated and propagated by `Labkit::Context`)
|
|
23
23
|
1. `Labkit::UserExperienceSli` for tracking User Experience SLIs. More on the [README](./lib/labkit/user_experience_sli/README.md).
|
|
24
|
+
1. `Labkit::ApplicationSli` for tracking Application SLIs (Apdex and error-rate). More on the [README](./lib/labkit/application_sli/README.md).
|
|
24
25
|
1. `Labkit::FIPS` for checking for FIPS mode and using FIPS-compliant algorithms.
|
|
25
26
|
1. `Labkit::Logging` for sanitizing log messages.
|
|
26
27
|
1. `Labkit::Metrics` for metrics. More on the [README](./lib/labkit/metrics/README.md).
|
data/lib/labkit/fields.rb
CHANGED
|
@@ -206,7 +206,7 @@ module Labkit
|
|
|
206
206
|
Fields::SEVERITY => %w[level],
|
|
207
207
|
Fields::LOG_MESSAGE => %w[msg custom_message extra.message fields.message graphql.message reason color_message exception.gitaly],
|
|
208
208
|
Fields::CLASS_NAME => %w[class author_class exception.class extra.class extra.class_name],
|
|
209
|
-
Fields::SERVICE_NAME => %w[service grpc.service_name auth_service
|
|
209
|
+
Fields::SERVICE_NAME => %w[service grpc.service_name auth_service component subcomponent],
|
|
210
210
|
Fields::GL_ORGANIZATION_ID => %w[organization_id],
|
|
211
211
|
Fields::GL_PROJECT_PATH => %w[project_path full_path root_pipeline_project_path requested_project_path auth_project_path extra.gl_project_path],
|
|
212
212
|
}.freeze
|
|
@@ -116,7 +116,8 @@ the same counter.
|
|
|
116
116
|
mutate Redis. It is useful when one code path should account for the request
|
|
117
117
|
(`check`) and another should gate a side-effect on whether the caller is
|
|
118
118
|
already over-limit. `peek` skips `:log` rules — their state is unobservable
|
|
119
|
-
without incrementing.
|
|
119
|
+
without incrementing. A matched `:skip` rule terminates `peek` the same way
|
|
120
|
+
it terminates `check`: matched, `:allow`, no Redis read, no `info`.
|
|
120
121
|
|
|
121
122
|
## Identifier
|
|
122
123
|
|
|
@@ -145,7 +146,7 @@ A `Rule` is a `Data.define` value object with the following fields:
|
|
|
145
146
|
| `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
147
|
| `limit` | Integer request threshold per `period`. May be a callable resolved on every check. |
|
|
147
148
|
| `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
|
+
| `action` | What the result reports when the limit is exceeded. One of `:block`, `:log`, `:allow`, `:skip`. Default `:block`. See [Actions](#actions). |
|
|
149
150
|
| `characteristics` | Array of identifier keys whose values are folded into the Redis counter key. Each unique combination gets its own counter. |
|
|
150
151
|
|
|
151
152
|
Making `limit` or `period` callable is the supported pattern for
|
|
@@ -189,7 +190,10 @@ flowchart TD
|
|
|
189
190
|
Start([check identifier]) --> Iter{Next rule?}
|
|
190
191
|
Iter -->|yes| Match{rule.match<br/>all satisfied?}
|
|
191
192
|
Match -->|no| Iter
|
|
192
|
-
Match -->|yes|
|
|
193
|
+
Match -->|yes| Skip{rule.action<br/>== :skip?}
|
|
194
|
+
Skip -->|"yes (no Redis op)"| SkipEmit[Emit calls_total<br/>action=skip]
|
|
195
|
+
SkipEmit --> SkipReturn([Return matched=true<br/>action=:allow])
|
|
196
|
+
Skip -->|no| Eval["INCR Redis counter<br/>(see Redis sequence below)"]
|
|
193
197
|
Eval --> Build[Build Result<br/>resolve limit/period]
|
|
194
198
|
Build --> Emit[Emit calls_total + limit/period gauges]
|
|
195
199
|
Emit --> Act{rule.action}
|
|
@@ -204,7 +208,8 @@ flowchart TD
|
|
|
204
208
|
### Actions
|
|
205
209
|
|
|
206
210
|
The rule's `action` controls how the `Result` reports an over-limit hit. The
|
|
207
|
-
counter is always incremented when a rule matches,
|
|
211
|
+
counter is always incremented when a rule matches, except for `:skip` rules,
|
|
212
|
+
which never touch Redis:
|
|
208
213
|
|
|
209
214
|
- `:block` — when exceeded, `Result#action` is `:block`. Caller should reject
|
|
210
215
|
the request (e.g. with HTTP 429). When under the limit, action is `:allow`.
|
|
@@ -218,6 +223,13 @@ counter is always incremented when a rule matches, regardless of `action`:
|
|
|
218
223
|
`:block`). Useful for "always allow this caller even if they're over the
|
|
219
224
|
limit" cases while still observing them via metrics. Evaluation terminates
|
|
220
225
|
on the first match.
|
|
226
|
+
- `:skip` — bypass. A matching rule terminates evaluation with
|
|
227
|
+
`Result#action` `:allow` **without any Redis operation**: nothing is
|
|
228
|
+
counted, so `limit`, `period`, `characteristics`, and `count_distinct` are
|
|
229
|
+
inert and the result carries no `info` (`to_response_headers` is `{}`).
|
|
230
|
+
The match is still observable via `calls_total{action="skip"}`. Use this
|
|
231
|
+
for bypasses that don't need a counter; use `:allow` only when you want
|
|
232
|
+
the bypassed traffic counted.
|
|
221
233
|
|
|
222
234
|
### Redis keys
|
|
223
235
|
|
|
@@ -302,7 +314,7 @@ should treat the request as allowed.
|
|
|
302
314
|
|
|
303
315
|
| metric | type | labels | meaning |
|
|
304
316
|
|-------------------------------------------------|---------|-------------------------------------|----------------------------------------------------------------------|
|
|
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. |
|
|
317
|
+
| `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"`, `"skip"`. `rule="unmatched", action="allow"` when no rule terminated. |
|
|
306
318
|
| `gitlab_labkit_rate_limiter_errors_total` | counter | `rate_limiter` | Fail-open events (any `StandardError` in the labkit path). |
|
|
307
319
|
| `gitlab_labkit_rate_limiter_limit` | gauge | `rate_limiter`, `rule` | Resolved limit at the last check (useful when `limit:` is callable). |
|
|
308
320
|
| `gitlab_labkit_rate_limiter_period_seconds` | gauge | `rate_limiter`, `rule` | Resolved period at the last check. |
|
|
@@ -91,6 +91,11 @@ module Labkit
|
|
|
91
91
|
# :log rules are non-terminating: they emit metrics and continue,
|
|
92
92
|
# so a shadow :log rule cannot disable a following :block rule.
|
|
93
93
|
#
|
|
94
|
+
# :skip rules terminate on match without touching Redis: no counter is
|
|
95
|
+
# incremented, so the branch sits before the count_distinct check
|
|
96
|
+
# (identifier completeness is irrelevant to a rule that builds no key).
|
|
97
|
+
# calls_total still increments so the bypass stays observable.
|
|
98
|
+
#
|
|
94
99
|
# SET-mode rules (rule.count_distinct set) that match but whose identifier
|
|
95
100
|
# is missing the count_distinct key fail open + log + bump errors_total, and
|
|
96
101
|
# the loop continues to the next rule (the rule is treated as not applicable
|
|
@@ -99,6 +104,11 @@ module Labkit
|
|
|
99
104
|
@rules.each do |rule|
|
|
100
105
|
next unless rule_matches?(rule, identifier)
|
|
101
106
|
|
|
107
|
+
if rule.action == :skip
|
|
108
|
+
report_skipped_metrics(rule)
|
|
109
|
+
return skip_result(rule)
|
|
110
|
+
end
|
|
111
|
+
|
|
102
112
|
if rule.count_distinct && missing_count_distinct_value?(rule, identifier)
|
|
103
113
|
log_missing_count_distinct(rule, identifier)
|
|
104
114
|
report_error_metrics
|
|
@@ -125,6 +135,8 @@ module Labkit
|
|
|
125
135
|
next if rule.action == :log
|
|
126
136
|
next unless rule_matches?(rule, identifier)
|
|
127
137
|
|
|
138
|
+
return skip_result(rule) if rule.action == :skip
|
|
139
|
+
|
|
128
140
|
return peek_rule(rule, identifier, rule_context)
|
|
129
141
|
end
|
|
130
142
|
|
|
@@ -135,6 +147,12 @@ module Labkit
|
|
|
135
147
|
rule.match.all? { |key, matcher| matcher.match?(identifier[key]) }
|
|
136
148
|
end
|
|
137
149
|
|
|
150
|
+
# A matched :skip rule allows without evaluating: no counter exists, so
|
|
151
|
+
# info is nil (to_response_headers is {} for skip results).
|
|
152
|
+
def skip_result(rule)
|
|
153
|
+
Result.new(matched: true, action: :allow, rule: rule)
|
|
154
|
+
end
|
|
155
|
+
|
|
138
156
|
def missing_count_distinct_value?(rule, identifier)
|
|
139
157
|
value = identifier[rule.count_distinct]
|
|
140
158
|
value.nil? || value.to_s.empty?
|
|
@@ -321,6 +339,17 @@ module Labkit
|
|
|
321
339
|
)
|
|
322
340
|
end
|
|
323
341
|
|
|
342
|
+
# calls_total carries action="skip" (the rule action, not the :allow the
|
|
343
|
+
# caller sees) so bypass traffic stays distinguishable from counted
|
|
344
|
+
# allows. No limit/period gauges: a skip rule has no limit to report.
|
|
345
|
+
def report_skipped_metrics(rule)
|
|
346
|
+
Metrics.calls_total.increment(
|
|
347
|
+
rate_limiter: @name,
|
|
348
|
+
rule: rule.name,
|
|
349
|
+
action: "skip"
|
|
350
|
+
)
|
|
351
|
+
end
|
|
352
|
+
|
|
324
353
|
def report_unmatched_metrics
|
|
325
354
|
Metrics.calls_total.increment(
|
|
326
355
|
rate_limiter: @name,
|
|
@@ -9,11 +9,13 @@ module Labkit
|
|
|
9
9
|
# :block = rule matched, exceeded, rule configured to block
|
|
10
10
|
# :log = rule matched, exceeded, rule configured to log only
|
|
11
11
|
# :allow = rule matched but count within limit, rule configured to allow,
|
|
12
|
+
# rule configured to skip (bypass, nothing counted),
|
|
12
13
|
# no rule matched, or error (fail-open)
|
|
13
14
|
# The rule's configured action is available via rule.action.
|
|
14
15
|
# rule - the matched Rule object (nil when matched? is false)
|
|
15
16
|
# error? - true if Redis was unavailable; result fails open (exceeded? is false)
|
|
16
|
-
# info - Result::Info with per-window counters; nil when matched? is false
|
|
17
|
+
# info - Result::Info with per-window counters; nil when matched? is false,
|
|
18
|
+
# error?, or the matched rule is :skip (no counter exists)
|
|
17
19
|
Result = Data.define(:matched, :exceeded, :action, :rule, :error, :info) do
|
|
18
20
|
def initialize(matched:, action: nil, exceeded: false, rule: nil, error: false, info: nil)
|
|
19
21
|
super
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Labkit
|
|
4
4
|
module RateLimit
|
|
5
|
-
KNOWN_ACTIONS = %i[block log allow].freeze
|
|
5
|
+
KNOWN_ACTIONS = %i[block log allow skip].freeze
|
|
6
6
|
RULE_NAME_PATTERN = /\A[a-z0-9_]+\z/
|
|
7
7
|
RULE_NAME_MAX_LENGTH = 64
|
|
8
8
|
|
|
@@ -13,9 +13,12 @@ module Labkit
|
|
|
13
13
|
# limit - request threshold; may be a callable (resolved per check)
|
|
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
|
-
# evaluation continues to subsequent rules),
|
|
16
|
+
# evaluation continues to subsequent rules), :allow
|
|
17
17
|
# (count but always permit; terminates evaluation on match
|
|
18
|
-
# regardless of whether the limit was exceeded)
|
|
18
|
+
# regardless of whether the limit was exceeded), or :skip
|
|
19
|
+
# (bypass: permit and terminate evaluation on match without
|
|
20
|
+
# counting; performs no Redis operation, so limit, period,
|
|
21
|
+
# characteristics, and count_distinct are inert)
|
|
19
22
|
# characteristics - identifier keys used to build the compound Redis counter key
|
|
20
23
|
# count_distinct - optional Symbol naming an identifier key. When set, the rule
|
|
21
24
|
# counts the number of distinct values seen for that key within
|
data/scripts/prepare-dev-env.sh
CHANGED
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: 2.
|
|
4
|
+
version: 2.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Newdigate
|
|
@@ -550,6 +550,7 @@ files:
|
|
|
550
550
|
- ".gitlab-ci-other-versions.yml"
|
|
551
551
|
- ".gitlab-ci.yml"
|
|
552
552
|
- ".gitlab/CODEOWNERS"
|
|
553
|
+
- ".gitlab/duo/agent-config.yml"
|
|
553
554
|
- ".gitlab/merge_request_templates/default.md"
|
|
554
555
|
- ".gitleaks.toml"
|
|
555
556
|
- ".mise.toml"
|
|
@@ -715,7 +716,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
715
716
|
- !ruby/object:Gem::Version
|
|
716
717
|
version: '0'
|
|
717
718
|
requirements: []
|
|
718
|
-
rubygems_version: 4.0.
|
|
719
|
+
rubygems_version: 4.0.10
|
|
719
720
|
specification_version: 4
|
|
720
721
|
summary: Instrumentation for GitLab
|
|
721
722
|
test_files: []
|