solid_queue_monitor 1.1.0 → 1.2.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/README.md +14 -1
- 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 +53 -57
- data/app/services/solid_queue_monitor/stats_calculator.rb +12 -8
- data/lib/generators/solid_queue_monitor/templates/initializer.rb +3 -0
- data/lib/solid_queue_monitor/version.rb +1 -1
- data/lib/solid_queue_monitor.rb +2 -1
- 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: bc739f10c21ca234c61c65a5b94d9c548cfc8d332dac0d1db3311d0d113fe035
|
|
4
|
+
data.tar.gz: 4a131b01c6c3e330b46413029278dbcee09e4f6410f1edf44b14da5902ec5b1a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 02b79913620ac69eb63c15b2c92a1ba76f44f981036f977eb72fe684faa28ae2057ea187582e8bbd5cb09c0de9f030be89e07b8e8e8e754ee0ffebd6e7c33f84
|
|
7
|
+
data.tar.gz: 853dc16edd433afa568a7cef1efb78b295d54595a72e1e7c177e7991ff50b3e396d80ab1092754e65972dbe0fa9072e2a4563ab68b4785ebe7b5d77b5373e086
|
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:
|
|
@@ -118,9 +118,22 @@ SolidQueueMonitor.setup do |config|
|
|
|
118
118
|
|
|
119
119
|
# Auto-refresh interval in seconds (default: 30)
|
|
120
120
|
config.auto_refresh_interval = 30
|
|
121
|
+
|
|
122
|
+
# Disable the chart on the overview page to skip chart queries entirely
|
|
123
|
+
# config.show_chart = true
|
|
121
124
|
end
|
|
122
125
|
```
|
|
123
126
|
|
|
127
|
+
### Performance at Scale
|
|
128
|
+
|
|
129
|
+
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.
|
|
130
|
+
|
|
131
|
+
If you don't need the job activity chart, disable it to skip chart queries entirely:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
config.show_chart = false
|
|
135
|
+
```
|
|
136
|
+
|
|
124
137
|
### Authentication
|
|
125
138
|
|
|
126
139
|
By default, Solid Queue Monitor does not require authentication to access the dashboard. This makes it easy to get started in development environments.
|
|
@@ -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,48 @@ 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
|
+
def bucket_index_expr(column, start_epoch, interval_seconds)
|
|
85
|
+
if sqlite?
|
|
86
|
+
"CAST((CAST(strftime('%s', #{column}) AS INTEGER) - #{start_epoch}) / #{interval_seconds} AS INTEGER)"
|
|
87
|
+
else
|
|
88
|
+
"CAST((EXTRACT(EPOCH FROM #{column}) - #{start_epoch}) / #{interval_seconds} AS INTEGER)"
|
|
95
89
|
end
|
|
90
|
+
end
|
|
96
91
|
|
|
97
|
-
|
|
92
|
+
def sqlite?
|
|
93
|
+
ActiveRecord::Base.connection.adapter_name.downcase.include?('sqlite')
|
|
98
94
|
end
|
|
99
95
|
end
|
|
100
96
|
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
|
data/lib/solid_queue_monitor.rb
CHANGED
|
@@ -7,7 +7,7 @@ module SolidQueueMonitor
|
|
|
7
7
|
class Error < StandardError; end
|
|
8
8
|
class << self
|
|
9
9
|
attr_accessor :username, :password, :jobs_per_page, :authentication_enabled,
|
|
10
|
-
:auto_refresh_enabled, :auto_refresh_interval
|
|
10
|
+
:auto_refresh_enabled, :auto_refresh_interval, :show_chart
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
@username = 'admin'
|
|
@@ -16,6 +16,7 @@ module SolidQueueMonitor
|
|
|
16
16
|
@authentication_enabled = false
|
|
17
17
|
@auto_refresh_enabled = true
|
|
18
18
|
@auto_refresh_interval = 30 # seconds
|
|
19
|
+
@show_chart = true
|
|
19
20
|
|
|
20
21
|
def self.setup
|
|
21
22
|
yield self
|