puma_metrics_engine 1.0.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 711453f75e30a751aaedaa85d58133fb0c0b5e5d4819339503f10af88bdbd964
4
- data.tar.gz: 291c8e013fd863f42b6adb83b95b6c38904de5402442902c3fe65c53f8fd231f
3
+ metadata.gz: 248a19d8e7fb9d4392b1f4c4bf3cfa13d8e4872426d843f171732264783eaf94
4
+ data.tar.gz: b09efaf7ede10a759b435b8be828f0dbebde7f54ece70efddb050180e474ac4a
5
5
  SHA512:
6
- metadata.gz: aaa668f89f9931652c7682ba0c45390bc4a37dde9fe8e9d692549a8ee246cbfb92128cdbfcfffd5b22015b0f19e599b9dafe59abf89103ee9e07ab94bf353f26
7
- data.tar.gz: afe36aab4715936a485b54279c803ed4dc6d8fe8027bdbff9bb487e5bac547bae3db37c1784c37080ae93a19478bf92f4251f836b934e395f40773b124db4ff7
6
+ metadata.gz: 4bbfb1639f3157a75afc8bbc538eecfd52492c05240be8d96576884ea2448cd6d8598b16e0a87c8fd2a93153b0b051e186842083e18bb874b8c5bb14bb17813c
7
+ data.tar.gz: 006066e431c7c1d7fed9cfb6c19b17715e2e7437a09ef23b8962838e33e063299a62ce7b47d6c6d535ebe535032ed8abd840f3851cf7e0505fab11bc023a041e
data/LICENSE.txt CHANGED
@@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
-
data/README.md CHANGED
@@ -1,22 +1,16 @@
1
1
  # PumaMetricsEngine
2
2
 
3
- A Rails engine gem that provides a `/matrix` endpoint for Puma metrics and queue time statistics.
3
+ A Rails engine that provides a `/matrix` endpoint for Puma metrics and queue time statistics.
4
4
 
5
5
  ## Installation
6
6
 
7
- Add this line to your application's Gemfile:
7
+ Add to your Gemfile:
8
8
 
9
9
  ```ruby
10
- gem "puma_metrics_engine", path: "../puma_metrics_engine" # For local development
11
- # Or from a gem server:
12
- # gem "puma_metrics_engine"
10
+ gem "puma_metrics_engine"
13
11
  ```
14
12
 
15
- And then execute:
16
-
17
- ```bash
18
- $ bundle install
19
- ```
13
+ Then run `bundle install`.
20
14
 
21
15
  ## Usage
22
16
 
@@ -26,19 +20,12 @@ Mount the engine in your routes:
26
20
  # config/routes.rb
27
21
  Rails.application.routes.draw do
28
22
  mount PumaMetricsEngine::Engine, at: "/"
29
- # ... other routes
30
23
  end
31
24
  ```
32
25
 
33
- The engine automatically provides a `/matrix` endpoint that returns JSON with:
34
-
35
- - `timestamp`: Current timestamp
36
- - `queue_time_ms`: Aggregate queue time statistics (avg, p50, p95, p99, max, min, sample_count)
37
- - `queue_time_windows`: Queue time averages for 10s, 20s, and 30s windows
38
- - `requests_per_minute`: Request count in the last minute
39
- - `puma`: Puma server statistics (workers, threads, backlog, etc.)
26
+ Access metrics at `/matrix` endpoint.
40
27
 
41
- ## Example Response
28
+ ## Response Format
42
29
 
43
30
  ```json
44
31
  {
@@ -57,65 +44,19 @@ The engine automatically provides a `/matrix` endpoint that returns JSON with:
57
44
  "20s": { "avg": 12.5, "sample_count": 100 },
58
45
  "30s": { "avg": 12.4, "sample_count": 150 }
59
46
  },
60
- "requests_per_minute": {
61
- "count": 120,
62
- "window_seconds": 60
63
- },
64
- "puma": {
65
- "workers": 2,
66
- "phase": 0,
67
- "booted_workers": 2,
68
- "backlog": 0,
69
- "running": 10,
70
- "pool_capacity": 20,
71
- "max_threads": 10
72
- }
47
+ "requests_per_minute": { "count": 120, "window_seconds": 60 },
48
+ "puma": { "workers": 2, "backlog": 0, "running": 10, ... }
73
49
  }
74
50
  ```
75
51
 
76
- ## Dependencies
52
+ ## Requirements
77
53
 
78
54
  - **Rails** >= 7.0
79
- - **Redis** >= 5.0 (for storing queue time and request data)
80
- - **Puma** (optional, for Puma-specific stats)
81
-
82
- ## Configuration
83
-
84
- The engine uses the following Redis keys (shared with ApplicationController if tracking is enabled):
85
- - `puma:request_timestamps` - Request timestamps
86
- - `puma:queue_times` - Queue time data
87
-
88
- The engine reads from the same Redis keys that `ApplicationController` writes to (if you have queue time tracking enabled in your ApplicationController).
55
+ - **Redis** >= 5.0
56
+ - **X-Request-Start header**: Set by your load balancer (nginx, HAProxy, etc.) for queue time tracking
89
57
 
90
58
  Redis connection uses `ENV["REDIS_URL"]` or defaults to `redis://localhost:6379/1`.
91
59
 
92
- ## Notes
93
-
94
- - The endpoint skips CSRF token verification
95
- - The engine is isolated in the `PumaMetricsEngine` namespace
96
- - No database migrations required
97
-
98
- ## Development
99
-
100
- After checking out the repo, run `bundle install` to install dependencies.
101
-
102
- To build the gem:
103
-
104
- ```bash
105
- gem build puma_metrics_engine.gemspec
106
- ```
107
-
108
- To install the gem locally:
109
-
110
- ```bash
111
- gem install ./puma_metrics_engine-1.0.0.gem
112
- ```
113
-
114
- ## Contributing
115
-
116
- Bug reports and pull requests are welcome.
117
-
118
60
  ## License
119
61
 
120
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
121
-
62
+ MIT
@@ -171,4 +171,3 @@ module PumaMetricsEngine
171
171
  end
172
172
  end
173
173
  end
174
-
data/config/routes.rb CHANGED
@@ -3,4 +3,3 @@
3
3
  PumaMetricsEngine::Engine.routes.draw do
4
4
  get "matrix", to: "matrix#show"
5
5
  end
6
-
@@ -3,6 +3,7 @@
3
3
  module PumaMetricsEngine
4
4
  class Engine < ::Rails::Engine
5
5
  isolate_namespace PumaMetricsEngine
6
+
7
+ config.app_middleware.use PumaMetricsEngine::QueueTimeTracker
6
8
  end
7
9
  end
8
-
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PumaMetricsEngine
4
+ class QueueTimeTracker
5
+ TTL_SECONDS = 300 # 5 minutes
6
+ REQUESTS_KEY = "puma:request_timestamps"
7
+ QUEUE_TIMES_KEY = "puma:queue_times"
8
+
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call(env)
14
+ request_start_time = extract_request_start_time(env)
15
+ process_start_time = Time.now.to_f
16
+
17
+ status, headers, response = @app.call(env)
18
+
19
+ # Calculate queue time if we have request start time
20
+ begin
21
+ if request_start_time
22
+ queue_time_ms = ((process_start_time - request_start_time) * 1000).round(2)
23
+
24
+ # Only store if queue time is reasonable (positive and less than 1 hour)
25
+ # Negative values indicate clock skew, very large values are likely errors
26
+ if queue_time_ms >= 0 && queue_time_ms < 3_600_000
27
+ timestamp = process_start_time
28
+ # Store in Redis asynchronously to avoid blocking the request
29
+ store_metrics_async(timestamp, queue_time_ms)
30
+ else
31
+ # Still track request timestamp even if queue time is invalid
32
+ store_request_timestamp_async(process_start_time)
33
+ end
34
+ else
35
+ # Still track request timestamp even without queue time
36
+ store_request_timestamp_async(process_start_time)
37
+ end
38
+ rescue StandardError => e
39
+ # Don't let tracking errors break the request
40
+ Rails.logger.error("QueueTimeTracker error: #{e.message}") if defined?(Rails)
41
+ end
42
+
43
+ [status, headers, response]
44
+ end
45
+
46
+ private
47
+
48
+ def extract_request_start_time(env)
49
+ # Try X-Request-Start header (common in nginx, HAProxy, etc.)
50
+ # Format: "t=1234567890.123" or "1234567890.123" or Unix timestamp in microseconds
51
+ header_value = env["HTTP_X_REQUEST_START"] || env["X-Request-Start"]
52
+
53
+ return nil unless header_value
54
+
55
+ # Handle different formats
56
+ # Format 1: "t=1234567890.123"
57
+ if header_value =~ /t=([\d.]+)/
58
+ timestamp_str = $1
59
+ # Format 2: "1234567890.123" (direct timestamp)
60
+ elsif header_value =~ /^[\d.]+$/
61
+ timestamp_str = header_value
62
+ else
63
+ return nil
64
+ end
65
+
66
+ timestamp = timestamp_str.to_f
67
+
68
+ # If timestamp is in microseconds (common with nginx), convert to seconds
69
+ # Timestamps > year 2100 are likely in microseconds
70
+ if timestamp > 4_102_444_800 # Year 2100 in seconds
71
+ timestamp = timestamp / 1_000_000.0
72
+ end
73
+
74
+ timestamp
75
+ rescue StandardError
76
+ nil
77
+ end
78
+
79
+ def store_metrics_async(timestamp, queue_time_ms)
80
+ # Use a thread pool or async job, but for simplicity, we'll do it synchronously
81
+ # In production, you might want to use a background job
82
+ Thread.new do
83
+ begin
84
+ redis_client = redis
85
+ # Store queue time with timestamp as score
86
+ redis_client.zadd(QUEUE_TIMES_KEY, timestamp, queue_time_ms)
87
+ # Store request timestamp
88
+ redis_client.zadd(REQUESTS_KEY, timestamp, timestamp)
89
+ # Cleanup old data (older than TTL)
90
+ cleanup_old_data(redis_client)
91
+ rescue StandardError => e
92
+ Rails.logger.error("Failed to store queue time metrics: #{e.message}") if defined?(Rails)
93
+ end
94
+ end
95
+ end
96
+
97
+ def store_request_timestamp_async(timestamp)
98
+ Thread.new do
99
+ begin
100
+ redis_client = redis
101
+ redis_client.zadd(REQUESTS_KEY, timestamp, timestamp)
102
+ cleanup_old_data(redis_client)
103
+ rescue StandardError => e
104
+ Rails.logger.error("Failed to store request timestamp: #{e.message}") if defined?(Rails)
105
+ end
106
+ end
107
+ end
108
+
109
+ def cleanup_old_data(redis_client)
110
+ # Only cleanup occasionally to avoid overhead (10% chance)
111
+ return unless rand < 0.1
112
+
113
+ cutoff_time = Time.now.to_f - TTL_SECONDS
114
+ # Remove old entries from both sorted sets
115
+ redis_client.zremrangebyscore(QUEUE_TIMES_KEY, "-inf", cutoff_time)
116
+ redis_client.zremrangebyscore(REQUESTS_KEY, "-inf", cutoff_time)
117
+ end
118
+
119
+ def redis
120
+ @redis ||= Redis.new(url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" })
121
+ end
122
+ end
123
+ end
124
+
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PumaMetricsEngine
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
6
-
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "puma_metrics_engine/version"
4
+ require "puma_metrics_engine/queue_time_tracker"
4
5
  require "puma_metrics_engine/engine"
5
-
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puma_metrics_engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Neeto
@@ -66,6 +66,76 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '13.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
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: '6.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-mocks
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.12'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.12'
97
+ - !ruby/object:Gem::Dependency
98
+ name: fakeredis
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.9'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.9'
111
+ - !ruby/object:Gem::Dependency
112
+ name: timecop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.9'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.9'
125
+ - !ruby/object:Gem::Dependency
126
+ name: webmock
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.18'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.18'
69
139
  description: A Rails engine that exposes Puma server metrics, queue time statistics,
70
140
  and request rate information via a /matrix endpoint
71
141
  email:
@@ -80,6 +150,7 @@ files:
80
150
  - config/routes.rb
81
151
  - lib/puma_metrics_engine.rb
82
152
  - lib/puma_metrics_engine/engine.rb
153
+ - lib/puma_metrics_engine/queue_time_tracker.rb
83
154
  - lib/puma_metrics_engine/version.rb
84
155
  homepage: https://github.com/bigbinary/puma_metrics_engine
85
156
  licenses:
@@ -103,7 +174,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
103
174
  - !ruby/object:Gem::Version
104
175
  version: '0'
105
176
  requirements: []
106
- rubygems_version: 3.5.22
177
+ rubygems_version: 3.5.10
107
178
  signing_key:
108
179
  specification_version: 4
109
180
  summary: Rails engine that provides a /matrix endpoint for Puma metrics and queue