gitlab-labkit 2.6.0 → 2.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15a3589c7770e0c9368d19d4acb2365fb627f40a7a2c3dd65c03057399585036
4
- data.tar.gz: 2f45f67fd34c8031306f1cf7197e69f82d19215dc3fdb6fcb38ff1ff0b5fd804
3
+ metadata.gz: 47f5227804a89cc5611c65a3c69e221b192e47cb40437c1c6f5ec722cd61e58b
4
+ data.tar.gz: 8c361ca03e53892c607cedf959adf94af0f45da7cf353915a67f284b5b4db3ab
5
5
  SHA512:
6
- metadata.gz: c6858aecff6a32e4ca9b3b9c4b512b43de320b9f7b838348bb83de3d878a910b20169f64634c41bea4bdfa9f67db94ee2bf84864353e453296f8d40fd8d1934a
7
- data.tar.gz: e4a9d5ac76d5e4b43220a469d7557c1bbfffb4fe9abb171978ee427981623ef4b1f1a20cde3c9b8c818b83e268eb0b509201a53f4aa47b74e924127ae03186b5
6
+ metadata.gz: cfc8ff84e29c5a292f409e91b4e44b74a6a7e18f243e22b4840070f7113be537fea7f2096e6033a980e2aa163db43718c7d6f5a5fbeb2797324018f697e77cb3
7
+ data.tar.gz: 4ca119aa945825d075dee174a6b9b2ec1d16f5786bb8eae1d778000ae66eb5243a3222259025f9d66fdf32086bb2c51eab4ba05fc8b53d5de071e3f7d9d13119
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.51.0
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
@@ -16,6 +16,10 @@ trim_trailing_whitespace = true
16
16
  [.copier-*.yml]
17
17
  insert_final_newline = unset
18
18
  indent_size = unset
19
+ charset = unset
20
+ end_of_line = unset
21
+ indent_style = unset
22
+ trim_trailing_whitespace = unset
19
23
 
20
24
  [*.md]
21
25
  indent_size = unset
@@ -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
@@ -1,5 +1,5 @@
1
1
  # DO NOT MANUALLY EDIT; Run ./scripts/update-asdf-version-variables.sh to update this
2
2
  variables:
3
- GL_ASDF_RUBY_VERSION: "4.0.1"
3
+ GL_ASDF_RUBY_VERSION: "4.0.5"
4
4
  GL_ASDF_SHELLCHECK_VERSION: "0.11"
5
5
  GL_ASDF_SHFMT_VERSION: "3.13"
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.8
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.8
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.8
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,
@@ -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.8 # renovate:managed
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
@@ -1,3 +1,3 @@
1
- ruby 4.0.1
1
+ ruby 4.0.5
2
2
  shfmt 3.13
3
3
  shellcheck 0.11
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011-2022 GitLab, Inc.
1
+ Copyright (c) 2011-2026 GitLab, Inc.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
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).
@@ -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| Eval["INCR Redis counter<br/>(see Redis sequence below)"]
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, regardless of `action`:
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?
@@ -287,9 +305,10 @@ module Labkit
287
305
 
288
306
  def log_error(error, identifier)
289
307
  @logger.warn(
290
- message: "rate_limit_error",
291
308
  name: @name,
292
- error: error.class.to_s,
309
+ Labkit::Fields::ERROR_TYPE => "rate_limit_error",
310
+ Labkit::Fields::CLASS_NAME => error.class.to_s,
311
+ Labkit::Fields::ERROR_MESSAGE => error.message,
293
312
  identifier: identifier&.to_h
294
313
  )
295
314
  end
@@ -320,6 +339,17 @@ module Labkit
320
339
  )
321
340
  end
322
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
+
323
353
  def report_unmatched_metrics
324
354
  Metrics.calls_total.increment(
325
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 or error?
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), or :allow
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
@@ -54,7 +54,7 @@ fi
54
54
  # install mise/asdf dependencies
55
55
  echo "installing required plugins with mise install.."
56
56
  mise plugins update -q
57
- mise trust
57
+ mise trust --yes
58
58
  mise install
59
59
 
60
60
  # set PROMPT_COMMAND to empty value for mise if unset
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.6.0
4
+ version: 2.7.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.3
719
+ rubygems_version: 4.0.10
719
720
  specification_version: 4
720
721
  summary: Instrumentation for GitLab
721
722
  test_files: []