yabeda-rack-queue 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 21cab04c6429f2f935c1f25dd635a57bef7b6f619775a06ee15c678cad907bc8
4
+ data.tar.gz: 2b0aedeb8ab33fe5c2380942c71221b64a743c2eb7999ba2ecd153ca4703820f
5
+ SHA512:
6
+ metadata.gz: 177604e2d5e1fc330c66a074dcae18c33ed42a49afca78755e91f75675b67b04451e1d27813ec3040375f2040e221a1aa20e8348b2b17e3e9e8d70cc71b12acc
7
+ data.tar.gz: 6a90ef452a54f42dd2e916ca6ef528b24cbe97993bc2dc6f0d19f767aeb1fc54bf942c37a82017d559656c57035c85ca6929107d91898f7759a3a67d6e6b9650
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ /.bundle/
2
+ /pkg/
3
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,120 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ yabeda-rack-queue (0.1.0)
5
+ yabeda (>= 0.14, < 1.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ anyway_config (2.8.0)
11
+ ruby-next-core (~> 1.0)
12
+ ast (2.4.3)
13
+ benchmark (0.5.0)
14
+ benchmark-ips (2.14.0)
15
+ concurrent-ruby (1.3.6)
16
+ dry-initializer (3.2.0)
17
+ json (2.18.1)
18
+ language_server-protocol (3.17.0.5)
19
+ lint_roller (1.1.0)
20
+ minitest (5.27.0)
21
+ nio4r (2.7.5)
22
+ parallel (1.27.0)
23
+ parser (3.3.10.2)
24
+ ast (~> 2.4.1)
25
+ racc
26
+ prism (1.9.0)
27
+ puma (7.2.0)
28
+ nio4r (~> 2.0)
29
+ racc (1.8.1)
30
+ rainbow (3.1.1)
31
+ rake (13.3.1)
32
+ regexp_parser (2.11.3)
33
+ rubocop (1.84.2)
34
+ json (~> 2.3)
35
+ language_server-protocol (~> 3.17.0.2)
36
+ lint_roller (~> 1.1.0)
37
+ parallel (~> 1.10)
38
+ parser (>= 3.3.0.2)
39
+ rainbow (>= 2.2.2, < 4.0)
40
+ regexp_parser (>= 2.9.3, < 3.0)
41
+ rubocop-ast (>= 1.49.0, < 2.0)
42
+ ruby-progressbar (~> 1.7)
43
+ unicode-display_width (>= 2.4.0, < 4.0)
44
+ rubocop-ast (1.49.0)
45
+ parser (>= 3.3.7.2)
46
+ prism (~> 1.7)
47
+ rubocop-performance (1.26.1)
48
+ lint_roller (~> 1.1)
49
+ rubocop (>= 1.75.0, < 2.0)
50
+ rubocop-ast (>= 1.47.1, < 2.0)
51
+ ruby-next-core (1.2.0)
52
+ ruby-progressbar (1.13.0)
53
+ standard (1.54.0)
54
+ language_server-protocol (~> 3.17.0.2)
55
+ lint_roller (~> 1.0)
56
+ rubocop (~> 1.84.0)
57
+ standard-custom (~> 1.0.0)
58
+ standard-performance (~> 1.8)
59
+ standard-custom (1.0.2)
60
+ lint_roller (~> 1.0)
61
+ rubocop (~> 1.50)
62
+ standard-performance (1.9.0)
63
+ lint_roller (~> 1.1)
64
+ rubocop-performance (~> 1.26.0)
65
+ unicode-display_width (3.2.0)
66
+ unicode-emoji (~> 4.1)
67
+ unicode-emoji (4.2.0)
68
+ yabeda (0.14.0)
69
+ anyway_config (>= 1.0, < 3)
70
+ concurrent-ruby
71
+ dry-initializer
72
+
73
+ PLATFORMS
74
+ arm64-darwin-24
75
+ ruby
76
+
77
+ DEPENDENCIES
78
+ benchmark (>= 0.4, < 1.0)
79
+ benchmark-ips (>= 2.14, < 3.0)
80
+ minitest (>= 5.22, < 6.0)
81
+ puma (>= 6, < 8)
82
+ rake (>= 13.0)
83
+ standard (~> 1.44)
84
+ yabeda-rack-queue!
85
+
86
+ CHECKSUMS
87
+ anyway_config (2.8.0) sha256=f6797a7231f81202dcd3d0c07284e836e45713e761d320180348b13a5c7c9306
88
+ ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
89
+ benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
90
+ benchmark-ips (2.14.0) sha256=b72bc8a65d525d5906f8cd94270dccf73452ee3257a32b89fbd6684d3e8a9b1d
91
+ concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
92
+ dry-initializer (3.2.0) sha256=37d59798f912dc0a1efe14a4db4a9306989007b302dcd5f25d0a2a20c166c4e3
93
+ json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986
94
+ language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
95
+ lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
96
+ minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
97
+ nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
98
+ parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
99
+ parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357
100
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
101
+ puma (7.2.0) sha256=bf8ef4ab514a4e6d4554cb4326b2004eba5036ae05cf765cfe51aba9706a72a8
102
+ racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
103
+ rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
104
+ rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
105
+ regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
106
+ rubocop (1.84.2) sha256=5692cea54168f3dc8cb79a6fe95c5424b7ea893c707ad7a4307b0585e88dbf5f
107
+ rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd
108
+ rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834
109
+ ruby-next-core (1.2.0) sha256=f6a7d00bb5186cecbb02f7f1845a0f3a2c9788d35b6ccff5c9be3f0d46799b86
110
+ ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
111
+ standard (1.54.0) sha256=7a4b08f83d9893083c8f03bc486f0feeb6a84d48233b40829c03ef4767ea0100
112
+ standard-custom (1.0.2) sha256=424adc84179a074f1a2a309bb9cf7cd6bfdb2b6541f20c6bf9436c0ba22a652b
113
+ standard-performance (1.9.0) sha256=49483d31be448292951d80e5e67cdcb576c2502103c7b40aec6f1b6e9c88e3f2
114
+ unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
115
+ unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
116
+ yabeda (0.14.0) sha256=bc517bf22d692ebd80a29fc9fd2246c257aaf92d10b2735a775e2419351a43bf
117
+ yabeda-rack-queue (0.1.0)
118
+
119
+ BUNDLED WITH
120
+ 4.0.3
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nate Berkopec
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # yabeda-rack-queue
2
+
3
+ Rack middleware that measures HTTP request queue time. It reports the result to [Yabeda](https://github.com/yabeda-rb/yabeda) as a histogram.
4
+
5
+ ## What is queue time?
6
+
7
+ A request may wait before your app handles it. A proxy or load balancer (like Nginx or Heroku) causes this wait. This is called queue time.
8
+
9
+ High queue time means your app is too busy. It cannot take new requests. That is a sign you need more capacity.
10
+
11
+ ## How it works
12
+
13
+ Load balancers can add a header to each request. The header records when the request arrived. Common headers are `X-Request-Start` and `X-Queue-Start`.
14
+
15
+ This middleware reads that header. It subtracts the header's timestamp from the current time. Then it reports that value as `rack_queue.rack_queue_duration`.
16
+
17
+ > [!NOTE]
18
+ > If neither header is present, no measurement is taken. The request passes through unchanged.
19
+
20
+ ## Installation
21
+
22
+ Add to your Gemfile:
23
+
24
+ ```ruby
25
+ gem "yabeda-rack-queue"
26
+ ```
27
+
28
+ Then run:
29
+
30
+ ```bash
31
+ bundle install
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ Add the middleware to your Rack stack. You also need a Yabeda adapter. For example, use [yabeda-prometheus](https://github.com/yabeda-rb/yabeda-prometheus).
37
+
38
+ ```ruby
39
+ require "yabeda/rack/queue"
40
+ require "yabeda/prometheus"
41
+
42
+ Yabeda.configure!
43
+
44
+ use Yabeda::Rack::Queue::Middleware
45
+ run MyRackApp
46
+ ```
47
+
48
+ For Rails, add it in `config/application.rb`:
49
+
50
+ ```ruby
51
+ config.middleware.use Yabeda::Rack::Queue::Middleware
52
+ ```
53
+
54
+ ## Metric
55
+
56
+ | Name | Group | Type | Unit |
57
+ |------|-------|------|------|
58
+ | `rack_queue_duration` | `rack_queue` | histogram | seconds |
59
+
60
+ Access it in code:
61
+
62
+ ```ruby
63
+ Yabeda.rack_queue.rack_queue_duration
64
+ ```
65
+
66
+ Histogram buckets: 1 ms, 5 ms, 10 ms, 25 ms, 50 ms, 100 ms, 250 ms, 500 ms, 1 s, 2.5 s, 5 s, 10 s, 30 s, 60 s.
67
+
68
+ ## Header formats
69
+
70
+ The middleware checks `X-Request-Start` first. If that header is absent, it tries `X-Queue-Start`.
71
+
72
+ Supported timestamp formats:
73
+
74
+ | Format | Example |
75
+ |--------|---------|
76
+ | Seconds (float) | `1609459200.123` |
77
+ | Milliseconds | `1609459200123` |
78
+ | Microseconds | `1609459200123456` |
79
+ | `t=` prefix | `t=1609459200.123` |
80
+
81
+ The middleware auto-detects the unit. It checks if the number fits a valid recent time.
82
+
83
+ ## Puma adjustment
84
+
85
+ Puma sets `puma.request_body_wait` (in milliseconds) in the Rack env. This records how long Puma spent reading the request body.
86
+
87
+ The middleware subtracts this value from queue time. Without this step, large bodies make queue time appear too long.
88
+
89
+ ## Configuration
90
+
91
+ The middleware accepts these keyword arguments:
92
+
93
+ | Argument | Default | Purpose |
94
+ |----------|---------|---------|
95
+ | `reporter:` | `YabedaReporter.new` | Writes the value to Yabeda. |
96
+ | `parser:` | `HeaderTimestampParser.new` | Parses the header timestamp. |
97
+ | `logger:` | stderr | Gets warning messages. |
98
+ | `clock:` | `Process.clock_gettime(CLOCK_REALTIME)` | Returns current time in seconds. |
99
+
100
+ Example with a custom logger:
101
+
102
+ ```ruby
103
+ use Yabeda::Rack::Queue::Middleware, logger: Rails.logger
104
+ ```
105
+
106
+ ## Requirements
107
+
108
+ - Ruby >= 3.1.
109
+ - yabeda >= 0.14, < 1.0.
110
+ - A Yabeda adapter. For example: [yabeda-prometheus](https://github.com/yabeda-rb/yabeda-prometheus).
111
+
112
+ ## Development
113
+
114
+ Run tests:
115
+
116
+ ```bash
117
+ bundle exec rake test
118
+ ```
119
+
120
+ Run the linter:
121
+
122
+ ```bash
123
+ bundle exec standardrb
124
+ ```
125
+
126
+ ## Contributing
127
+
128
+ Bug reports and pull requests are welcome at <https://github.com/speedshop/yabeda-rack-queue>.
129
+
130
+ ## License
131
+
132
+ MIT. See [LICENSE.txt](LICENSE.txt).
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ task :lint do
7
+ sh "bundle exec standardrb"
8
+ end
9
+
10
+ Rake::TestTask.new(:test) do |test|
11
+ test.libs << "test"
12
+ test.pattern = "test/**/*_test.rb"
13
+ end
14
+
15
+ task default: %i[lint test]
data/SPEC.md ADDED
@@ -0,0 +1,156 @@
1
+ # yabeda-rack-queue Specification
2
+
3
+ ## Purpose
4
+
5
+ This gem measures HTTP request queue time — the duration between when a reverse
6
+ proxy or load balancer first receives a request and when the Ruby application
7
+ begins processing it. It reports this as a Yabeda histogram metric.
8
+
9
+ The gem follows the Rack SPEC and uses Rack env/request-response conventions,
10
+ but it should not declare `rack` as a gem dependency.
11
+
12
+ ## Testing Framework
13
+
14
+ The test suite uses Minitest.
15
+
16
+ ## Linting
17
+
18
+ Linting uses standardrb.
19
+
20
+ ## Metric
21
+
22
+ | Name | Type | Group | Unit | Description |
23
+ |------|------|-------|------|-------------|
24
+ | `rack_queue_duration` | histogram | `rack_queue` | seconds | Time a request waited in the upstream queue before reaching the application |
25
+
26
+ ### Histogram Buckets
27
+
28
+ `[0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60]`
29
+
30
+ No tags are recorded by default.
31
+
32
+ ## Headers
33
+
34
+ The middleware inspects these Rack environment keys, in order:
35
+
36
+ 1. `HTTP_X_REQUEST_START`
37
+ 2. `HTTP_X_QUEUE_START`
38
+
39
+ The first header that yields a valid timestamp is used. If neither header is
40
+ present or parseable, no metric is recorded and the request passes through
41
+ unaffected.
42
+
43
+ ## Middleware Behavior
44
+
45
+ - The middleware always calls the downstream Rack app, regardless of header state.
46
+ - Queue time is computed **before** calling the downstream app (measures time
47
+ *before* application processing, not including it).
48
+ - The middleware does not modify the Rack env or the response in any way.
49
+ - The middleware never raises exceptions due to invalid input.
50
+ - Current time is measured via `Process.clock_gettime(Process::CLOCK_REALTIME)`
51
+ (wall clock, not monotonic — necessary because the header timestamp comes from
52
+ a different process).
53
+
54
+ ## Performance Requirement
55
+
56
+ For a no-op Rack app (for example, one that returns `hello world`), middleware
57
+ throughput MUST exceed `1_000_000` calls/second.
58
+
59
+ This requirement is enforced with a `benchmark-ips` benchmark test.
60
+
61
+ ## Header Value Parsing
62
+
63
+ Header values are parsed for compatibility with common reverse proxies and APM
64
+ agents.
65
+
66
+ 1. First, extract a numeric timestamp token from the header value:
67
+ - `t=<number>` (preferred)
68
+ - or plain `<number>`
69
+ 2. Then normalize units by trying divisors in order:
70
+ - divide by `1_000_000` (microseconds)
71
+ - else divide by `1_000` (milliseconds)
72
+ - else divide by `1` (seconds)
73
+ 3. The first normalized value that is after the minimum acceptable epoch
74
+ (`2000-01-01`) is used.
75
+
76
+ Notes:
77
+
78
+ - This supports seconds, milliseconds, and microseconds (integer or decimal).
79
+ - Leading/trailing whitespace is ignored.
80
+ - If a header contains comma-separated values, use the first value.
81
+
82
+ ### Known Format Examples
83
+
84
+ | Source | Header | Format | Example Value |
85
+ |--------|--------|--------|---------------|
86
+ | Heroku | `X-Request-Start` | milliseconds | `1512379167574` |
87
+ | Nginx | `X-Request-Start` | `t=` seconds.ms | `t=1512379167.574` |
88
+ | Apache | `X-Request-Start` | `t=` microseconds | `t=1570633834463123` |
89
+ | HAProxy (<1.9) | `X-Request-Start` | `t=` integer seconds | `t=1512379167` |
90
+ | F5 | `X-Request-Start` | `t=` milliseconds | `t=1512379167574` |
91
+ | Contour/Envoy | `X-Request-Start` | `t=` seconds.ms | `t=1512379167.574` |
92
+
93
+ ## Validation
94
+
95
+ A parsed timestamp is rejected (treated as absent) if:
96
+
97
+ - The header value contains no extractable numeric portion
98
+ - The resulting timestamp is before 2000-01-01 00:00:00 UTC (epoch 946684800)
99
+ - The resulting timestamp is more than 30 seconds in the future
100
+
101
+ If a timestamp is invalid, we do not record anything. We should not raise any exceptions, it should just be a no-op.
102
+
103
+ ### Negative Queue Time (Clock Skew)
104
+
105
+ If the computed queue time (`now - request_start`) is negative — which can happen
106
+ when the application server's clock is slightly ahead of the load balancer's
107
+ clock — the observation is dropped (no metric is recorded).
108
+
109
+ We'll log a WARN level message in this case.
110
+
111
+ ## Puma Request Body Wait Adjustment
112
+
113
+ If `env["puma.request_body_wait"]` exists, subtract it from the computed queue
114
+ time to avoid counting time spent waiting for slow clients to upload request
115
+ bodies.
116
+
117
+ - `puma.request_body_wait` is interpreted as milliseconds and converted to seconds before subtraction
118
+ - Numeric strings are coerced to float before subtraction
119
+ - Non-numeric or negative values are treated as absent (no subtraction)
120
+ - If subtraction would make queue time negative, clamp to `0.0` seconds
121
+
122
+ ## Parsing Truth Table
123
+
124
+ Assumptions used below:
125
+
126
+ - Minimum acceptable epoch: `946684800` (2000-01-01 UTC)
127
+ - `now = 1700000000` (example current time)
128
+ - Future cutoff = `now + 30` seconds
129
+
130
+ | Header value | Extracted token | Chosen normalization | Parsed timestamp (s) | Result |
131
+ |---|---:|---|---:|---|
132
+ | `t=1512379167.574` | `1512379167.574` | `/1` (seconds) | `1512379167.574` | accepted |
133
+ | `1512379167.574` | `1512379167.574` | `/1` (seconds) | `1512379167.574` | accepted |
134
+ | `t=1512379167574` | `1512379167574` | `/1000` (milliseconds) | `1512379167.574` | accepted |
135
+ | `1512379167574` | `1512379167574` | `/1000` (milliseconds) | `1512379167.574` | accepted |
136
+ | `t=1570633834463123` | `1570633834463123` | `/1000000` (microseconds) | `1570633834.463123` | accepted |
137
+ | `1570633834463123` | `1570633834463123` | `/1000000` (microseconds) | `1570633834.463123` | accepted |
138
+ | `t=1512379167` | `1512379167` | `/1` (seconds) | `1512379167` | accepted |
139
+ | `1512379167` | `1512379167` | `/1` (seconds) | `1512379167` | accepted |
140
+ | ` t=1512379167.574 ` | `1512379167.574` | `/1` (seconds) | `1512379167.574` | accepted (whitespace ignored) |
141
+ | `t=1512379167.574, t=1512379168.000` | `1512379167.574` (first value) | `/1` (seconds) | `1512379167.574` | accepted |
142
+ | `invalid` | _none_ | — | — | rejected (no numeric token) |
143
+ | `t=` | _none_ | — | — | rejected (empty token) |
144
+ | `t=0` | `0` | none pass minimum epoch | — | rejected (too old) |
145
+ | `t=915148800` | `915148800` | none pass minimum epoch | — | rejected (too old) |
146
+ | `t=1700000035` | `1700000035` | `/1` (seconds) | `1700000035` | rejected (more than 30s in future for assumed `now`) |
147
+
148
+ ### Queue time post-processing examples
149
+
150
+ | Parsed `request_start` | `now` | Raw queue time (s) | `puma.request_body_wait` | Final outcome |
151
+ |---:|---:|---:|---:|---|
152
+ | `1699999999.900` | `1700000000.000` | `0.100` | _absent_ | `0.100` |
153
+ | `1699999999.900` | `1700000000.000` | `0.100` | `40` (ms) | `0.060` |
154
+ | `1699999999.900` | `1700000000.000` | `0.100` | `"40"` (ms) | `0.060` |
155
+ | `1700000000.050` | `1700000000.000` | `-0.050` | _absent_ | dropped (clock skew, WARN logged) |
156
+ | `1699999999.900` | `1700000000.000` | `0.100` | `200` (ms) | `0.000` (post-subtraction clamp) |
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yabeda
4
+ module Rack
5
+ module Queue
6
+ class HeaderTimestampParser
7
+ MIN_EPOCH = Time.utc(2000, 1, 1).to_f
8
+ FUTURE_TOLERANCE = 30.0
9
+ DIVISORS = [1_000_000.0, 1_000.0, 1.0].freeze
10
+ NUMBER_RE = /[+-]?(?:\d+(?:\.\d+)?|\.\d+)/
11
+ T_EQUALS_RE = /t\s*=\s*(#{NUMBER_RE.source})/i
12
+
13
+ def parse(value, now:)
14
+ first = value.to_s.split(",", 2).first.to_s.strip
15
+ return if first.empty?
16
+
17
+ token = first[T_EQUALS_RE, 1] || first[NUMBER_RE, 0]
18
+ normalize(Float(token), now) if token
19
+ rescue ArgumentError, TypeError
20
+ end
21
+
22
+ private
23
+
24
+ def normalize(raw, now)
25
+ max = now + FUTURE_TOLERANCE
26
+ divisor = DIVISORS.find { |d| (raw / d).between?(MIN_EPOCH, max) }
27
+ raw / divisor if divisor
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yabeda"
4
+
5
+ Yabeda.configure do
6
+ group :rack_queue do
7
+ histogram :rack_queue_duration,
8
+ comment: "Time a request waited in the upstream queue before reaching the application",
9
+ unit: :seconds,
10
+ buckets: [0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60]
11
+ end
12
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yabeda
4
+ module Rack
5
+ module Queue
6
+ class Middleware
7
+ class StderrLogger
8
+ def warn(message) = Kernel.warn(message)
9
+ end
10
+
11
+ class YabedaReporter
12
+ def observe(value) = Yabeda.rack_queue.rack_queue_duration.measure({}, value)
13
+ end
14
+
15
+ def initialize(app, reporter: YabedaReporter.new, logger: nil, clock: nil)
16
+ @app = app
17
+ @reporter = reporter
18
+ @parser = HeaderTimestampParser.new
19
+ @logger = logger || StderrLogger.new
20
+ @clock = clock || -> { Process.clock_gettime(Process::CLOCK_REALTIME) }
21
+ end
22
+
23
+ def call(env)
24
+ measure_queue_time(env) if env["HTTP_X_REQUEST_START"] || env["HTTP_X_QUEUE_START"]
25
+ @app.call(env)
26
+ end
27
+
28
+ private
29
+
30
+ def measure_queue_time(env)
31
+ now = @clock.call
32
+ start = @parser.parse(env["HTTP_X_REQUEST_START"], now: now) ||
33
+ @parser.parse(env["HTTP_X_QUEUE_START"], now: now)
34
+ report_queue_time(env, now, start) if start
35
+ end
36
+
37
+ def report_queue_time(env, now, request_start)
38
+ queue_time = now - request_start
39
+ return @logger.warn("Negative rack queue duration (#{queue_time}); dropping") if queue_time.negative?
40
+
41
+ body_wait = parse_body_wait(env["puma.request_body_wait"])
42
+ @reporter.observe([queue_time - (body_wait || 0), 0.0].max)
43
+ end
44
+
45
+ def parse_body_wait(value)
46
+ ms = Float(value)
47
+ ms / 1_000.0 unless ms.negative?
48
+ rescue ArgumentError, TypeError
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yabeda
4
+ module Rack
5
+ module Queue
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "queue/version"
4
+ require_relative "queue/metric"
5
+ require_relative "queue/header_timestamp_parser"
6
+ require_relative "queue/middleware"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yabeda/rack/queue"
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require "net/http"
5
+ require "puma"
6
+ require "socket"
7
+ require "timeout"
8
+
9
+ class PumaServerHarness
10
+ attr_reader :port
11
+
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def start
17
+ @server = Puma::Server.new(@app, nil, min_threads: 0, max_threads: 4)
18
+ @server.add_tcp_listener("127.0.0.1", 0)
19
+ @port = @server.connected_ports.first
20
+ @server.run(true, thread_name: "puma-e2e")
21
+ wait_until_ready
22
+ end
23
+
24
+ def stop
25
+ @server&.stop(true)
26
+ end
27
+
28
+ private
29
+
30
+ def wait_until_ready
31
+ Timeout.timeout(5) do
32
+ loop do
33
+ socket = TCPSocket.new("127.0.0.1", port)
34
+ socket.close
35
+ break
36
+ rescue Errno::ECONNREFUSED
37
+ sleep 0.01
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ class PumaIntegrationTest < Minitest::Test
44
+ def setup
45
+ super
46
+ rack_app = Yabeda::Rack::Queue::Middleware.new(
47
+ ->(_env) { [200, {"content-type" => "text/plain"}, ["ok"]] }
48
+ )
49
+ @server = PumaServerHarness.new(rack_app)
50
+ @server.start
51
+ end
52
+
53
+ def teardown
54
+ @server&.stop
55
+ super
56
+ end
57
+
58
+ def test_records_rack_queue_duration_histogram_via_yabeda_on_real_http_request
59
+ requested_queue_time_seconds = 0.12
60
+ request_start_ms = ((Time.now.to_f - requested_queue_time_seconds) * 1_000).to_i
61
+ uri = URI("http://127.0.0.1:#{@server.port}/")
62
+ request = Net::HTTP::Get.new(uri)
63
+ request["X-Request-Start"] = request_start_ms.to_s
64
+
65
+ response = Net::HTTP.start(uri.host, uri.port) { |http| http.request(request) }
66
+
67
+ assert_equal "200", response.code
68
+
69
+ metric = Yabeda.rack_queue.rack_queue_duration
70
+ measured = Yabeda::TestAdapter.instance.histograms.fetch(metric).fetch({})
71
+
72
+ assert_kind_of Float, measured
73
+ assert_operator measured, :>=, requested_queue_time_seconds
74
+ end
75
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "minitest/autorun"
5
+ require "yabeda/test_adapter"
6
+ require "yabeda/rack/queue"
7
+
8
+ Yabeda.register_adapter(:test, Yabeda::TestAdapter.instance)
9
+ Yabeda.configure! unless Yabeda.configured?
10
+
11
+ class Minitest::Test
12
+ def setup
13
+ super
14
+ Yabeda::TestAdapter.instance.reset!
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class GemspecTest < Minitest::Test
6
+ def test_does_not_declare_rack_runtime_dependency
7
+ gemspec_path = File.expand_path("../../../../yabeda-rack-queue.gemspec", __dir__)
8
+ spec = Gem::Specification.load(gemspec_path)
9
+
10
+ refute_nil spec
11
+ assert_nil spec.runtime_dependencies.find { |dependency| dependency.name == "rack" }
12
+ end
13
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class HeaderTimestampParserTest < Minitest::Test
6
+ def setup
7
+ super
8
+ @parser = Yabeda::Rack::Queue::HeaderTimestampParser.new
9
+ @now = 1_700_000_000.0
10
+ end
11
+
12
+ def test_accepts_known_valid_values_from_truth_table
13
+ expectations = {
14
+ "t=1512379167.574" => 1_512_379_167.574,
15
+ "1512379167.574" => 1_512_379_167.574,
16
+ "t=1512379167574" => 1_512_379_167.574,
17
+ "1512379167574" => 1_512_379_167.574,
18
+ "t=1570633834463123" => 1_570_633_834.463123,
19
+ "1570633834463123" => 1_570_633_834.463123,
20
+ "t=1512379167" => 1_512_379_167.0,
21
+ "1512379167" => 1_512_379_167.0,
22
+ " t=1512379167.574 " => 1_512_379_167.574,
23
+ "t=1512379167.574, t=1512379168.000" => 1_512_379_167.574
24
+ }
25
+
26
+ expectations.each do |header_value, expected|
27
+ actual = @parser.parse(header_value, now: @now)
28
+ assert_in_delta expected, actual, 1e-9, header_value
29
+ end
30
+ end
31
+
32
+ def test_rejects_known_invalid_values_from_truth_table
33
+ ["invalid", "t=", "t=0", "t=915148800", "t=1700000035"].each do |header_value|
34
+ assert_nil @parser.parse(header_value, now: @now), header_value
35
+ end
36
+ end
37
+
38
+ def test_prefers_t_equals_token_over_plain_token_when_both_are_present
39
+ value = @parser.parse("1512370000 t=1512379167.574", now: @now)
40
+ assert_in_delta 1_512_379_167.574, value, 1e-9
41
+ end
42
+
43
+ def test_returns_nil_for_non_string_values_that_cannot_be_parsed
44
+ assert_nil @parser.parse(nil, now: @now)
45
+ assert_nil @parser.parse(Object.new, now: @now)
46
+ end
47
+
48
+ def test_rejects_values_more_than_30_seconds_in_the_future
49
+ header_value = "t=#{@now + 30.001}"
50
+ assert_nil @parser.parse(header_value, now: @now)
51
+ end
52
+
53
+ def test_accepts_values_exactly_30_seconds_in_the_future
54
+ header_value = "t=#{@now + 30.0}"
55
+ assert_in_delta @now + 30.0, @parser.parse(header_value, now: @now), 1e-9
56
+ end
57
+
58
+ def test_uses_only_first_comma_separated_value
59
+ assert_nil @parser.parse("invalid, t=1512379167.574", now: @now)
60
+ end
61
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class MetricTest < Minitest::Test
6
+ def test_registers_rack_queue_duration_histogram_with_required_metadata
7
+ metric = Yabeda.rack_queue.rack_queue_duration
8
+
9
+ assert_instance_of Yabeda::Histogram, metric
10
+ assert_equal :rack_queue, metric.group
11
+ assert_equal :seconds, metric.unit
12
+ assert_equal "Time a request waited in the upstream queue before reaching the application", metric.comment
13
+ assert_equal [], metric.tags
14
+ assert_equal [0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60], metric.buckets
15
+ end
16
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require "benchmark/ips"
5
+
6
+ class MiddlewarePerformanceTest < Minitest::Test
7
+ MINIMUM_IPS = 1_000_000.0
8
+
9
+ def test_processes_noop_rack_app_above_one_million_calls_per_second
10
+ response = [200, {"content-type" => "text/plain"}, ["hello world"]].freeze
11
+ app = ->(_env) { response }
12
+ middleware = Yabeda::Rack::Queue::Middleware.new(app)
13
+ env = {}.freeze
14
+
15
+ report = Benchmark.ips do |x|
16
+ x.config(time: 1, warmup: 0.5)
17
+ x.report("middleware noop call") { middleware.call(env) }
18
+ end
19
+
20
+ observed_ips = report.entries.fetch(0).ips
21
+ assert_operator observed_ips, :>, MINIMUM_IPS
22
+ end
23
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class CapturingReporter
6
+ attr_reader :values
7
+
8
+ def initialize
9
+ @values = []
10
+ end
11
+
12
+ def observe(value)
13
+ @values << value
14
+ end
15
+ end
16
+
17
+ class CapturingLogger
18
+ attr_reader :warnings
19
+
20
+ def initialize
21
+ @warnings = []
22
+ end
23
+
24
+ def warn(message)
25
+ @warnings << message
26
+ end
27
+ end
28
+
29
+ class AppSpy
30
+ attr_reader :called_count, :last_env
31
+
32
+ def initialize(response)
33
+ @response = response
34
+ @called_count = 0
35
+ end
36
+
37
+ def call(env)
38
+ @called_count += 1
39
+ @last_env = env
40
+ @response
41
+ end
42
+ end
43
+
44
+ class MiddlewareTest < Minitest::Test
45
+ def setup
46
+ super
47
+ @response = [201, {"content-type" => "text/plain"}, ["ok"]]
48
+ @app = AppSpy.new(@response)
49
+ @reporter = CapturingReporter.new
50
+ @now = 1_700_000_000.0
51
+ @clock = -> { @now }
52
+ @logger = CapturingLogger.new
53
+ @middleware = Yabeda::Rack::Queue::Middleware.new(
54
+ @app,
55
+ reporter: @reporter,
56
+ clock: @clock,
57
+ logger: @logger
58
+ )
59
+ end
60
+
61
+ def test_always_calls_downstream_app_and_returns_response_unchanged
62
+ env = {}
63
+
64
+ result = @middleware.call(env)
65
+
66
+ assert_same @response, result
67
+ assert_equal 1, @app.called_count
68
+ assert_same env, @app.last_env
69
+ end
70
+
71
+ def test_does_not_mutate_rack_env
72
+ env = {"HTTP_X_REQUEST_START" => "t=1699999999.9", "custom.key" => "value"}
73
+ original = env.dup
74
+
75
+ @middleware.call(env)
76
+
77
+ assert_equal original, env
78
+ end
79
+
80
+ def test_records_nothing_when_neither_header_is_present
81
+ @middleware.call({})
82
+
83
+ assert_empty @reporter.values
84
+ end
85
+
86
+ def test_uses_x_request_start_before_x_queue_start_when_both_are_valid
87
+ env = {
88
+ "HTTP_X_REQUEST_START" => "t=1699999999.9",
89
+ "HTTP_X_QUEUE_START" => "t=1699999999.8"
90
+ }
91
+
92
+ @middleware.call(env)
93
+
94
+ assert_in_delta 0.1, @reporter.values.last, 1e-4
95
+ end
96
+
97
+ def test_falls_back_to_x_queue_start_when_x_request_start_is_invalid
98
+ env = {
99
+ "HTTP_X_REQUEST_START" => "invalid",
100
+ "HTTP_X_QUEUE_START" => "t=1699999999.9"
101
+ }
102
+
103
+ @middleware.call(env)
104
+
105
+ assert_in_delta 0.1, @reporter.values.last, 1e-4
106
+ end
107
+
108
+ def test_computes_queue_time_before_calling_downstream_app
109
+ sleeping_app = lambda do |_env|
110
+ sleep 0.05
111
+ @response
112
+ end
113
+ test_middleware = Yabeda::Rack::Queue::Middleware.new(
114
+ sleeping_app,
115
+ reporter: @reporter,
116
+ clock: @clock,
117
+ logger: @logger
118
+ )
119
+
120
+ test_middleware.call("HTTP_X_REQUEST_START" => "t=1699999999.9")
121
+
122
+ assert_in_delta 0.1, @reporter.values.last, 1e-4
123
+ end
124
+
125
+ def test_uses_process_clock_gettime_realtime_by_default
126
+ middleware = Yabeda::Rack::Queue::Middleware.new(@app, reporter: @reporter, logger: @logger)
127
+ observed_clock_ids = []
128
+ clock_gettime_stub = lambda do |clock_id|
129
+ observed_clock_ids << clock_id
130
+ @now
131
+ end
132
+
133
+ Process.stub(:clock_gettime, clock_gettime_stub) do
134
+ middleware.call("HTTP_X_REQUEST_START" => "t=1699999999.9")
135
+ end
136
+
137
+ refute_empty @reporter.values
138
+ assert_equal [Process::CLOCK_REALTIME], observed_clock_ids
139
+ end
140
+
141
+ def test_never_raises_on_invalid_header_values
142
+ @middleware.call("HTTP_X_REQUEST_START" => Object.new, "HTTP_X_QUEUE_START" => "")
143
+ assert_equal 1, @app.called_count
144
+ assert_empty @reporter.values
145
+ end
146
+
147
+ def test_drops_negative_queue_times_and_logs_warning
148
+ @middleware.call("HTTP_X_REQUEST_START" => "t=1700000000.1")
149
+
150
+ assert_equal 1, @app.called_count
151
+ assert_empty @reporter.values
152
+ assert_includes @logger.warnings.join("\n"), "Negative rack queue duration"
153
+ end
154
+
155
+ def test_subtracts_puma_request_body_wait_milliseconds_from_queue_time
156
+ @middleware.call(
157
+ "HTTP_X_REQUEST_START" => "t=1699999999.9",
158
+ "puma.request_body_wait" => 40
159
+ )
160
+
161
+ assert_in_delta 0.06, @reporter.values.last, 1e-4
162
+ end
163
+
164
+ def test_coerces_string_puma_request_body_wait_values
165
+ @middleware.call(
166
+ "HTTP_X_REQUEST_START" => "t=1699999999.9",
167
+ "puma.request_body_wait" => "40"
168
+ )
169
+
170
+ assert_in_delta 0.06, @reporter.values.last, 1e-4
171
+ end
172
+
173
+ def test_ignores_non_numeric_puma_request_body_wait_values
174
+ @middleware.call(
175
+ "HTTP_X_REQUEST_START" => "t=1699999999.9",
176
+ "puma.request_body_wait" => "not-a-number"
177
+ )
178
+
179
+ assert_in_delta 0.1, @reporter.values.last, 1e-4
180
+ end
181
+
182
+ def test_ignores_negative_puma_request_body_wait_values
183
+ @middleware.call(
184
+ "HTTP_X_REQUEST_START" => "t=1699999999.9",
185
+ "puma.request_body_wait" => -40
186
+ )
187
+
188
+ assert_in_delta 0.1, @reporter.values.last, 1e-4
189
+ end
190
+
191
+ def test_clamps_to_zero_after_puma_request_body_wait_subtraction
192
+ @middleware.call(
193
+ "HTTP_X_REQUEST_START" => "t=1699999999.9",
194
+ "puma.request_body_wait" => 200
195
+ )
196
+
197
+ assert_equal 0.0, @reporter.values.last
198
+ end
199
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/yabeda/rack/queue/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "yabeda-rack-queue"
7
+ spec.version = Yabeda::Rack::Queue::VERSION
8
+ spec.authors = ["Nate Berkopec"]
9
+ spec.email = ["nate.berkopec@speedshop.co"]
10
+
11
+ spec.summary = "Yabeda middleware for HTTP request queue duration"
12
+ spec.description = <<~DESCRIPTION
13
+ Rack middleware that measures HTTP request queue duration from upstream
14
+ headers and reports it to Yabeda as a histogram metric.
15
+ DESCRIPTION
16
+ spec.homepage = "https://github.com/speedshop/yabeda-rack-queue"
17
+ spec.license = "MIT"
18
+ spec.required_ruby_version = ">= 3.1"
19
+
20
+ spec.metadata = {
21
+ "bug_tracker_uri" => "https://github.com/speedshop/yabeda-rack-queue/issues",
22
+ "changelog_uri" => "https://github.com/speedshop/yabeda-rack-queue/releases",
23
+ "homepage_uri" => spec.homepage,
24
+ "source_code_uri" => "https://github.com/speedshop/yabeda-rack-queue",
25
+ "rubygems_mfa_required" => "true"
26
+ }
27
+
28
+ spec.files = Dir.chdir(__dir__) do
29
+ `git ls-files -z`.split("\x0").reject do |file|
30
+ file.start_with?(".github/", ".pi/")
31
+ end
32
+ end
33
+ spec.require_paths = ["lib"]
34
+
35
+ spec.add_dependency "yabeda", ">= 0.14", "< 1.0"
36
+
37
+ spec.add_development_dependency "puma", ">= 6", "< 8"
38
+ spec.add_development_dependency "rake", ">= 13.0"
39
+ spec.add_development_dependency "minitest", ">= 5.22", "< 6.0"
40
+ spec.add_development_dependency "benchmark", ">= 0.4", "< 1.0"
41
+ spec.add_development_dependency "benchmark-ips", ">= 2.14", "< 3.0"
42
+ spec.add_development_dependency "standard", "~> 1.44"
43
+ end
metadata ADDED
@@ -0,0 +1,196 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yabeda-rack-queue
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nate Berkopec
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: yabeda
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0.14'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '1.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '0.14'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '1.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: puma
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '6'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '8'
42
+ type: :development
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '6'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '8'
52
+ - !ruby/object:Gem::Dependency
53
+ name: rake
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '13.0'
59
+ type: :development
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '13.0'
66
+ - !ruby/object:Gem::Dependency
67
+ name: minitest
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '5.22'
73
+ - - "<"
74
+ - !ruby/object:Gem::Version
75
+ version: '6.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '5.22'
83
+ - - "<"
84
+ - !ruby/object:Gem::Version
85
+ version: '6.0'
86
+ - !ruby/object:Gem::Dependency
87
+ name: benchmark
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0.4'
93
+ - - "<"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0.4'
103
+ - - "<"
104
+ - !ruby/object:Gem::Version
105
+ version: '1.0'
106
+ - !ruby/object:Gem::Dependency
107
+ name: benchmark-ips
108
+ requirement: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '2.14'
113
+ - - "<"
114
+ - !ruby/object:Gem::Version
115
+ version: '3.0'
116
+ type: :development
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '2.14'
123
+ - - "<"
124
+ - !ruby/object:Gem::Version
125
+ version: '3.0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: standard
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - "~>"
131
+ - !ruby/object:Gem::Version
132
+ version: '1.44'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: '1.44'
140
+ description: |
141
+ Rack middleware that measures HTTP request queue duration from upstream
142
+ headers and reports it to Yabeda as a histogram metric.
143
+ email:
144
+ - nate.berkopec@speedshop.co
145
+ executables: []
146
+ extensions: []
147
+ extra_rdoc_files: []
148
+ files:
149
+ - ".gitignore"
150
+ - Gemfile
151
+ - Gemfile.lock
152
+ - LICENSE.txt
153
+ - README.md
154
+ - Rakefile
155
+ - SPEC.md
156
+ - lib/yabeda-rack-queue.rb
157
+ - lib/yabeda/rack/queue.rb
158
+ - lib/yabeda/rack/queue/header_timestamp_parser.rb
159
+ - lib/yabeda/rack/queue/metric.rb
160
+ - lib/yabeda/rack/queue/middleware.rb
161
+ - lib/yabeda/rack/queue/version.rb
162
+ - test/e2e/puma_integration_test.rb
163
+ - test/test_helper.rb
164
+ - test/yabeda/rack/queue/gemspec_test.rb
165
+ - test/yabeda/rack/queue/header_timestamp_parser_test.rb
166
+ - test/yabeda/rack/queue/metric_test.rb
167
+ - test/yabeda/rack/queue/middleware_performance_test.rb
168
+ - test/yabeda/rack/queue/middleware_test.rb
169
+ - yabeda-rack-queue.gemspec
170
+ homepage: https://github.com/speedshop/yabeda-rack-queue
171
+ licenses:
172
+ - MIT
173
+ metadata:
174
+ bug_tracker_uri: https://github.com/speedshop/yabeda-rack-queue/issues
175
+ changelog_uri: https://github.com/speedshop/yabeda-rack-queue/releases
176
+ homepage_uri: https://github.com/speedshop/yabeda-rack-queue
177
+ source_code_uri: https://github.com/speedshop/yabeda-rack-queue
178
+ rubygems_mfa_required: 'true'
179
+ rdoc_options: []
180
+ require_paths:
181
+ - lib
182
+ required_ruby_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '3.1'
187
+ required_rubygems_version: !ruby/object:Gem::Requirement
188
+ requirements:
189
+ - - ">="
190
+ - !ruby/object:Gem::Version
191
+ version: '0'
192
+ requirements: []
193
+ rubygems_version: 4.0.3
194
+ specification_version: 4
195
+ summary: Yabeda middleware for HTTP request queue duration
196
+ test_files: []