pyroscope 0.3.2 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e4579716329cb055e247d5c05419d38889bae214ad4167dff49aa298c52f3b0
4
- data.tar.gz: c2679ebbd856b7603c49ab20b956470ee32aaf03f8c37e145ea2bd0467dd7f94
3
+ metadata.gz: df36b61e5b8fde45ebc360aedc8e109e3d37c7817d539721a48c00f59f1f66bc
4
+ data.tar.gz: 9bdb35db6a01df630a489716caee628d67c52984391d867b1f2693e5f137b85a
5
5
  SHA512:
6
- metadata.gz: a7f059b8a98c2b9c3bf553f296524a5004017f07fb1a7ea52821e6c2f4111f101a501f4edd1bd6d4a82a14a3e75aed3bb543bd2fff0d0c5e40d5a99395f7af61
7
- data.tar.gz: 90dddc7d21658e6fa87d66dca7460e9856d771455464b3ca05776345496d3fe19fc9eb4f85a8bcee5cc480e89a6280a4a43a0f4598548ff00e8f7866d8b32d18
6
+ metadata.gz: 816ece1b66998b8e128ab8e7fd8a625c5ea37d941fefd4f5025b5cb06fb8a2fa5dd27bd8ab0dca0ab5473818ae315c9e749edb7e5d126e524a80ae56917df802
7
+ data.tar.gz: 468ef393fa6008d5f63d49554820be41b307c59b0d414f712e96da677a51c66880cbcf01b751b24f80a0913d0e9f5c9e174d0f3561717c9d11ae5db90d269198
data/README.md CHANGED
@@ -1,57 +1,71 @@
1
- Pyroscope Ruby Integration --Beta--
2
- =====================================
1
+ # Pyroscope Ruby Gem
3
2
 
4
- **note**: This is a beta release. It requires local compilation, might be
5
- buggy and is frequently updated. For the initial implementation, find it [here](https://github.com/pyroscope-io/pyroscope-ruby). Please report any [issues](https://github.com/pyroscope-io/pyroscope-rs/issues).
3
+ **Pyroscope integration for Ruby**
6
4
 
7
- ## Installation
5
+ [![license](https://img.shields.io/badge/license-Apache2.0-blue.svg)](LICENSE)
6
+ ![tests](https://github.com/pyroscope-io/pyroscope-rs/workflows/Tests/badge.svg)
7
+ ![build](https://github.com/pyroscope-io/pyroscope-rs/workflows/Build/badge.svg)
8
+ [![Gem version](https://badge.fury.io/rb/pyroscope.svg)](https://badge.fury.io/rb/pyroscope)
8
9
 
9
- 1. You need the Rust toolchain to compile the library locally. To install
10
- Rust:
10
+ ---
11
11
 
12
- ```
13
- curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain stable -y
14
- export PATH=$PATH:/root/.cargo/bin
15
- ```
12
+ ### What is Pyroscope
13
+ [Pyroscope](https://github.com/pyroscope-io/pyroscope) is a tool that lets you continuously profile your applications to prevent and debug performance issues in your code. It consists of a low-overhead agent which sends data to the Pyroscope server which includes a custom-built storage engine. This allows for you to store and query any applications profiling data in an extremely efficient and cost effective way.
16
14
 
17
- 2. Building/Insalling from Rubygems
18
15
 
19
- ```
20
- gem install pyroscope_beta
21
- ```
16
+ ### Supported platforms
22
17
 
23
- 3. Building/Installing from source
18
+ | Linux | macOS | Windows | Docker |
19
+ |:-----:|:-----:|:-------:|:------:|
20
+ | ✅ | ✅ | | ✅ |
24
21
 
25
- Change directory to `pyroscope_ffi/ruby` and run
22
+ ### Profiling Ruby applications
26
23
 
24
+ Add the `pyroscope` gem to your Gemfile:
25
+
26
+ ```bash
27
+ bundle add pyroscope
27
28
  ```
28
- gem build pyroscope.gemspec
29
- gem install ./pyroscope.gemspec
30
- ```
31
29
 
32
- ## Configuration
30
+ ### Basic Configuration
31
+
32
+ Add the following code to your application. If you're using rails, put this into `config/initializers` directory. This code will initialize pyroscope profiler and start profiling:
33
33
 
34
- Configuration is similar to the old package except for `application_name`:
34
+ ```ruby
35
+ require 'pyroscope'
35
36
 
37
+ Pyroscope.configure do |config|
38
+ config.application_name = "my.ruby.app" # replace this with some name for your application
39
+ config.server_address = "http://my-pyroscope-server:4040" # replace this with the address of your pyroscope server
40
+ # config.auth_token = "{YOUR_API_KEY}" # optionally, if authentication is enabled, specify the API key
41
+ end
36
42
  ```
37
- require 'pyroscope_beta'
43
+
44
+ ### Tags
45
+
46
+ Pyroscope ruby integration provides a number of ways to tag profiling data. For example, you can provide tags when you're initializing the profiler:
47
+
48
+ ```ruby
49
+ require 'pyroscope'
38
50
 
39
51
  Pyroscope.configure do |config|
40
- config.application_name = "ruby.app"
41
- config.server_address = "http://localhost:4040"
42
- config.detect_subprocesses = true
52
+ config.application_name = "my.ruby.app"
53
+ config.server_address = "http://my-pyroscope-server:4040"
54
+
43
55
  config.tags = {
44
- :key => "value",
56
+ "hostname" => ENV["HOSTNAME"],
45
57
  }
46
58
  end
47
59
  ```
48
60
 
49
- ## Adding tags
61
+ or you can dynamically tag certain parts of your code:
50
62
 
51
- Tags passed to configure are global. To tag code locally, you can use:
52
-
53
- ```
54
- Pyroscope.tag_wrapper({"profile": "profile-1"}) do
55
- // Tagged profile
63
+ ```ruby
64
+ Pyroscope.tag_wrapper({ "controller": "slow_controller_i_want_to_profile" }) do
65
+ slow_code
56
66
  end
57
67
  ```
68
+
69
+ ### Example
70
+
71
+ Check out this [example ruby project in our repository](https://github.com/pyroscope-io/pyroscope/tree/main/examples/ruby) for examples of how you can use these features.
data/ext/rbspy/src/lib.rs CHANGED
@@ -1,12 +1,16 @@
1
- use ffikit::Signal;
2
- use pyroscope::backend::{Report, StackFrame, Tag};
3
- use pyroscope::PyroscopeAgent;
4
- use pyroscope_rbspy::{rbspy_backend, RbspyConfig};
5
1
  use std::collections::hash_map::DefaultHasher;
2
+ use std::env;
6
3
  use std::ffi::CStr;
7
4
  use std::hash::Hasher;
8
5
  use std::os::raw::c_char;
9
- use std::env;
6
+ use std::str::FromStr;
7
+
8
+ use ffikit::Signal;
9
+ use pyroscope_rbspy::{rbspy_backend, RbspyConfig};
10
+
11
+ use pyroscope::{pyroscope::Compression, PyroscopeAgent};
12
+ use pyroscope::backend::{Report, StackFrame, Tag};
13
+ use pyroscope::pyroscope::ReportEncoding;
10
14
 
11
15
  pub fn transform_report(report: Report) -> Report {
12
16
  let cwd = env::current_dir().unwrap();
@@ -24,31 +28,25 @@ pub fn transform_report(report: Report) -> Report {
24
28
  let mut s = frame.filename.unwrap();
25
29
  match s.find(cwd) {
26
30
  Some(i) => {
27
- s = s[(i+cwd.len()+1)..].to_string();
31
+ s = s[(i + cwd.len() + 1)..].to_string();
28
32
  }
29
- None => {
30
- match s.find("/gems/") {
33
+ None => match s.find("/gems/") {
34
+ Some(i) => {
35
+ s = s[(i + 1)..].to_string();
36
+ }
37
+ None => match s.find("/ruby/") {
31
38
  Some(i) => {
32
- s = s[(i+1)..].to_string();
33
- }
34
- None => {
35
- match s.find("/ruby/") {
39
+ s = s[(i + 6)..].to_string();
40
+ match s.find("/") {
36
41
  Some(i) => {
37
- s = s[(i+6)..].to_string();
38
- match s.find("/") {
39
- Some(i) => {
40
- s = s[(i+1)..].to_string();
41
- }
42
- None => {
43
- }
44
- }
45
- }
46
- None => {
42
+ s = s[(i + 1)..].to_string();
47
43
  }
44
+ None => {}
48
45
  }
49
46
  }
50
- }
51
- }
47
+ None => {}
48
+ },
49
+ },
52
50
  }
53
51
 
54
52
  // something
@@ -76,11 +74,42 @@ pub fn transform_report(report: Report) -> Report {
76
74
  new_report
77
75
  }
78
76
 
77
+ #[no_mangle]
78
+ pub extern "C" fn initialize_logging(logging_level: u32) -> bool {
79
+ // Force rustc to display the log messages in the console.
80
+ match logging_level {
81
+ 50 => {
82
+ std::env::set_var("RUST_LOG", "error");
83
+ }
84
+ 40 => {
85
+ std::env::set_var("RUST_LOG", "warn");
86
+ }
87
+ 30 => {
88
+ std::env::set_var("RUST_LOG", "info");
89
+ }
90
+ 20 => {
91
+ std::env::set_var("RUST_LOG", "debug");
92
+ }
93
+ 10 => {
94
+ std::env::set_var("RUST_LOG", "trace");
95
+ }
96
+ _ => {
97
+ std::env::set_var("RUST_LOG", "debug");
98
+ }
99
+ }
100
+
101
+ // Initialize the logger.
102
+ pretty_env_logger::init_timed();
103
+
104
+ true
105
+ }
106
+
79
107
  #[no_mangle]
80
108
  pub extern "C" fn initialize_agent(
81
109
  application_name: *const c_char, server_address: *const c_char, auth_token: *const c_char,
82
- sample_rate: u32, detect_subprocesses: bool, on_cpu: bool, report_pid: bool,
83
- report_thread_id: bool, tags: *const c_char,
110
+ sample_rate: u32, detect_subprocesses: bool, oncpu: bool, report_pid: bool,
111
+ report_thread_id: bool, tags: *const c_char, compression: *const c_char,
112
+ report_encoding: *const c_char
84
113
  ) -> bool {
85
114
  // Initialize FFIKit
86
115
  let recv = ffikit::initialize_ffi().unwrap();
@@ -105,13 +134,27 @@ pub extern "C" fn initialize_agent(
105
134
  .unwrap()
106
135
  .to_string();
107
136
 
137
+ let compression_string = unsafe { CStr::from_ptr(compression) }
138
+ .to_str()
139
+ .unwrap()
140
+ .to_string();
141
+
142
+ let report_encoding = unsafe { CStr::from_ptr(report_encoding) }
143
+ .to_str()
144
+ .unwrap()
145
+ .to_string();
146
+
147
+ let compression = Compression::from_str(&compression_string);
148
+ let report_encoding = ReportEncoding::from_str(&report_encoding)
149
+ .unwrap_or(ReportEncoding::FOLDED);
150
+
108
151
  let pid = std::process::id();
109
152
 
110
153
  let rbspy_config = RbspyConfig::new(pid.try_into().unwrap())
111
154
  .sample_rate(sample_rate)
112
155
  .lock_process(false)
113
- .with_subprocesses(detect_subprocesses)
114
- .on_cpu(on_cpu)
156
+ .detect_subprocesses(detect_subprocesses)
157
+ .oncpu(oncpu)
115
158
  .report_pid(report_pid)
116
159
  .report_thread_id(report_thread_id);
117
160
 
@@ -122,12 +165,17 @@ pub extern "C" fn initialize_agent(
122
165
  let mut agent_builder = PyroscopeAgent::builder(server_address, application_name)
123
166
  .backend(rbspy)
124
167
  .func(transform_report)
125
- .tags(tags);
168
+ .tags(tags)
169
+ .report_encoding(report_encoding);
126
170
 
127
171
  if auth_token != "" {
128
172
  agent_builder = agent_builder.auth_token(auth_token);
129
173
  }
130
174
 
175
+ if let Ok(compression) = compression {
176
+ agent_builder = agent_builder.compression(compression);
177
+ }
178
+
131
179
  let agent = agent_builder.build().unwrap();
132
180
 
133
181
  let agent_running = agent.start().unwrap();
@@ -1,3 +1,3 @@
1
1
  module Pyroscope
2
- VERSION = '0.3.2'.freeze
2
+ VERSION = '0.5.1'.freeze
3
3
  end
data/lib/pyroscope.rb CHANGED
@@ -1,10 +1,14 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
1
4
  require 'ffi'
2
5
 
3
6
  module Pyroscope
4
7
  module Rust
5
8
  extend FFI::Library
6
9
  ffi_lib File.expand_path(File.dirname(__FILE__)) + "/rbspy/rbspy.#{RbConfig::CONFIG["DLEXT"]}"
7
- attach_function :initialize_agent, [:string, :string, :string, :int, :bool, :bool, :bool, :bool, :string], :bool
10
+ attach_function :initialize_logging, [:int], :bool
11
+ attach_function :initialize_agent, [:string, :string, :string, :int, :bool, :bool, :bool, :bool, :string, :string, :string], :bool
8
12
  attach_function :add_thread_tag, [:uint64, :string, :string], :bool
9
13
  attach_function :remove_thread_tag, [:uint64, :string, :string], :bool
10
14
  attach_function :add_global_tag, [:string, :string], :bool
@@ -18,42 +22,107 @@ module Pyroscope
18
22
  attach_function :thread_id, [], :uint64
19
23
  end
20
24
 
21
- Config = Struct.new(:application_name, :app_name, :server_address, :auth_token, :sample_rate, :detect_subprocesses, :on_cpu, :report_pid, :report_thread_id, :log_level, :tags) do
25
+ if defined?(::Rails::Engine)
26
+ class Engine < ::Rails::Engine
27
+ config.after_initialize do
28
+ next unless ::Pyroscope.current_config && ::Pyroscope.current_config.autoinstrument_rails
29
+
30
+ ::Pyroscope.initialize_rails_hooks
31
+ end
32
+ end
33
+ end
34
+
35
+ Config = Struct.new(
36
+ :application_name,
37
+ :app_name,
38
+ :server_address,
39
+ :auth_token,
40
+ :log_level,
41
+ :sample_rate,
42
+ :detect_subprocesses,
43
+ :oncpu,
44
+ :report_pid,
45
+ :report_thread_id,
46
+ :tags,
47
+ :compression,
48
+ :report_encoding,
49
+ :autoinstrument_rails,
50
+ ) do
22
51
  def initialize(*)
52
+ super
53
+ # defaults:
23
54
  self.application_name = ''
24
55
  self.server_address = 'http://localhost:4040'
25
56
  self.auth_token = ''
26
57
  self.sample_rate = 100
27
58
  self.detect_subprocesses = false
28
- self.on_cpu = true
59
+ self.oncpu = true
29
60
  self.report_pid = false
30
61
  self.report_thread_id = false
31
- self.log_level = 'info'
62
+ self.log_level = 'error'
32
63
  self.tags = {}
33
- super
64
+ self.compression = 'gzip'
65
+ self.report_encoding = 'pprof'
66
+ self.autoinstrument_rails = true
34
67
  end
35
68
  end
36
69
 
37
70
  class << self
71
+ def current_config
72
+ @config
73
+ end
74
+
38
75
  def configure
39
76
  @config = Config.new
40
77
 
41
78
  # Pass config to the block
42
79
  yield @config
43
80
 
81
+ # Determine Logging level (kinda like an enum).
82
+ case @config.log_level
83
+ when 'trace'
84
+ @log_level = 10
85
+ when 'debug'
86
+ @log_level = 20
87
+ when 'info'
88
+ @log_level = 30
89
+ when 'warn'
90
+ @log_level = 40
91
+ when 'error'
92
+ @log_level = 50
93
+ else
94
+ @log_level = 50
95
+ end
96
+
97
+ Rust.initialize_logging(@log_level)
98
+
44
99
  Rust.initialize_agent(
100
+ # these are defaults in case user-provided values are nil:
45
101
  @config.app_name || @config.application_name || "",
46
102
  @config.server_address || "",
47
103
  @config.auth_token || "",
48
104
  @config.sample_rate || 100,
49
105
  @config.detect_subprocesses || false,
50
- @config.on_cpu || false,
106
+ @config.oncpu || false,
51
107
  @config.report_pid || false,
52
108
  @config.report_thread_id || false,
53
- tags_to_string(@config.tags || {})
109
+ tags_to_string(@config.tags || {}),
110
+ @config.compression || "",
111
+ @config.report_encoding || "pprof"
54
112
  )
55
113
  end
56
114
 
115
+ def initialize_rails_hooks
116
+ block = lambda do |ctrl, action|
117
+ Pyroscope.tag_wrapper({
118
+ "action" => "#{ctrl.controller_name}/#{ctrl.action_name}"
119
+ }, &action)
120
+ end
121
+
122
+ ActionController::API.__send__(:around_action, block) if defined? ActionController::API
123
+ ActionController::Base.__send__(:around_action, block) if defined? ActionController::Base
124
+ end
125
+
57
126
  def tag_wrapper(tags)
58
127
  tid = thread_id
59
128
  _add_tags(tid, tags)
@@ -72,32 +141,34 @@ module Pyroscope
72
141
  warn("deprecated. Use `Pyroscope.tag_wrapper` instead.")
73
142
  end
74
143
 
75
- # convert tags object to string
76
- def tags_to_string(tags)
77
- tags.map { |k, v| "#{k}=#{v}" }.join(',')
78
- end
79
-
80
- # get thread id
81
144
  def thread_id
82
145
  return Utils.thread_id
83
146
  end
84
147
 
85
- # add tags
86
148
  def _add_tags(thread_id, tags)
87
149
  tags.each do |tag_name, tag_value|
88
150
  Rust.add_thread_tag(thread_id, tag_name.to_s, tag_value.to_s)
89
151
  end
90
152
  end
91
153
 
92
- # remove tags
93
154
  def _remove_tags(thread_id, tags)
94
155
  tags.each do |tag_name, tag_value|
95
156
  Rust.remove_thread_tag(thread_id, tag_name.to_s, tag_value.to_s)
96
157
  end
97
158
  end
98
159
 
99
- def drop
160
+ def stop
100
161
  Rust.drop_agent
101
162
  end
163
+
164
+ def shutdown
165
+ stop
166
+ end
167
+
168
+ private
169
+
170
+ def tags_to_string(tags)
171
+ tags.map { |k, v| "#{k}=#{v}" }.join(',')
172
+ end
102
173
  end
103
174
  end
data/pyroscope.gemspec CHANGED
@@ -1,4 +1,11 @@
1
- require_relative "lib/pyroscope/version"
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require File.expand_path(File.join(File.dirname(__FILE__), "lib/pyroscope/version"))
6
+ rescue LoadError
7
+ puts "WARNING: Could not load Pyroscope::VERSION"
8
+ end
2
9
 
3
10
  Gem::Specification.new do |s|
4
11
  s.name = 'pyroscope'
@@ -9,6 +16,13 @@ Gem::Specification.new do |s|
9
16
  s.email = ['contact@pyroscope.io']
10
17
  s.homepage = 'https://pyroscope.io'
11
18
  s.license = 'Apache-2.0'
19
+ s.metadata = {
20
+ "homepage_uri" => "https://pyroscope.io",
21
+ "bug_tracker_uri" => "https://github.com/pyroscope-io/pyroscope-rs/issues",
22
+ "documentation_uri" => "https://pyroscope.io/docs/ruby/",
23
+ "changelog_uri" => "https://github.com/pyroscope-io/pyroscope-rs/tree/main/pyroscope_ffi/ruby/CHANGELOG.md",
24
+ "source_code_uri" => "https://github.com/pyroscope-io/pyroscope-rs/tree/main/pyroscope_ffi/ruby",
25
+ }
12
26
 
13
27
  # Specify which files should be added to the gem when it is released.
14
28
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -21,7 +35,7 @@ Gem::Specification.new do |s|
21
35
 
22
36
  s.platform = Gem::Platform::RUBY
23
37
 
24
- s.required_ruby_version = ">= 2.5.9"
38
+ s.required_ruby_version = ">= 1.9.3"
25
39
 
26
40
  s.extensions = ['ext/rbspy/extconf.rb', 'ext/thread_id/extconf.rb']
27
41
 
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "pyroscope"
4
+ require "pyroscope/version"
5
+
6
+ puts Pyroscope::VERSION
7
+ puts RUBY_VERSION
8
+
9
+ Pyroscope.configure do |config|
10
+ config.application_name = "#{ENV["PYROSCOPE_RUN_ID"]}"
11
+ config.server_address = "https://ingest.pyroscope.cloud"
12
+ config.auth_token = ENV["PYROSCOPE_API_TOKEN"]
13
+ config.detect_subprocesses = ENV["PYROSCOPE_DETECT_SUBPROCESSES"] == "1"
14
+ config.oncpu = ENV["PYROSCOPE_ONCPU"] == "1"
15
+ config.log_level = "trace"
16
+ config.report_pid = true
17
+ config.report_thread_id = true
18
+ config.tags = {
19
+ :region => "us-east",
20
+ :detect_subprocesses => ENV["PYROSCOPE_DETECT_SUBPROCESSES"],
21
+ :oncpu => ENV["PYROSCOPE_ONCPU"],
22
+ :version => ENV["RUBY_VERSION"],
23
+ :arch => ENV["PYROSCOPE_ARCH"]
24
+ }
25
+ end
26
+
27
+ def work(n)
28
+ i = 0
29
+ while i < n
30
+ i += 1
31
+ end
32
+ end
33
+
34
+ def fast_function
35
+ Pyroscope.tag_wrapper({"function": "fast"}) do
36
+ work(2001002000)
37
+ end
38
+ end
39
+
40
+ def slow_function
41
+ work(8001008000)
42
+ end
43
+
44
+ child_pid = fork do
45
+ puts "This is the child process"
46
+ Pyroscope.tag_wrapper({"fork": "forked"}) do
47
+ slow_function()
48
+ end
49
+ end
50
+
51
+ puts "This is the master process."
52
+
53
+ Pyroscope.tag_wrapper({"fork": "master"}) do
54
+ fast_function()
55
+ end
56
+
57
+ puts "The PID of the child process is #{child_pid}"
58
+
59
+ Pyroscope.shutdown()
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pyroscope
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pyroscope Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-22 00:00:00.000000000 Z
11
+ date: 2022-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -89,10 +89,16 @@ files:
89
89
  - lib/pyroscope/version.rb
90
90
  - pyroscope.gemspec
91
91
  - scripts/docker.sh
92
+ - scripts/tests/test.rb
92
93
  homepage: https://pyroscope.io
93
94
  licenses:
94
95
  - Apache-2.0
95
- metadata: {}
96
+ metadata:
97
+ homepage_uri: https://pyroscope.io
98
+ bug_tracker_uri: https://github.com/pyroscope-io/pyroscope-rs/issues
99
+ documentation_uri: https://pyroscope.io/docs/ruby/
100
+ changelog_uri: https://github.com/pyroscope-io/pyroscope-rs/tree/main/pyroscope_ffi/ruby/CHANGELOG.md
101
+ source_code_uri: https://github.com/pyroscope-io/pyroscope-rs/tree/main/pyroscope_ffi/ruby
96
102
  post_install_message:
97
103
  rdoc_options: []
98
104
  require_paths:
@@ -101,7 +107,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
101
107
  requirements:
102
108
  - - ">="
103
109
  - !ruby/object:Gem::Version
104
- version: 2.5.9
110
+ version: 1.9.3
105
111
  required_rubygems_version: !ruby/object:Gem::Requirement
106
112
  requirements:
107
113
  - - ">="