Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: CI

on:
push:
branches: ["**"]
pull_request:
workflow_dispatch:

jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.1"
bundler-cache: true

- name: Run linter
run: bundle exec standardrb

test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ruby: ["3.1", "3.4", "4.0"]
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true

- name: Run tests
run: bundle exec rake test

package:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "4.0"
bundler-cache: true

- name: Verify gem can be built
run: bundle exec rake build
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/.bundle/
/pkg/
*.gem
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
source "https://rubygems.org"

gemspec
114 changes: 114 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
PATH
remote: .
specs:
yabeda-rack-queue (0.1.0)
yabeda (>= 0.14, < 1.0)

GEM
remote: https://rubygems.org/
specs:
anyway_config (2.8.0)
ruby-next-core (~> 1.0)
ast (2.4.3)
concurrent-ruby (1.3.6)
dry-initializer (3.2.0)
json (2.18.1)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
minitest (5.27.0)
nio4r (2.7.5)
parallel (1.27.0)
parser (3.3.10.2)
ast (~> 2.4.1)
racc
prism (1.9.0)
puma (7.2.0)
nio4r (~> 2.0)
racc (1.8.1)
rainbow (3.1.1)
rake (13.3.1)
regexp_parser (2.11.3)
rubocop (1.84.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.0)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-performance (1.26.1)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0)
ruby-next-core (1.2.0)
ruby-progressbar (1.13.0)
standard (1.54.0)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.84.0)
standard-custom (~> 1.0.0)
standard-performance (~> 1.8)
standard-custom (1.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.50)
standard-performance (1.9.0)
lint_roller (~> 1.1)
rubocop-performance (~> 1.26.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
yabeda (0.14.0)
anyway_config (>= 1.0, < 3)
concurrent-ruby
dry-initializer

PLATFORMS
arm64-darwin-24
ruby

DEPENDENCIES
minitest (>= 5.22, < 6.0)
puma (>= 6, < 8)
rake (>= 13.0)
standard (~> 1.44)
yabeda-rack-queue!

CHECKSUMS
anyway_config (2.8.0) sha256=f6797a7231f81202dcd3d0c07284e836e45713e761d320180348b13a5c7c9306
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
dry-initializer (3.2.0) sha256=37d59798f912dc0a1efe14a4db4a9306989007b302dcd5f25d0a2a20c166c4e3
json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
puma (7.2.0) sha256=bf8ef4ab514a4e6d4554cb4326b2004eba5036ae05cf765cfe51aba9706a72a8
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
rubocop (1.84.2) sha256=5692cea54168f3dc8cb79a6fe95c5424b7ea893c707ad7a4307b0585e88dbf5f
rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd
rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834
ruby-next-core (1.2.0) sha256=f6a7d00bb5186cecbb02f7f1845a0f3a2c9788d35b6ccff5c9be3f0d46799b86
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
standard (1.54.0) sha256=7a4b08f83d9893083c8f03bc486f0feeb6a84d48233b40829c03ef4767ea0100
standard-custom (1.0.2) sha256=424adc84179a074f1a2a309bb9cf7cd6bfdb2b6541f20c6bf9436c0ba22a652b
standard-performance (1.9.0) sha256=49483d31be448292951d80e5e67cdcb576c2502103c7b40aec6f1b6e9c88e3f2
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
yabeda (0.14.0) sha256=bc517bf22d692ebd80a29fc9fd2246c257aaf92d10b2735a775e2419351a43bf
yabeda-rack-queue (0.1.0)

BUNDLED WITH
4.0.3
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# yabeda-rack-queue

Rack middleware that measures HTTP request queue time and reports it to
[Yabeda core](https://github.com/yabeda-rb/yabeda) as a histogram.

## Features

- Reports upstream queue wait time before your app starts handling the request
- Reads common queue headers (`X-Request-Start`, `X-Queue-Start`)
- Exposes `rack_queue.rack_queue_duration` (seconds)
- Supports Puma request body wait adjustment (`puma.request_body_wait`)

## Installation

Add the gem:

```ruby
gem "yabeda-rack-queue"
```

Then install dependencies:

```bash
bundle install
```

## Quickstart

```ruby
require "yabeda/rack/queue"
require "yabeda/prometheus"

Yabeda.configure!

use Yabeda::Rack::Queue::Middleware
run MyRackApp
```

Send a request with an upstream queue header (for example `X-Request-Start`) and
the middleware will record `rack_queue.rack_queue_duration`.

## Metric

- Name: `rack_queue_duration`
- Group: `rack_queue`
- Type: histogram
- Unit: seconds

## Development

Run tests:

```bash
bundle exec rake test
```

Run lint:

```bash
bundle exec standardrb
```

## Contributing

Issues and pull requests are welcome.

## License

MIT, see [LICENSE.txt](LICENSE.txt).
11 changes: 11 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

require "bundler/gem_tasks"
require "rake/testtask"

Rake::TestTask.new(:test) do |test|
test.libs << "test"
test.pattern = "test/**/*_test.rb"
end

task default: :test
4 changes: 3 additions & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ This gem measures HTTP request queue time — the duration between when a revers
proxy or load balancer first receives a request and when the Ruby application
begins processing it. It reports this as a Yabeda histogram metric.

The gem follows the Rack SPEC and uses Rack env/request-response conventions,
but it should not declare `rack` as a gem dependency.

## Metric

| Name | Type | Group | Unit | Description |
Expand Down Expand Up @@ -136,4 +139,3 @@ Assumptions used below:
| `1699999999.900` | `1700000000.000` | `0.100` | `"40"` (ms) | `0.060` |
| `1700000000.050` | `1700000000.000` | `-0.050` | _absent_ | dropped (clock skew, WARN logged) |
| `1699999999.900` | `1700000000.000` | `0.100` | `200` (ms) | `0.000` (post-subtraction clamp) |

3 changes: 3 additions & 0 deletions lib/yabeda-rack-queue.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# frozen_string_literal: true

require "yabeda/rack/queue"
6 changes: 6 additions & 0 deletions lib/yabeda/rack/queue.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

require_relative "queue/version"
require_relative "queue/metric"
require_relative "queue/header_timestamp_parser"
require_relative "queue/middleware"
51 changes: 51 additions & 0 deletions lib/yabeda/rack/queue/header_timestamp_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

module Yabeda
module Rack
module Queue
class HeaderTimestampParser
MIN_EPOCH_SECONDS = Time.utc(2000, 1, 1).to_f
FUTURE_TOLERANCE_SECONDS = 30.0
NORMALIZATION_DIVISORS = [1_000_000.0, 1_000.0, 1.0].freeze
NUMBER_PATTERN = /[+-]?(?:\d+(?:\.\d+)?|\.\d+)/
T_EQUALS_PATTERN = /t\s*=\s*(#{NUMBER_PATTERN.source})/i

def parse(value, now:)
first_value = first_header_value(value)
return nil if first_value.empty?

token = extract_numeric_token(first_value)
return nil if token.nil?

normalize(Float(token), now)
rescue ArgumentError, TypeError
nil
end

private

def first_header_value(value)
value.to_s.split(",", 2).first.to_s.strip
end

def extract_numeric_token(value)
value[T_EQUALS_PATTERN, 1] || value[NUMBER_PATTERN, 0]
end

def normalize(raw_timestamp, now)
max_allowed = now + FUTURE_TOLERANCE_SECONDS

NORMALIZATION_DIVISORS.each do |divisor|
candidate = raw_timestamp / divisor
next if candidate < MIN_EPOCH_SECONDS
next if candidate > max_allowed

return candidate
end

nil
end
end
end
end
end
27 changes: 27 additions & 0 deletions lib/yabeda/rack/queue/metric.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

require "yabeda"

module Yabeda
module Rack
module Queue
HISTOGRAM_BUCKETS = [
0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60
].freeze

METRIC_NAME = :rack_queue_duration
METRIC_GROUP = :rack_queue
METRIC_UNIT = :seconds
METRIC_DESCRIPTION = "Time a request waited in the upstream queue before reaching the application"
end
end
end

Yabeda.configure do
group Yabeda::Rack::Queue::METRIC_GROUP do
histogram Yabeda::Rack::Queue::METRIC_NAME,
comment: Yabeda::Rack::Queue::METRIC_DESCRIPTION,
unit: Yabeda::Rack::Queue::METRIC_UNIT,
buckets: Yabeda::Rack::Queue::HISTOGRAM_BUCKETS
end
end
Loading