solid_queue_monitor 1.1.0 → 1.2.1
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/README.md +31 -2
- data/app/controllers/solid_queue_monitor/base_controller.rb +15 -31
- data/app/controllers/solid_queue_monitor/in_progress_jobs_controller.rb +2 -4
- data/app/controllers/solid_queue_monitor/overview_controller.rb +8 -8
- data/app/controllers/solid_queue_monitor/queues_controller.rb +19 -9
- data/app/presenters/solid_queue_monitor/queues_presenter.rb +8 -21
- data/app/presenters/solid_queue_monitor/stats_presenter.rb +1 -2
- data/app/services/solid_queue_monitor/chart_data_service.rb +56 -57
- data/app/services/solid_queue_monitor/stats_calculator.rb +12 -8
- data/lib/generators/solid_queue_monitor/templates/initializer.rb +7 -0
- data/lib/solid_queue_monitor/version.rb +1 -1
- data/lib/solid_queue_monitor.rb +18 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6de5c6ef734c44e24c72e13d194fa9e6243fa5cefce591f430daa6f6dc1f175d
|
|
4
|
+
data.tar.gz: 7065afd18960ef8f61b7131fa96fd7c46120b75c52c2b633a34f63b686b8ca2c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6e899fddd4e1b08cf84beeff8b93f8ca67c199ce98d795983335300bf15fc087fe8ab0009d2a75746ae4d90b68f0b609f03c7ca44fb50ac0784551fc1e111a6b
|
|
7
|
+
data.tar.gz: eecbd91a4eee221f13431810542e92c3fa06403d2279a00e447e186c00a4d34479c134461864c80274171493c985d83d4ea7ac351e93dc09d13238d00ee825d6
|
data/README.md
CHANGED
|
@@ -72,7 +72,7 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
|
|
|
72
72
|
Add this line to your application's Gemfile:
|
|
73
73
|
|
|
74
74
|
```ruby
|
|
75
|
-
gem 'solid_queue_monitor', '~> 1.
|
|
75
|
+
gem 'solid_queue_monitor', '~> 1.2'
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
Then execute:
|
|
@@ -104,9 +104,11 @@ SolidQueueMonitor.setup do |config|
|
|
|
104
104
|
config.authentication_enabled = false
|
|
105
105
|
|
|
106
106
|
# Set the username for HTTP Basic Authentication (only used if authentication is enabled)
|
|
107
|
+
# Supports static values, ENV variables, or callables (Proc/Lambda)
|
|
107
108
|
config.username = 'admin'
|
|
108
109
|
|
|
109
110
|
# Set the password for HTTP Basic Authentication (only used if authentication is enabled)
|
|
111
|
+
# Supports static values, ENV variables, or callables (Proc/Lambda)
|
|
110
112
|
config.password = 'password'
|
|
111
113
|
|
|
112
114
|
# Number of jobs to display per page
|
|
@@ -118,9 +120,22 @@ SolidQueueMonitor.setup do |config|
|
|
|
118
120
|
|
|
119
121
|
# Auto-refresh interval in seconds (default: 30)
|
|
120
122
|
config.auto_refresh_interval = 30
|
|
123
|
+
|
|
124
|
+
# Disable the chart on the overview page to skip chart queries entirely
|
|
125
|
+
# config.show_chart = true
|
|
121
126
|
end
|
|
122
127
|
```
|
|
123
128
|
|
|
129
|
+
### Performance at Scale
|
|
130
|
+
|
|
131
|
+
SolidQueueMonitor is optimized for large datasets (millions of rows in `solid_queue_jobs`). All dashboard queries are designed to stay fast regardless of table size.
|
|
132
|
+
|
|
133
|
+
If you don't need the job activity chart, disable it to skip chart queries entirely:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
config.show_chart = false
|
|
137
|
+
```
|
|
138
|
+
|
|
124
139
|
### Authentication
|
|
125
140
|
|
|
126
141
|
By default, Solid Queue Monitor does not require authentication to access the dashboard. This makes it easy to get started in development environments.
|
|
@@ -128,7 +143,21 @@ By default, Solid Queue Monitor does not require authentication to access the da
|
|
|
128
143
|
For production environments, it's strongly recommended to enable authentication:
|
|
129
144
|
|
|
130
145
|
1. **Enable authentication**: Set `config.authentication_enabled = true` in the initializer
|
|
131
|
-
2. **Configure secure credentials
|
|
146
|
+
2. **Configure secure credentials** using any of these approaches:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
# Static values
|
|
150
|
+
config.username = 'admin'
|
|
151
|
+
config.password = 'secure_password'
|
|
152
|
+
|
|
153
|
+
# Environment variables
|
|
154
|
+
config.username = ENV['SOLID_QUEUE_MONITOR_USERNAME']
|
|
155
|
+
config.password = ENV['SOLID_QUEUE_MONITOR_PASSWORD']
|
|
156
|
+
|
|
157
|
+
# Rails credentials (use a lambda for deferred evaluation)
|
|
158
|
+
config.username = -> { Rails.application.credentials.dig(:solid_queue_monitor, :username) }
|
|
159
|
+
config.password = -> { Rails.application.credentials.dig(:solid_queue_monitor, :password) }
|
|
160
|
+
```
|
|
132
161
|
|
|
133
162
|
## Usage
|
|
134
163
|
|
|
@@ -91,17 +91,13 @@ module SolidQueueMonitor
|
|
|
91
91
|
when 'completed'
|
|
92
92
|
relation = relation.where.not(finished_at: nil)
|
|
93
93
|
when 'failed'
|
|
94
|
-
|
|
95
|
-
relation = relation.where(id: failed_job_ids)
|
|
94
|
+
relation = relation.where(id: SolidQueue::FailedExecution.select(:job_id))
|
|
96
95
|
when 'scheduled'
|
|
97
|
-
|
|
98
|
-
relation = relation.where(id: scheduled_job_ids)
|
|
96
|
+
relation = relation.where(id: SolidQueue::ScheduledExecution.select(:job_id))
|
|
99
97
|
when 'pending'
|
|
100
|
-
# Pending jobs are those that are not completed, failed, or scheduled
|
|
101
|
-
failed_job_ids = SolidQueue::FailedExecution.pluck(:job_id)
|
|
102
|
-
scheduled_job_ids = SolidQueue::ScheduledExecution.pluck(:job_id)
|
|
103
98
|
relation = relation.where(finished_at: nil)
|
|
104
|
-
.where.not(id:
|
|
99
|
+
.where.not(id: SolidQueue::FailedExecution.select(:job_id))
|
|
100
|
+
.where.not(id: SolidQueue::ScheduledExecution.select(:job_id))
|
|
105
101
|
end
|
|
106
102
|
end
|
|
107
103
|
|
|
@@ -117,16 +113,13 @@ module SolidQueueMonitor
|
|
|
117
113
|
return relation unless params[:class_name].present? || params[:queue_name].present? || params[:arguments].present?
|
|
118
114
|
|
|
119
115
|
if params[:class_name].present?
|
|
120
|
-
|
|
121
|
-
relation = relation.where(job_id: job_ids)
|
|
116
|
+
relation = relation.where(job_id: SolidQueue::Job.where('class_name LIKE ?', "%#{params[:class_name]}%").select(:id))
|
|
122
117
|
end
|
|
123
118
|
|
|
124
119
|
relation = relation.where('queue_name LIKE ?', "%#{params[:queue_name]}%") if params[:queue_name].present?
|
|
125
120
|
|
|
126
|
-
# Add arguments filtering
|
|
127
121
|
if params[:arguments].present?
|
|
128
|
-
|
|
129
|
-
relation = relation.where(job_id: job_ids)
|
|
122
|
+
relation = relation.where(job_id: SolidQueue::Job.where('arguments::text ILIKE ?', "%#{params[:arguments]}%").select(:id))
|
|
130
123
|
end
|
|
131
124
|
|
|
132
125
|
relation
|
|
@@ -136,16 +129,13 @@ module SolidQueueMonitor
|
|
|
136
129
|
return relation unless params[:class_name].present? || params[:queue_name].present? || params[:arguments].present?
|
|
137
130
|
|
|
138
131
|
if params[:class_name].present?
|
|
139
|
-
|
|
140
|
-
relation = relation.where(job_id: job_ids)
|
|
132
|
+
relation = relation.where(job_id: SolidQueue::Job.where('class_name LIKE ?', "%#{params[:class_name]}%").select(:id))
|
|
141
133
|
end
|
|
142
134
|
|
|
143
135
|
relation = relation.where('queue_name LIKE ?', "%#{params[:queue_name]}%") if params[:queue_name].present?
|
|
144
136
|
|
|
145
|
-
# Add arguments filtering
|
|
146
137
|
if params[:arguments].present?
|
|
147
|
-
|
|
148
|
-
relation = relation.where(job_id: job_ids)
|
|
138
|
+
relation = relation.where(job_id: SolidQueue::Job.where('arguments::text ILIKE ?', "%#{params[:arguments]}%").select(:id))
|
|
149
139
|
end
|
|
150
140
|
|
|
151
141
|
relation
|
|
@@ -170,25 +160,19 @@ module SolidQueueMonitor
|
|
|
170
160
|
return relation unless params[:class_name].present? || params[:queue_name].present? || params[:arguments].present?
|
|
171
161
|
|
|
172
162
|
if params[:class_name].present?
|
|
173
|
-
|
|
174
|
-
relation = relation.where(job_id: job_ids)
|
|
163
|
+
relation = relation.where(job_id: SolidQueue::Job.where('class_name LIKE ?', "%#{params[:class_name]}%").select(:id))
|
|
175
164
|
end
|
|
176
165
|
|
|
177
166
|
if params[:queue_name].present?
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
job_ids = SolidQueue::Job.where('queue_name LIKE ?', "%#{params[:queue_name]}%").pluck(:id)
|
|
184
|
-
relation = relation.where(job_id: job_ids)
|
|
185
|
-
end
|
|
167
|
+
relation = if relation.column_names.include?('queue_name')
|
|
168
|
+
relation.where('queue_name LIKE ?', "%#{params[:queue_name]}%")
|
|
169
|
+
else
|
|
170
|
+
relation.where(job_id: SolidQueue::Job.where('queue_name LIKE ?', "%#{params[:queue_name]}%").select(:id))
|
|
171
|
+
end
|
|
186
172
|
end
|
|
187
173
|
|
|
188
|
-
# Add arguments filtering
|
|
189
174
|
if params[:arguments].present?
|
|
190
|
-
|
|
191
|
-
relation = relation.where(job_id: job_ids)
|
|
175
|
+
relation = relation.where(job_id: SolidQueue::Job.where('arguments::text ILIKE ?', "%#{params[:arguments]}%").select(:id))
|
|
192
176
|
end
|
|
193
177
|
|
|
194
178
|
relation
|
|
@@ -22,13 +22,11 @@ module SolidQueueMonitor
|
|
|
22
22
|
return relation if params[:class_name].blank? && params[:arguments].blank?
|
|
23
23
|
|
|
24
24
|
if params[:class_name].present?
|
|
25
|
-
|
|
26
|
-
relation = relation.where(job_id: job_ids)
|
|
25
|
+
relation = relation.where(job_id: SolidQueue::Job.where('class_name LIKE ?', "%#{params[:class_name]}%").select(:id))
|
|
27
26
|
end
|
|
28
27
|
|
|
29
28
|
if params[:arguments].present?
|
|
30
|
-
|
|
31
|
-
relation = relation.where(job_id: job_ids)
|
|
29
|
+
relation = relation.where(job_id: SolidQueue::Job.where('arguments::text ILIKE ?', "%#{params[:arguments]}%").select(:id))
|
|
32
30
|
end
|
|
33
31
|
|
|
34
32
|
relation
|
|
@@ -6,7 +6,7 @@ module SolidQueueMonitor
|
|
|
6
6
|
|
|
7
7
|
def index
|
|
8
8
|
@stats = SolidQueueMonitor::StatsCalculator.calculate
|
|
9
|
-
@chart_data = SolidQueueMonitor::ChartDataService.new(time_range: time_range_param).calculate
|
|
9
|
+
@chart_data = SolidQueueMonitor.show_chart ? SolidQueueMonitor::ChartDataService.new(time_range: time_range_param).calculate : nil
|
|
10
10
|
|
|
11
11
|
recent_jobs_query = SolidQueue::Job.limit(100)
|
|
12
12
|
sorted_query = apply_sorting(filter_jobs(recent_jobs_query), SORTABLE_COLUMNS, 'created_at', :desc)
|
|
@@ -29,13 +29,13 @@ module SolidQueueMonitor
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def generate_overview_content
|
|
32
|
-
SolidQueueMonitor::StatsPresenter.new(@stats).render
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
html = SolidQueueMonitor::StatsPresenter.new(@stats).render
|
|
33
|
+
html += SolidQueueMonitor::ChartPresenter.new(@chart_data).render if @chart_data
|
|
34
|
+
html + SolidQueueMonitor::JobsPresenter.new(@recent_jobs[:records],
|
|
35
|
+
current_page: @recent_jobs[:current_page],
|
|
36
|
+
total_pages: @recent_jobs[:total_pages],
|
|
37
|
+
filters: filter_params,
|
|
38
|
+
sort: sort_params).render
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
end
|
|
@@ -10,8 +10,13 @@ module SolidQueueMonitor
|
|
|
10
10
|
.select('queue_name, COUNT(*) as job_count')
|
|
11
11
|
@queues = apply_queue_sorting(base_query)
|
|
12
12
|
@paused_queues = QueuePauseService.paused_queues
|
|
13
|
+
@queue_stats = aggregate_queue_stats
|
|
13
14
|
|
|
14
|
-
render_page('Queues', SolidQueueMonitor::QueuesPresenter.new(
|
|
15
|
+
render_page('Queues', SolidQueueMonitor::QueuesPresenter.new(
|
|
16
|
+
@queues, @paused_queues,
|
|
17
|
+
queue_stats: @queue_stats,
|
|
18
|
+
sort: sort_params
|
|
19
|
+
).render)
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
def show
|
|
@@ -57,6 +62,15 @@ module SolidQueueMonitor
|
|
|
57
62
|
|
|
58
63
|
private
|
|
59
64
|
|
|
65
|
+
def aggregate_queue_stats
|
|
66
|
+
{
|
|
67
|
+
ready: SolidQueue::ReadyExecution.group(:queue_name).count,
|
|
68
|
+
scheduled: SolidQueue::ScheduledExecution.group(:queue_name).count,
|
|
69
|
+
failed: SolidQueue::FailedExecution.joins(:job)
|
|
70
|
+
.group('solid_queue_jobs.queue_name').count
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
60
74
|
def calculate_queue_counts(queue_name)
|
|
61
75
|
{
|
|
62
76
|
total: SolidQueue::Job.where(queue_name: queue_name).count,
|
|
@@ -77,17 +91,13 @@ module SolidQueueMonitor
|
|
|
77
91
|
when 'completed'
|
|
78
92
|
relation = relation.where.not(finished_at: nil)
|
|
79
93
|
when 'failed'
|
|
80
|
-
|
|
81
|
-
relation = relation.where(id: failed_job_ids)
|
|
94
|
+
relation = relation.where(id: SolidQueue::FailedExecution.select(:job_id))
|
|
82
95
|
when 'scheduled'
|
|
83
|
-
|
|
84
|
-
relation = relation.where(id: scheduled_job_ids)
|
|
96
|
+
relation = relation.where(id: SolidQueue::ScheduledExecution.select(:job_id))
|
|
85
97
|
when 'pending'
|
|
86
|
-
|
|
87
|
-
relation = relation.where(id: ready_job_ids)
|
|
98
|
+
relation = relation.where(id: SolidQueue::ReadyExecution.select(:job_id))
|
|
88
99
|
when 'in_progress'
|
|
89
|
-
|
|
90
|
-
relation = relation.where(id: claimed_job_ids)
|
|
100
|
+
relation = relation.where(id: SolidQueue::ClaimedExecution.select(:job_id))
|
|
91
101
|
end
|
|
92
102
|
end
|
|
93
103
|
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidQueueMonitor
|
|
4
4
|
class QueuesPresenter < BasePresenter
|
|
5
|
-
def initialize(records, paused_queues = [], sort: {})
|
|
6
|
-
@records
|
|
5
|
+
def initialize(records, paused_queues = [], sort: {}, queue_stats: {})
|
|
6
|
+
@records = records
|
|
7
7
|
@paused_queues = paused_queues
|
|
8
|
-
@sort
|
|
8
|
+
@sort = sort
|
|
9
|
+
@queue_stats = queue_stats
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def render
|
|
@@ -39,16 +40,16 @@ module SolidQueueMonitor
|
|
|
39
40
|
|
|
40
41
|
def generate_row(queue)
|
|
41
42
|
queue_name = queue.queue_name || 'default'
|
|
42
|
-
paused
|
|
43
|
+
paused = @paused_queues.include?(queue_name)
|
|
43
44
|
|
|
44
45
|
<<-HTML
|
|
45
46
|
<tr class="#{paused ? 'queue-paused' : ''}">
|
|
46
47
|
<td>#{queue_link(queue_name)}</td>
|
|
47
48
|
<td>#{status_badge(paused)}</td>
|
|
48
49
|
<td>#{queue.job_count}</td>
|
|
49
|
-
<td>#{
|
|
50
|
-
<td>#{
|
|
51
|
-
<td>#{
|
|
50
|
+
<td>#{@queue_stats.dig(:ready, queue_name) || 0}</td>
|
|
51
|
+
<td>#{@queue_stats.dig(:scheduled, queue_name) || 0}</td>
|
|
52
|
+
<td>#{@queue_stats.dig(:failed, queue_name) || 0}</td>
|
|
52
53
|
<td class="actions-cell">#{action_button(queue_name, paused)}</td>
|
|
53
54
|
</tr>
|
|
54
55
|
HTML
|
|
@@ -84,19 +85,5 @@ module SolidQueueMonitor
|
|
|
84
85
|
HTML
|
|
85
86
|
end
|
|
86
87
|
end
|
|
87
|
-
|
|
88
|
-
def ready_jobs_count(queue_name)
|
|
89
|
-
SolidQueue::ReadyExecution.where(queue_name: queue_name).count
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def scheduled_jobs_count(queue_name)
|
|
93
|
-
SolidQueue::ScheduledExecution.where(queue_name: queue_name).count
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def failed_jobs_count(queue_name)
|
|
97
|
-
SolidQueue::FailedExecution.joins(:job)
|
|
98
|
-
.where(solid_queue_jobs: { queue_name: queue_name })
|
|
99
|
-
.count
|
|
100
|
-
end
|
|
101
88
|
end
|
|
102
89
|
end
|
|
@@ -11,13 +11,12 @@ module SolidQueueMonitor
|
|
|
11
11
|
<div class="stats-container">
|
|
12
12
|
<h3>Queue Statistics</h3>
|
|
13
13
|
<div class="stats">
|
|
14
|
-
#{generate_stat_card('
|
|
14
|
+
#{generate_stat_card('Active Jobs', @stats[:active_jobs])}
|
|
15
15
|
#{generate_stat_card('Ready', @stats[:ready])}
|
|
16
16
|
#{generate_stat_card('In Progress', @stats[:in_progress])}
|
|
17
17
|
#{generate_stat_card('Scheduled', @stats[:scheduled])}
|
|
18
18
|
#{generate_stat_card('Recurring', @stats[:recurring])}
|
|
19
19
|
#{generate_stat_card('Failed', @stats[:failed])}
|
|
20
|
-
#{generate_stat_card('Completed', @stats[:completed])}
|
|
21
20
|
</div>
|
|
22
21
|
</div>
|
|
23
22
|
HTML
|
|
@@ -5,47 +5,42 @@ module SolidQueueMonitor
|
|
|
5
5
|
TIME_RANGES = {
|
|
6
6
|
'15m' => { duration: 15.minutes, buckets: 15, label_format: '%H:%M', label: 'Last 15 minutes' },
|
|
7
7
|
'30m' => { duration: 30.minutes, buckets: 15, label_format: '%H:%M', label: 'Last 30 minutes' },
|
|
8
|
-
'1h' => { duration: 1.hour,
|
|
9
|
-
'3h' => { duration: 3.hours,
|
|
10
|
-
'6h' => { duration: 6.hours,
|
|
8
|
+
'1h' => { duration: 1.hour, buckets: 12, label_format: '%H:%M', label: 'Last 1 hour' },
|
|
9
|
+
'3h' => { duration: 3.hours, buckets: 18, label_format: '%H:%M', label: 'Last 3 hours' },
|
|
10
|
+
'6h' => { duration: 6.hours, buckets: 24, label_format: '%H:%M', label: 'Last 6 hours' },
|
|
11
11
|
'12h' => { duration: 12.hours, buckets: 24, label_format: '%H:%M', label: 'Last 12 hours' },
|
|
12
|
-
'1d' => { duration: 1.day,
|
|
13
|
-
'3d' => { duration: 3.days,
|
|
14
|
-
'1w' => { duration: 7.days,
|
|
12
|
+
'1d' => { duration: 1.day, buckets: 24, label_format: '%H:%M', label: 'Last 24 hours' },
|
|
13
|
+
'3d' => { duration: 3.days, buckets: 36, label_format: '%m/%d %H:%M', label: 'Last 3 days' },
|
|
14
|
+
'1w' => { duration: 7.days, buckets: 28, label_format: '%m/%d', label: 'Last 7 days' }
|
|
15
15
|
}.freeze
|
|
16
16
|
|
|
17
17
|
DEFAULT_TIME_RANGE = '1d'
|
|
18
18
|
|
|
19
19
|
def initialize(time_range: DEFAULT_TIME_RANGE)
|
|
20
20
|
@time_range = TIME_RANGES.key?(time_range) ? time_range : DEFAULT_TIME_RANGE
|
|
21
|
-
@config
|
|
21
|
+
@config = TIME_RANGES[@time_range]
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def calculate
|
|
25
|
-
end_time
|
|
26
|
-
start_time
|
|
27
|
-
|
|
25
|
+
end_time = Time.current
|
|
26
|
+
start_time = end_time - @config[:duration]
|
|
27
|
+
bucket_seconds = (@config[:duration] / @config[:buckets]).to_i
|
|
28
|
+
buckets = build_buckets(start_time, bucket_seconds)
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
created_data = bucket_counts(SolidQueue::Job, :created_at, start_time, end_time, bucket_seconds)
|
|
31
|
+
completed_data = bucket_counts(SolidQueue::Job, :finished_at, start_time, end_time, bucket_seconds, exclude_nil: true)
|
|
32
|
+
failed_data = bucket_counts(SolidQueue::FailedExecution, :created_at, start_time, end_time, bucket_seconds)
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
created_data = assign_to_buckets(created_counts, buckets, bucket_duration)
|
|
36
|
-
completed_data = assign_to_buckets(completed_counts, buckets, bucket_duration)
|
|
37
|
-
failed_data = assign_to_buckets(failed_counts, buckets, bucket_duration)
|
|
34
|
+
created_arr = fill_buckets(buckets, created_data)
|
|
35
|
+
completed_arr = fill_buckets(buckets, completed_data)
|
|
36
|
+
failed_arr = fill_buckets(buckets, failed_data)
|
|
38
37
|
|
|
39
38
|
{
|
|
40
39
|
labels: buckets.map { |b| b[:label] }, # rubocop:disable Rails/Pluck
|
|
41
|
-
created:
|
|
42
|
-
completed:
|
|
43
|
-
failed:
|
|
44
|
-
totals: {
|
|
45
|
-
created: created_data.sum,
|
|
46
|
-
completed: completed_data.sum,
|
|
47
|
-
failed: failed_data.sum
|
|
48
|
-
},
|
|
40
|
+
created: created_arr,
|
|
41
|
+
completed: completed_arr,
|
|
42
|
+
failed: failed_arr,
|
|
43
|
+
totals: { created: created_arr.sum, completed: completed_arr.sum, failed: failed_arr.sum },
|
|
49
44
|
time_range: @time_range,
|
|
50
45
|
time_range_label: @config[:label],
|
|
51
46
|
available_ranges: TIME_RANGES.transform_values { |v| v[:label] }
|
|
@@ -54,47 +49,51 @@ module SolidQueueMonitor
|
|
|
54
49
|
|
|
55
50
|
private
|
|
56
51
|
|
|
57
|
-
def build_buckets(start_time,
|
|
52
|
+
def build_buckets(start_time, bucket_seconds)
|
|
58
53
|
@config[:buckets].times.map do |i|
|
|
59
|
-
bucket_start = start_time + (i *
|
|
60
|
-
{
|
|
61
|
-
start: bucket_start,
|
|
62
|
-
end: bucket_start + bucket_duration,
|
|
63
|
-
label: bucket_start.strftime(@config[:label_format])
|
|
64
|
-
}
|
|
54
|
+
bucket_start = start_time + (i * bucket_seconds)
|
|
55
|
+
{ index: i, start: bucket_start, label: bucket_start.strftime(@config[:label_format]) }
|
|
65
56
|
end
|
|
66
57
|
end
|
|
67
58
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
59
|
+
# Returns a Hash of { bucket_index => count } using SQL GROUP BY.
|
|
60
|
+
# The bucket index is computed as: (epoch(column) - epoch(start_time)) / interval
|
|
61
|
+
# This works identically on PostgreSQL and SQLite.
|
|
62
|
+
def bucket_counts(model, column, start_time, end_time, interval, exclude_nil: false)
|
|
63
|
+
start_epoch = start_time.to_i
|
|
64
|
+
expr = bucket_index_expr(column, start_epoch, interval)
|
|
65
|
+
|
|
66
|
+
scope = model.where(column => start_time..end_time)
|
|
67
|
+
scope = scope.where.not(column => nil) if exclude_nil
|
|
68
|
+
|
|
69
|
+
# rubocop:disable Style/HashTransformKeys -- pluck returns Array<Array>, not Hash
|
|
70
|
+
scope
|
|
71
|
+
.group(Arel.sql(expr))
|
|
72
|
+
.pluck(Arel.sql("#{expr} AS bucket_idx, COUNT(*) AS cnt"))
|
|
73
|
+
.to_h { |idx, cnt| [idx.to_i, cnt] }
|
|
74
|
+
# rubocop:enable Style/HashTransformKeys
|
|
79
75
|
end
|
|
80
76
|
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
.where(created_at: start_time..end_time)
|
|
84
|
-
.pluck(:created_at)
|
|
77
|
+
def fill_buckets(buckets, index_counts)
|
|
78
|
+
buckets.map { |b| index_counts.fetch(b[:index], 0) }
|
|
85
79
|
end
|
|
86
80
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
81
|
+
# Cross-DB bucket index expression.
|
|
82
|
+
# PostgreSQL: CAST((EXTRACT(EPOCH FROM col) - start) / interval AS INTEGER)
|
|
83
|
+
# SQLite: CAST((CAST(strftime('%s', col) AS INTEGER) - start) / interval AS INTEGER)
|
|
84
|
+
# MySQL: CAST((UNIX_TIMESTAMP(col) - start) / interval AS SIGNED)
|
|
85
|
+
def bucket_index_expr(column, start_epoch, interval_seconds)
|
|
86
|
+
if adapter?('sqlite')
|
|
87
|
+
"CAST((CAST(strftime('%s', #{column}) AS INTEGER) - #{start_epoch}) / #{interval_seconds} AS INTEGER)"
|
|
88
|
+
elsif adapter?('mysql')
|
|
89
|
+
"CAST((UNIX_TIMESTAMP(#{column}) - #{start_epoch}) / #{interval_seconds} AS SIGNED)"
|
|
90
|
+
else
|
|
91
|
+
"CAST((EXTRACT(EPOCH FROM #{column}) - #{start_epoch}) / #{interval_seconds} AS INTEGER)"
|
|
95
92
|
end
|
|
93
|
+
end
|
|
96
94
|
|
|
97
|
-
|
|
95
|
+
def adapter?(name)
|
|
96
|
+
ActiveRecord::Base.connection.adapter_name.downcase.include?(name)
|
|
98
97
|
end
|
|
99
98
|
end
|
|
100
99
|
end
|
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
module SolidQueueMonitor
|
|
4
4
|
class StatsCalculator
|
|
5
5
|
def self.calculate
|
|
6
|
+
scheduled = SolidQueue::ScheduledExecution.count
|
|
7
|
+
ready = SolidQueue::ReadyExecution.count
|
|
8
|
+
failed = SolidQueue::FailedExecution.count
|
|
9
|
+
in_progress = SolidQueue::ClaimedExecution.count
|
|
10
|
+
recurring = SolidQueue::RecurringTask.count
|
|
11
|
+
|
|
6
12
|
{
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
completed: SolidQueue::Job.where.not(finished_at: nil).count,
|
|
14
|
-
recurring: SolidQueue::RecurringTask.count
|
|
13
|
+
active_jobs: ready + scheduled + in_progress + failed,
|
|
14
|
+
scheduled: scheduled,
|
|
15
|
+
ready: ready,
|
|
16
|
+
failed: failed,
|
|
17
|
+
in_progress: in_progress,
|
|
18
|
+
recurring: recurring
|
|
15
19
|
}
|
|
16
20
|
end
|
|
17
21
|
end
|
|
@@ -7,9 +7,13 @@ SolidQueueMonitor.setup do |config|
|
|
|
7
7
|
|
|
8
8
|
# Set the username for HTTP Basic Authentication (only used if authentication is enabled)
|
|
9
9
|
# config.username = 'admin'
|
|
10
|
+
# config.username = ENV['SOLID_QUEUE_MONITOR_USERNAME']
|
|
11
|
+
# config.username = -> { Rails.application.credentials.dig(:solid_queue_monitor, :username) }
|
|
10
12
|
|
|
11
13
|
# Set the password for HTTP Basic Authentication (only used if authentication is enabled)
|
|
12
14
|
# config.password = 'password'
|
|
15
|
+
# config.password = ENV['SOLID_QUEUE_MONITOR_PASSWORD']
|
|
16
|
+
# config.password = -> { Rails.application.credentials.dig(:solid_queue_monitor, :password) }
|
|
13
17
|
|
|
14
18
|
# Number of jobs to display per page
|
|
15
19
|
# config.jobs_per_page = 25
|
|
@@ -20,4 +24,7 @@ SolidQueueMonitor.setup do |config|
|
|
|
20
24
|
|
|
21
25
|
# Auto-refresh interval in seconds (default: 30)
|
|
22
26
|
# config.auto_refresh_interval = 30
|
|
27
|
+
|
|
28
|
+
# Disable the chart on the overview page to skip chart queries entirely.
|
|
29
|
+
# config.show_chart = true
|
|
23
30
|
end
|
data/lib/solid_queue_monitor.rb
CHANGED
|
@@ -6,8 +6,23 @@ require_relative 'solid_queue_monitor/engine'
|
|
|
6
6
|
module SolidQueueMonitor
|
|
7
7
|
class Error < StandardError; end
|
|
8
8
|
class << self
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
attr_writer :username, :password
|
|
10
|
+
attr_accessor :jobs_per_page, :authentication_enabled,
|
|
11
|
+
:auto_refresh_enabled, :auto_refresh_interval, :show_chart
|
|
12
|
+
|
|
13
|
+
def username
|
|
14
|
+
resolve_value(@username)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def password
|
|
18
|
+
resolve_value(@password)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def resolve_value(value)
|
|
24
|
+
value.respond_to?(:call) ? value.call : value
|
|
25
|
+
end
|
|
11
26
|
end
|
|
12
27
|
|
|
13
28
|
@username = 'admin'
|
|
@@ -16,6 +31,7 @@ module SolidQueueMonitor
|
|
|
16
31
|
@authentication_enabled = false
|
|
17
32
|
@auto_refresh_enabled = true
|
|
18
33
|
@auto_refresh_interval = 30 # seconds
|
|
34
|
+
@show_chart = true
|
|
19
35
|
|
|
20
36
|
def self.setup
|
|
21
37
|
yield self
|