solid_queue_monitor 0.6.0 → 1.0.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 +29 -6
- data/app/controllers/solid_queue_monitor/jobs_controller.rb +72 -0
- data/app/controllers/solid_queue_monitor/queues_controller.rb +73 -2
- data/app/controllers/solid_queue_monitor/scheduled_jobs_controller.rb +9 -0
- data/app/controllers/solid_queue_monitor/workers_controller.rb +74 -0
- data/app/presenters/solid_queue_monitor/base_presenter.rb +7 -0
- data/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb +3 -7
- data/app/presenters/solid_queue_monitor/in_progress_jobs_presenter.rb +2 -1
- data/app/presenters/solid_queue_monitor/job_details_presenter.rb +696 -0
- data/app/presenters/solid_queue_monitor/jobs_presenter.rb +3 -3
- data/app/presenters/solid_queue_monitor/queue_details_presenter.rb +194 -0
- data/app/presenters/solid_queue_monitor/queues_presenter.rb +1 -1
- data/app/presenters/solid_queue_monitor/ready_jobs_presenter.rb +2 -2
- data/app/presenters/solid_queue_monitor/recurring_jobs_presenter.rb +1 -1
- data/app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb +2 -2
- data/app/presenters/solid_queue_monitor/workers_presenter.rb +319 -0
- data/app/services/solid_queue_monitor/html_generator.rb +2 -1
- data/app/services/solid_queue_monitor/stylesheet_generator.rb +249 -0
- data/config/routes.rb +7 -0
- data/lib/solid_queue_monitor/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 80a07917e201c33bfb207f22490ca2a3afdeb22a8c842b1a33f3c187afc4f28e
|
|
4
|
+
data.tar.gz: d773d2950d4be0f2b3852512a0bfd2e450206d5a6562eef4fa926cff7e7b32c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fedc69f3858f7b5834ac3c0cbad61693fd1428800f3ff5a6d9be0d74473adc2695105594c2633168e42f328ec6f2298d15ea81e5e3dbf811984ce00c54a360b1
|
|
7
|
+
data.tar.gz: 41095950dce1063081f219558f8297e58c0346ffe034d36c991d51433697165465756e1468ebbcab25cff48b6337f65908f5968eea64fc5fc1714877b1f76354
|
data/README.md
CHANGED
|
@@ -18,6 +18,16 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
|
|
|
18
18
|
- **Dashboard Overview**: Get a quick snapshot of your queue's health with statistics on all job types
|
|
19
19
|
- **Job Activity Chart**: Visual line chart showing jobs created, completed, and failed over time with 9 time range options (15m to 1 week)
|
|
20
20
|
- **Dark Theme**: Toggle between light and dark themes with system preference detection and localStorage persistence
|
|
21
|
+
- **Worker Monitoring**: Real-time view of all Solid Queue processes (workers, dispatchers, schedulers)
|
|
22
|
+
- Health status indicators (healthy, stale, dead) based on heartbeat
|
|
23
|
+
- Shows queues each worker is processing and jobs currently being executed
|
|
24
|
+
- Prune dead processes with one click
|
|
25
|
+
- **Job Details Page**: Dedicated page for viewing complete job information
|
|
26
|
+
- Full job timeline showing created, scheduled, started, and finished states
|
|
27
|
+
- Timing breakdown with queue wait time and execution duration
|
|
28
|
+
- Complete error details with backtrace for failed jobs
|
|
29
|
+
- Job arguments displayed in formatted JSON
|
|
30
|
+
- **Queue Details Page**: Detailed view for individual queues with job counts and filtering
|
|
21
31
|
- **Ready Jobs**: View jobs that are ready to be executed
|
|
22
32
|
- **In Progress Jobs**: Monitor jobs currently being processed by workers
|
|
23
33
|
- **Scheduled Jobs**: See upcoming jobs scheduled for future execution with ability to execute immediately or reject permanently
|
|
@@ -43,16 +53,24 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
|
|
|
43
53
|
|
|
44
54
|

|
|
45
55
|
|
|
56
|
+
### Worker Monitoring
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
### Queue Management
|
|
61
|
+
|
|
62
|
+

|
|
63
|
+
|
|
46
64
|
### Failed Jobs
|
|
47
65
|
|
|
48
|
-

|
|
49
67
|
|
|
50
68
|
## Installation
|
|
51
69
|
|
|
52
70
|
Add this line to your application's Gemfile:
|
|
53
71
|
|
|
54
72
|
```ruby
|
|
55
|
-
gem 'solid_queue_monitor', '~>
|
|
73
|
+
gem 'solid_queue_monitor', '~> 1.0'
|
|
56
74
|
```
|
|
57
75
|
|
|
58
76
|
Then execute:
|
|
@@ -116,12 +134,15 @@ After installation, visit `/solid_queue` in your browser to access the dashboard
|
|
|
116
134
|
|
|
117
135
|
The dashboard provides several views:
|
|
118
136
|
|
|
119
|
-
- **Overview**: Shows statistics and
|
|
137
|
+
- **Overview**: Shows statistics, recent jobs, and job activity chart
|
|
120
138
|
- **Ready Jobs**: Jobs that are ready to be executed
|
|
121
139
|
- **Scheduled Jobs**: Jobs scheduled for future execution with execute and reject actions
|
|
122
140
|
- **Recurring Jobs**: Jobs that run on a recurring schedule
|
|
123
141
|
- **Failed Jobs**: Jobs that have failed with error details and retry/discard actions
|
|
124
|
-
- **Queues**: Distribution of jobs across different queues
|
|
142
|
+
- **Queues**: Distribution of jobs across different queues with pause/resume controls
|
|
143
|
+
- **Workers**: Real-time monitoring of all Solid Queue processes with health status
|
|
144
|
+
|
|
145
|
+
Click on any job class name to view detailed information including timeline, timing breakdown, arguments, and error details (for failed jobs).
|
|
125
146
|
|
|
126
147
|
### API-only Applications
|
|
127
148
|
|
|
@@ -141,9 +162,11 @@ This makes it easy to find specific jobs when debugging issues in your applicati
|
|
|
141
162
|
## Use Cases
|
|
142
163
|
|
|
143
164
|
- **Production Monitoring**: Keep an eye on your background job processing in production environments
|
|
144
|
-
- **
|
|
165
|
+
- **Worker Health Monitoring**: Track the health of your Solid Queue processes and identify dead workers
|
|
166
|
+
- **Debugging**: Quickly identify and troubleshoot failed jobs with detailed error information and backtraces
|
|
145
167
|
- **Job Management**: Execute scheduled jobs on demand or reject unwanted jobs permanently
|
|
146
|
-
- **
|
|
168
|
+
- **Incident Response**: Pause queues during incidents to prevent job processing while investigating issues
|
|
169
|
+
- **Performance Analysis**: Track job distribution, timing metrics, and identify bottlenecks
|
|
147
170
|
- **DevOps Integration**: Easily integrate with your monitoring stack
|
|
148
171
|
|
|
149
172
|
## Compatibility
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueMonitor
|
|
4
|
+
class JobsController < BaseController
|
|
5
|
+
def show
|
|
6
|
+
@job = SolidQueue::Job.find_by(id: params[:id])
|
|
7
|
+
|
|
8
|
+
unless @job
|
|
9
|
+
set_flash_message('Job not found.', 'error')
|
|
10
|
+
redirect_to root_path
|
|
11
|
+
return
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
job_data = load_job_data(@job)
|
|
15
|
+
|
|
16
|
+
render_page("Job ##{@job.id}", SolidQueueMonitor::JobDetailsPresenter.new(
|
|
17
|
+
@job,
|
|
18
|
+
**job_data
|
|
19
|
+
).render)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def load_job_data(job)
|
|
25
|
+
{
|
|
26
|
+
failed_execution: SolidQueue::FailedExecution.find_by(job_id: job.id),
|
|
27
|
+
claimed_execution: load_claimed_execution(job),
|
|
28
|
+
scheduled_execution: SolidQueue::ScheduledExecution.find_by(job_id: job.id),
|
|
29
|
+
recent_executions: load_recent_executions(job),
|
|
30
|
+
back_path: determine_back_path
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def load_claimed_execution(job)
|
|
35
|
+
claimed = SolidQueue::ClaimedExecution.find_by(job_id: job.id)
|
|
36
|
+
return nil unless claimed
|
|
37
|
+
|
|
38
|
+
# Preload process info
|
|
39
|
+
claimed.instance_variable_set(:@process, SolidQueue::Process.find_by(id: claimed.process_id))
|
|
40
|
+
claimed
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def load_recent_executions(job)
|
|
44
|
+
SolidQueue::Job
|
|
45
|
+
.where(class_name: job.class_name)
|
|
46
|
+
.where.not(id: job.id)
|
|
47
|
+
.order(created_at: :desc)
|
|
48
|
+
.limit(10)
|
|
49
|
+
.includes(:failed_execution, :claimed_execution, :ready_execution, :scheduled_execution)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def determine_back_path
|
|
53
|
+
referer = request.referer
|
|
54
|
+
return root_path unless referer
|
|
55
|
+
|
|
56
|
+
# Extract path from referer
|
|
57
|
+
uri = URI.parse(referer)
|
|
58
|
+
path = uri.path
|
|
59
|
+
|
|
60
|
+
# Return referer if it's within the engine
|
|
61
|
+
if path.include?('/failed_jobs') || path.include?('/ready_jobs') ||
|
|
62
|
+
path.include?('/scheduled_jobs') || path.include?('/in_progress_jobs') ||
|
|
63
|
+
path.include?('/recurring_jobs')
|
|
64
|
+
referer
|
|
65
|
+
else
|
|
66
|
+
root_path
|
|
67
|
+
end
|
|
68
|
+
rescue URI::InvalidURIError
|
|
69
|
+
root_path
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -11,12 +11,36 @@ module SolidQueueMonitor
|
|
|
11
11
|
render_page('Queues', SolidQueueMonitor::QueuesPresenter.new(@queues, @paused_queues).render)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def show
|
|
15
|
+
@queue_name = params[:queue_name]
|
|
16
|
+
@paused = QueuePauseService.paused_queues.include?(@queue_name)
|
|
17
|
+
|
|
18
|
+
# Get all jobs for this queue with filtering and pagination
|
|
19
|
+
base_query = SolidQueue::Job.where(queue_name: @queue_name).order(created_at: :desc)
|
|
20
|
+
filtered_query = filter_queue_jobs(base_query)
|
|
21
|
+
@jobs = paginate(filtered_query)
|
|
22
|
+
preload_job_statuses(@jobs[:records])
|
|
23
|
+
|
|
24
|
+
@counts = calculate_queue_counts(@queue_name)
|
|
25
|
+
|
|
26
|
+
render_page("Queue: #{@queue_name}",
|
|
27
|
+
SolidQueueMonitor::QueueDetailsPresenter.new(
|
|
28
|
+
queue_name: @queue_name,
|
|
29
|
+
paused: @paused,
|
|
30
|
+
jobs: @jobs[:records],
|
|
31
|
+
counts: @counts,
|
|
32
|
+
current_page: @jobs[:current_page],
|
|
33
|
+
total_pages: @jobs[:total_pages],
|
|
34
|
+
filters: queue_filter_params
|
|
35
|
+
).render)
|
|
36
|
+
end
|
|
37
|
+
|
|
14
38
|
def pause
|
|
15
39
|
queue_name = params[:queue_name]
|
|
16
40
|
result = QueuePauseService.new(queue_name).pause
|
|
17
41
|
|
|
18
42
|
set_flash_message(result[:message], result[:success] ? 'success' : 'error')
|
|
19
|
-
redirect_to queues_path
|
|
43
|
+
redirect_to params[:redirect_to] || queues_path
|
|
20
44
|
end
|
|
21
45
|
|
|
22
46
|
def resume
|
|
@@ -24,7 +48,54 @@ module SolidQueueMonitor
|
|
|
24
48
|
result = QueuePauseService.new(queue_name).resume
|
|
25
49
|
|
|
26
50
|
set_flash_message(result[:message], result[:success] ? 'success' : 'error')
|
|
27
|
-
redirect_to queues_path
|
|
51
|
+
redirect_to params[:redirect_to] || queues_path
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def calculate_queue_counts(queue_name)
|
|
57
|
+
{
|
|
58
|
+
total: SolidQueue::Job.where(queue_name: queue_name).count,
|
|
59
|
+
ready: SolidQueue::ReadyExecution.where(queue_name: queue_name).count,
|
|
60
|
+
scheduled: SolidQueue::ScheduledExecution.where(queue_name: queue_name).count,
|
|
61
|
+
in_progress: SolidQueue::ClaimedExecution.joins(:job).where(solid_queue_jobs: { queue_name: queue_name }).count,
|
|
62
|
+
failed: SolidQueue::FailedExecution.joins(:job).where(solid_queue_jobs: { queue_name: queue_name }).count,
|
|
63
|
+
completed: SolidQueue::Job.where(queue_name: queue_name).where.not(finished_at: nil).count
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def filter_queue_jobs(relation)
|
|
68
|
+
relation = relation.where('class_name LIKE ?', "%#{params[:class_name]}%") if params[:class_name].present?
|
|
69
|
+
relation = filter_by_arguments(relation) if params[:arguments].present?
|
|
70
|
+
|
|
71
|
+
if params[:status].present?
|
|
72
|
+
case params[:status]
|
|
73
|
+
when 'completed'
|
|
74
|
+
relation = relation.where.not(finished_at: nil)
|
|
75
|
+
when 'failed'
|
|
76
|
+
failed_job_ids = SolidQueue::FailedExecution.pluck(:job_id)
|
|
77
|
+
relation = relation.where(id: failed_job_ids)
|
|
78
|
+
when 'scheduled'
|
|
79
|
+
scheduled_job_ids = SolidQueue::ScheduledExecution.pluck(:job_id)
|
|
80
|
+
relation = relation.where(id: scheduled_job_ids)
|
|
81
|
+
when 'pending'
|
|
82
|
+
ready_job_ids = SolidQueue::ReadyExecution.pluck(:job_id)
|
|
83
|
+
relation = relation.where(id: ready_job_ids)
|
|
84
|
+
when 'in_progress'
|
|
85
|
+
claimed_job_ids = SolidQueue::ClaimedExecution.pluck(:job_id)
|
|
86
|
+
relation = relation.where(id: claimed_job_ids)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
relation
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def queue_filter_params
|
|
94
|
+
{
|
|
95
|
+
class_name: params[:class_name],
|
|
96
|
+
arguments: params[:arguments],
|
|
97
|
+
status: params[:status]
|
|
98
|
+
}
|
|
28
99
|
end
|
|
29
100
|
end
|
|
30
101
|
end
|
|
@@ -22,6 +22,15 @@ module SolidQueueMonitor
|
|
|
22
22
|
redirect_to scheduled_jobs_path
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
def execute
|
|
26
|
+
SolidQueueMonitor::ExecuteJobService.new.call(params[:id])
|
|
27
|
+
set_flash_message('Job moved to ready queue', 'success')
|
|
28
|
+
redirect_to params[:redirect_to] || scheduled_jobs_path
|
|
29
|
+
rescue ActiveRecord::RecordNotFound
|
|
30
|
+
set_flash_message('Job not found', 'error')
|
|
31
|
+
redirect_to scheduled_jobs_path
|
|
32
|
+
end
|
|
33
|
+
|
|
25
34
|
def reject_all
|
|
26
35
|
result = SolidQueueMonitor::RejectJobService.new.reject_many(params[:job_ids])
|
|
27
36
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueMonitor
|
|
4
|
+
class WorkersController < BaseController
|
|
5
|
+
def index
|
|
6
|
+
base_query = SolidQueue::Process.order(created_at: :desc)
|
|
7
|
+
filtered_query = filter_workers(base_query)
|
|
8
|
+
@processes = paginate(filtered_query)
|
|
9
|
+
|
|
10
|
+
render_page('Workers', SolidQueueMonitor::WorkersPresenter.new(
|
|
11
|
+
@processes[:records],
|
|
12
|
+
current_page: @processes[:current_page],
|
|
13
|
+
total_pages: @processes[:total_pages],
|
|
14
|
+
filters: worker_filter_params
|
|
15
|
+
).render)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def remove
|
|
19
|
+
process = SolidQueue::Process.find_by(id: params[:id])
|
|
20
|
+
|
|
21
|
+
if process
|
|
22
|
+
process.destroy
|
|
23
|
+
set_flash_message('Process removed successfully.', 'success')
|
|
24
|
+
else
|
|
25
|
+
set_flash_message('Process not found.', 'error')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
redirect_to workers_path
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def prune
|
|
32
|
+
dead_threshold = 10.minutes.ago
|
|
33
|
+
dead_processes = SolidQueue::Process.where(last_heartbeat_at: ..dead_threshold)
|
|
34
|
+
count = dead_processes.count
|
|
35
|
+
|
|
36
|
+
if count.positive?
|
|
37
|
+
dead_processes.destroy_all
|
|
38
|
+
set_flash_message("Successfully removed #{count} dead process#{'es' if count > 1}.", 'success')
|
|
39
|
+
else
|
|
40
|
+
set_flash_message('No dead processes to remove.', 'success')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
redirect_to workers_path
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def filter_workers(relation)
|
|
49
|
+
relation = relation.where(kind: params[:kind]) if params[:kind].present?
|
|
50
|
+
relation = relation.where('hostname LIKE ?', "%#{params[:hostname]}%") if params[:hostname].present?
|
|
51
|
+
|
|
52
|
+
if params[:status].present?
|
|
53
|
+
case params[:status]
|
|
54
|
+
when 'healthy'
|
|
55
|
+
relation = relation.where('last_heartbeat_at > ?', 5.minutes.ago)
|
|
56
|
+
when 'stale'
|
|
57
|
+
relation = relation.where('last_heartbeat_at <= ? AND last_heartbeat_at > ?', 5.minutes.ago, 10.minutes.ago)
|
|
58
|
+
when 'dead'
|
|
59
|
+
relation = relation.where(last_heartbeat_at: ..10.minutes.ago)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
relation
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def worker_filter_params
|
|
67
|
+
{
|
|
68
|
+
kind: params[:kind],
|
|
69
|
+
hostname: params[:hostname],
|
|
70
|
+
status: params[:status]
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -111,6 +111,13 @@ module SolidQueueMonitor
|
|
|
111
111
|
"<code>#{formatted}</code>"
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
+
def queue_link(queue_name, css_class: nil)
|
|
115
|
+
return '-' if queue_name.blank?
|
|
116
|
+
|
|
117
|
+
classes = ['queue-link', css_class].compact.join(' ')
|
|
118
|
+
"<a href=\"#{queue_details_path(queue_name: queue_name)}\" class=\"#{classes}\">#{queue_name}</a>"
|
|
119
|
+
end
|
|
120
|
+
|
|
114
121
|
def request_path
|
|
115
122
|
if defined?(controller) && controller.respond_to?(:request)
|
|
116
123
|
controller.request.path
|
|
@@ -251,23 +251,19 @@ module SolidQueueMonitor
|
|
|
251
251
|
<tr>
|
|
252
252
|
<td><input type="checkbox" class="job-checkbox" value="#{failed_execution.id}"></td>
|
|
253
253
|
<td>
|
|
254
|
-
<div class="job-class">#{job.class_name}</div>
|
|
254
|
+
<div class="job-class"><a href="#{job_path(job)}" class="job-class-link">#{job.class_name}</a></div>
|
|
255
255
|
<div class="job-meta">
|
|
256
256
|
<span class="job-timestamp">Queued at: #{format_datetime(job.created_at)}</span>
|
|
257
257
|
</div>
|
|
258
258
|
</td>
|
|
259
259
|
<td>
|
|
260
|
-
<div class="job-queue">#{job.queue_name}</div>
|
|
260
|
+
<div class="job-queue">#{queue_link(job.queue_name)}</div>
|
|
261
261
|
</td>
|
|
262
262
|
<td>
|
|
263
|
-
<div class="error-message">#{error[:message]}</div>
|
|
263
|
+
<div class="error-message">#{error[:message].to_s.truncate(100)}</div>
|
|
264
264
|
<div class="job-meta">
|
|
265
265
|
<span class="job-timestamp">Failed at: #{format_datetime(failed_execution.created_at)}</span>
|
|
266
266
|
</div>
|
|
267
|
-
<details>
|
|
268
|
-
<summary>Backtrace</summary>
|
|
269
|
-
<pre class="error-backtrace">#{error[:backtrace]}</pre>
|
|
270
|
-
</details>
|
|
271
267
|
</td>
|
|
272
268
|
<td>#{format_arguments(job.arguments)}</td>
|
|
273
269
|
<td class="actions-cell">
|
|
@@ -67,11 +67,12 @@ module SolidQueueMonitor
|
|
|
67
67
|
<<-HTML
|
|
68
68
|
<tr>
|
|
69
69
|
<td>
|
|
70
|
-
<div class="job-class">#{job.class_name}</div>
|
|
70
|
+
<div class="job-class"><a href="#{job_path(job)}" class="job-class-link">#{job.class_name}</a></div>
|
|
71
71
|
<div class="job-meta">
|
|
72
72
|
<span class="job-timestamp">Queued at: #{format_datetime(job.created_at)}</span>
|
|
73
73
|
</div>
|
|
74
74
|
</td>
|
|
75
|
+
<td>#{queue_link(job.queue_name)}</td>
|
|
75
76
|
<td>#{format_arguments(job.arguments)}</td>
|
|
76
77
|
<td>#{format_datetime(execution.created_at)}</td>
|
|
77
78
|
<td>#{execution.process_id}</td>
|