gvl_metrics_middleware 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81e9afea6ad5c3271fb91da6b0f69efcd3e1f49153b7e5a278483701295ebbac
4
- data.tar.gz: 22363d1d03ee82e7a5e6475226fdf73414d9b643728dd0da6c4b7f05b86c6b29
3
+ metadata.gz: 49d838fbb9b6654650f146cff085b6dc40a1569c07c6d2e03453007bcbab6a58
4
+ data.tar.gz: ebad112fc9f619e494b54a100551c74859d885446d2c70b93d1119b0324dc99f
5
5
  SHA512:
6
- metadata.gz: 4ba04171e7a5a1ae4eb7da1f9cbb32302d79e07679d05edc12f9d6dcc100bce0f514002368e94440f9b6cd00c313372d35b22accfda0905dae16083a29e26341
7
- data.tar.gz: cdcf224fafb3879e806e8a033c5622431baf77c14fda3b76baef184f4e8cf60862ce41838527b31ebc831124349d6b4995da1248114d8af483e8a19027688fef
6
+ metadata.gz: 33ba9dfbd6c8f8b8bccb3820d830d1e713e8bee067598035eb17d094b1e0040ffbbbd1ea68e9badafe78a09da3e258fec5ad45d2c468180e6256200582472e97
7
+ data.tar.gz: 6f0fafc8ba4e04a8834e7e0c79b98bfd4570f7c5a531863be30ed54ce0c8190aa66f6b8189167754115ec51d7fdb79362054a834bc3d509206e1a53d4fa58598
data/Appraisals CHANGED
@@ -31,3 +31,31 @@ appraise "rails_71" do
31
31
  gem "railties", "~> 7.1.0"
32
32
  gem "activesupport", "~> 7.1.0"
33
33
  end
34
+
35
+ appraise "sidekiq_52" do
36
+ gem "sidekiq", "~> 5.2"
37
+ gem "rails", "~> 7.1.0"
38
+ gem "railties", "~> 7.1.0"
39
+ gem "activesupport", "~> 7.1.0"
40
+ end
41
+
42
+ appraise "sidekiq_6x" do
43
+ gem "sidekiq", "~> 6.5", ">= 6.5.12"
44
+ gem "rails", "~> 7.1.0"
45
+ gem "railties", "~> 7.1.0"
46
+ gem "activesupport", "~> 7.1.0"
47
+ end
48
+
49
+ appraise "sidekiq_7x" do
50
+ gem "sidekiq", "~> 7.3"
51
+ gem "rails", "~> 8.0.0"
52
+ gem "railties", "~> 8.0.0"
53
+ gem "activesupport", "~> 8.0.0"
54
+ end
55
+
56
+ appraise "sidekiq_8x" do
57
+ gem "sidekiq", "~> 8.1"
58
+ gem "rails", "~> 8.0.0"
59
+ gem "railties", "~> 8.0.0"
60
+ gem "activesupport", "~> 8.0.0"
61
+ end
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2026-06-19
4
+
5
+ - The Rack reporter now receives a `route:` keyword option: the request's route template — `"controller#action"` on Rails or the matched route on Sinatra (`nil` when no route matched). This lets reporters segment GVL metrics by endpoint (e.g. the CPU/IO ratio per action), mirroring the `queue`/`job_class` options the Sidekiq reporter already receives. Reporters defined as strict lambdas should accept the keyword (e.g. `->(total, running, io_wait, gvl_wait, **options) {}`); block-style reporters are unaffected.
6
+
7
+ ## [0.3.0] - 2026-01-26
8
+
9
+ - Add Ruby 4.0 support ([#25](https://github.com/speedshop/gvl_metrics_middleware/pull/25))
10
+ - Add support for Sidekiq 5.2 ([#23](https://github.com/speedshop/gvl_metrics_middleware/pull/23))
11
+ - Skip GVL measurement when no reporter is configured, improving performance ([#21](https://github.com/speedshop/gvl_metrics_middleware/pull/21))
12
+
3
13
  ## [0.2.1] - 2025-11-14
4
14
 
5
15
  - Fixed errors when middleware stack may have been modified ([#18](https://github.com/speedshop/gvl_metrics_middleware/pull/18))
data/README.md CHANGED
@@ -1,13 +1,10 @@
1
1
  # GVL Metrics Middleware
2
2
 
3
- `gvl_metrics_middleware` is a Ruby gem that provides middleware for monitoring and gathering metrics on Global VM Lock (
4
- GVL) in multi-threaded Ruby applications. This gem leverages
5
- [the `gvl_timing` gem](https://github.com/jhawthorn/gvl_timing) to measure time spent in the GVL and invokes a provided
6
- callback with the collected metrics data.
3
+ `gvl_metrics_middleware` is a Ruby gem that monitors the Global VM Lock (GVL) in multi-threaded Ruby applications. This gem uses [the `gvl_timing` gem](https://github.com/jhawthorn/gvl_timing) to measure time spent in the GVL. It then calls your callback with the metrics.
7
4
 
8
5
  ## Installation
9
6
 
10
- Add the gem to your applications Gemfile, and run `bundle install` to install the gem:
7
+ Add the gem to your application's Gemfile, and run `bundle install` to install the gem:
11
8
 
12
9
  ```bash
13
10
  gem 'gvl_metrics_middleware', git: "https://github.com/speedshop/gvl_metrics_middleware.git"
@@ -15,9 +12,7 @@ gem 'gvl_metrics_middleware', git: "https://github.com/speedshop/gvl_metrics_mid
15
12
 
16
13
  ## Usage
17
14
 
18
- The `gvl_metrics_middleware` automatically inserts the necessary middleware into your applications stack. To get
19
- started, configure a callback to handle the GVL metrics. Place the following configuration in your
20
- application’s `config/initializers`directory:
15
+ The `gvl_metrics_middleware` automatically adds itself to your application. To get started, set up a callback to handle the GVL metrics. Put this code in your application's `config/initializers` folder:
21
16
 
22
17
  ```ruby
23
18
  # config/initializers/gvl_metrics_middleware.rb
@@ -25,17 +20,20 @@ GvlMetricsMiddleware.configure do |config|
25
20
  # Optional: Set sampling rate (0.0 to 1.0, defaults to 0.01 for 1% sampling)
26
21
  config.sampling_rate = 0.1 # Sample 10% of requests/jobs
27
22
 
28
- config.rack do |total, running, io_wait, gvl_wait|
23
+ config.rack do |total, running, io_wait, gvl_wait, options|
24
+ # options[:route] is the request's route — "controller#action" on Rails or the
25
+ # matched route on Sinatra (nil when no route matched).
29
26
  # Your code here...
30
27
  end
31
28
 
32
- config.sidekiq do |total, running, io_wait, gvl_wait|
29
+ config.sidekiq do |total, running, io_wait, gvl_wait, options|
30
+ # options[:queue] and options[:job_class] describe the job.
33
31
  # Your code here...
34
32
  end
35
33
  end
36
34
  ```
37
35
 
38
- For example, if you would like to record the GVL metrics in New Relic, you can use the following configuration.
36
+ For example, here is how to record the GVL metrics in New Relic.
39
37
 
40
38
  ```ruby
41
39
  # config/initializers/gvl_metrics_middleware.rb
@@ -43,11 +41,19 @@ GvlMetricsMiddleware.configure do |config|
43
41
  # Increase sampling from default 1% to 10% for more data
44
42
  config.sampling_rate = 0.1
45
43
 
46
- config.rack do |total, running, io_wait, gvl_wait|
44
+ config.rack do |total, running, io_wait, gvl_wait, options|
47
45
  NewRelic::Agent.record_metric("Custom/Rack/GVL/total", total)
48
46
  NewRelic::Agent.record_metric("Custom/Rack/GVL/running", running)
49
47
  NewRelic::Agent.record_metric("Custom/Rack/GVL/io_wait", io_wait)
50
48
  NewRelic::Agent.record_metric("Custom/Rack/GVL/gvl_wait", gvl_wait)
49
+
50
+ # Segment by route ("users#show" on Rails, "GET /users/:id" on Sinatra) to see
51
+ # the CPU/IO ratio per action:
52
+ route = options[:route]
53
+ if route
54
+ NewRelic::Agent.record_metric("Custom/Rack/GVL/#{route}/running", running)
55
+ NewRelic::Agent.record_metric("Custom/Rack/GVL/#{route}/io_wait", io_wait)
56
+ end
51
57
  end
52
58
 
53
59
  config.sidekiq do |total, running, io_wait, gvl_wait, options|
@@ -67,28 +73,29 @@ GvlMetricsMiddleware.configure do |config|
67
73
  end
68
74
  ```
69
75
 
70
- After configuring, you should be able to set up custom charts in New Relic to visualize the GVL metrics. Here is an
71
- example chart:
76
+ After you set this up, you can create custom charts in New Relic to see the GVL metrics. Here is an example chart:
72
77
 
73
78
  ![Screenshot of the GVL metrics chart on New Relic](gvl-metrics.png)
74
79
 
75
80
  ## Available Metrics
76
81
 
77
- The `gvl_metrics_middleware` reports the following metrics. The metrics are all reported in nanoseconds.
82
+ The `gvl_metrics_middleware` reports these metrics. All metrics are in nanoseconds.
83
+
84
+ - **`total`**: The total time to process the request. This equals `running` + `io_wait` + `gvl_wait`. Comes from `GVLTiming::Timer#duration` in `gvl_timing`.
85
+ - **`running`**: The time a thread held the GVL and did work. Also called CPU time. Comes from `GVLTiming::Timer#cpu_duration`.
86
+ - **`io_wait`**: The time spent waiting for I/O after the thread released the GVL. Comes from `GVLTiming::Timer#idle_duration`.
87
+ - **`gvl_wait`**: The time spent waiting to get the GVL. Comes from `GVLTiming::Timer#stalled_duration`.
88
+
89
+ ## Per-request context
90
+
91
+ Besides the four timing values, each callback receives a final `options` hash describing what was measured, so you can segment the metrics:
78
92
 
79
- - **`total`**: The total duration of the request processing time, which is the sum of `running`, `io_wait`,
80
- and `gvl_wait`. This corresponds to the returned value from the value returned by the `GVLTiming::Timer#duration`
81
- method in `gvl_timing`.
82
- - **`running`**: The time a thread held the GVL and performed work (a.k.a CPU time). This corresponds to the
83
- value returned by the `GVLTiming::Timer#cpu_duration` method.
84
- - **`io_wait`**: The time spent waiting for I/O operations after releasing the GVL. This corresponds to the
85
- value returned by the `GVLTiming::Timer#idle_duration` method.
86
- - **`gvl_wait`**: The time spent waiting to acquire the GVL. This corresponds to the value returned by
87
- the `GVLTiming::Timer#stalled_duration` method.
93
+ - **Rack** `options[:route]`: the request's route *template*. On Rails this is the `"controller#action"` pair (e.g. `users#show`, `admin/users#index`); on Sinatra it is the matched route (e.g. `GET /users/:id`). It is `nil` when no route matched (404s, or a plain Rack app). Only the route template is used — never the raw path — so it stays low-cardinality.
94
+ - **Sidekiq** — `options[:queue]` and `options[:job_class]`: the queue the job ran on and its class name.
88
95
 
89
96
  ## Sampling
90
97
 
91
- By default, the middleware samples 1% of requests and jobs to minimize overhead. You can adjust this sampling rate:
98
+ By default, the middleware samples 1% of requests and jobs to keep overhead low. You can change this rate:
92
99
  ```ruby
93
100
  GvlMetricsMiddleware.configure do |config|
94
101
  # Sample 25% of requests/jobs
@@ -98,46 +105,32 @@ GvlMetricsMiddleware.configure do |config|
98
105
  end
99
106
  ```
100
107
 
101
- The `sampling_rate` option accepts a value between `0.0` (no sampling) and `1.0` (100% sampling). The default is `0.01` (1%).
108
+ The `sampling_rate` option takes a value between `0.0` (no sampling) and `1.0` (100% sampling). The default is `0.01` (1%).
102
109
 
103
- The middleware will randomly decide whether to instrument each request or job based on the configured rate. The default 1% sampling provides meaningful metrics while maintaining minimal performance overhead.
110
+ The middleware will randomly pick which requests or jobs to measure based on this rate. The default 1% sampling gives you useful metrics while keeping overhead low.
104
111
 
105
- **Important**: Your metrics will only represent the sampled portion of traffic. You may need to adjust your alerting and analysis accordingly.
112
+ **Important**: Your metrics only show the sampled part of your traffic. You may need to adjust your alerts and analysis to match.
106
113
 
107
114
  ## Performance Overhead
108
115
 
109
- The overhead introduced by `gvl_metrics_middleware` is minimal. Benchmarking has shown only a 1% difference in response
110
- times between scenarios with and without GVL metrics collection. This result has been validated through an A/A test,
111
- where both the control and experiment groups ran identical code, and the difference remained at 1%. This 1% overhead is
112
- consistent with those reported in [Shopify's `gvltools`](https://github.com/Shopify/gvltools?tab=readme-ov-file#usage),
113
- where the overhead was measured at 1-5% in a production environment.
116
+ The overhead from `gvl_metrics_middleware` is small. Tests show only a 1% difference in response times. We ran tests with and without GVL metrics. We checked this with an A/A test. Both groups ran the same code. The difference stayed at 1%. This matches what [Shopify's `gvltools`](https://github.com/Shopify/gvltools?tab=readme-ov-file#usage) found. Their overhead was 1-5% in production.
114
117
 
115
- As stated in [the README of `gvltools`](https://github.com/Shopify/gvltools?tab=readme-ov-file#usage), the exact
116
- overhead is not yet known. However, unless you observe an immediate, significant jump in response times after
117
- deployment, the overhead is negligible and should not impact application performance. This makes the middleware a
118
- reliable choice for tracking GVL metrics in production environments.
118
+ As [the README of `gvltools`](https://github.com/Shopify/gvltools?tab=readme-ov-file#usage) says, the exact overhead is not known yet. But if you don't see a big jump in response times after you deploy, the overhead is small. It will not hurt your app's speed. This makes the middleware good for tracking GVL metrics in production.
119
119
 
120
120
  ## Development
121
121
 
122
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can
123
- also run `bin/console` for an interactive prompt that will allow you to experiment.
122
+ After you check out the repo, run `bin/setup` to install what you need. Then run `rake test` to run the tests. You can also run `bin/console`. This gives you an interactive prompt where you can try things out.
124
123
 
125
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
126
- version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
127
- push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
124
+ To install this gem on your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`. Then run `bundle exec rake release`. This will create a git tag for the version. It will push git commits and the tag. It will also push the `.gem` file to [rubygems.org](https://rubygems.org).
128
125
 
129
126
  ## Contributing
130
127
 
131
- Bug reports and pull requests are welcome on GitHub at https://github.com/speedshop/gvl_metrics_middleware. This
132
- project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to
133
- the [code of conduct](https://github.com/speedshop/gvl_metrics_middleware/blob/main/CODE_OF_CONDUCT.md).
128
+ Bug reports and pull requests are welcome on GitHub at https://github.com/speedshop/gvl_metrics_middleware. This project should be a safe, welcoming place for everyone. All contributors must follow the [code of conduct](https://github.com/speedshop/gvl_metrics_middleware/blob/main/CODE_OF_CONDUCT.md).
134
129
 
135
130
  ## License
136
131
 
137
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
132
+ This gem is open source under the [MIT License](https://opensource.org/licenses/MIT).
138
133
 
139
134
  ## Code of Conduct
140
135
 
141
- Everyone interacting in the GvlMetricsMiddleware project's codebases, issue trackers, chat rooms and mailing lists is
142
- expected to follow
143
- the [code of conduct](https://github.com/speedshop/gvl_metrics_middleware/blob/main/CODE_OF_CONDUCT.md).
136
+ Everyone who uses the GvlMetricsMiddleware project must follow the [code of conduct](https://github.com/speedshop/gvl_metrics_middleware/blob/main/CODE_OF_CONDUCT.md). This includes codebases, issue trackers, chat rooms, and mailing lists.
@@ -0,0 +1,14 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "minitest", "~> 5.16"
6
+ gem "rake", "~> 13.0"
7
+ gem "rubocop", require: false
8
+ gem "rack-attack", "~> 6.8"
9
+ gem "sidekiq", "~> 5.2"
10
+ gem "rails", "~> 7.1.0"
11
+ gem "railties", "~> 7.1.0"
12
+ gem "activesupport", "~> 7.1.0"
13
+
14
+ gemspec path: "../"
@@ -0,0 +1,14 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "minitest", "~> 5.16"
6
+ gem "rake", "~> 13.0"
7
+ gem "rubocop", require: false
8
+ gem "rack-attack", "~> 6.8"
9
+ gem "sidekiq", "~> 6.5", ">= 6.5.12"
10
+ gem "rails", "~> 7.1.0"
11
+ gem "railties", "~> 7.1.0"
12
+ gem "activesupport", "~> 7.1.0"
13
+
14
+ gemspec path: "../"
@@ -0,0 +1,14 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "minitest", "~> 5.16"
6
+ gem "rake", "~> 13.0"
7
+ gem "rubocop", require: false
8
+ gem "rack-attack", "~> 6.8"
9
+ gem "sidekiq", "~> 7.3"
10
+ gem "rails", "~> 8.0.0"
11
+ gem "railties", "~> 8.0.0"
12
+ gem "activesupport", "~> 8.0.0"
13
+
14
+ gemspec path: "../"
@@ -0,0 +1,14 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "minitest", "~> 5.16"
6
+ gem "rake", "~> 13.0"
7
+ gem "rubocop", require: false
8
+ gem "rack-attack", "~> 6.8"
9
+ gem "sidekiq", "~> 8.1"
10
+ gem "rails", "~> 8.0.0"
11
+ gem "railties", "~> 8.0.0"
12
+ gem "activesupport", "~> 8.0.0"
13
+
14
+ gemspec path: "../"
@@ -19,7 +19,8 @@ module GvlMetricsMiddleware
19
19
  end
20
20
 
21
21
  def call(env)
22
- return @app.call(env) unless GvlMetricsMiddleware.should_sample?
22
+ reporter = self.class.reporter
23
+ return @app.call(env) if reporter.nil? || !GvlMetricsMiddleware.should_sample?
23
24
 
24
25
  response = nil
25
26
 
@@ -28,7 +29,7 @@ module GvlMetricsMiddleware
28
29
  end
29
30
 
30
31
  begin
31
- self.class.reporter&.call(gvl_times.duration_ns, gvl_times.running_duration_ns, gvl_times.idle_duration_ns, gvl_times.stalled_duration_ns)
32
+ reporter.call(gvl_times.duration_ns, gvl_times.running_duration_ns, gvl_times.idle_duration_ns, gvl_times.stalled_duration_ns, route: route_for(env))
32
33
  rescue => exception
33
34
  GvlMetricsMiddleware.on_report_failure&.call("Rack", exception)
34
35
 
@@ -37,5 +38,24 @@ module GvlMetricsMiddleware
37
38
 
38
39
  response
39
40
  end
41
+
42
+ private
43
+
44
+ # The request's route template, passed to the reporter so it can segment GVL
45
+ # metrics by endpoint (for example, the CPU/IO ratio per action). On Rails it
46
+ # is the "controller#action" pair; on Sinatra it is the matched route. Only the
47
+ # route *template* is used -- never the raw path -- so it stays low-cardinality,
48
+ # and on Rails only :controller and :action are read (path_parameters also holds
49
+ # :id, :format, ...). Returns nil when no route matched (404s, or a plain Rack
50
+ # app), leaving any fallback to the reporter.
51
+ def route_for(env)
52
+ if (path_parameters = env["action_dispatch.request.path_parameters"]) # Rails
53
+ controller = path_parameters[:controller]
54
+ action = path_parameters[:action]
55
+ return "#{controller}##{action}" if controller && action
56
+ end
57
+
58
+ env["sinatra.route"] # Sinatra, e.g. "GET /users/:id"
59
+ end
40
60
  end
41
61
  end
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "gvl_timing"
4
- require "sidekiq/middleware/modules"
4
+
5
+ if ::Sidekiq::VERSION >= "6.5.0"
6
+ require "sidekiq/middleware/modules"
7
+ end
5
8
 
6
9
  module GvlMetricsMiddleware
7
10
  class Sidekiq
@@ -15,15 +18,18 @@ module GvlMetricsMiddleware
15
18
  end
16
19
  end
17
20
 
18
- include ::Sidekiq::ServerMiddleware
21
+ if ::Sidekiq::VERSION >= "6.5.0"
22
+ include ::Sidekiq::ServerMiddleware
23
+ end
19
24
 
20
25
  def call(job_instance, _job_payload, queue)
21
- return yield unless GvlMetricsMiddleware.should_sample?
26
+ reporter = self.class.reporter
27
+ return yield if reporter.nil? || !GvlMetricsMiddleware.should_sample?
22
28
 
23
29
  gvl_times = GVLTiming.measure { yield }
24
30
 
25
31
  begin
26
- self.class.reporter&.call(gvl_times.duration_ns, gvl_times.running_duration_ns, gvl_times.idle_duration_ns, gvl_times.stalled_duration_ns, job_class: job_instance.class.to_s, queue: queue)
32
+ reporter.call(gvl_times.duration_ns, gvl_times.running_duration_ns, gvl_times.idle_duration_ns, gvl_times.stalled_duration_ns, job_class: job_instance.class.to_s, queue: queue)
27
33
  rescue => exception
28
34
  GvlMetricsMiddleware.on_report_failure&.call("Sidekiq", exception)
29
35
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GvlMetricsMiddleware
4
- VERSION = "0.2.1"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gvl_metrics_middleware
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Berkopec
@@ -86,6 +86,10 @@ files:
86
86
  - gemfiles/rails_80.gemfile
87
87
  - gemfiles/rails_81.gemfile
88
88
  - gemfiles/rails_edge.gemfile
89
+ - gemfiles/sidekiq_52.gemfile
90
+ - gemfiles/sidekiq_6x.gemfile
91
+ - gemfiles/sidekiq_7x.gemfile
92
+ - gemfiles/sidekiq_8x.gemfile
89
93
  - gvl-metrics.png
90
94
  - lib/gvl_metrics_middleware.rb
91
95
  - lib/gvl_metrics_middleware/rack.rb