puma_metrics_engine 1.2.0 → 1.3.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/lib/puma_metrics_engine/queue_time_tracker.rb +27 -33
- data/lib/puma_metrics_engine/version.rb +1 -1
- data/lib/puma_metrics_engine/writer.rb +110 -0
- data/lib/puma_metrics_engine.rb +3 -0
- metadata +10 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c38944df392cee8b15a51aebd6130426a41421a84441822c94ab97010368b3e7
|
|
4
|
+
data.tar.gz: 47bcfd77948a243f840f2609d802855a1e441108efa3ec6ce84556d3afebac2a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 47940a6f3a8d99b046301df2cf5b5dc31e5236ccfc39a09dae2cdb7d0f2b044cfe09f665f00311e66e9f42cb1e76a3c01f14bc01ebc94fe17ab95418b39378d8
|
|
7
|
+
data.tar.gz: 406da9dd0fd2721cb640eaf5e772f43fb4a0c2c45eeb6696521cbfe1f28718f61d9bc4cc156341d42991989a450d39a75d81ef81987eb738a64aafde60aaee42
|
|
@@ -8,6 +8,12 @@ module PumaMetricsEngine
|
|
|
8
8
|
|
|
9
9
|
def initialize(app)
|
|
10
10
|
@app = app
|
|
11
|
+
@writer = Writer.new(
|
|
12
|
+
queue_times_key: QUEUE_TIMES_KEY,
|
|
13
|
+
requests_key: REQUESTS_KEY,
|
|
14
|
+
redis_url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" },
|
|
15
|
+
after_write: method(:cleanup_old_data)
|
|
16
|
+
)
|
|
11
17
|
end
|
|
12
18
|
|
|
13
19
|
def call(env)
|
|
@@ -57,7 +63,7 @@ module PumaMetricsEngine
|
|
|
57
63
|
|
|
58
64
|
def extract_request_start_time(env)
|
|
59
65
|
# Try X-Request-Start header (common in nginx, HAProxy, etc.)
|
|
60
|
-
# Format: "t=1234567890.123" or "1234567890.123" or Unix timestamp in microseconds
|
|
66
|
+
# Format: "t=1234567890.123" or "1234567890.123" or Unix timestamp in milliseconds/microseconds
|
|
61
67
|
header_value = env["HTTP_X_REQUEST_START"] || env["X-Request-Start"]
|
|
62
68
|
|
|
63
69
|
return nil unless header_value
|
|
@@ -75,10 +81,25 @@ module PumaMetricsEngine
|
|
|
75
81
|
|
|
76
82
|
timestamp = timestamp_str.to_f
|
|
77
83
|
|
|
78
|
-
#
|
|
79
|
-
#
|
|
84
|
+
# Detect timestamp format based on magnitude:
|
|
85
|
+
# - 10 digits or less: seconds (e.g., 1764410986)
|
|
86
|
+
# - 11-13 digits: milliseconds (e.g., 1764410986245)
|
|
87
|
+
# - 14+ digits: microseconds (e.g., 1764410986245000)
|
|
80
88
|
if timestamp > 4_102_444_800 # Year 2100 in seconds
|
|
81
|
-
|
|
89
|
+
# Count digits in integer part to determine format
|
|
90
|
+
integer_part = timestamp.to_i.to_s
|
|
91
|
+
digit_count = integer_part.length
|
|
92
|
+
|
|
93
|
+
if digit_count <= 10
|
|
94
|
+
# Already in seconds
|
|
95
|
+
timestamp
|
|
96
|
+
elsif digit_count <= 13
|
|
97
|
+
# Milliseconds - convert to seconds
|
|
98
|
+
timestamp = timestamp / 1_000.0
|
|
99
|
+
else
|
|
100
|
+
# Microseconds - convert to seconds
|
|
101
|
+
timestamp = timestamp / 1_000_000.0
|
|
102
|
+
end
|
|
82
103
|
end
|
|
83
104
|
|
|
84
105
|
timestamp
|
|
@@ -87,34 +108,11 @@ module PumaMetricsEngine
|
|
|
87
108
|
end
|
|
88
109
|
|
|
89
110
|
def store_metrics_async(timestamp, queue_time_ms)
|
|
90
|
-
|
|
91
|
-
# In production, you might want to use a background job
|
|
92
|
-
Thread.new do
|
|
93
|
-
begin
|
|
94
|
-
redis_client = redis
|
|
95
|
-
# Store queue time with timestamp as score
|
|
96
|
-
redis_client.zadd(QUEUE_TIMES_KEY, timestamp, queue_time_ms)
|
|
97
|
-
# Store request timestamp
|
|
98
|
-
redis_client.zadd(REQUESTS_KEY, timestamp, timestamp)
|
|
99
|
-
# Cleanup old data (older than TTL)
|
|
100
|
-
cleanup_old_data(redis_client)
|
|
101
|
-
rescue StandardError => e
|
|
102
|
-
Rails.logger.error("[QueueTimeTracker] Failed to store metrics: #{e.message}") if defined?(Rails)
|
|
103
|
-
Rails.logger.error("[QueueTimeTracker] Redis error backtrace: #{e.backtrace.first(3).join("\n")}") if defined?(Rails)
|
|
104
|
-
end
|
|
105
|
-
end
|
|
111
|
+
@writer.record(timestamp, queue_time_ms)
|
|
106
112
|
end
|
|
107
113
|
|
|
108
114
|
def store_request_timestamp_async(timestamp)
|
|
109
|
-
|
|
110
|
-
begin
|
|
111
|
-
redis_client = redis
|
|
112
|
-
redis_client.zadd(REQUESTS_KEY, timestamp, timestamp)
|
|
113
|
-
cleanup_old_data(redis_client)
|
|
114
|
-
rescue StandardError => e
|
|
115
|
-
Rails.logger.error("[QueueTimeTracker] Failed to store request timestamp: #{e.message}") if defined?(Rails)
|
|
116
|
-
end
|
|
117
|
-
end
|
|
115
|
+
@writer.record(timestamp, nil)
|
|
118
116
|
end
|
|
119
117
|
|
|
120
118
|
def cleanup_old_data(redis_client)
|
|
@@ -126,10 +124,6 @@ module PumaMetricsEngine
|
|
|
126
124
|
redis_client.zremrangebyscore(QUEUE_TIMES_KEY, "-inf", cutoff_time)
|
|
127
125
|
redis_client.zremrangebyscore(REQUESTS_KEY, "-inf", cutoff_time)
|
|
128
126
|
end
|
|
129
|
-
|
|
130
|
-
def redis
|
|
131
|
-
@redis ||= Redis.new(url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" })
|
|
132
|
-
end
|
|
133
127
|
end
|
|
134
128
|
end
|
|
135
129
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module PumaMetricsEngine
|
|
6
|
+
# Sends samples to Redis from one background thread, so a request never
|
|
7
|
+
# waits on Redis and no thread is started per request.
|
|
8
|
+
#
|
|
9
|
+
# Each sample gets a random suffix in its sorted-set member. Members are
|
|
10
|
+
# unique, so storing the bare value made equal values overwrite each other.
|
|
11
|
+
# Readers parse members with String#to_f, which stops at the colon, so they
|
|
12
|
+
# read "12.5:1a2b3c4d" as 12.5 without any change.
|
|
13
|
+
class Writer
|
|
14
|
+
# Samples waiting to be written. When Redis is unreachable the queue stops
|
|
15
|
+
# growing here and new samples are dropped instead.
|
|
16
|
+
MAX_PENDING = 10_000
|
|
17
|
+
BATCH_SIZE = 500
|
|
18
|
+
|
|
19
|
+
def initialize(queue_times_key:, requests_key:, redis_url:, after_write: nil)
|
|
20
|
+
@queue_times_key = queue_times_key
|
|
21
|
+
@requests_key = requests_key
|
|
22
|
+
@redis_url = redis_url
|
|
23
|
+
@after_write = after_write
|
|
24
|
+
@queue = Thread::Queue.new
|
|
25
|
+
@mutex = Mutex.new
|
|
26
|
+
@thread = nil
|
|
27
|
+
@pid = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# queue_time_ms is nil when the request carried no usable start time; the
|
|
31
|
+
# request still counts towards requests per minute.
|
|
32
|
+
def record(timestamp, queue_time_ms)
|
|
33
|
+
ensure_thread
|
|
34
|
+
return if @queue.size >= MAX_PENDING
|
|
35
|
+
|
|
36
|
+
@queue << [timestamp, queue_time_ms]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def ensure_thread
|
|
42
|
+
return if running?
|
|
43
|
+
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
next if running?
|
|
46
|
+
|
|
47
|
+
# After a fork the thread and connection belong to the parent.
|
|
48
|
+
if @pid != Process.pid
|
|
49
|
+
@queue = Thread::Queue.new
|
|
50
|
+
@redis = nil
|
|
51
|
+
end
|
|
52
|
+
@pid = Process.pid
|
|
53
|
+
@thread = Thread.new { run }
|
|
54
|
+
@thread.name = "puma_metrics_writer"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def running?
|
|
59
|
+
@pid == Process.pid && @thread&.alive?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def run
|
|
63
|
+
loop do
|
|
64
|
+
write(next_batch)
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
log_error(e)
|
|
67
|
+
sleep(1)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def next_batch
|
|
72
|
+
batch = [@queue.pop]
|
|
73
|
+
while batch.size < BATCH_SIZE
|
|
74
|
+
sample = begin
|
|
75
|
+
@queue.pop(true)
|
|
76
|
+
rescue ThreadError
|
|
77
|
+
break
|
|
78
|
+
end
|
|
79
|
+
batch << sample
|
|
80
|
+
end
|
|
81
|
+
batch
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def write(batch)
|
|
85
|
+
queue_times = []
|
|
86
|
+
requests = []
|
|
87
|
+
batch.each do |timestamp, queue_time_ms|
|
|
88
|
+
id = SecureRandom.hex(4)
|
|
89
|
+
queue_times << [timestamp, "#{queue_time_ms}:#{id}"] if queue_time_ms
|
|
90
|
+
requests << [timestamp, "#{timestamp}:#{id}"]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
redis.pipelined do |pipeline|
|
|
94
|
+
pipeline.zadd(@queue_times_key, queue_times) if queue_times.any?
|
|
95
|
+
pipeline.zadd(@requests_key, requests)
|
|
96
|
+
end
|
|
97
|
+
@after_write&.call(redis)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def redis
|
|
101
|
+
@redis ||= Redis.new(url: @redis_url)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def log_error(error)
|
|
105
|
+
return unless defined?(Rails) && Rails.logger
|
|
106
|
+
|
|
107
|
+
Rails.logger.error("[QueueTimeTracker] Failed to store metrics: #{error.class}: #{error.message}")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
data/lib/puma_metrics_engine.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
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.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
8
|
-
autorequire:
|
|
7
|
+
- Neeraj Singh
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rails
|
|
@@ -94,20 +93,6 @@ dependencies:
|
|
|
94
93
|
- - "~>"
|
|
95
94
|
- !ruby/object:Gem::Version
|
|
96
95
|
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
96
|
- !ruby/object:Gem::Dependency
|
|
112
97
|
name: timecop
|
|
113
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -139,7 +124,7 @@ dependencies:
|
|
|
139
124
|
description: A Rails engine that exposes Puma server metrics, queue time statistics,
|
|
140
125
|
and request rate information via a /matrix endpoint
|
|
141
126
|
email:
|
|
142
|
-
-
|
|
127
|
+
- neeraj@neeto.com
|
|
143
128
|
executables: []
|
|
144
129
|
extensions: []
|
|
145
130
|
extra_rdoc_files: []
|
|
@@ -153,14 +138,14 @@ files:
|
|
|
153
138
|
- lib/puma_metrics_engine/engine.rb
|
|
154
139
|
- lib/puma_metrics_engine/queue_time_tracker.rb
|
|
155
140
|
- lib/puma_metrics_engine/version.rb
|
|
156
|
-
|
|
141
|
+
- lib/puma_metrics_engine/writer.rb
|
|
142
|
+
homepage: https://github.com/neetozone/puma_metrics_engine
|
|
157
143
|
licenses:
|
|
158
144
|
- MIT
|
|
159
145
|
metadata:
|
|
160
|
-
homepage_uri: https://github.com/
|
|
161
|
-
source_code_uri: https://github.com/
|
|
162
|
-
changelog_uri: https://github.com/
|
|
163
|
-
post_install_message:
|
|
146
|
+
homepage_uri: https://github.com/neetozone/puma_metrics_engine
|
|
147
|
+
source_code_uri: https://github.com/neetozone/puma_metrics_engine
|
|
148
|
+
changelog_uri: https://github.com/neetozone/puma_metrics_engine/blob/main/CHANGELOG.md
|
|
164
149
|
rdoc_options: []
|
|
165
150
|
require_paths:
|
|
166
151
|
- lib
|
|
@@ -175,8 +160,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
175
160
|
- !ruby/object:Gem::Version
|
|
176
161
|
version: '0'
|
|
177
162
|
requirements: []
|
|
178
|
-
rubygems_version: 3.
|
|
179
|
-
signing_key:
|
|
163
|
+
rubygems_version: 3.6.9
|
|
180
164
|
specification_version: 4
|
|
181
165
|
summary: Rails engine that provides a /matrix endpoint for Puma metrics and queue
|
|
182
166
|
time statistics
|