solid_queue_heroku_autoscaler 0.1.0 → 0.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.
@@ -0,0 +1,138 @@
1
+ <h2 class="mb-3">Dashboard Overview</h2>
2
+
3
+ <!-- Stats Row -->
4
+ <div class="grid grid-4 mb-3">
5
+ <div class="card stat-card info">
6
+ <div class="value"><%= @status.values.sum { |w| w[:current_workers] } %></div>
7
+ <div class="label">Total Workers</div>
8
+ </div>
9
+ <div class="card stat-card">
10
+ <div class="value"><%= @status.values.sum { |w| w[:metrics][:queue_depth] } %></div>
11
+ <div class="label">Queue Depth</div>
12
+ </div>
13
+ <div class="card stat-card success">
14
+ <div class="value"><%= @stats[:scale_up_count] || 0 %></div>
15
+ <div class="label">Scale Ups (24h)</div>
16
+ </div>
17
+ <div class="card stat-card warning">
18
+ <div class="value"><%= @stats[:scale_down_count] || 0 %></div>
19
+ <div class="label">Scale Downs (24h)</div>
20
+ </div>
21
+ </div>
22
+
23
+ <!-- Workers Overview -->
24
+ <div class="grid grid-2 mb-3">
25
+ <% @status.each do |name, worker| %>
26
+ <div class="card">
27
+ <h3>
28
+ <% if worker[:enabled] %>
29
+ <span class="badge badge-success">Active</span>
30
+ <% else %>
31
+ <span class="badge badge-neutral">Disabled</span>
32
+ <% end %>
33
+ <%= name %>
34
+ <% if worker[:dry_run] %>
35
+ <span class="badge badge-warning">Dry Run</span>
36
+ <% end %>
37
+ </h3>
38
+
39
+ <div class="d-flex justify-between mb-2">
40
+ <div>
41
+ <strong><%= worker[:current_workers] %></strong> / <%= worker[:max_workers] %> workers
42
+ <div class="progress-bar" style="width: 150px;">
43
+ <% pct = (worker[:current_workers].to_f / worker[:max_workers] * 100).round %>
44
+ <div class="fill" style="width: <%= pct %>%; background: <%= pct > 80 ? 'var(--warning)' : 'var(--success)' %>;"></div>
45
+ </div>
46
+ </div>
47
+ <div class="text-muted">
48
+ Process: <strong><%= worker[:process_type] %></strong>
49
+ </div>
50
+ </div>
51
+
52
+ <div class="grid grid-3 mt-2">
53
+ <div>
54
+ <div class="text-muted">Queue Depth</div>
55
+ <strong class="<%= worker[:metrics][:queue_depth] > worker[:thresholds][:scale_up_queue_depth] ? 'text-danger' : '' %>">
56
+ <%= worker[:metrics][:queue_depth] %>
57
+ </strong>
58
+ </div>
59
+ <div>
60
+ <div class="text-muted">Latency</div>
61
+ <strong class="<%= worker[:metrics][:latency_seconds] > worker[:thresholds][:scale_up_latency] ? 'text-danger' : '' %>">
62
+ <%= worker[:metrics][:latency_seconds].round %>s
63
+ </strong>
64
+ </div>
65
+ <div>
66
+ <div class="text-muted">Jobs/min</div>
67
+ <strong><%= worker[:metrics][:jobs_per_minute] %></strong>
68
+ </div>
69
+ </div>
70
+
71
+ <div class="mt-2">
72
+ <%= link_to 'View Details', worker_path(name), class: 'btn btn-secondary btn-sm' %>
73
+ </div>
74
+ </div>
75
+ <% end %>
76
+ </div>
77
+
78
+ <!-- Recent Events -->
79
+ <div class="card">
80
+ <div class="d-flex justify-between align-center mb-2">
81
+ <h3>Recent Scale Events</h3>
82
+ <%= link_to 'View All', events_path, class: 'btn btn-secondary btn-sm' %>
83
+ </div>
84
+
85
+ <% if !events_available? %>
86
+ <p class="text-muted">
87
+ Events table not found. Run:
88
+ <code>rails generate solid_queue_heroku_autoscaler:dashboard</code>
89
+ </p>
90
+ <% elsif @recent_events.empty? %>
91
+ <p class="text-muted">No scale events recorded yet.</p>
92
+ <% else %>
93
+ <table>
94
+ <thead>
95
+ <tr>
96
+ <th>Time</th>
97
+ <th>Worker</th>
98
+ <th>Action</th>
99
+ <th>Workers</th>
100
+ <th>Reason</th>
101
+ </tr>
102
+ </thead>
103
+ <tbody>
104
+ <% @recent_events.each do |event| %>
105
+ <tr>
106
+ <td class="time-ago"><%= event.created_at.strftime('%H:%M:%S') %></td>
107
+ <td><%= event.worker_name %></td>
108
+ <td>
109
+ <% case event.action %>
110
+ <% when 'scale_up' %>
111
+ <span class="badge badge-success">↑ Scale Up</span>
112
+ <% when 'scale_down' %>
113
+ <span class="badge badge-warning">↓ Scale Down</span>
114
+ <% when 'no_change' %>
115
+ <span class="badge badge-neutral">— No Change</span>
116
+ <% when 'skipped' %>
117
+ <span class="badge badge-info">⏭ Skipped</span>
118
+ <% when 'error' %>
119
+ <span class="badge badge-danger">✕ Error</span>
120
+ <% end %>
121
+ <% if event.dry_run %>
122
+ <span class="badge badge-neutral">DRY</span>
123
+ <% end %>
124
+ </td>
125
+ <td>
126
+ <% if event.scaled? %>
127
+ <%= event.from_workers %> → <%= event.to_workers %>
128
+ <% else %>
129
+ <%= event.from_workers %>
130
+ <% end %>
131
+ </td>
132
+ <td class="text-muted"><%= truncate(event.reason, length: 60) %></td>
133
+ </tr>
134
+ <% end %>
135
+ </tbody>
136
+ </table>
137
+ <% end %>
138
+ </div>
@@ -0,0 +1,102 @@
1
+ <h2 class="mb-3">Scale Events</h2>
2
+
3
+ <!-- Filter -->
4
+ <div class="card mb-3">
5
+ <div class="d-flex gap-2 align-center">
6
+ <span class="text-muted">Filter by worker:</span>
7
+ <%= link_to 'All', events_path, class: "btn btn-sm #{@worker_filter.nil? ? 'btn-primary' : 'btn-secondary'}" %>
8
+ <% autoscaler_status.keys.each do |name| %>
9
+ <%= link_to name, events_path(worker: name), class: "btn btn-sm #{@worker_filter == name.to_s ? 'btn-primary' : 'btn-secondary'}" %>
10
+ <% end %>
11
+ </div>
12
+ </div>
13
+
14
+ <!-- Stats -->
15
+ <div class="grid grid-4 mb-3">
16
+ <div class="card stat-card">
17
+ <div class="value"><%= @stats[:total] %></div>
18
+ <div class="label">Total Events (24h)</div>
19
+ </div>
20
+ <div class="card stat-card success">
21
+ <div class="value"><%= @stats[:scale_up_count] || 0 %></div>
22
+ <div class="label">Scale Ups</div>
23
+ </div>
24
+ <div class="card stat-card warning">
25
+ <div class="value"><%= @stats[:scale_down_count] || 0 %></div>
26
+ <div class="label">Scale Downs</div>
27
+ </div>
28
+ <div class="card stat-card">
29
+ <div class="value"><%= (@stats[:avg_latency] || 0).round %>s</div>
30
+ <div class="label">Avg Latency</div>
31
+ </div>
32
+ </div>
33
+
34
+ <!-- Events Table -->
35
+ <div class="card">
36
+ <% if !events_available? %>
37
+ <p class="text-muted">
38
+ Events table not found. Run:
39
+ <code>rails generate solid_queue_heroku_autoscaler:dashboard</code>
40
+ </p>
41
+ <% elsif @events.empty? %>
42
+ <p class="text-muted">No scale events recorded yet.</p>
43
+ <% else %>
44
+ <table>
45
+ <thead>
46
+ <tr>
47
+ <th>Time</th>
48
+ <th>Worker</th>
49
+ <th>Action</th>
50
+ <th>Workers</th>
51
+ <th>Queue Depth</th>
52
+ <th>Latency</th>
53
+ <th>Reason</th>
54
+ </tr>
55
+ </thead>
56
+ <tbody>
57
+ <% @events.each do |event| %>
58
+ <tr>
59
+ <td>
60
+ <div><%= event.created_at.strftime('%Y-%m-%d') %></div>
61
+ <div class="time-ago"><%= event.created_at.strftime('%H:%M:%S') %></div>
62
+ </td>
63
+ <td>
64
+ <%= link_to event.worker_name, worker_path(event.worker_name) %>
65
+ </td>
66
+ <td>
67
+ <% case event.action %>
68
+ <% when 'scale_up' %>
69
+ <span class="badge badge-success">↑ Scale Up</span>
70
+ <% when 'scale_down' %>
71
+ <span class="badge badge-warning">↓ Scale Down</span>
72
+ <% when 'no_change' %>
73
+ <span class="badge badge-neutral">— No Change</span>
74
+ <% when 'skipped' %>
75
+ <span class="badge badge-info">⏭ Skipped</span>
76
+ <% when 'error' %>
77
+ <span class="badge badge-danger">✕ Error</span>
78
+ <% end %>
79
+ <% if event.dry_run %>
80
+ <span class="badge badge-neutral">DRY</span>
81
+ <% end %>
82
+ </td>
83
+ <td>
84
+ <% if event.scaled? %>
85
+ <strong><%= event.from_workers %></strong> → <strong><%= event.to_workers %></strong>
86
+ <% delta = event.to_workers - event.from_workers %>
87
+ <span class="<%= delta > 0 ? 'text-success' : 'text-warning' %>">
88
+ (<%= delta > 0 ? '+' : '' %><%= delta %>)
89
+ </span>
90
+ <% else %>
91
+ <%= event.from_workers %>
92
+ <% end %>
93
+ </td>
94
+ <td><%= event.queue_depth %></td>
95
+ <td><%= event.latency_seconds.round %>s</td>
96
+ <td class="text-muted" style="max-width: 300px;"><%= event.reason %></td>
97
+ </tr>
98
+ <% end %>
99
+ </tbody>
100
+ </table>
101
+ <% end %>
102
+ </div>
@@ -0,0 +1,106 @@
1
+ <h2 class="mb-3">Workers</h2>
2
+
3
+ <div class="grid grid-2">
4
+ <% @workers.each do |name, worker| %>
5
+ <div class="card">
6
+ <h3>
7
+ <% if worker[:enabled] %>
8
+ <span class="badge badge-success">Active</span>
9
+ <% else %>
10
+ <span class="badge badge-neutral">Disabled</span>
11
+ <% end %>
12
+ <%= name %>
13
+ <% if worker[:dry_run] %>
14
+ <span class="badge badge-warning">Dry Run</span>
15
+ <% end %>
16
+ </h3>
17
+
18
+ <!-- Worker Status -->
19
+ <div class="d-flex justify-between mb-2">
20
+ <div>
21
+ <strong class="text-info" style="font-size: 1.5rem;"><%= worker[:current_workers] %></strong>
22
+ <span class="text-muted">/ <%= worker[:max_workers] %> workers</span>
23
+ <div class="progress-bar" style="width: 200px;">
24
+ <% pct = (worker[:current_workers].to_f / worker[:max_workers] * 100).round %>
25
+ <div class="fill" style="width: <%= pct %>%; background: <%= pct > 80 ? 'var(--warning)' : 'var(--success)' %>;"></div>
26
+ </div>
27
+ </div>
28
+ </div>
29
+
30
+ <!-- Configuration -->
31
+ <div class="mb-2">
32
+ <div class="text-muted mb-1">Configuration</div>
33
+ <table style="width: auto;">
34
+ <tr>
35
+ <td class="text-muted" style="padding: 4px 12px 4px 0;">Process Type</td>
36
+ <td><code><%= worker[:process_type] %></code></td>
37
+ </tr>
38
+ <tr>
39
+ <td class="text-muted" style="padding: 4px 12px 4px 0;">Queues</td>
40
+ <td><code><%= Array(worker[:queues]).join(', ') %></code></td>
41
+ </tr>
42
+ <tr>
43
+ <td class="text-muted" style="padding: 4px 12px 4px 0;">Strategy</td>
44
+ <td><code><%= worker[:scaling_strategy] %></code></td>
45
+ </tr>
46
+ <tr>
47
+ <td class="text-muted" style="padding: 4px 12px 4px 0;">Workers Range</td>
48
+ <td><%= worker[:min_workers] %> - <%= worker[:max_workers] %></td>
49
+ </tr>
50
+ </table>
51
+ </div>
52
+
53
+ <!-- Metrics -->
54
+ <div class="grid grid-3 mb-2">
55
+ <div>
56
+ <div class="text-muted">Queue Depth</div>
57
+ <strong class="<%= worker[:metrics][:queue_depth] > worker[:thresholds][:scale_up_queue_depth] ? 'text-danger' : '' %>">
58
+ <%= worker[:metrics][:queue_depth] %>
59
+ </strong>
60
+ <div class="text-muted" style="font-size: 0.8rem;">
61
+ threshold: <%= worker[:thresholds][:scale_up_queue_depth] %>
62
+ </div>
63
+ </div>
64
+ <div>
65
+ <div class="text-muted">Latency</div>
66
+ <strong class="<%= worker[:metrics][:latency_seconds] > worker[:thresholds][:scale_up_latency] ? 'text-danger' : '' %>">
67
+ <%= worker[:metrics][:latency_seconds].round %>s
68
+ </strong>
69
+ <div class="text-muted" style="font-size: 0.8rem;">
70
+ threshold: <%= worker[:thresholds][:scale_up_latency] %>s
71
+ </div>
72
+ </div>
73
+ <div>
74
+ <div class="text-muted">Jobs/min</div>
75
+ <strong><%= worker[:metrics][:jobs_per_minute] %></strong>
76
+ </div>
77
+ </div>
78
+
79
+ <!-- Cooldowns -->
80
+ <div class="mb-2">
81
+ <div class="text-muted mb-1">Cooldowns</div>
82
+ <div class="d-flex gap-2">
83
+ <% if worker[:cooldowns][:scale_up_remaining] > 0 %>
84
+ <span class="badge badge-info">
85
+ Scale Up: <%= worker[:cooldowns][:scale_up_remaining] %>s
86
+ </span>
87
+ <% end %>
88
+ <% if worker[:cooldowns][:scale_down_remaining] > 0 %>
89
+ <span class="badge badge-warning">
90
+ Scale Down: <%= worker[:cooldowns][:scale_down_remaining] %>s
91
+ </span>
92
+ <% end %>
93
+ <% if worker[:cooldowns][:scale_up_remaining] == 0 && worker[:cooldowns][:scale_down_remaining] == 0 %>
94
+ <span class="text-muted">No active cooldowns</span>
95
+ <% end %>
96
+ </div>
97
+ </div>
98
+
99
+ <!-- Actions -->
100
+ <div class="d-flex gap-2">
101
+ <%= link_to 'View Details', worker_path(name), class: 'btn btn-secondary btn-sm' %>
102
+ <%= button_to 'Scale Now', scale_worker_path(name), method: :post, class: 'btn btn-primary btn-sm', disabled: !worker[:enabled] %>
103
+ </div>
104
+ </div>
105
+ <% end %>
106
+ </div>
@@ -0,0 +1,209 @@
1
+ <div class="d-flex justify-between align-center mb-3">
2
+ <h2>
3
+ Worker: <%= @worker[:name] %>
4
+ <% if @worker[:enabled] %>
5
+ <span class="badge badge-success">Active</span>
6
+ <% else %>
7
+ <span class="badge badge-neutral">Disabled</span>
8
+ <% end %>
9
+ <% if @worker[:dry_run] %>
10
+ <span class="badge badge-warning">Dry Run</span>
11
+ <% end %>
12
+ </h2>
13
+ <div class="d-flex gap-2">
14
+ <%= link_to '← Back', workers_path, class: 'btn btn-secondary btn-sm' %>
15
+ <%= button_to 'Scale Now', scale_worker_path(@worker[:name]), method: :post, class: 'btn btn-primary btn-sm', disabled: !@worker[:enabled] %>
16
+ </div>
17
+ </div>
18
+
19
+ <div class="grid grid-2 mb-3">
20
+ <!-- Current Status -->
21
+ <div class="card">
22
+ <h3>Current Status</h3>
23
+
24
+ <div class="d-flex justify-between align-center mb-2">
25
+ <div>
26
+ <strong class="text-info" style="font-size: 2rem;"><%= @worker[:current_workers] %></strong>
27
+ <span class="text-muted">/ <%= @worker[:max_workers] %> workers</span>
28
+ </div>
29
+ <div class="text-muted">
30
+ Min: <%= @worker[:min_workers] %> | Max: <%= @worker[:max_workers] %>
31
+ </div>
32
+ </div>
33
+
34
+ <div class="progress-bar">
35
+ <% pct = (@worker[:current_workers].to_f / @worker[:max_workers] * 100).round %>
36
+ <div class="fill" style="width: <%= pct %>%; background: <%= pct > 80 ? 'var(--warning)' : 'var(--success)' %>;"></div>
37
+ </div>
38
+
39
+ <div class="grid grid-2 mt-2">
40
+ <div>
41
+ <div class="text-muted">Process Type</div>
42
+ <code><%= @worker[:process_type] %></code>
43
+ </div>
44
+ <div>
45
+ <div class="text-muted">Strategy</div>
46
+ <code><%= @worker[:scaling_strategy] %></code>
47
+ </div>
48
+ <div>
49
+ <div class="text-muted">Queues</div>
50
+ <code><%= Array(@worker[:queues]).join(', ') %></code>
51
+ </div>
52
+ </div>
53
+ </div>
54
+
55
+ <!-- Metrics -->
56
+ <div class="card">
57
+ <h3>Current Metrics</h3>
58
+
59
+ <div class="grid grid-2">
60
+ <div class="mb-2">
61
+ <div class="text-muted">Queue Depth</div>
62
+ <strong class="<%= @worker[:metrics][:queue_depth] > @worker[:thresholds][:scale_up_queue_depth] ? 'text-danger' : '' %>" style="font-size: 1.5rem;">
63
+ <%= @worker[:metrics][:queue_depth] %>
64
+ </strong>
65
+ <div class="text-muted" style="font-size: 0.8rem;">
66
+ ↑ threshold: <%= @worker[:thresholds][:scale_up_queue_depth] %> | ↓ threshold: <%= @worker[:thresholds][:scale_down_queue_depth] %>
67
+ </div>
68
+ </div>
69
+
70
+ <div class="mb-2">
71
+ <div class="text-muted">Oldest Job Age</div>
72
+ <strong class="<%= @worker[:metrics][:latency_seconds] > @worker[:thresholds][:scale_up_latency] ? 'text-danger' : '' %>" style="font-size: 1.5rem;">
73
+ <%= @worker[:metrics][:latency_seconds].round %>s
74
+ </strong>
75
+ <div class="text-muted" style="font-size: 0.8rem;">
76
+ ↑ threshold: <%= @worker[:thresholds][:scale_up_latency] %>s | ↓ threshold: <%= @worker[:thresholds][:scale_down_latency] %>s
77
+ </div>
78
+ </div>
79
+
80
+ <div>
81
+ <div class="text-muted">Jobs/Minute</div>
82
+ <strong style="font-size: 1.5rem;"><%= @worker[:metrics][:jobs_per_minute] %></strong>
83
+ </div>
84
+
85
+ <div>
86
+ <div class="text-muted">Claimed Jobs</div>
87
+ <strong style="font-size: 1.5rem;"><%= @worker[:metrics][:claimed_jobs] %></strong>
88
+ </div>
89
+
90
+ <div>
91
+ <div class="text-muted">Failed Jobs</div>
92
+ <strong class="<%= @worker[:metrics][:failed_jobs] > 0 ? 'text-danger' : '' %>" style="font-size: 1.5rem;">
93
+ <%= @worker[:metrics][:failed_jobs] %>
94
+ </strong>
95
+ </div>
96
+
97
+ <div>
98
+ <div class="text-muted">Active Workers</div>
99
+ <strong style="font-size: 1.5rem;"><%= @worker[:metrics][:active_workers] %></strong>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- Cooldowns -->
106
+ <div class="card mb-3">
107
+ <h3>Cooldown Status</h3>
108
+
109
+ <div class="grid grid-2">
110
+ <div>
111
+ <div class="text-muted mb-1">Scale Up Cooldown</div>
112
+ <% if @worker[:cooldowns][:scale_up_remaining] > 0 %>
113
+ <span class="badge badge-info">
114
+ <%= @worker[:cooldowns][:scale_up_remaining] %>s remaining
115
+ </span>
116
+ <% else %>
117
+ <span class="text-success">Ready</span>
118
+ <% end %>
119
+ <% if @worker[:cooldowns][:last_scale_up] %>
120
+ <div class="text-muted" style="font-size: 0.8rem;">
121
+ Last: <%= @worker[:cooldowns][:last_scale_up].strftime('%Y-%m-%d %H:%M:%S') %>
122
+ </div>
123
+ <% end %>
124
+ </div>
125
+
126
+ <div>
127
+ <div class="text-muted mb-1">Scale Down Cooldown</div>
128
+ <% if @worker[:cooldowns][:scale_down_remaining] > 0 %>
129
+ <span class="badge badge-warning">
130
+ <%= @worker[:cooldowns][:scale_down_remaining] %>s remaining
131
+ </span>
132
+ <% else %>
133
+ <span class="text-success">Ready</span>
134
+ <% end %>
135
+ <% if @worker[:cooldowns][:last_scale_down] %>
136
+ <div class="text-muted" style="font-size: 0.8rem;">
137
+ Last: <%= @worker[:cooldowns][:last_scale_down].strftime('%Y-%m-%d %H:%M:%S') %>
138
+ </div>
139
+ <% end %>
140
+ </div>
141
+ </div>
142
+ </div>
143
+
144
+ <!-- Recent Events -->
145
+ <div class="card">
146
+ <div class="d-flex justify-between align-center mb-2">
147
+ <h3>Recent Events</h3>
148
+ <%= link_to 'View All', events_path(worker: @worker[:name]), class: 'btn btn-secondary btn-sm' %>
149
+ </div>
150
+
151
+ <% if !events_available? %>
152
+ <p class="text-muted">
153
+ Events table not found. Run:
154
+ <code>rails generate solid_queue_heroku_autoscaler:dashboard</code>
155
+ </p>
156
+ <% elsif @events.empty? %>
157
+ <p class="text-muted">No scale events recorded for this worker.</p>
158
+ <% else %>
159
+ <table>
160
+ <thead>
161
+ <tr>
162
+ <th>Time</th>
163
+ <th>Action</th>
164
+ <th>Workers</th>
165
+ <th>Queue Depth</th>
166
+ <th>Latency</th>
167
+ <th>Reason</th>
168
+ </tr>
169
+ </thead>
170
+ <tbody>
171
+ <% @events.each do |event| %>
172
+ <tr>
173
+ <td>
174
+ <div><%= event.created_at.strftime('%Y-%m-%d') %></div>
175
+ <div class="time-ago"><%= event.created_at.strftime('%H:%M:%S') %></div>
176
+ </td>
177
+ <td>
178
+ <% case event.action %>
179
+ <% when 'scale_up' %>
180
+ <span class="badge badge-success">↑ Scale Up</span>
181
+ <% when 'scale_down' %>
182
+ <span class="badge badge-warning">↓ Scale Down</span>
183
+ <% when 'no_change' %>
184
+ <span class="badge badge-neutral">— No Change</span>
185
+ <% when 'skipped' %>
186
+ <span class="badge badge-info">⏭ Skipped</span>
187
+ <% when 'error' %>
188
+ <span class="badge badge-danger">✕ Error</span>
189
+ <% end %>
190
+ <% if event.dry_run %>
191
+ <span class="badge badge-neutral">DRY</span>
192
+ <% end %>
193
+ </td>
194
+ <td>
195
+ <% if event.scaled? %>
196
+ <strong><%= event.from_workers %></strong> → <strong><%= event.to_workers %></strong>
197
+ <% else %>
198
+ <%= event.from_workers %>
199
+ <% end %>
200
+ </td>
201
+ <td><%= event.queue_depth %></td>
202
+ <td><%= event.latency_seconds.round %>s</td>
203
+ <td class="text-muted"><%= event.reason %></td>
204
+ </tr>
205
+ <% end %>
206
+ </tbody>
207
+ </table>
208
+ <% end %>
209
+ </div>
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'dashboard/engine' if defined?(Rails::Engine)
4
+
5
+ module SolidQueueHerokuAutoscaler
6
+ # Dashboard module provides a web UI for monitoring the autoscaler.
7
+ # Integrates with Mission Control Solid Queue when available.
8
+ module Dashboard
9
+ class << self
10
+ # Returns current autoscaler status for all workers
11
+ # @return [Hash] Status information for all workers
12
+ def status
13
+ workers = SolidQueueHerokuAutoscaler.registered_workers
14
+ workers = [:default] if workers.empty?
15
+
16
+ workers.each_with_object({}) do |name, status|
17
+ status[name] = worker_status(name)
18
+ end
19
+ end
20
+
21
+ # Returns status for a specific worker
22
+ # @param name [Symbol] Worker name
23
+ # @return [Hash] Status information
24
+ def worker_status(name)
25
+ config = SolidQueueHerokuAutoscaler.config(name)
26
+ metrics = safe_metrics(name)
27
+ tracker = CooldownTracker.new(config: config, key: name.to_s)
28
+
29
+ {
30
+ name: name,
31
+ enabled: config.enabled?,
32
+ dry_run: config.dry_run?,
33
+ current_workers: safe_current_workers(name),
34
+ min_workers: config.min_workers,
35
+ max_workers: config.max_workers,
36
+ queues: config.queues || ['all'],
37
+ process_type: config.process_type,
38
+ scaling_strategy: config.scaling_strategy,
39
+ metrics: {
40
+ queue_depth: metrics&.queue_depth || 0,
41
+ latency_seconds: metrics&.oldest_job_age_seconds || 0,
42
+ jobs_per_minute: metrics&.jobs_per_minute || 0,
43
+ claimed_jobs: metrics&.claimed_jobs || 0,
44
+ failed_jobs: metrics&.failed_jobs || 0,
45
+ active_workers: metrics&.active_workers || 0
46
+ },
47
+ cooldowns: {
48
+ scale_up_remaining: tracker.scale_up_cooldown_remaining.round,
49
+ scale_down_remaining: tracker.scale_down_cooldown_remaining.round,
50
+ last_scale_up: tracker.last_scale_up_at,
51
+ last_scale_down: tracker.last_scale_down_at
52
+ },
53
+ thresholds: {
54
+ scale_up_queue_depth: config.scale_up_queue_depth,
55
+ scale_up_latency: config.scale_up_latency_seconds,
56
+ scale_down_queue_depth: config.scale_down_queue_depth,
57
+ scale_down_latency: config.scale_down_latency_seconds
58
+ }
59
+ }
60
+ end
61
+
62
+ # Returns recent scale events
63
+ # @param limit [Integer] Maximum events to return
64
+ # @param worker_name [String, nil] Filter by worker
65
+ # @return [Array<ScaleEvent>] Recent events
66
+ def recent_events(limit: 50, worker_name: nil)
67
+ ScaleEvent.recent(limit: limit, worker_name: worker_name)
68
+ end
69
+
70
+ # Returns event statistics
71
+ # @param since [Time] Start time
72
+ # @param worker_name [String, nil] Filter by worker
73
+ # @return [Hash] Statistics
74
+ def event_stats(since: 24.hours.ago, worker_name: nil)
75
+ ScaleEvent.stats(since: since, worker_name: worker_name)
76
+ end
77
+
78
+ # Checks if the events table is available
79
+ # @return [Boolean] True if events can be recorded
80
+ def events_table_available?
81
+ ScaleEvent.table_exists?
82
+ end
83
+
84
+ private
85
+
86
+ def safe_metrics(name)
87
+ SolidQueueHerokuAutoscaler.metrics(name)
88
+ rescue StandardError
89
+ nil
90
+ end
91
+
92
+ def safe_current_workers(name)
93
+ SolidQueueHerokuAutoscaler.current_workers(name)
94
+ rescue StandardError
95
+ 0
96
+ end
97
+ end
98
+ end
99
+ end