solid_queue_monitor 0.5.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.
Files changed (25) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +37 -8
  3. data/app/controllers/solid_queue_monitor/jobs_controller.rb +72 -0
  4. data/app/controllers/solid_queue_monitor/overview_controller.rb +11 -0
  5. data/app/controllers/solid_queue_monitor/queues_controller.rb +73 -2
  6. data/app/controllers/solid_queue_monitor/scheduled_jobs_controller.rb +9 -0
  7. data/app/controllers/solid_queue_monitor/workers_controller.rb +74 -0
  8. data/app/presenters/solid_queue_monitor/base_presenter.rb +7 -0
  9. data/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb +3 -7
  10. data/app/presenters/solid_queue_monitor/in_progress_jobs_presenter.rb +2 -1
  11. data/app/presenters/solid_queue_monitor/job_details_presenter.rb +696 -0
  12. data/app/presenters/solid_queue_monitor/jobs_presenter.rb +3 -3
  13. data/app/presenters/solid_queue_monitor/queue_details_presenter.rb +194 -0
  14. data/app/presenters/solid_queue_monitor/queues_presenter.rb +1 -1
  15. data/app/presenters/solid_queue_monitor/ready_jobs_presenter.rb +2 -2
  16. data/app/presenters/solid_queue_monitor/recurring_jobs_presenter.rb +1 -1
  17. data/app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb +2 -2
  18. data/app/presenters/solid_queue_monitor/workers_presenter.rb +319 -0
  19. data/app/services/solid_queue_monitor/chart_data_service.rb +100 -0
  20. data/app/services/solid_queue_monitor/chart_presenter.rb +239 -0
  21. data/app/services/solid_queue_monitor/html_generator.rb +169 -8
  22. data/app/services/solid_queue_monitor/stylesheet_generator.rb +650 -33
  23. data/config/routes.rb +9 -0
  24. data/lib/solid_queue_monitor/version.rb +1 -1
  25. metadata +8 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2597291a879f3f6720e783c6aae3fa203434a67652ba24c5e65a6fdd87081120
4
- data.tar.gz: d01e77ac3c412bf2fcdab1c4f1a02aa072786f7d88ec42798758615159ddc67c
3
+ metadata.gz: 80a07917e201c33bfb207f22490ca2a3afdeb22a8c842b1a33f3c187afc4f28e
4
+ data.tar.gz: d773d2950d4be0f2b3852512a0bfd2e450206d5a6562eef4fa926cff7e7b32c5
5
5
  SHA512:
6
- metadata.gz: 9f95324749998dd5c7a6e0598a25edd14f82bbc32610499dcf688592729f4cbf805bf3ac8da8ad3f776c0dc3231ebaa6fa710fac5537806553cc3cc50f3a7d66
7
- data.tar.gz: be751aaa21b617d1ba1f09eb473a9bbd83eeccfbe79c918040fe2df420c76258b012fad5c26eefa1aaaafe50e62fdf6d566b08469042006a501d9f354cd49658
6
+ metadata.gz: fedc69f3858f7b5834ac3c0cbad61693fd1428800f3ff5a6d9be0d74473adc2695105594c2633168e42f328ec6f2298d15ea81e5e3dbf811984ce00c54a360b1
7
+ data.tar.gz: 41095950dce1063081f219558f8297e58c0346ffe034d36c991d51433697165465756e1468ebbcab25cff48b6337f65908f5968eea64fc5fc1714877b1f76354
data/README.md CHANGED
@@ -16,6 +16,18 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
16
16
  ## Features
17
17
 
18
18
  - **Dashboard Overview**: Get a quick snapshot of your queue's health with statistics on all job types
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
+ - **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
19
31
  - **Ready Jobs**: View jobs that are ready to be executed
20
32
  - **In Progress Jobs**: Monitor jobs currently being processed by workers
21
33
  - **Scheduled Jobs**: See upcoming jobs scheduled for future execution with ability to execute immediately or reject permanently
@@ -33,20 +45,32 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
33
45
 
34
46
  ## Screenshots
35
47
 
36
- ### Dashboard Overview
48
+ ### Dashboard Overview (Light Theme)
37
49
 
38
- ![Dashboard Overview](screenshots/dashboard-3.png)
50
+ ![Dashboard Overview - Light Theme](screenshots/dashboard-light.png)
51
+
52
+ ### Dashboard Overview (Dark Theme)
53
+
54
+ ![Dashboard Overview - Dark Theme](screenshots/dashboard-dark.png)
55
+
56
+ ### Worker Monitoring
57
+
58
+ ![Worker Monitoring](screenshots/workers.png)
59
+
60
+ ### Queue Management
61
+
62
+ ![Queue Management](screenshots/queues.png)
39
63
 
40
64
  ### Failed Jobs
41
65
 
42
- ![Failed Jobs](screenshots/failed-jobs-2.png)
66
+ ![Failed Jobs](screenshots/failed-jobs.png)
43
67
 
44
68
  ## Installation
45
69
 
46
70
  Add this line to your application's Gemfile:
47
71
 
48
72
  ```ruby
49
- gem 'solid_queue_monitor', '~> 0.4.0'
73
+ gem 'solid_queue_monitor', '~> 1.0'
50
74
  ```
51
75
 
52
76
  Then execute:
@@ -110,12 +134,15 @@ After installation, visit `/solid_queue` in your browser to access the dashboard
110
134
 
111
135
  The dashboard provides several views:
112
136
 
113
- - **Overview**: Shows statistics and recent jobs
137
+ - **Overview**: Shows statistics, recent jobs, and job activity chart
114
138
  - **Ready Jobs**: Jobs that are ready to be executed
115
139
  - **Scheduled Jobs**: Jobs scheduled for future execution with execute and reject actions
116
140
  - **Recurring Jobs**: Jobs that run on a recurring schedule
117
141
  - **Failed Jobs**: Jobs that have failed with error details and retry/discard actions
118
- - **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).
119
146
 
120
147
  ### API-only Applications
121
148
 
@@ -135,9 +162,11 @@ This makes it easy to find specific jobs when debugging issues in your applicati
135
162
  ## Use Cases
136
163
 
137
164
  - **Production Monitoring**: Keep an eye on your background job processing in production environments
138
- - **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
139
167
  - **Job Management**: Execute scheduled jobs on demand or reject unwanted jobs permanently
140
- - **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
141
170
  - **DevOps Integration**: Easily integrate with your monitoring stack
142
171
 
143
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
@@ -4,6 +4,7 @@ module SolidQueueMonitor
4
4
  class OverviewController < BaseController
5
5
  def index
6
6
  @stats = SolidQueueMonitor::StatsCalculator.calculate
7
+ @chart_data = SolidQueueMonitor::ChartDataService.new(time_range: time_range_param).calculate
7
8
 
8
9
  recent_jobs_query = SolidQueue::Job.order(created_at: :desc).limit(100)
9
10
  @recent_jobs = paginate(filter_jobs(recent_jobs_query))
@@ -13,10 +14,20 @@ module SolidQueueMonitor
13
14
  render_page('Overview', generate_overview_content)
14
15
  end
15
16
 
17
+ def chart_data
18
+ chart_data = SolidQueueMonitor::ChartDataService.new(time_range: time_range_param).calculate
19
+ render json: chart_data
20
+ end
21
+
16
22
  private
17
23
 
24
+ def time_range_param
25
+ params[:time_range] || ChartDataService::DEFAULT_TIME_RANGE
26
+ end
27
+
18
28
  def generate_overview_content
19
29
  SolidQueueMonitor::StatsPresenter.new(@stats).render +
30
+ SolidQueueMonitor::ChartPresenter.new(@chart_data).render +
20
31
  SolidQueueMonitor::JobsPresenter.new(@recent_jobs[:records],
21
32
  current_page: @recent_jobs[:current_page],
22
33
  total_pages: @recent_jobs[:total_pages],
@@ -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>