gitlab-labkit 0.40.0 → 0.41.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-ci-asdf-versions.yml +1 -1
- data/.pre-commit-config.yaml +2 -2
- data/.rubocop_todo.yml +1 -1
- data/.tool-versions +1 -1
- data/README.md +1 -1
- data/lib/labkit/covered_experience/README.md +51 -0
- data/lib/labkit/covered_experience/current.rb +34 -0
- data/lib/labkit/covered_experience/error.rb +2 -0
- data/lib/labkit/covered_experience/experience.rb +116 -27
- data/lib/labkit/covered_experience/null.rb +9 -2
- data/lib/labkit/covered_experience.rb +37 -10
- data/lib/labkit/logging/json_logger.rb +1 -1
- data/lib/labkit/middleware/rack.rb +23 -0
- data/lib/labkit/middleware/sidekiq/client.rb +1 -0
- data/lib/labkit/middleware/sidekiq/covered_experience/client.rb +27 -0
- data/lib/labkit/middleware/sidekiq/covered_experience/server.rb +25 -0
- data/lib/labkit/middleware/sidekiq/covered_experience.rb +14 -0
- data/lib/labkit/middleware/sidekiq/server.rb +1 -0
- data/lib/labkit/middleware/sidekiq.rb +1 -0
- data/lib/labkit/rspec/README.md +11 -0
- data/lib/labkit/rspec/matchers/covered_experience_matchers.rb +10 -4
- metadata +5 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 762df6d0e3348526c7b49929a3a6cdd82db05c80a9d83569a76bd10ddb131ca2
|
4
|
+
data.tar.gz: 0affeb965dd17cfc202a0b64419d46bb92419a051a70972cbab5d57e5844d027
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c703503bc9cf08155d72a0ffc61f7f8cc2c573353d67382e7b4d7b22ea554c2a14ee2e2d04f8804696fa95584843cd4a342cbcdb5e9cb8ac775bd0d36df006b3
|
7
|
+
data.tar.gz: 2a598ffe29d7382689637c605db8ba721c993e3fc4b033176b273b99515033577b16d82961007bb9e99da3feac5ec2419e58c7a45b354dfd820e4d9bfa9baa4b
|
data/.pre-commit-config.yaml
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
# exclude: '^fixtures/'
|
6
6
|
repos:
|
7
7
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
8
|
-
rev:
|
8
|
+
rev: v6.0.0 # renovate:managed
|
9
9
|
hooks:
|
10
10
|
- id: trailing-whitespace
|
11
11
|
- id: end-of-file-fixer
|
@@ -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: v2.
|
28
|
+
rev: v2.92 # renovate:managed
|
29
29
|
|
30
30
|
hooks:
|
31
31
|
- id: shellcheck # Run shellcheck for changed Shell files
|
data/.rubocop_todo.yml
CHANGED
data/.tool-versions
CHANGED
data/README.md
CHANGED
@@ -20,7 +20,7 @@ LabKit-Ruby provides functionality in a number of areas:
|
|
20
20
|
|
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
|
-
1. `Labkit::CoveredExperience` for tracking covered experiences. More on the [README](./lib/labkit/
|
23
|
+
1. `Labkit::CoveredExperience` for tracking covered experiences. More on the [README](./lib/labkit/covered_experience/README.md).
|
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).
|
@@ -16,6 +16,23 @@ end
|
|
16
16
|
|
17
17
|
This configuration affects all Covered Experience instances and their logging output.
|
18
18
|
|
19
|
+
### Registry Path Configuration
|
20
|
+
|
21
|
+
By default, covered experience definitions are loaded from the `config/covered_experiences` directory. You can configure a custom registry path:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
Labkit::CoveredExperience.configure do |config|
|
25
|
+
config.registry_path = "my/custom/path"
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
This allows you to:
|
30
|
+
- Store covered experience definitions in a different directory structure
|
31
|
+
- Use different paths for different environments
|
32
|
+
- Organize definitions according to your application's needs
|
33
|
+
|
34
|
+
**Note:** The registry is automatically reset when the configuration changes, so the new path takes effect immediately.
|
35
|
+
|
19
36
|
### Covered Experience Definitions
|
20
37
|
|
21
38
|
Covered experience definitions will be lazy loaded from the default directory (`config/covered_experiences`).
|
@@ -100,6 +117,40 @@ experience.checkpoint
|
|
100
117
|
experience.complete
|
101
118
|
```
|
102
119
|
|
120
|
+
#### Resuming Experiences
|
121
|
+
|
122
|
+
You can resume a covered experience that was previously started and stored in the context. This is useful for distributed operations or when work spans multiple processes.
|
123
|
+
|
124
|
+
Just like the start method, we can use a block to automatically complete a covered experience:
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
# Resume an experience from context (with block)
|
128
|
+
Labkit::CoveredExperience.resume(:merge_request_creation) do |experience|
|
129
|
+
# Continue the work from where it left off
|
130
|
+
finalize_merge_request
|
131
|
+
|
132
|
+
# Add checkpoints as needed
|
133
|
+
experience.checkpoint
|
134
|
+
|
135
|
+
send_notifications
|
136
|
+
end
|
137
|
+
```
|
138
|
+
Or manually:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
# Resume an experience from context (manual control)
|
142
|
+
experience = Labkit::CoveredExperience.resume(:merge_request_creation)
|
143
|
+
|
144
|
+
# Continue the work
|
145
|
+
finalize_merge_request
|
146
|
+
experience.checkpoint
|
147
|
+
|
148
|
+
# Complete the experience
|
149
|
+
experience.complete
|
150
|
+
```
|
151
|
+
|
152
|
+
**Note:** The `resume` method loads the start time from the Labkit context. If no covered experience data exists in the context, it behaves the same as calling methods on an unstarted experience (raises `UnstartedError` in development/test environments, or safely ignores in other environments).
|
153
|
+
|
103
154
|
### Error Handling
|
104
155
|
|
105
156
|
When using the block form, errors are automatically captured:
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
require "labkit/covered_experience/experience"
|
5
|
+
|
6
|
+
module Labkit
|
7
|
+
module CoveredExperience
|
8
|
+
# The `Current` class represents a container for the current set
|
9
|
+
# of `Labkit::CoveredExperience::Experience` instances started and
|
10
|
+
# not yet completed.
|
11
|
+
#
|
12
|
+
# It uses `ActiveSupport::CurrentAttributes` to provide a thread-safe way to
|
13
|
+
# store and access experiences throughout the request and background job lifecycle.
|
14
|
+
#
|
15
|
+
# Example usage:
|
16
|
+
# Labkit::CoveredExperience::Current.active_experiences << my_experience
|
17
|
+
# Labkit::CoveredExperience::Current.rehydrate("create_merge_request", "start_time" => "2025-08-22T10:02:15.237Z")
|
18
|
+
class Current < ActiveSupport::CurrentAttributes
|
19
|
+
AGGREGATION_KEY = 'labkit_covered_experiences'
|
20
|
+
|
21
|
+
attribute :_active_experiences
|
22
|
+
|
23
|
+
def active_experiences
|
24
|
+
self._active_experiences ||= {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def rehydrate(experience_id, **data)
|
28
|
+
instance = Labkit::CoveredExperience.get(experience_id).rehydrate(data)
|
29
|
+
active_experiences[instance.id] = instance
|
30
|
+
instance
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -4,6 +4,8 @@ module Labkit
|
|
4
4
|
module CoveredExperience
|
5
5
|
CoveredExperienceError = Class.new(StandardError)
|
6
6
|
UnstartedError = Class.new(CoveredExperienceError)
|
7
|
+
CompletedError = Class.new(CoveredExperienceError)
|
7
8
|
NotFoundError = Class.new(CoveredExperienceError)
|
9
|
+
ReservedKeywordError = Class.new(CoveredExperienceError)
|
8
10
|
end
|
9
11
|
end
|
@@ -1,6 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'active_support/hash_with_indifferent_access'
|
4
|
+
require 'forwardable'
|
3
5
|
require 'labkit/context'
|
6
|
+
require 'labkit/covered_experience/current'
|
4
7
|
require 'labkit/covered_experience/error'
|
5
8
|
|
6
9
|
module Labkit
|
@@ -12,15 +15,48 @@ module Labkit
|
|
12
15
|
async_slow: 300
|
13
16
|
}.freeze
|
14
17
|
|
18
|
+
RESERVED_KEYWORDS = %w[
|
19
|
+
checkpoint
|
20
|
+
covered_experience
|
21
|
+
feature_category
|
22
|
+
urgency
|
23
|
+
start_time
|
24
|
+
checkpoint_time
|
25
|
+
end_time
|
26
|
+
elapsed_time_s
|
27
|
+
urgency_threshold_s
|
28
|
+
error
|
29
|
+
error_message
|
30
|
+
success
|
31
|
+
].freeze
|
32
|
+
|
15
33
|
# The `Experience` class represents a single Covered Experience
|
16
34
|
# event to be measured and reported.
|
17
35
|
class Experience
|
18
|
-
|
36
|
+
extend Forwardable
|
37
|
+
|
38
|
+
attr_reader :error, :start_time
|
19
39
|
|
20
40
|
def initialize(definition)
|
21
41
|
@definition = definition
|
22
42
|
end
|
23
43
|
|
44
|
+
def id
|
45
|
+
@definition.covered_experience
|
46
|
+
end
|
47
|
+
|
48
|
+
# Rehydrate an Experience instance from serialized data.
|
49
|
+
#
|
50
|
+
# @param data [Hash] A hash of serialized data.
|
51
|
+
# @return [Experience]
|
52
|
+
def rehydrate(data = {})
|
53
|
+
@start_time = Time.iso8601(data["start_time"]) if data&.has_key?("start_time") && data["start_time"]
|
54
|
+
self
|
55
|
+
rescue ArgumentError
|
56
|
+
warn("Invalid #{id}, start_time: #{data['start_time']}")
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
24
60
|
# Start the Covered Experience.
|
25
61
|
#
|
26
62
|
# @yield [self] When a block is provided, the experience will be completed automatically.
|
@@ -41,20 +77,14 @@ module Labkit
|
|
41
77
|
# experience.complete
|
42
78
|
def start(**extra, &)
|
43
79
|
@start_time = Time.now.utc
|
44
|
-
checkpoint_counter.increment(checkpoint: "start")
|
80
|
+
checkpoint_counter.increment(checkpoint: "start", **base_labels)
|
45
81
|
log_event("start", **extra)
|
46
82
|
|
83
|
+
Labkit::CoveredExperience::Current.active_experiences[id] = self
|
84
|
+
|
47
85
|
return self unless block_given?
|
48
86
|
|
49
|
-
|
50
|
-
yield self
|
51
|
-
self
|
52
|
-
rescue StandardError => e
|
53
|
-
error!(e)
|
54
|
-
raise
|
55
|
-
ensure
|
56
|
-
complete(**extra)
|
57
|
-
end
|
87
|
+
completable(**extra, &)
|
58
88
|
end
|
59
89
|
|
60
90
|
# Checkpoint the Covered Experience.
|
@@ -66,12 +96,26 @@ module Labkit
|
|
66
96
|
return unless ensure_started!
|
67
97
|
|
68
98
|
@checkpoint_time = Time.now.utc
|
69
|
-
checkpoint_counter.increment(checkpoint: "intermediate")
|
99
|
+
checkpoint_counter.increment(checkpoint: "intermediate", **base_labels)
|
70
100
|
log_event("intermediate", **extra)
|
71
101
|
|
72
102
|
self
|
73
103
|
end
|
74
104
|
|
105
|
+
# Resume the Covered Experience.
|
106
|
+
#
|
107
|
+
# @yield [self] When a block is provided, the experience will be completed automatically.
|
108
|
+
# @param extra [Hash] Additional data to include in the log
|
109
|
+
def resume(**extra, &)
|
110
|
+
return unless ensure_started!
|
111
|
+
|
112
|
+
checkpoint(checkpoint_action: 'resume', **extra)
|
113
|
+
|
114
|
+
return self unless block_given?
|
115
|
+
|
116
|
+
completable(**extra, &)
|
117
|
+
end
|
118
|
+
|
75
119
|
# Complete the Covered Experience.
|
76
120
|
#
|
77
121
|
# @param extra [Hash] Additional data to include in the log event
|
@@ -79,14 +123,16 @@ module Labkit
|
|
79
123
|
# @return [self]
|
80
124
|
def complete(**extra)
|
81
125
|
return unless ensure_started!
|
126
|
+
return unless ensure_incomplete!
|
82
127
|
|
83
128
|
begin
|
84
129
|
@end_time = Time.now.utc
|
85
130
|
ensure
|
86
|
-
checkpoint_counter.increment(checkpoint: "end")
|
87
|
-
total_counter.increment(error: has_error
|
88
|
-
apdex_counter.increment(success: apdex_success
|
131
|
+
checkpoint_counter.increment(checkpoint: "end", **base_labels)
|
132
|
+
total_counter.increment(error: has_error?, **base_labels)
|
133
|
+
apdex_counter.increment(success: apdex_success?, **base_labels) unless has_error?
|
89
134
|
log_event("end", **extra)
|
135
|
+
Labkit::CoveredExperience::Current.active_experiences.delete(id)
|
90
136
|
end
|
91
137
|
|
92
138
|
self
|
@@ -105,6 +151,12 @@ module Labkit
|
|
105
151
|
!!@error
|
106
152
|
end
|
107
153
|
|
154
|
+
def to_h
|
155
|
+
return {} unless ensure_started!
|
156
|
+
|
157
|
+
{ id => { "start_time" => @start_time&.iso8601(3) } }
|
158
|
+
end
|
159
|
+
|
108
160
|
private
|
109
161
|
|
110
162
|
def base_labels
|
@@ -120,6 +172,28 @@ module Labkit
|
|
120
172
|
raise(err) if %w[development test].include?(ENV['RAILS_ENV'])
|
121
173
|
end
|
122
174
|
|
175
|
+
def completable(**extra, &)
|
176
|
+
begin
|
177
|
+
yield self
|
178
|
+
rescue StandardError => e
|
179
|
+
error!(e)
|
180
|
+
raise
|
181
|
+
ensure
|
182
|
+
complete(**extra)
|
183
|
+
end
|
184
|
+
|
185
|
+
self
|
186
|
+
end
|
187
|
+
|
188
|
+
def ensure_incomplete!
|
189
|
+
return true if @end_time.nil?
|
190
|
+
|
191
|
+
err = CompletedError.new("Covered Experience #{@definition.covered_experience} already completed")
|
192
|
+
|
193
|
+
warn(err)
|
194
|
+
raise(err) if %w[development test].include?(ENV['RAILS_ENV'])
|
195
|
+
end
|
196
|
+
|
123
197
|
def urgency_threshold
|
124
198
|
URGENCY_THRESHOLDS_IN_SECONDS[@definition.urgency.to_sym]
|
125
199
|
end
|
@@ -136,36 +210,35 @@ module Labkit
|
|
136
210
|
def checkpoint_counter
|
137
211
|
@checkpoint_counter ||= Labkit::Metrics::Client.counter(
|
138
212
|
:gitlab_covered_experience_checkpoint_total,
|
139
|
-
'Total checkpoints for covered experiences'
|
140
|
-
base_labels
|
213
|
+
'Total checkpoints for covered experiences'
|
141
214
|
)
|
142
215
|
end
|
143
216
|
|
144
217
|
def total_counter
|
145
218
|
@total_counter ||= Labkit::Metrics::Client.counter(
|
146
219
|
:gitlab_covered_experience_total,
|
147
|
-
'Total covered experience events (success/failure)'
|
148
|
-
base_labels
|
220
|
+
'Total covered experience events (success/failure)'
|
149
221
|
)
|
150
222
|
end
|
151
223
|
|
152
224
|
def apdex_counter
|
153
225
|
@apdex_counter ||= Labkit::Metrics::Client.counter(
|
154
226
|
:gitlab_covered_experience_apdex_total,
|
155
|
-
'Total covered experience apdex events'
|
156
|
-
base_labels
|
227
|
+
'Total covered experience apdex events'
|
157
228
|
)
|
158
229
|
end
|
159
230
|
|
160
231
|
def log_event(event_type, **extra)
|
232
|
+
validate_extra_parameters!(extra)
|
233
|
+
|
161
234
|
log_data = build_log_data(event_type, **extra)
|
162
235
|
logger.info(log_data)
|
163
236
|
end
|
164
237
|
|
165
238
|
def build_log_data(event_type, **extra)
|
166
|
-
log_data =
|
239
|
+
log_data = ActiveSupport::HashWithIndifferentAccess.new(
|
167
240
|
checkpoint: event_type,
|
168
|
-
covered_experience:
|
241
|
+
covered_experience: id,
|
169
242
|
feature_category: @definition.feature_category,
|
170
243
|
urgency: @definition.urgency,
|
171
244
|
start_time: @start_time,
|
@@ -173,8 +246,8 @@ module Labkit
|
|
173
246
|
end_time: @end_time,
|
174
247
|
elapsed_time_s: elapsed_time,
|
175
248
|
urgency_threshold_s: urgency_threshold
|
176
|
-
|
177
|
-
log_data.
|
249
|
+
)
|
250
|
+
log_data.reverse_merge!(extra) if extra
|
178
251
|
|
179
252
|
if has_error?
|
180
253
|
log_data[:error] = true
|
@@ -186,8 +259,24 @@ module Labkit
|
|
186
259
|
log_data
|
187
260
|
end
|
188
261
|
|
189
|
-
def warn(
|
190
|
-
|
262
|
+
def warn(err, **extra)
|
263
|
+
case err
|
264
|
+
when StandardError
|
265
|
+
logger.warn(component: self.class.name, message: err.message, **extra)
|
266
|
+
when String
|
267
|
+
logger.warn(component: self.class.name, message: err, **extra)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def validate_extra_parameters!(extra)
|
272
|
+
return if extra.empty?
|
273
|
+
|
274
|
+
reserved_keys = extra.keys.map(&:to_s) & RESERVED_KEYWORDS
|
275
|
+
return if reserved_keys.empty?
|
276
|
+
|
277
|
+
err = ReservedKeywordError.new("Reserved keywords found in extra parameters: #{reserved_keys.join(', ')}")
|
278
|
+
|
279
|
+
raise(err) if %w[development test].include?(ENV['RAILS_ENV'])
|
191
280
|
end
|
192
281
|
|
193
282
|
def logger
|
@@ -6,9 +6,14 @@ module Labkit
|
|
6
6
|
class Null
|
7
7
|
include Singleton
|
8
8
|
|
9
|
-
|
9
|
+
def id = 'null'
|
10
10
|
|
11
|
-
def start(
|
11
|
+
def start(**_extra)
|
12
|
+
yield self if block_given?
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def resume(**_extra)
|
12
17
|
yield self if block_given?
|
13
18
|
self
|
14
19
|
end
|
@@ -17,6 +22,8 @@ module Labkit
|
|
17
22
|
def checkpoint(*_args) = self
|
18
23
|
def complete(*_args) = self
|
19
24
|
def error!(*_args) = self
|
25
|
+
def rehydrate(*_args, **_kwargs) = self
|
26
|
+
def to_h = {}
|
20
27
|
end
|
21
28
|
end
|
22
29
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'labkit/covered_experience/current'
|
3
4
|
require 'labkit/covered_experience/error'
|
4
5
|
require 'labkit/covered_experience/experience'
|
5
6
|
require 'labkit/covered_experience/null'
|
@@ -15,10 +16,11 @@ module Labkit
|
|
15
16
|
module CoveredExperience
|
16
17
|
# Configuration class for CoveredExperience
|
17
18
|
class Configuration
|
18
|
-
attr_accessor :logger
|
19
|
+
attr_accessor :logger, :registry_path
|
19
20
|
|
20
21
|
def initialize
|
21
22
|
@logger = Labkit::Logging::JsonLogger.new($stdout)
|
23
|
+
@registry_path = File.join("config", "covered_experiences")
|
22
24
|
end
|
23
25
|
end
|
24
26
|
|
@@ -33,28 +35,45 @@ module Labkit
|
|
33
35
|
|
34
36
|
def configure
|
35
37
|
yield(configuration) if block_given?
|
38
|
+
# Reset registry when configuration changes to pick up new registry_path
|
39
|
+
@registry = nil
|
36
40
|
end
|
37
41
|
|
38
42
|
def registry
|
39
|
-
@registry ||= Registry.new
|
43
|
+
@registry ||= Registry.new(dir: configuration.registry_path)
|
40
44
|
end
|
41
45
|
|
42
46
|
def reset
|
43
47
|
@registry = nil
|
48
|
+
reset_configuration
|
44
49
|
end
|
45
50
|
|
51
|
+
# Retrieves a covered experience using the experience_id.
|
52
|
+
# It retrieves from the current context when available,
|
53
|
+
# otherwise it instantiates a new experience with the definition
|
54
|
+
# from the registry.
|
55
|
+
#
|
56
|
+
# @param experience_id [String, Symbol] The ID of the experience to retrieve.
|
57
|
+
# @return [Experience, Null] The found experience or a Null object if not found (in production/staging).
|
46
58
|
def get(experience_id)
|
47
|
-
|
59
|
+
find_current(experience_id) || raise_or_null(experience_id)
|
60
|
+
end
|
48
61
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
62
|
+
# Starts a covered experience using the experience_id.
|
63
|
+
#
|
64
|
+
# @param experience_id [String, Symbol] The ID of the experience to start.
|
65
|
+
# @param extra [Hash] Additional data to include in the log event.
|
66
|
+
# @return [Experience, Null] The started experience or a Null object if not found (in production/staging).
|
67
|
+
def start(experience_id, **extra, &)
|
68
|
+
get(experience_id).start(**extra, &)
|
54
69
|
end
|
55
70
|
|
56
|
-
|
57
|
-
|
71
|
+
# Resumes a covered experience using the experience_id.
|
72
|
+
#
|
73
|
+
# @param experience_id [String, Symbol] The ID of the experience to resume.
|
74
|
+
# @return [Experience, Null] The started experience or a Null object if not found (in production/staging).
|
75
|
+
def resume(experience_id, **extra, &)
|
76
|
+
get(experience_id).resume(**extra, &)
|
58
77
|
end
|
59
78
|
|
60
79
|
private
|
@@ -64,6 +83,14 @@ module Labkit
|
|
64
83
|
|
65
84
|
raise(NotFoundError, "Covered Experience #{experience_id} not found in the registry")
|
66
85
|
end
|
86
|
+
|
87
|
+
def find_current(experience_id)
|
88
|
+
xp = Current.active_experiences[experience_id.to_s]
|
89
|
+
return xp unless xp.nil?
|
90
|
+
|
91
|
+
definition = registry[experience_id]
|
92
|
+
Experience.new(definition) if definition
|
93
|
+
end
|
67
94
|
end
|
68
95
|
end
|
69
96
|
end
|
@@ -25,6 +25,8 @@ module Labkit
|
|
25
25
|
|
26
26
|
[status, headers, response]
|
27
27
|
end
|
28
|
+
ensure
|
29
|
+
reset_current_experiences
|
28
30
|
end
|
29
31
|
|
30
32
|
private
|
@@ -44,6 +46,27 @@ module Labkit
|
|
44
46
|
.merge("version" => "1")
|
45
47
|
.to_json
|
46
48
|
end
|
49
|
+
|
50
|
+
# Reset Current experiences to prevent memory leaks and cross-request contamination.
|
51
|
+
#
|
52
|
+
# Rails applications automatically call ActiveSupport::CurrentAttributes.reset_all
|
53
|
+
# after each request via https://github.com/rails/rails/blob/2d4f445051b63853d48c642d0ab59ab026aa1d6a/activesupport/lib/active_support/railtie.rb,
|
54
|
+
# but we must handle other scenarios where this middleware might be used:
|
55
|
+
#
|
56
|
+
# 1. Non-Rails Rack applications (Sinatra, Grape, plain Rack apps)
|
57
|
+
# 2. Custom Rack stacks that don't include ActiveSupport::Railtie
|
58
|
+
# 3. Test environments where Rails middleware stack might be bypassed
|
59
|
+
# 4. Microservices or API-only applications with minimal middleware
|
60
|
+
# 5. Background job processors that use Rack but not Rails' full stack
|
61
|
+
#
|
62
|
+
# By explicitly resetting here, we ensure covered experiences are properly
|
63
|
+
# cleaned up regardless of the application framework or middleware configuration.
|
64
|
+
def reset_current_experiences
|
65
|
+
# Only reset if the Current class is loaded to avoid unnecessary dependencies
|
66
|
+
return unless defined?(Labkit::CoveredExperience::Current)
|
67
|
+
|
68
|
+
Labkit::CoveredExperience::Current.reset
|
69
|
+
end
|
47
70
|
end
|
48
71
|
end
|
49
72
|
end
|
@@ -13,6 +13,7 @@ module Labkit
|
|
13
13
|
@chain ||= ::Sidekiq::Middleware::Chain.new do |chain|
|
14
14
|
chain.add Labkit::Middleware::Sidekiq::Context::Client
|
15
15
|
chain.add Labkit::Middleware::Sidekiq::Tracing::Client if Labkit::Tracing.enabled?
|
16
|
+
chain.add Labkit::Middleware::Sidekiq::CoveredExperience::Client
|
16
17
|
end
|
17
18
|
end
|
18
19
|
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'labkit/covered_experience/current'
|
4
|
+
|
5
|
+
module Labkit
|
6
|
+
module Middleware
|
7
|
+
module Sidekiq
|
8
|
+
module CoveredExperience
|
9
|
+
# This middleware for Sidekiq-client wraps scheduling jobs with covered
|
10
|
+
# experience context. It retrieves the current experiences and
|
11
|
+
# populates the job with them.
|
12
|
+
class Client
|
13
|
+
def call(worker_class, job, _queue, _redis_pool)
|
14
|
+
data = Labkit::CoveredExperience::Current.active_experiences.inject({}) do |data, (_, xp)|
|
15
|
+
xp.checkpoint(checkpoint_action: "sidekiq_job_scheduled", worker: worker_class.to_s)
|
16
|
+
data.merge!(xp.to_h)
|
17
|
+
end
|
18
|
+
|
19
|
+
job[Labkit::CoveredExperience::Current::AGGREGATION_KEY] = data unless data.empty?
|
20
|
+
|
21
|
+
yield
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Labkit
|
4
|
+
module Middleware
|
5
|
+
module Sidekiq
|
6
|
+
module CoveredExperience
|
7
|
+
# This middleware for Sidekiq-server rehydrates the current experiences
|
8
|
+
# serialized to the job
|
9
|
+
class Server
|
10
|
+
def call(_worker_class, job, _queue)
|
11
|
+
job[Labkit::CoveredExperience::Current::AGGREGATION_KEY]&.each do |experience_id, data|
|
12
|
+
xp = Labkit::CoveredExperience::Current.rehydrate(experience_id, **data)
|
13
|
+
xp.checkpoint(checkpoint_action: "sidekiq_job_started", worker: job["class"].to_s)
|
14
|
+
end
|
15
|
+
|
16
|
+
yield
|
17
|
+
|
18
|
+
ensure
|
19
|
+
Labkit::CoveredExperience::Current.reset
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Labkit
|
4
|
+
module Middleware
|
5
|
+
module Sidekiq
|
6
|
+
# This module contains all the sidekiq middleware regarding covered
|
7
|
+
# experiences
|
8
|
+
module CoveredExperience
|
9
|
+
autoload :Client, "labkit/middleware/sidekiq/covered_experience/client"
|
10
|
+
autoload :Server, "labkit/middleware/sidekiq/covered_experience/server"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -13,6 +13,7 @@ module Labkit
|
|
13
13
|
@chain ||= ::Sidekiq::Middleware::Chain.new do |chain|
|
14
14
|
chain.add Labkit::Middleware::Sidekiq::Context::Server
|
15
15
|
chain.add Labkit::Middleware::Sidekiq::Tracing::Server if Labkit::Tracing.enabled?
|
16
|
+
chain.add Labkit::Middleware::Sidekiq::CoveredExperience::Server
|
16
17
|
end
|
17
18
|
end
|
18
19
|
|
@@ -7,6 +7,7 @@ module Labkit
|
|
7
7
|
autoload :Client, "labkit/middleware/sidekiq/client"
|
8
8
|
autoload :Server, "labkit/middleware/sidekiq/server"
|
9
9
|
autoload :Context, "labkit/middleware/sidekiq/context"
|
10
|
+
autoload :CoveredExperience, "labkit/middleware/sidekiq/covered_experience"
|
10
11
|
autoload :Tracing, "labkit/middleware/sidekiq/tracing"
|
11
12
|
end
|
12
13
|
end
|
data/lib/labkit/rspec/README.md
CHANGED
@@ -45,6 +45,17 @@ expect { subject }.to checkpoint_covered_experience('rails_request')
|
|
45
45
|
expect { subject }.not_to checkpoint_covered_experience('rails_request')
|
46
46
|
```
|
47
47
|
|
48
|
+
#### `resume_covered_experience`
|
49
|
+
|
50
|
+
Tests that a covered experience is resumed (checkpoint=intermediate metric is incremented). This is an alias for `checkpoint_covered_experience` that provides more semantic meaning when testing code that resumes a covered experience previously started.
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
expect { subject }.to resume_covered_experience('rails_request')
|
54
|
+
|
55
|
+
# Test that it does NOT resume
|
56
|
+
expect { subject }.not_to resume_covered_experience('rails_request')
|
57
|
+
```
|
58
|
+
|
48
59
|
#### `complete_covered_experience`
|
49
60
|
|
50
61
|
Tests that a covered experience is completed with the expected metrics:
|
@@ -81,7 +81,7 @@ end
|
|
81
81
|
#
|
82
82
|
# Parameters:
|
83
83
|
# - covered_experience_id: Required. The ID of the covered experience (e.g., 'rails_request')
|
84
|
-
RSpec::Matchers.define :checkpoint_covered_experience do |covered_experience_id|
|
84
|
+
RSpec::Matchers.define :checkpoint_covered_experience do |covered_experience_id, by: 1|
|
85
85
|
include Labkit::RSpec::Matchers::CoveredExperience
|
86
86
|
|
87
87
|
description { "checkpoint covered experience '#{covered_experience_id}'" }
|
@@ -97,12 +97,15 @@ RSpec::Matchers.define :checkpoint_covered_experience do |covered_experience_id|
|
|
97
97
|
checkpoint_after = checkpoint_counter&.get(labels.merge(checkpoint: "intermediate")).to_i
|
98
98
|
@checkpoint_change = checkpoint_after - checkpoint_before
|
99
99
|
|
100
|
-
|
100
|
+
# Automatic checkpoints can be created in-between depending on the context,
|
101
|
+
# such as pushing experiences to background jobs. For this reason, we check
|
102
|
+
# that the value increases by at least "by".
|
103
|
+
@checkpoint_change >= by
|
101
104
|
end
|
102
105
|
|
103
106
|
failure_message do
|
104
107
|
"Failed to checkpoint covered experience '#{covered_experience_id}':\n" \
|
105
|
-
"expected checkpoint='intermediate' counter to increase by
|
108
|
+
"expected checkpoint='intermediate' counter to increase by at least #{by}, but increased by #{@checkpoint_change}"
|
106
109
|
end
|
107
110
|
|
108
111
|
match_when_negated do |actual|
|
@@ -120,10 +123,13 @@ RSpec::Matchers.define :checkpoint_covered_experience do |covered_experience_id|
|
|
120
123
|
|
121
124
|
failure_message_when_negated do
|
122
125
|
"Expected covered experience '#{covered_experience_id}' NOT to checkpoint:\n" \
|
123
|
-
"expected checkpoint='intermediate' counter to increase
|
126
|
+
"expected checkpoint='intermediate' counter not to increase, but increased by #{@checkpoint_change}"
|
124
127
|
end
|
125
128
|
end
|
126
129
|
|
130
|
+
# Alias for checkpoint_covered_experience matcher
|
131
|
+
RSpec::Matchers.alias_matcher :resume_covered_experience, :checkpoint_covered_experience
|
132
|
+
|
127
133
|
# Matcher for verifying CoveredExperience completion metrics instrumentation.
|
128
134
|
#
|
129
135
|
# Usage:
|
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: 0.
|
4
|
+
version: 0.41.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Newdigate
|
@@ -460,6 +460,7 @@ files:
|
|
460
460
|
- lib/labkit/correlation/grpc/server_interceptor.rb
|
461
461
|
- lib/labkit/covered_experience.rb
|
462
462
|
- lib/labkit/covered_experience/README.md
|
463
|
+
- lib/labkit/covered_experience/current.rb
|
463
464
|
- lib/labkit/covered_experience/error.rb
|
464
465
|
- lib/labkit/covered_experience/experience.rb
|
465
466
|
- lib/labkit/covered_experience/null.rb
|
@@ -485,6 +486,9 @@ files:
|
|
485
486
|
- lib/labkit/middleware/sidekiq/context.rb
|
486
487
|
- lib/labkit/middleware/sidekiq/context/client.rb
|
487
488
|
- lib/labkit/middleware/sidekiq/context/server.rb
|
489
|
+
- lib/labkit/middleware/sidekiq/covered_experience.rb
|
490
|
+
- lib/labkit/middleware/sidekiq/covered_experience/client.rb
|
491
|
+
- lib/labkit/middleware/sidekiq/covered_experience/server.rb
|
488
492
|
- lib/labkit/middleware/sidekiq/server.rb
|
489
493
|
- lib/labkit/middleware/sidekiq/tracing.rb
|
490
494
|
- lib/labkit/middleware/sidekiq/tracing/client.rb
|