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 +4 -4
- data/LICENSE.txt +0 -1
- data/README.md +12 -71
- data/app/controllers/puma_metrics_engine/matrix_controller.rb +0 -1
- data/config/routes.rb +0 -1
- data/lib/puma_metrics_engine/engine.rb +2 -1
- data/lib/puma_metrics_engine/queue_time_tracker.rb +124 -0
- data/lib/puma_metrics_engine/version.rb +1 -2
- data/lib/puma_metrics_engine.rb +1 -1
- metadata +73 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 248a19d8e7fb9d4392b1f4c4bf3cfa13d8e4872426d843f171732264783eaf94
|
|
4
|
+
data.tar.gz: b09efaf7ede10a759b435b8be828f0dbebde7f54ece70efddb050180e474ac4a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4bbfb1639f3157a75afc8bbc538eecfd52492c05240be8d96576884ea2448cd6d8598b16e0a87c8fd2a93153b0b051e186842083e18bb874b8c5bb14bb17813c
|
|
7
|
+
data.tar.gz: 006066e431c7c1d7fed9cfb6c19b17715e2e7437a09ef23b8962838e33e063299a62ce7b47d6c6d535ebe535032ed8abd840f3851cf7e0505fab11bc023a041e
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
# PumaMetricsEngine
|
|
2
2
|
|
|
3
|
-
A Rails engine
|
|
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
|
|
7
|
+
Add to your Gemfile:
|
|
8
8
|
|
|
9
9
|
```ruby
|
|
10
|
-
gem "puma_metrics_engine"
|
|
11
|
-
# Or from a gem server:
|
|
12
|
-
# gem "puma_metrics_engine"
|
|
10
|
+
gem "puma_metrics_engine"
|
|
13
11
|
```
|
|
14
12
|
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
##
|
|
52
|
+
## Requirements
|
|
77
53
|
|
|
78
54
|
- **Rails** >= 7.0
|
|
79
|
-
- **Redis** >= 5.0
|
|
80
|
-
- **
|
|
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
|
-
|
|
121
|
-
|
|
62
|
+
MIT
|
data/config/routes.rb
CHANGED
|
@@ -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
|
+
|
data/lib/puma_metrics_engine.rb
CHANGED
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.
|
|
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.
|
|
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
|