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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 385e4d7ff7dc636b9448cefef256495f8c7aefd2e3318c57a6637c69d4cb5c1e
4
- data.tar.gz: c4f7d1ee427a0cd5255b345bcff159fc88f3120b893fe103fb94701c8a33f446
3
+ metadata.gz: 80a07917e201c33bfb207f22490ca2a3afdeb22a8c842b1a33f3c187afc4f28e
4
+ data.tar.gz: d773d2950d4be0f2b3852512a0bfd2e450206d5a6562eef4fa926cff7e7b32c5
5
5
  SHA512:
6
- metadata.gz: 29410fb9b7d8dfba02eb65f673462c822fdef643701c637fa7531aa11cac3e5ce9530146127b32a68f2128666e71f23fd9ecd2df8de924752193274393b03b18
7
- data.tar.gz: c01b1718f58067a0488d75f359d33c6ef1802105f528410f03ac52b17f1944adcc767fc7fb46500a5e88f33004497ab980328f5c948eb25d2b477c44e6c19cdf
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
  ![Dashboard Overview - Dark Theme](screenshots/dashboard-dark.png)
45
55
 
56
+ ### Worker Monitoring
57
+
58
+ ![Worker Monitoring](screenshots/workers.png)
59
+
60
+ ### Queue Management
61
+
62
+ ![Queue Management](screenshots/queues.png)
63
+
46
64
  ### Failed Jobs
47
65
 
48
- ![Failed Jobs](screenshots/failed-jobs-2.png)
66
+ ![Failed Jobs](screenshots/failed-jobs.png)
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', '~> 0.6.0'
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 recent jobs
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
- - **Debugging**: Quickly identify and troubleshoot failed jobs
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
- - **Performance Analysis**: Track job distribution and identify bottlenecks
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>