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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ab335fa7843d45934fd97c5442b9bfd3a87006066b6f14ef99123a9316ab165
4
- data.tar.gz: 7bd9134d5a436aecebd2a3f2a4073a5b51c46f3fb3d4a8b2391464e108326503
3
+ metadata.gz: c38944df392cee8b15a51aebd6130426a41421a84441822c94ab97010368b3e7
4
+ data.tar.gz: 47bcfd77948a243f840f2609d802855a1e441108efa3ec6ce84556d3afebac2a
5
5
  SHA512:
6
- metadata.gz: c9f71532254eed460c8169a894c41e0ac03c7a089c37a133fc097d7632a0ab2bd2e282bbe84b7cd3ea667ea1adfa9f5a9cb3afc4aaaeb8d470107e5f403ab016
7
- data.tar.gz: b704e4c376bdbdad8324b004c71ed20d176dbdda1febaaa63c6c411da33beee40cdb98460671dd8c558feb085a491602f2babc220d3ede42f2d77e90594b00d7
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
- # If timestamp is in microseconds (common with nginx), convert to seconds
79
- # Timestamps > year 2100 are likely in microseconds
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
- timestamp = timestamp / 1_000_000.0
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
- # Use a thread pool or async job, but for simplicity, we'll do it synchronously
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
- Thread.new do
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PumaMetricsEngine
4
- VERSION = "1.2.0"
4
+ VERSION = "1.3.0"
5
5
  end
@@ -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
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rails"
4
+ require "redis"
3
5
  require "puma_metrics_engine/version"
6
+ require "puma_metrics_engine/writer"
4
7
  require "puma_metrics_engine/queue_time_tracker"
5
8
  require "puma_metrics_engine/engine"
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.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
- - Neeto
8
- autorequire:
7
+ - Neeraj Singh
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-11-29 00:00:00.000000000 Z
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
- - engineering@bigbinary.com
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
- homepage: https://github.com/bigbinary/puma_metrics_engine
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/bigbinary/puma_metrics_engine
161
- source_code_uri: https://github.com/bigbinary/puma_metrics_engine
162
- changelog_uri: https://github.com/bigbinary/puma_metrics_engine/blob/main/CHANGELOG.md
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.5.10
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