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
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
# LabKit-Ruby 🔬🔬🔬🔬🔬
|
2
2
|
|
3
3
|
LabKit-Ruby is minimalist library to provide functionality for Ruby services at GitLab.
|
4
4
|
|
@@ -19,9 +19,12 @@ The changelog is available via [**tagged release notes**](https://gitlab.com/git
|
|
19
19
|
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
|
-
1. `Labkit::Correlation`
|
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/covered_experiences/README.md).
|
23
24
|
1. `Labkit::FIPS` for checking for FIPS mode and using FIPS-compliant algorithms.
|
24
25
|
1. `Labkit::Logging` for sanitizing log messages.
|
26
|
+
1. `Labkit::Metrics` for metrics. More on the [README](./lib/labkit/metrics/README.md).
|
27
|
+
1. `Labkit::RSpec` for RSpec matchers to test Labkit functionality (requires selective loading). More on the [README](./lib/labkit/rspec/README.md).
|
25
28
|
1. `Labkit::Tracing` for handling and propagating distributed traces.
|
26
29
|
|
27
30
|
## Developing
|
@@ -40,8 +43,6 @@ $ # Run tests, linters
|
|
40
43
|
$ bundle exec rake verify
|
41
44
|
```
|
42
45
|
|
43
|
-
Note that LabKit-Ruby uses the [`rufo`](https://github.com/ruby-formatter/rufo) for auto-formatting. Please run `bundle exec rake fix` to auto-format your code before pushing.
|
44
|
-
|
45
46
|
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.
|
46
47
|
|
47
48
|
To work on some of the scripts we use for releasing a new version,
|
@@ -51,7 +52,7 @@ make sure to add a new `.env.sh`.
|
|
51
52
|
cp .env.example.sh .env.sh`
|
52
53
|
```
|
53
54
|
|
54
|
-
Inside `.env.sh`, add a personal acccess token for the `
|
55
|
+
Inside `.env.sh`, add a personal acccess token for the `CHANGELOG_GITLAB_TOKEN`
|
55
56
|
environment variable. Next source the file:
|
56
57
|
|
57
58
|
```console
|
data/Rakefile
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "bundler/gem_tasks"
|
4
|
-
require "rufo"
|
5
4
|
|
6
5
|
require "rspec/core/rake_task"
|
7
6
|
RSpec::Core::RakeTask.new(:spec)
|
@@ -11,27 +10,9 @@ RuboCop::RakeTask.new(:rubocop) do |task|
|
|
11
10
|
task.options = %w[--parallel]
|
12
11
|
end
|
13
12
|
|
14
|
-
desc "
|
15
|
-
|
16
|
-
|
17
|
-
namespace :rufo do
|
18
|
-
require "rufo"
|
19
|
-
|
20
|
-
def rufo_command(*switches, rake_args)
|
21
|
-
files_or_dirs = rake_args[:files_or_dirs] || "."
|
22
|
-
args = switches + files_or_dirs.split(" ")
|
23
|
-
Rufo::Command.run(args)
|
24
|
-
end
|
25
|
-
|
26
|
-
desc "Format Ruby code in current directory"
|
27
|
-
task :run, [:files_or_dirs] do |_task, rake_args|
|
28
|
-
rufo_command(rake_args)
|
29
|
-
end
|
30
|
-
|
31
|
-
desc "Check that no formatting changes are produced"
|
32
|
-
task :check, [:files_or_dirs] do |_task, rake_args|
|
33
|
-
rufo_command("--check", rake_args)
|
34
|
-
end
|
13
|
+
desc "update the rubocop todo config"
|
14
|
+
RuboCop::RakeTask.new("rubocop:config") do |task|
|
15
|
+
task.options = %w[--auto-gen-config]
|
35
16
|
end
|
36
17
|
|
37
18
|
desc "Generate test protobuf stubs"
|
@@ -40,8 +21,17 @@ task :gen_test_proto do
|
|
40
21
|
Rufo::Command.run(["spec/support/grpc_service/test_pb.rb", "spec/support/grpc_service/test_services_pb.rb"])
|
41
22
|
end
|
42
23
|
|
43
|
-
task :fix =>
|
24
|
+
task :fix => ["rubocop:autocorrect"]
|
44
25
|
|
45
|
-
task :verify => %w[spec
|
26
|
+
task :verify => %w[spec rubocop]
|
46
27
|
|
47
28
|
task :default => %w[verify build]
|
29
|
+
|
30
|
+
desc "Start an IRB console with gem pre-loaded"
|
31
|
+
task :console do
|
32
|
+
$LOAD_PATH.unshift(File.expand_path("lib", __dir__))
|
33
|
+
require "irb"
|
34
|
+
require "gitlab-labkit"
|
35
|
+
ARGV.clear
|
36
|
+
IRB.start
|
37
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
{
|
2
|
+
"$schema": "http://json-schema.org/draft-06/schema#",
|
3
|
+
"$id": "https://gitlab.com/gitlab-org/ruby/gems/labkit-ruby/-/raw/master/config/covered_experiences/schema.json",
|
4
|
+
"title": "Covered Experience Definition",
|
5
|
+
"description": "Schema for GitLab Covered Experience files",
|
6
|
+
"type": "object",
|
7
|
+
"properties": {
|
8
|
+
"description": {
|
9
|
+
"type": "string",
|
10
|
+
"minLength": 1,
|
11
|
+
"description": "Human-readable description of the covered experience"
|
12
|
+
},
|
13
|
+
"feature_category": {
|
14
|
+
"type": "string",
|
15
|
+
"minLength": 1,
|
16
|
+
"description": "GitLab feature category this experience belongs to"
|
17
|
+
},
|
18
|
+
"urgency": {
|
19
|
+
"type": "string",
|
20
|
+
"enum": [
|
21
|
+
"sync_fast",
|
22
|
+
"sync_slow",
|
23
|
+
"async_fast",
|
24
|
+
"async_slow"
|
25
|
+
],
|
26
|
+
"description": "Urgency level for this covered experience"
|
27
|
+
}
|
28
|
+
},
|
29
|
+
"required": [
|
30
|
+
"description",
|
31
|
+
"feature_category",
|
32
|
+
"urgency"
|
33
|
+
]
|
34
|
+
}
|
35
|
+
|
data/gitlab-labkit.gemspec
CHANGED
@@ -5,7 +5,8 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "gitlab-labkit"
|
8
|
-
|
8
|
+
version = ENV["CI_COMMIT_TAG"] || `git describe --tag --match "v[0-9]*.[0-9]*.[0-9]*"`
|
9
|
+
spec.version = version.chomp.gsub(/^v/, "")
|
9
10
|
spec.authors = ["Andrew Newdigate"]
|
10
11
|
spec.email = ["andrew@gitlab.com"]
|
11
12
|
|
@@ -16,32 +17,36 @@ Gem::Specification.new do |spec|
|
|
16
17
|
|
17
18
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|tools)/}) }
|
18
19
|
spec.require_paths = ["lib"]
|
19
|
-
spec.required_ruby_version = "
|
20
|
+
spec.required_ruby_version = "~> 3.2"
|
20
21
|
|
21
22
|
# Please maintain alphabetical order for dependencies
|
22
23
|
spec.add_runtime_dependency "actionpack", ">= 5.0.0", "< 8.1.0"
|
23
24
|
spec.add_runtime_dependency "activesupport", ">= 5.0.0", "< 8.1.0"
|
24
25
|
spec.add_runtime_dependency "grpc", ">= 1.62" # Be sure to update the "grpc-tools" dev_dependency too
|
26
|
+
spec.add_runtime_dependency "google-protobuf", "~> 3" # Keep the major version to 3 until we update the `grpc` gem
|
25
27
|
spec.add_runtime_dependency "jaeger-client", "~> 1.1.0"
|
28
|
+
spec.add_runtime_dependency 'json-schema', '~> 5.1'
|
26
29
|
spec.add_runtime_dependency "opentracing", "~> 0.4"
|
27
|
-
spec.add_runtime_dependency "pg_query", ">=
|
30
|
+
spec.add_runtime_dependency "pg_query", ">= 6.1.0", "< 7.0"
|
31
|
+
spec.add_runtime_dependency "prometheus-client-mmap", "~> 1.2.9"
|
28
32
|
spec.add_runtime_dependency "redis", "> 3.0.0", "< 6.0.0"
|
29
33
|
|
30
34
|
# Please maintain alphabetical order for dev dependencies
|
31
35
|
spec.add_development_dependency "excon", "~> 0.78.1"
|
32
36
|
spec.add_development_dependency "faraday", "~> 1.10.3"
|
33
37
|
spec.add_development_dependency "gitlab-dangerfiles", "~> 2.11.0"
|
34
|
-
spec.add_development_dependency "gitlab-styles", "~>
|
38
|
+
spec.add_development_dependency "gitlab-styles", "~> 13.0.2"
|
35
39
|
spec.add_development_dependency "grpc-tools", ">= 1.62"
|
36
|
-
spec.add_development_dependency "httparty", "~> 0.
|
37
|
-
spec.add_development_dependency "httpclient", "~> 2.
|
40
|
+
spec.add_development_dependency "httparty", "~> 0.22.0"
|
41
|
+
spec.add_development_dependency "httpclient", "~> 2.9.0"
|
42
|
+
spec.add_development_dependency "irb", "~> 1.15.2"
|
38
43
|
spec.add_development_dependency "pry", "~> 0.12"
|
44
|
+
spec.add_development_dependency "pry-byebug", "~> 3.11"
|
39
45
|
spec.add_development_dependency "rack", "~> 2.0"
|
40
|
-
spec.add_development_dependency "rake", "~>
|
46
|
+
spec.add_development_dependency "rake", "~> 13.2"
|
41
47
|
spec.add_development_dependency "rest-client", "~> 2.1.0"
|
42
48
|
spec.add_development_dependency "rspec", "~> 3.12.0"
|
43
49
|
spec.add_development_dependency "rspec-parameterized", "~> 1.0"
|
44
|
-
spec.add_development_dependency "rufo", "0.9.0"
|
45
50
|
spec.add_development_dependency "sidekiq", ">= 5.2", "< 7"
|
46
51
|
spec.add_development_dependency "webrick", "~> 1.7.0"
|
47
52
|
end
|
data/lib/gitlab-labkit.rb
CHANGED
@@ -7,11 +7,13 @@
|
|
7
7
|
module Labkit
|
8
8
|
autoload :System, "labkit/system"
|
9
9
|
|
10
|
-
autoload :Correlation, "labkit/correlation"
|
11
10
|
autoload :Context, "labkit/context"
|
11
|
+
autoload :Correlation, "labkit/correlation"
|
12
|
+
autoload :CoveredExperience, "labkit/covered_experience"
|
12
13
|
autoload :FIPS, "labkit/fips"
|
13
14
|
autoload :Tracing, "labkit/tracing"
|
14
15
|
autoload :Logging, "labkit/logging"
|
16
|
+
autoload :Metrics, "labkit/metrics"
|
15
17
|
autoload :Middleware, "labkit/middleware"
|
16
18
|
|
17
19
|
# Publishers to publish notifications whenever a HTTP reqeust is made.
|
data/lib/labkit/context.rb
CHANGED
@@ -5,6 +5,7 @@ require "securerandom"
|
|
5
5
|
require "active_support/core_ext/module/delegation"
|
6
6
|
require "active_support/core_ext/string/starts_ends_with"
|
7
7
|
require "active_support/core_ext/string/inflections"
|
8
|
+
require "active_support/core_ext/object/blank"
|
8
9
|
|
9
10
|
module Labkit
|
10
11
|
# A context can be used to provide structured information on what resources
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# Covered Experience
|
2
|
+
|
3
|
+
This module covers the definition for Covered Experiences, as described in the [blueprint](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/covered_experience_slis/#covered-experience-definition).
|
4
|
+
|
5
|
+
## Configuration
|
6
|
+
|
7
|
+
### Logger Configuration
|
8
|
+
|
9
|
+
By default, `Labkit::CoveredExperience` uses `Labkit::Logging::JsonLogger.new($stdout)` for logging. You can configure a custom logger:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
Labkit::CoveredExperience.configure do |config|
|
13
|
+
config.logger = Labkit::Logging::JsonLogger.new($stdout)
|
14
|
+
end
|
15
|
+
```
|
16
|
+
|
17
|
+
This configuration affects all Covered Experience instances and their logging output.
|
18
|
+
|
19
|
+
### Covered Experience Definitions
|
20
|
+
|
21
|
+
Covered experience definitions will be lazy loaded from the default directory (`config/covered_experiences`).
|
22
|
+
|
23
|
+
Create a new covered experience file in the registry directory, e.g. config/covered_experiences/merge_request_creation.yaml
|
24
|
+
|
25
|
+
The basename of the file will be taken as the covered_experience_id.
|
26
|
+
|
27
|
+
The schema header is optional, but if you're using VSCode (or any other editor with support), you can get them validated
|
28
|
+
instantaneously in the editor via a [JSON schema plugin](https://marketplace.visualstudio.com/items?itemName=remcohaszing.schemastore).
|
29
|
+
|
30
|
+
```yaml
|
31
|
+
# yaml-language-server: $schema=https://gitlab.com/gitlab-org/ruby/gems/labkit-ruby/-/raw/master/config/covered_experiences/schema.json
|
32
|
+
description: "Creating a new merge request in a project"
|
33
|
+
feature_category: "source_code_management"
|
34
|
+
urgency: "sync_fast"
|
35
|
+
```
|
36
|
+
|
37
|
+
**Feature category**
|
38
|
+
|
39
|
+
https://docs.gitlab.com/development/feature_categorization/#feature-categorization.
|
40
|
+
|
41
|
+
**Urgency**
|
42
|
+
|
43
|
+
| Threshold | Description | Examples | Value |
|
44
|
+
|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|-------|
|
45
|
+
| `sync_fast` | A user is awaiting a synchronous response which needs to be returned before they can continue with their action | A full-page render | 2s |
|
46
|
+
| `sync_slow` | A user is awaiting a synchronous response which needs to be returned before they can continue with their action, but which the user may accept a slower response | Displaying a full-text search response while displaying an amusement animation | 5s |
|
47
|
+
| `async_fast` | An async process which may block a user from continuing with their user journey | MR diff update after git push | 15s |
|
48
|
+
| `async_slow` | An async process which will not block a user and will not be immediately noticed as being slow | Notification following an assignment | 5m |
|
49
|
+
|
50
|
+
## Usage
|
51
|
+
|
52
|
+
The `Labkit::CoveredExperience` module provides a simple API for measuring and tracking covered experiences in your application.
|
53
|
+
|
54
|
+
|
55
|
+
#### Accessing a Covered Experience
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
# Get a covered experience by ID
|
59
|
+
experience = Labkit::CoveredExperience.get('merge_request_creation')
|
60
|
+
```
|
61
|
+
|
62
|
+
#### Using with a Block (Recommended)
|
63
|
+
|
64
|
+
The simplest way to use covered experiences is with a block, which automatically handles starting and completing the experience:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
Labkit::CoveredExperience.start('merge_request_creation') do |experience|
|
68
|
+
# Your code here
|
69
|
+
create_merge_request
|
70
|
+
|
71
|
+
# Add checkpoints for important milestones
|
72
|
+
experience.checkpoint
|
73
|
+
|
74
|
+
validate_merge_request
|
75
|
+
experience.checkpoint
|
76
|
+
|
77
|
+
send_notifications
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
#### Manual Control
|
82
|
+
|
83
|
+
For more control, you can manually start, checkpoint, and complete experiences:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
experience = Labkit::CoveredExperience.get('merge_request_creation')
|
87
|
+
experience.start
|
88
|
+
|
89
|
+
# Perform some work
|
90
|
+
create_merge_request
|
91
|
+
|
92
|
+
# Mark important milestones
|
93
|
+
experience.checkpoint
|
94
|
+
|
95
|
+
# Perform more work
|
96
|
+
validate_merge_request
|
97
|
+
experience.checkpoint
|
98
|
+
|
99
|
+
# Complete the experience
|
100
|
+
experience.complete
|
101
|
+
```
|
102
|
+
|
103
|
+
### Error Handling
|
104
|
+
|
105
|
+
When using the block form, errors are automatically captured:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
Labkit::CoveredExperience.start('merge_request_creation') do |experience|
|
109
|
+
# If this raises an exception, it will be captured automatically
|
110
|
+
risky_operation
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
For manual control, you can mark errors explicitly:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
experience = Labkit::CoveredExperience.get('merge_request_creation')
|
118
|
+
experience.start
|
119
|
+
|
120
|
+
begin
|
121
|
+
risky_operation
|
122
|
+
rescue StandardError => e
|
123
|
+
experience.error!(e)
|
124
|
+
raise
|
125
|
+
ensure
|
126
|
+
experience.complete
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
### Error Behavior
|
131
|
+
|
132
|
+
- In `development` and `test` environments, accessing a non-existent covered experience will raise a `NotFoundError`
|
133
|
+
- In other environments, a null object is returned that safely ignores all method calls
|
134
|
+
- Attempting to checkpoint or complete an unstarted experience will raise an `UnstartedError` in `development` and `test` environments
|
@@ -0,0 +1,198 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'labkit/context'
|
4
|
+
require 'labkit/covered_experience/error'
|
5
|
+
|
6
|
+
module Labkit
|
7
|
+
module CoveredExperience
|
8
|
+
URGENCY_THRESHOLDS_IN_SECONDS = {
|
9
|
+
sync_fast: 2,
|
10
|
+
sync_slow: 5,
|
11
|
+
async_fast: 15,
|
12
|
+
async_slow: 300
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
# The `Experience` class represents a single Covered Experience
|
16
|
+
# event to be measured and reported.
|
17
|
+
class Experience
|
18
|
+
attr_reader :error
|
19
|
+
|
20
|
+
def initialize(definition)
|
21
|
+
@definition = definition
|
22
|
+
end
|
23
|
+
|
24
|
+
# Start the Covered Experience.
|
25
|
+
#
|
26
|
+
# @yield [self] When a block is provided, the experience will be completed automatically.
|
27
|
+
# @param extra [Hash] Additional data to include in the log event
|
28
|
+
# @return [self]
|
29
|
+
# @raise [CoveredExperienceError] If the block raises an error.
|
30
|
+
#
|
31
|
+
# Usage:
|
32
|
+
#
|
33
|
+
# CoveredExperience.new(definition).start do |experience|
|
34
|
+
# experience.checkpoint
|
35
|
+
# experience.checkpoint
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# experience = CoveredExperience.new(definition)
|
39
|
+
# experience.start
|
40
|
+
# experience.checkpoint
|
41
|
+
# experience.complete
|
42
|
+
def start(**extra, &)
|
43
|
+
@start_time = Time.now.utc
|
44
|
+
checkpoint_counter.increment(checkpoint: "start")
|
45
|
+
log_event("start", **extra)
|
46
|
+
|
47
|
+
return self unless block_given?
|
48
|
+
|
49
|
+
begin
|
50
|
+
yield self
|
51
|
+
self
|
52
|
+
rescue StandardError => e
|
53
|
+
error!(e)
|
54
|
+
raise
|
55
|
+
ensure
|
56
|
+
complete(**extra)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Checkpoint the Covered Experience.
|
61
|
+
#
|
62
|
+
# @param extra [Hash] Additional data to include in the log event
|
63
|
+
# @raise [UnstartedError] If the experience has not been started and RAILS_ENV is development or test.
|
64
|
+
# @return [self]
|
65
|
+
def checkpoint(**extra)
|
66
|
+
return unless ensure_started!
|
67
|
+
|
68
|
+
@checkpoint_time = Time.now.utc
|
69
|
+
checkpoint_counter.increment(checkpoint: "intermediate")
|
70
|
+
log_event("intermediate", **extra)
|
71
|
+
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
# Complete the Covered Experience.
|
76
|
+
#
|
77
|
+
# @param extra [Hash] Additional data to include in the log event
|
78
|
+
# @raise [UnstartedError] If the experience has not been started and RAILS_ENV is development or test.
|
79
|
+
# @return [self]
|
80
|
+
def complete(**extra)
|
81
|
+
return unless ensure_started!
|
82
|
+
|
83
|
+
begin
|
84
|
+
@end_time = Time.now.utc
|
85
|
+
ensure
|
86
|
+
checkpoint_counter.increment(checkpoint: "end")
|
87
|
+
total_counter.increment(error: has_error?)
|
88
|
+
apdex_counter.increment(success: apdex_success?) unless has_error?
|
89
|
+
log_event("end", **extra)
|
90
|
+
end
|
91
|
+
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
# Marks the experience as failed with an error
|
96
|
+
#
|
97
|
+
# @param error [StandardError, String] The error that caused the experience to fail.
|
98
|
+
# @return [self]
|
99
|
+
def error!(error)
|
100
|
+
@error = error
|
101
|
+
self
|
102
|
+
end
|
103
|
+
|
104
|
+
def has_error?
|
105
|
+
!!@error
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def base_labels
|
111
|
+
@base_labels ||= @definition.to_h.slice(:covered_experience, :feature_category, :urgency)
|
112
|
+
end
|
113
|
+
|
114
|
+
def ensure_started!
|
115
|
+
return @start_time unless @start_time.nil?
|
116
|
+
|
117
|
+
err = UnstartedError.new("Covered Experience #{@definition.covered_experience} not started")
|
118
|
+
|
119
|
+
warn(err)
|
120
|
+
raise(err) if %w[development test].include?(ENV['RAILS_ENV'])
|
121
|
+
end
|
122
|
+
|
123
|
+
def urgency_threshold
|
124
|
+
URGENCY_THRESHOLDS_IN_SECONDS[@definition.urgency.to_sym]
|
125
|
+
end
|
126
|
+
|
127
|
+
def elapsed_time
|
128
|
+
last_time = @end_time || @checkpoint_time || @start_time
|
129
|
+
last_time - @start_time
|
130
|
+
end
|
131
|
+
|
132
|
+
def apdex_success?
|
133
|
+
elapsed_time <= urgency_threshold
|
134
|
+
end
|
135
|
+
|
136
|
+
def checkpoint_counter
|
137
|
+
@checkpoint_counter ||= Labkit::Metrics::Client.counter(
|
138
|
+
:gitlab_covered_experience_checkpoint_total,
|
139
|
+
'Total checkpoints for covered experiences',
|
140
|
+
base_labels
|
141
|
+
)
|
142
|
+
end
|
143
|
+
|
144
|
+
def total_counter
|
145
|
+
@total_counter ||= Labkit::Metrics::Client.counter(
|
146
|
+
:gitlab_covered_experience_total,
|
147
|
+
'Total covered experience events (success/failure)',
|
148
|
+
base_labels
|
149
|
+
)
|
150
|
+
end
|
151
|
+
|
152
|
+
def apdex_counter
|
153
|
+
@apdex_counter ||= Labkit::Metrics::Client.counter(
|
154
|
+
:gitlab_covered_experience_apdex_total,
|
155
|
+
'Total covered experience apdex events',
|
156
|
+
base_labels
|
157
|
+
)
|
158
|
+
end
|
159
|
+
|
160
|
+
def log_event(event_type, **extra)
|
161
|
+
log_data = build_log_data(event_type, **extra)
|
162
|
+
logger.info(log_data)
|
163
|
+
end
|
164
|
+
|
165
|
+
def build_log_data(event_type, **extra)
|
166
|
+
log_data = {
|
167
|
+
checkpoint: event_type,
|
168
|
+
covered_experience: @definition.covered_experience,
|
169
|
+
feature_category: @definition.feature_category,
|
170
|
+
urgency: @definition.urgency,
|
171
|
+
start_time: @start_time,
|
172
|
+
checkpoint_time: @checkpoint_time,
|
173
|
+
end_time: @end_time,
|
174
|
+
elapsed_time_s: elapsed_time,
|
175
|
+
urgency_threshold_s: urgency_threshold
|
176
|
+
}
|
177
|
+
log_data.merge!(extra) if extra
|
178
|
+
|
179
|
+
if has_error?
|
180
|
+
log_data[:error] = true
|
181
|
+
log_data[:error_message] = @error.inspect
|
182
|
+
end
|
183
|
+
|
184
|
+
log_data.compact!
|
185
|
+
|
186
|
+
log_data
|
187
|
+
end
|
188
|
+
|
189
|
+
def warn(exception)
|
190
|
+
logger.warn(component: self.class.name, message: exception.message)
|
191
|
+
end
|
192
|
+
|
193
|
+
def logger
|
194
|
+
Labkit::CoveredExperience.configuration.logger
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Labkit
|
4
|
+
module CoveredExperience
|
5
|
+
# Fakes Labkit::CoveredExperience::Experience.
|
6
|
+
class Null
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
attr_reader :id, :description, :feature_category, :urgency
|
10
|
+
|
11
|
+
def start(*_args)
|
12
|
+
yield self if block_given?
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def push_attributes!(*_args) = self
|
17
|
+
def checkpoint(*_args) = self
|
18
|
+
def complete(*_args) = self
|
19
|
+
def error!(*_args) = self
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'json-schema'
|
5
|
+
require 'pathname'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
module Labkit
|
9
|
+
module CoveredExperience
|
10
|
+
Definition = Data.define(:covered_experience, :description, :feature_category, :urgency)
|
11
|
+
|
12
|
+
class Registry
|
13
|
+
extend Forwardable
|
14
|
+
|
15
|
+
SCHEMA_PATH = File.expand_path('../../../config/covered_experiences/schema.json', __dir__)
|
16
|
+
|
17
|
+
def_delegator :@experiences, :empty?
|
18
|
+
|
19
|
+
# @param dir [String, Pathname] Directory path containing YAML file definitions
|
20
|
+
# Defaults to 'config/covered_experiences' relative to the calling application's root
|
21
|
+
def initialize(dir: File.join("config", "covered_experiences"))
|
22
|
+
@dir = Pathname.new(Dir.pwd).join(dir)
|
23
|
+
@experiences = load_on_demand
|
24
|
+
end
|
25
|
+
|
26
|
+
# Retrieve a definition experience given a covered_experience_id.
|
27
|
+
#
|
28
|
+
# @param covered_experience_id [String, Symbol] Covered experience identifier
|
29
|
+
# @return [Experience, nil] An experience if present, otherwise nil
|
30
|
+
def [](covered_experience_id)
|
31
|
+
@experiences[covered_experience_id.to_s]
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Initialize a hash that loads experiences on-demand
|
37
|
+
#
|
38
|
+
# @return [Hash] Hash with lazy loading behavior
|
39
|
+
def load_on_demand
|
40
|
+
unless readable_dir?
|
41
|
+
warn("Directory not readable: #{@dir}")
|
42
|
+
return {}
|
43
|
+
end
|
44
|
+
|
45
|
+
Hash.new do |result, experience_id|
|
46
|
+
experience = load_experience(experience_id.to_s)
|
47
|
+
# we also store nil to memoize the value and avoid triggering load_experience again
|
48
|
+
result[experience_id.to_s] = experience
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Load a covered experience definition.
|
53
|
+
#
|
54
|
+
# @param experience_id [String] Experience identifier
|
55
|
+
# @return [Experience, nil] Loaded experience or nil if not found/invalid
|
56
|
+
def load_experience(experience_id)
|
57
|
+
file_path = @dir.join("#{experience_id}.yml")
|
58
|
+
|
59
|
+
unless file_path.exist?
|
60
|
+
warn("Invalid Covered Experience definition: #{experience_id}")
|
61
|
+
return nil
|
62
|
+
end
|
63
|
+
|
64
|
+
read_experience(file_path, experience_id)
|
65
|
+
end
|
66
|
+
|
67
|
+
def readable_dir?
|
68
|
+
@dir.exist? && @dir.directory? && @dir.readable?
|
69
|
+
end
|
70
|
+
|
71
|
+
# Read and validate a definition experience file
|
72
|
+
#
|
73
|
+
# @param file_path [Pathname] Path to the definition file
|
74
|
+
# @param experience_id [String] Expected experience ID
|
75
|
+
# @return [Experience, nil] Parsed experience or nil if invalid
|
76
|
+
def read_experience(file_path, experience_id)
|
77
|
+
content = YAML.safe_load(file_path.read)
|
78
|
+
return nil unless content.is_a?(Hash)
|
79
|
+
|
80
|
+
errors = JSON::Validator.fully_validate(schema, content)
|
81
|
+
return Definition.new(covered_experience: experience_id, **content) if errors.empty?
|
82
|
+
|
83
|
+
warn("Invalid schema for #{file_path}")
|
84
|
+
|
85
|
+
nil
|
86
|
+
rescue Psych::SyntaxError => e
|
87
|
+
warn("Invalid definition file #{file_path}: #{e.message}")
|
88
|
+
rescue StandardError => e
|
89
|
+
warn("Unexpected error processing #{file_path}: #{e.message}")
|
90
|
+
end
|
91
|
+
|
92
|
+
def schema
|
93
|
+
@schema ||= JSON.parse(File.read(SCHEMA_PATH))
|
94
|
+
end
|
95
|
+
|
96
|
+
def warn(message)
|
97
|
+
logger.warn(component: self.class.name, message: message)
|
98
|
+
end
|
99
|
+
|
100
|
+
def logger
|
101
|
+
Labkit::CoveredExperience.configuration.logger
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|