gitlab-labkit 0.37.0 → 0.40.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 +16 -0
- data/.editorconfig +28 -0
- data/.gitlab-ci-asdf-versions.yml +5 -0
- data/.gitlab-ci.yml +32 -37
- data/.gitleaks.toml +10 -0
- data/.mise.toml +8 -0
- data/.pre-commit-config.yaml +38 -0
- data/.releaserc.json +19 -0
- data/.rubocop.yml +1 -58
- data/.rubocop_todo.yml +399 -77
- data/.tool-versions +3 -1
- data/.yamllint.yaml +11 -0
- data/CODEOWNERS +4 -0
- data/Dangerfile +7 -1
- data/LICENSE +1 -3
- data/README.md +6 -5
- data/Rakefile +14 -24
- data/config/covered_experiences/schema.json +35 -0
- data/config/covered_experiences/testing_sample.yml +4 -0
- data/gitlab-labkit.gemspec +13 -8
- data/lib/gitlab-labkit.rb +3 -1
- data/lib/labkit/context.rb +1 -0
- data/lib/labkit/covered_experience/README.md +134 -0
- data/lib/labkit/covered_experience/error.rb +9 -0
- data/lib/labkit/covered_experience/experience.rb +198 -0
- data/lib/labkit/covered_experience/null.rb +22 -0
- data/lib/labkit/covered_experience/registry.rb +105 -0
- data/lib/labkit/covered_experience.rb +69 -0
- data/lib/labkit/logging/json_logger.rb +11 -0
- data/lib/labkit/metrics/README.md +98 -0
- data/lib/labkit/metrics/client.rb +90 -0
- data/lib/labkit/metrics/null.rb +20 -0
- data/lib/labkit/metrics/rack_exporter.rb +12 -0
- data/lib/labkit/metrics/registry.rb +69 -0
- data/lib/labkit/metrics.rb +19 -0
- data/lib/labkit/rspec/README.md +121 -0
- data/lib/labkit/rspec/matchers/covered_experience_matchers.rb +198 -0
- data/lib/labkit/rspec/matchers.rb +10 -0
- data/renovate.json +7 -0
- data/scripts/install-asdf-plugins.sh +13 -0
- data/scripts/prepare-dev-env.sh +68 -0
- data/scripts/update-asdf-version-variables.sh +30 -0
- metadata +115 -34
- data/.ruby-version +0 -1
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'labkit/covered_experience/error'
|
4
|
+
require 'labkit/covered_experience/experience'
|
5
|
+
require 'labkit/covered_experience/null'
|
6
|
+
require 'labkit/covered_experience/registry'
|
7
|
+
require 'labkit/logging/json_logger'
|
8
|
+
|
9
|
+
module Labkit
|
10
|
+
# Labkit::CoveredExperience namespace module.
|
11
|
+
#
|
12
|
+
# This module is responsible for managing covered experiences, which are
|
13
|
+
# specific events or activities within the application that are measured
|
14
|
+
# and reported for performance monitoring and analysis.
|
15
|
+
module CoveredExperience
|
16
|
+
# Configuration class for CoveredExperience
|
17
|
+
class Configuration
|
18
|
+
attr_accessor :logger
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@logger = Labkit::Logging::JsonLogger.new($stdout)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class << self
|
26
|
+
def configuration
|
27
|
+
@configuration ||= Configuration.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def reset_configuration
|
31
|
+
@configuration = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def configure
|
35
|
+
yield(configuration) if block_given?
|
36
|
+
end
|
37
|
+
|
38
|
+
def registry
|
39
|
+
@registry ||= Registry.new
|
40
|
+
end
|
41
|
+
|
42
|
+
def reset
|
43
|
+
@registry = nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def get(experience_id)
|
47
|
+
definition = registry[experience_id]
|
48
|
+
|
49
|
+
if definition
|
50
|
+
Experience.new(definition)
|
51
|
+
else
|
52
|
+
raise_or_null(experience_id)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def start(experience_id, &)
|
57
|
+
get(experience_id).start(&)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def raise_or_null(experience_id)
|
63
|
+
return Null.instance unless %w[development test].include?(ENV['RAILS_ENV'])
|
64
|
+
|
65
|
+
raise(NotFoundError, "Covered Experience #{experience_id} not found in the registry")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -52,6 +52,7 @@ module Labkit
|
|
52
52
|
data[:message] = message
|
53
53
|
when Hash
|
54
54
|
reject_reserved_log_keys!(message)
|
55
|
+
format_time!(data)
|
55
56
|
data.merge!(message)
|
56
57
|
end
|
57
58
|
|
@@ -77,6 +78,16 @@ module Labkit
|
|
77
78
|
"\n\nUse key names that are descriptive e.g. by using a prefix."
|
78
79
|
end
|
79
80
|
end
|
81
|
+
|
82
|
+
def format_time!(hash)
|
83
|
+
hash.each do |key, value|
|
84
|
+
if value.is_a?(Time)
|
85
|
+
hash[key] = value.utc.iso8601(3)
|
86
|
+
elsif value.is_a?(Hash)
|
87
|
+
format_time(value)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
80
91
|
end
|
81
92
|
end
|
82
93
|
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# Labkit::Metrics
|
2
|
+
|
3
|
+
## Usage
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
# create a new counter metric and returns it
|
7
|
+
http_requests = Labkit::Metrics::Client.counter(:http_requests, 'A counter of HTTP requests made')
|
8
|
+
# start using the counter
|
9
|
+
http_requests.increment
|
10
|
+
|
11
|
+
# resets the registry and reinitializes all metrics files
|
12
|
+
Labkit::Metrics::Client.reset!
|
13
|
+
|
14
|
+
# retrieves the metric (be it a counter, gauge, histogram, summary)
|
15
|
+
http_requests = Labkit::Metrics::Client.get(:http_requests)
|
16
|
+
```
|
17
|
+
|
18
|
+
### Counter
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
counter = Labkit::Metrics::Client.counter(:service_requests_total, '...')
|
22
|
+
|
23
|
+
# increment the counter for a given label set
|
24
|
+
counter.increment({ service: 'foo' })
|
25
|
+
|
26
|
+
# increment by a given value
|
27
|
+
counter.increment({ service: 'bar' }, 5)
|
28
|
+
|
29
|
+
# get current value for a given label set
|
30
|
+
counter.get({ service: 'bar' })
|
31
|
+
# => 5
|
32
|
+
```
|
33
|
+
|
34
|
+
### Gauge
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
gauge = Labkit::Metrics::Client.gauge(:room_temperature_celsius, '...')
|
38
|
+
|
39
|
+
# set a value
|
40
|
+
gauge.set({ room: 'kitchen' }, 21.534)
|
41
|
+
|
42
|
+
# retrieve the current value for a given label set
|
43
|
+
gauge.get({ room: 'kitchen' })
|
44
|
+
# => 21.534
|
45
|
+
```
|
46
|
+
|
47
|
+
### Histogram
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
histogram = Labkit::Metrics::Client.histogram(:service_latency_seconds, '...')
|
51
|
+
|
52
|
+
# record a value
|
53
|
+
histogram.observe({ service: 'users' }, Benchmark.realtime { service.call(arg) })
|
54
|
+
|
55
|
+
# retrieve the current bucket values
|
56
|
+
histogram.get({ service: 'users' })
|
57
|
+
# => { 0.005 => 3, 0.01 => 15, 0.025 => 18, ..., 2.5 => 42, 5 => 42, 10 => 42 }
|
58
|
+
```
|
59
|
+
|
60
|
+
### Summary
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
summary = Labkit::Metrics::Client.summary(:service_latency_seconds, '...')
|
64
|
+
|
65
|
+
# record a value
|
66
|
+
summary.observe({ service: 'database' }, Benchmark.realtime { service.call() })
|
67
|
+
|
68
|
+
# retrieve the current quantile values
|
69
|
+
summary.get({ service: 'database' })
|
70
|
+
# => { 0.5 => 0.1233122, 0.9 => 3.4323, 0.99 => 5.3428231 }
|
71
|
+
```
|
72
|
+
|
73
|
+
## Rack middleware
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
# config.ru
|
77
|
+
|
78
|
+
require 'rack'
|
79
|
+
require 'labkit/metrics/rack_exporter'
|
80
|
+
|
81
|
+
use Labkit::Metrics::RackExporter
|
82
|
+
|
83
|
+
run ->(env) { [200, {'Content-Type' => 'text/html'}, ['OK']] }
|
84
|
+
```
|
85
|
+
|
86
|
+
## Configuration
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
# config/initializers/metrics.rb
|
90
|
+
|
91
|
+
Labkit::Metrics::Client.reinitialize_on_pid_change(force: true)
|
92
|
+
|
93
|
+
Labkit::Metrics::Client.configure do |config|
|
94
|
+
config.logger = Gitlab::AppLogger
|
95
|
+
config.multiprocess_files_dir = 'tmp/prometheus_multiproc_dir'
|
96
|
+
config.pid_provider = ::Prometheus::PidProvider.method(:worker_id)
|
97
|
+
end
|
98
|
+
```
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
require "singleton"
|
5
|
+
require 'concurrent-ruby'
|
6
|
+
require "prometheus/client"
|
7
|
+
require "labkit/metrics/registry"
|
8
|
+
require "labkit/metrics/null"
|
9
|
+
|
10
|
+
module Labkit
|
11
|
+
module Metrics
|
12
|
+
InvalidLabelSet = Class.new(RuntimeError)
|
13
|
+
|
14
|
+
# A thin wrapper around Prometheus::Client from the prometheus-client-mmap gem
|
15
|
+
# https://gitlab.com/gitlab-org/ruby/gems/prometheus-client-mmap
|
16
|
+
class Client
|
17
|
+
include Singleton
|
18
|
+
extend Forwardable
|
19
|
+
|
20
|
+
def_delegators :wrapped_client, :configure, :reinitialize_on_pid_change, :configuration
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@enabled = Concurrent::AtomicBoolean.new(true)
|
24
|
+
end
|
25
|
+
|
26
|
+
def wrapped_client
|
27
|
+
@client ||= ::Prometheus::Client
|
28
|
+
end
|
29
|
+
|
30
|
+
def disable!
|
31
|
+
@enabled.make_false
|
32
|
+
end
|
33
|
+
|
34
|
+
def enable!
|
35
|
+
@enabled.make_true
|
36
|
+
end
|
37
|
+
|
38
|
+
def enabled?
|
39
|
+
@enabled.true? && metrics_folder_present?
|
40
|
+
end
|
41
|
+
|
42
|
+
def safe_provide_metric(metric_type, name, *args)
|
43
|
+
return Null.instance unless enabled?
|
44
|
+
|
45
|
+
Registry.safe_register(metric_type, name, *args)
|
46
|
+
end
|
47
|
+
|
48
|
+
def metrics_folder_present?
|
49
|
+
dir = configuration.multiprocess_files_dir
|
50
|
+
dir && Dir.exist?(dir) && File.writable?(dir)
|
51
|
+
end
|
52
|
+
|
53
|
+
def counter(name, docstring, base_labels = {})
|
54
|
+
safe_provide_metric(:counter, name, docstring, base_labels)
|
55
|
+
end
|
56
|
+
|
57
|
+
def summary(name, docstring, base_labels = {})
|
58
|
+
safe_provide_metric(:summary, name, docstring, base_labels)
|
59
|
+
end
|
60
|
+
|
61
|
+
def histogram(
|
62
|
+
name, docstring, base_labels = {},
|
63
|
+
buckets = Prometheus::Client::Histogram::DEFAULT_BUCKETS)
|
64
|
+
safe_provide_metric(:histogram, name, docstring, base_labels, buckets)
|
65
|
+
end
|
66
|
+
|
67
|
+
def gauge(name, docstring, base_labels = {}, multiprocess_mode = :all)
|
68
|
+
safe_provide_metric(:gauge, name, docstring, base_labels, multiprocess_mode)
|
69
|
+
end
|
70
|
+
|
71
|
+
def reset!
|
72
|
+
Registry.reset!
|
73
|
+
end
|
74
|
+
|
75
|
+
def get(metric_name)
|
76
|
+
Registry.get(metric_name)
|
77
|
+
end
|
78
|
+
|
79
|
+
class << self
|
80
|
+
extend Forwardable
|
81
|
+
|
82
|
+
def_delegators :instance,
|
83
|
+
:enable!, :disable!, :counter, :gauge, :histogram, :summary, :reset!, :enabled?,
|
84
|
+
:configure, :reinitialize_on_pid_change, :configuration, :get
|
85
|
+
|
86
|
+
private :instance
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Labkit
|
4
|
+
module Metrics
|
5
|
+
# Fakes Prometheus::Client::Metric and all derived metrics.
|
6
|
+
# Explicitly avoiding meta-programming to make interface more obvious to interact with.
|
7
|
+
class Null
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
attr_reader :name, :docstring, :base_labels
|
11
|
+
|
12
|
+
def get(*args); end
|
13
|
+
def set(*args); end
|
14
|
+
def increment(*args); end
|
15
|
+
def decrement(*args); end
|
16
|
+
def observe(*args); end
|
17
|
+
def values(*args); end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "prometheus/client/rack/exporter"
|
4
|
+
|
5
|
+
module Labkit
|
6
|
+
module Metrics
|
7
|
+
# A wrapper around the Rack exporter middleware provided by
|
8
|
+
# prometheus-client-mmap gem
|
9
|
+
# https://gitlab.com/gitlab-org/ruby/gems/prometheus-client-mmap
|
10
|
+
class RackExporter < Prometheus::Client::Rack::Exporter; end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "prometheus/client"
|
4
|
+
|
5
|
+
module Labkit
|
6
|
+
module Metrics
|
7
|
+
InvalidMetricType = Class.new(StandardError)
|
8
|
+
|
9
|
+
# A thin wrapper around Prometheus::Client::Registry.
|
10
|
+
# It provides a thread-safe way to register metrics with the Prometheus registry.
|
11
|
+
class Registry
|
12
|
+
class << self
|
13
|
+
INIT_REGISTRY_MUTEX = Mutex.new
|
14
|
+
REGISTER_MUTEX = Mutex.new
|
15
|
+
|
16
|
+
# Registers a metric with the Prometheus registry in a thread-safe manner.
|
17
|
+
# If the metric already exists, it returns the existing metric.
|
18
|
+
# If the metric does not exist, it creates a new one.
|
19
|
+
# Each metric-name is only registered once for a type (counter, gauge, histogram, summary),
|
20
|
+
# even if multiple threads attempt to register the same metric simultaneously.
|
21
|
+
#
|
22
|
+
# @param metric_type [Symbol] The type of metric to register (:counter, :gauge, :histogram, :summary)
|
23
|
+
# @param name [Symbol, String] The name of the metric
|
24
|
+
# @param args [Array] Additional arguments to pass to the metric constructor
|
25
|
+
# @return [Prometheus::Client::Metric] The registered metric
|
26
|
+
# @raise [InvalidMetricType] If the metric_type is not supported
|
27
|
+
#
|
28
|
+
# @example
|
29
|
+
# # Register a counter
|
30
|
+
# counter = Registry.safe_register(:counter, :http_requests_total, 'Total HTTP requests')
|
31
|
+
def safe_register(metric_type, name, *args)
|
32
|
+
REGISTER_MUTEX.synchronize do
|
33
|
+
get(name) || wrapped_registry.method(metric_type).call(name, *args)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Cleans up the Prometheus registry and resets it to a new state.
|
38
|
+
def reset!
|
39
|
+
INIT_REGISTRY_MUTEX.synchronize do
|
40
|
+
Prometheus::Client.cleanup!
|
41
|
+
Prometheus::Client.reset!
|
42
|
+
@registry = nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the metric for the given name from the Prometheus registry.
|
47
|
+
#
|
48
|
+
# @param metric_name [Symbol, String] The name of the metric
|
49
|
+
# @return [Prometheus::Client::Metric, nil] The registered metric or nil if it does not exist
|
50
|
+
def get(metric_name)
|
51
|
+
wrapped_registry.get(metric_name)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def wrapped_registry
|
57
|
+
@registry ||= init_registry
|
58
|
+
end
|
59
|
+
|
60
|
+
def init_registry
|
61
|
+
# Prometheus::Client.registry initializes a new registry with an underlying hash
|
62
|
+
# storing metrics and a mutex synchronizing the writes to that hash.
|
63
|
+
# This means we need to make sure we only build one registry, for accessing from within Labkit.
|
64
|
+
INIT_REGISTRY_MUTEX.synchronize { Prometheus::Client.registry }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "prometheus/client/formats/text"
|
4
|
+
|
5
|
+
module Labkit
|
6
|
+
# Metrics provides functionality for producing metrics
|
7
|
+
module Metrics
|
8
|
+
autoload :Client, "labkit/metrics/client"
|
9
|
+
autoload :RackExporter, "labkit/metrics/rack_exporter"
|
10
|
+
autoload :Null, "labkit/metrics/null"
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def prometheus_metrics_text
|
14
|
+
dir = Client.configuration.multiprocess_files_dir
|
15
|
+
::Prometheus::Client::Formats::Text.marshal_multiprocess(dir)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# Labkit RSpec Support
|
2
|
+
|
3
|
+
This module provides RSpec matchers for testing Labkit functionality in your Rails applications.
|
4
|
+
|
5
|
+
## Setup
|
6
|
+
|
7
|
+
You must explicitly require the RSpec matchers in your test files:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# In your spec_helper.rb or rails_helper.rb
|
11
|
+
require 'labkit/rspec/matchers'
|
12
|
+
```
|
13
|
+
|
14
|
+
This approach ensures that:
|
15
|
+
- Test dependencies are not loaded in production environments
|
16
|
+
- You have explicit control over which matchers are available
|
17
|
+
- The gem remains lightweight for non-testing use cases
|
18
|
+
|
19
|
+
|
20
|
+
## Available Matchers
|
21
|
+
|
22
|
+
### Covered Experience Matchers
|
23
|
+
|
24
|
+
These matchers help you test that your code properly instruments covered experiences with the expected metrics.
|
25
|
+
|
26
|
+
#### `start_covered_experience`
|
27
|
+
|
28
|
+
Tests that a covered experience is started (checkpoint=start metric is incremented).
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
expect { subject }.to start_covered_experience('rails_request')
|
32
|
+
|
33
|
+
# Test that it does NOT start
|
34
|
+
expect { subject }.not_to start_covered_experience('rails_request')
|
35
|
+
```
|
36
|
+
|
37
|
+
#### `checkpoint_covered_experience`
|
38
|
+
|
39
|
+
Tests that a covered experience checkpoint is recorded (checkpoint=intermediate metric is incremented).
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
expect { subject }.to checkpoint_covered_experience('rails_request')
|
43
|
+
|
44
|
+
# Test that it does NOT checkpoint
|
45
|
+
expect { subject }.not_to checkpoint_covered_experience('rails_request')
|
46
|
+
```
|
47
|
+
|
48
|
+
#### `complete_covered_experience`
|
49
|
+
|
50
|
+
Tests that a covered experience is completed with the expected metrics:
|
51
|
+
- `gitlab_covered_experience_checkpoint_total` (with checkpoint=end)
|
52
|
+
- `gitlab_covered_experience_total` (with error flag)
|
53
|
+
- `gitlab_covered_experience_apdex_total` (with success flag)
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
# Test successful completion
|
57
|
+
expect { subject }.to complete_covered_experience('rails_request')
|
58
|
+
|
59
|
+
# Test completion with error
|
60
|
+
expect { subject }.to complete_covered_experience('rails_request', error: true, success: false)
|
61
|
+
|
62
|
+
# Test that it does NOT complete
|
63
|
+
expect { subject }.not_to complete_covered_experience('rails_request')
|
64
|
+
```
|
65
|
+
|
66
|
+
## Example Usage
|
67
|
+
|
68
|
+
### In your spec_helper.rb or rails_helper.rb:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
# spec/spec_helper.rb or spec/rails_helper.rb
|
72
|
+
require 'gitlab-labkit'
|
73
|
+
|
74
|
+
# Explicitly require the RSpec matchers
|
75
|
+
require 'labkit/rspec/matchers'
|
76
|
+
|
77
|
+
RSpec.configure do |config|
|
78
|
+
# Your other RSpec configuration...
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
### In your test files:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
RSpec.describe MyController, type: :controller do
|
86
|
+
describe '#index' do
|
87
|
+
it 'instruments the request properly' do
|
88
|
+
expect { get :index }.to start_covered_experience('rails_request')
|
89
|
+
.and complete_covered_experience('rails_request')
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'when an error occurs' do
|
93
|
+
before do
|
94
|
+
allow(MyService).to receive(:call).and_raise(StandardError)
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'records the error in metrics' do
|
98
|
+
expect { get :index }.to complete_covered_experience('rails_request', error: true, success: false)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
### For individual spec files (alternative approach):
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
# spec/controllers/my_controller_spec.rb
|
109
|
+
require 'spec_helper'
|
110
|
+
require 'labkit/rspec/matchers' # Can also be required per-file if needed
|
111
|
+
|
112
|
+
RSpec.describe MyController do
|
113
|
+
# Your tests using the matchers...
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
## Requirements
|
118
|
+
|
119
|
+
- The covered experience must be registered in `Labkit::CoveredExperience::Registry`
|
120
|
+
- Metrics must be properly configured in your test environment
|
121
|
+
- The code under test must use Labkit's covered experience instrumentation
|