Skip to content

Commit 8e3c307

Browse files
authored
FEATURE: Add new metrics busy_threads from Puma (#333)
New Puma version has a [nice addition](puma/puma#3517) of metric on busy threads. > busy_threads: running - how many threads are waiting to receive work + how many requests are waiting for a thread to pick them up. this is a "wholistic" stat reflecting the overall current state of work to be done and the capacity to do it. [source](https://github.com/puma/puma/blob/master/docs/stats.md#single-mode-and-individual-workers-in-cluster-mode)
1 parent 74fd1f2 commit 8e3c307

File tree

7 files changed

+52
-15
lines changed

7 files changed

+52
-15
lines changed

CHANGELOG

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
- FEATURE: Added puma_busy_threads metric that provides a holistic view of server workload by calculating (active threads - idle threads) + queued requests
11+
812
## [2.2.0] - 2024-12-05
913

1014
### Added

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -609,15 +609,16 @@ end
609609

610610
#### Metrics collected by Puma Instrumentation
611611

612-
| Type | Name | Description |
613-
| --- | --- | --- |
614-
| Gauge | `puma_workers` | Number of puma workers |
615-
| Gauge | `puma_booted_workers` | Number of puma workers booted |
616-
| Gauge | `puma_old_workers` | Number of old puma workers |
617-
| Gauge | `puma_running_threads` | Number of puma threads currently running |
618-
| Gauge | `puma_request_backlog` | Number of requests waiting to be processed by a puma thread |
619-
| Gauge | `puma_thread_pool_capacity` | Number of puma threads available at current scale |
620-
| Gauge | `puma_max_threads` | Number of puma threads at available at max scale |
612+
| Type | Name | Description |
613+
| --- | --- | --- |
614+
| Gauge | `puma_workers` | Number of puma workers |
615+
| Gauge | `puma_booted_workers` | Number of puma workers booted |
616+
| Gauge | `puma_old_workers` | Number of old puma workers |
617+
| Gauge | `puma_running_threads` | How many threads are spawned. A spawned thread may be busy processing a request or waiting for a new request |
618+
| Gauge | `puma_request_backlog` | Number of requests waiting to be processed by a puma thread |
619+
| Gauge | `puma_thread_pool_capacity` | Number of puma threads available at current scale |
620+
| Gauge | `puma_max_threads` | Number of puma threads at available at max scale |
621+
| Gauge | `puma_busy_threads` | Running - how many threads are waiting to receive work + how many requests are waiting for a thread to pick them up |
621622

622623
All metrics may have a `phase` label and all custom labels provided with the `labels` option.
623624

lib/prometheus_exporter/instrumentation/puma.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,13 @@ def collect_worker_status(metric, status)
6161
metric[:running_threads] ||= 0
6262
metric[:thread_pool_capacity] ||= 0
6363
metric[:max_threads] ||= 0
64+
metric[:busy_threads] ||= 0
6465

6566
metric[:request_backlog] += status["backlog"]
6667
metric[:running_threads] += status["running"]
6768
metric[:thread_pool_capacity] += status["pool_capacity"]
6869
metric[:max_threads] += status["max_threads"]
70+
metric[:busy_threads] += status["busy_threads"]
6971
end
7072
end
7173
end

lib/prometheus_exporter/server/puma_collector.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ class PumaCollector < TypeCollector
1313
max_threads: "Number of puma threads at available at max scale.",
1414
}
1515

16+
if defined?(::Puma::Const) && Gem::Version.new(::Puma::Const::VERSION) >= Gem::Version.new('6.6.0')
17+
PUMA_GAUGES[:busy_threads] = "Wholistic stat reflecting the overall current state of work to be done and the capacity to do it"
18+
end
19+
1620
def initialize
1721
@puma_metrics = MetricsContainer.new(ttl: MAX_PUMA_METRIC_AGE)
1822
@puma_metrics.filter = ->(new_metric, old_metric) do

test/server/collector_test.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,7 +1038,7 @@ def test_it_can_collect_puma_metrics
10381038
mock_puma = Minitest::Mock.new
10391039
mock_puma.expect(
10401040
:stats,
1041-
'{ "workers": 1, "phase": 0, "booted_workers": 1, "old_workers": 0, "worker_status": [{ "pid": 87819, "index": 0, "phase": 0, "booted": true, "last_checkin": "2018-10-16T11:50:31Z", "last_status": { "backlog":0, "running":8, "pool_capacity":32, "max_threads": 32 } }] }',
1041+
'{ "workers": 1, "phase": 0, "booted_workers": 1, "old_workers": 0, "worker_status": [{ "pid": 87819, "index": 0, "phase": 0, "booted": true, "last_checkin": "2018-10-16T11:50:31Z", "last_status": { "backlog":0, "running":8, "pool_capacity":32, "max_threads": 32, "busy_threads": 4 } }] }',
10421042
)
10431043

10441044
instrument = PrometheusExporter::Instrumentation::Puma.new
@@ -1061,6 +1061,10 @@ def test_it_can_collect_puma_metrics
10611061
result.include?('puma_thread_pool_capacity{phase="0",service="service1"} 32'),
10621062
"has pool capacity",
10631063
)
1064+
assert(
1065+
result.include?('puma_busy_threads{phase="0",service="service1"} 4'),
1066+
"has total busy threads",
1067+
)
10641068
mock_puma.verify
10651069
end
10661070

@@ -1071,7 +1075,7 @@ def test_it_can_collect_puma_metrics_with_metric_labels
10711075
mock_puma = Minitest::Mock.new
10721076
mock_puma.expect(
10731077
:stats,
1074-
'{ "workers": 1, "phase": 0, "booted_workers": 1, "old_workers": 0, "worker_status": [{ "pid": 87819, "index": 0, "phase": 0, "booted": true, "last_checkin": "2018-10-16T11:50:31Z", "last_status": { "backlog":0, "running":8, "pool_capacity":32, "max_threads": 32 } }] }',
1078+
'{ "workers": 1, "phase": 0, "booted_workers": 1, "old_workers": 0, "worker_status": [{ "pid": 87819, "index": 0, "phase": 0, "booted": true, "last_checkin": "2018-10-16T11:50:31Z", "last_status": { "backlog":0, "running":8, "pool_capacity":32, "max_threads": 32, "busy_threads": 4 } }] }',
10751079
)
10761080

10771081
instrument = PrometheusExporter::Instrumentation::Puma.new({ foo: "bar" })
@@ -1094,6 +1098,10 @@ def test_it_can_collect_puma_metrics_with_metric_labels
10941098
result.include?('puma_thread_pool_capacity{phase="0",service="service1",foo="bar"} 32'),
10951099
"has pool capacity",
10961100
)
1101+
assert(
1102+
result.include?('puma_busy_threads{phase="0",service="service1",foo="bar"} 4'),
1103+
"has total busy threads",
1104+
)
10971105
mock_puma.verify
10981106
end
10991107

test/server/puma_collector_test.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def test_collecting_metrics_for_different_hosts_without_custom_labels
2525
"running_threads" => 4,
2626
"thread_pool_capacity" => 10,
2727
"max_threads" => 10,
28+
"busy_threads" => 2,
2829
)
2930

3031
collector.collect(
@@ -39,6 +40,7 @@ def test_collecting_metrics_for_different_hosts_without_custom_labels
3940
"running_threads" => 9,
4041
"thread_pool_capacity" => 10,
4142
"max_threads" => 10,
43+
"busy_threads" => 3,
4244
)
4345

4446
# overwriting previous metrics from first host
@@ -54,10 +56,11 @@ def test_collecting_metrics_for_different_hosts_without_custom_labels
5456
"running_threads" => 8,
5557
"thread_pool_capacity" => 10,
5658
"max_threads" => 10,
59+
"busy_threads" => 4,
5760
)
5861

5962
metrics = collector.metrics
60-
assert_equal 7, metrics.size
63+
assert_equal 8, metrics.size
6164
assert_equal "puma_workers{phase=\"0\"} 3", metrics.first.metric_text
6265
end
6366

@@ -74,6 +77,7 @@ def test_collecting_metrics_for_different_hosts_with_custom_labels
7477
"running_threads" => 4,
7578
"thread_pool_capacity" => 10,
7679
"max_threads" => 10,
80+
"busy_threads" => 2,
7781
"custom_labels" => {
7882
"hostname" => "test1.example.com",
7983
},
@@ -91,6 +95,7 @@ def test_collecting_metrics_for_different_hosts_with_custom_labels
9195
"running_threads" => 9,
9296
"thread_pool_capacity" => 10,
9397
"max_threads" => 10,
98+
"busy_threads" => 3,
9499
"custom_labels" => {
95100
"hostname" => "test2.example.com",
96101
},
@@ -109,13 +114,14 @@ def test_collecting_metrics_for_different_hosts_with_custom_labels
109114
"running_threads" => 8,
110115
"thread_pool_capacity" => 10,
111116
"max_threads" => 10,
117+
"busy_threads" => 4,
112118
"custom_labels" => {
113119
"hostname" => "test1.example.com",
114120
},
115121
)
116122

117123
metrics = collector.metrics
118-
assert_equal 7, metrics.size
124+
assert_equal 8, metrics.size
119125
assert_equal "puma_workers{phase=\"0\",hostname=\"test2.example.com\"} 4\n" \
120126
"puma_workers{phase=\"0\",hostname=\"test1.example.com\"} 3",
121127
metrics.first.metric_text

test/test_helper.rb

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99

1010
module TestingMod
1111
class FakeConnection
12-
def call_pipelined(_, _, _)
12+
def call_pipelined(...)
1313
end
1414

15-
def call(_, _)
15+
def call(...)
1616
end
1717

1818
def connected?
@@ -64,6 +64,18 @@ def call_pipelined(command, _config)
6464
RedisClient::Middlewares.prepend(TestingMod)
6565
RedisClient.register(RedisValidationMiddleware)
6666

67+
unless defined?(::Puma)
68+
module Puma
69+
module Const
70+
VERSION = "6.6.0"
71+
end
72+
73+
def self.stats
74+
'{}'
75+
end
76+
end
77+
end
78+
6779
class TestHelper
6880
def self.wait_for(time, &blk)
6981
(time / 0.001).to_i.times do

0 commit comments

Comments
 (0)