puma_metrics_engine 1.2.1 → 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 +8 -29
- 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)
|
|
@@ -102,34 +108,11 @@ module PumaMetricsEngine
|
|
|
102
108
|
end
|
|
103
109
|
|
|
104
110
|
def store_metrics_async(timestamp, queue_time_ms)
|
|
105
|
-
|
|
106
|
-
# In production, you might want to use a background job
|
|
107
|
-
Thread.new do
|
|
108
|
-
begin
|
|
109
|
-
redis_client = redis
|
|
110
|
-
# Store queue time with timestamp as score
|
|
111
|
-
redis_client.zadd(QUEUE_TIMES_KEY, timestamp, queue_time_ms)
|
|
112
|
-
# Store request timestamp
|
|
113
|
-
redis_client.zadd(REQUESTS_KEY, timestamp, timestamp)
|
|
114
|
-
# Cleanup old data (older than TTL)
|
|
115
|
-
cleanup_old_data(redis_client)
|
|
116
|
-
rescue StandardError => e
|
|
117
|
-
Rails.logger.error("[QueueTimeTracker] Failed to store metrics: #{e.message}") if defined?(Rails)
|
|
118
|
-
Rails.logger.error("[QueueTimeTracker] Redis error backtrace: #{e.backtrace.first(3).join("\n")}") if defined?(Rails)
|
|
119
|
-
end
|
|
120
|
-
end
|
|
111
|
+
@writer.record(timestamp, queue_time_ms)
|
|
121
112
|
end
|
|
122
113
|
|
|
123
114
|
def store_request_timestamp_async(timestamp)
|
|
124
|
-
|
|
125
|
-
begin
|
|
126
|
-
redis_client = redis
|
|
127
|
-
redis_client.zadd(REQUESTS_KEY, timestamp, timestamp)
|
|
128
|
-
cleanup_old_data(redis_client)
|
|
129
|
-
rescue StandardError => e
|
|
130
|
-
Rails.logger.error("[QueueTimeTracker] Failed to store request timestamp: #{e.message}") if defined?(Rails)
|
|
131
|
-
end
|
|
132
|
-
end
|
|
115
|
+
@writer.record(timestamp, nil)
|
|
133
116
|
end
|
|
134
117
|
|
|
135
118
|
def cleanup_old_data(redis_client)
|
|
@@ -141,10 +124,6 @@ module PumaMetricsEngine
|
|
|
141
124
|
redis_client.zremrangebyscore(QUEUE_TIMES_KEY, "-inf", cutoff_time)
|
|
142
125
|
redis_client.zremrangebyscore(REQUESTS_KEY, "-inf", cutoff_time)
|
|
143
126
|
end
|
|
144
|
-
|
|
145
|
-
def redis
|
|
146
|
-
@redis ||= Redis.new(url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" })
|
|
147
|
-
end
|
|
148
127
|
end
|
|
149
128
|
end
|
|
150
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
|