solid_queue_autoscaler 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 +7 -0
- data/CHANGELOG.md +189 -0
- data/LICENSE.txt +21 -0
- data/README.md +553 -0
- data/lib/generators/solid_queue_autoscaler/dashboard_generator.rb +54 -0
- data/lib/generators/solid_queue_autoscaler/install_generator.rb +21 -0
- data/lib/generators/solid_queue_autoscaler/migration_generator.rb +29 -0
- data/lib/generators/solid_queue_autoscaler/templates/README +41 -0
- data/lib/generators/solid_queue_autoscaler/templates/create_solid_queue_autoscaler_events.rb.erb +24 -0
- data/lib/generators/solid_queue_autoscaler/templates/create_solid_queue_autoscaler_state.rb.erb +15 -0
- data/lib/generators/solid_queue_autoscaler/templates/initializer.rb +58 -0
- data/lib/solid_queue_autoscaler/adapters/base.rb +102 -0
- data/lib/solid_queue_autoscaler/adapters/heroku.rb +93 -0
- data/lib/solid_queue_autoscaler/adapters/kubernetes.rb +158 -0
- data/lib/solid_queue_autoscaler/adapters.rb +57 -0
- data/lib/solid_queue_autoscaler/advisory_lock.rb +71 -0
- data/lib/solid_queue_autoscaler/autoscale_job.rb +71 -0
- data/lib/solid_queue_autoscaler/configuration.rb +269 -0
- data/lib/solid_queue_autoscaler/cooldown_tracker.rb +153 -0
- data/lib/solid_queue_autoscaler/dashboard/engine.rb +136 -0
- data/lib/solid_queue_autoscaler/dashboard/views/layouts/solid_queue_heroku_autoscaler/dashboard/application.html.erb +206 -0
- data/lib/solid_queue_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/dashboard/index.html.erb +138 -0
- data/lib/solid_queue_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/events/index.html.erb +102 -0
- data/lib/solid_queue_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/workers/index.html.erb +106 -0
- data/lib/solid_queue_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/workers/show.html.erb +209 -0
- data/lib/solid_queue_autoscaler/dashboard.rb +99 -0
- data/lib/solid_queue_autoscaler/decision_engine.rb +228 -0
- data/lib/solid_queue_autoscaler/errors.rb +44 -0
- data/lib/solid_queue_autoscaler/metrics.rb +172 -0
- data/lib/solid_queue_autoscaler/railtie.rb +179 -0
- data/lib/solid_queue_autoscaler/scale_event.rb +292 -0
- data/lib/solid_queue_autoscaler/scaler.rb +294 -0
- data/lib/solid_queue_autoscaler/version.rb +5 -0
- data/lib/solid_queue_autoscaler.rb +108 -0
- metadata +179 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Solid Queue Autoscaler</title>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<%= csrf_meta_tags %>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg-primary: #1a1a2e;
|
|
10
|
+
--bg-secondary: #16213e;
|
|
11
|
+
--bg-card: #0f3460;
|
|
12
|
+
--text-primary: #eee;
|
|
13
|
+
--text-secondary: #aaa;
|
|
14
|
+
--accent: #e94560;
|
|
15
|
+
--success: #00d26a;
|
|
16
|
+
--warning: #ffbe0b;
|
|
17
|
+
--info: #00b4d8;
|
|
18
|
+
--border: #333;
|
|
19
|
+
}
|
|
20
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
21
|
+
body {
|
|
22
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
23
|
+
background: var(--bg-primary);
|
|
24
|
+
color: var(--text-primary);
|
|
25
|
+
line-height: 1.6;
|
|
26
|
+
}
|
|
27
|
+
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
|
|
28
|
+
header {
|
|
29
|
+
background: var(--bg-secondary);
|
|
30
|
+
padding: 15px 0;
|
|
31
|
+
border-bottom: 1px solid var(--border);
|
|
32
|
+
margin-bottom: 30px;
|
|
33
|
+
}
|
|
34
|
+
header .container {
|
|
35
|
+
display: flex;
|
|
36
|
+
justify-content: space-between;
|
|
37
|
+
align-items: center;
|
|
38
|
+
}
|
|
39
|
+
header h1 {
|
|
40
|
+
font-size: 1.5rem;
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 10px;
|
|
44
|
+
}
|
|
45
|
+
header h1 span { color: var(--accent); }
|
|
46
|
+
nav { display: flex; gap: 20px; }
|
|
47
|
+
nav a {
|
|
48
|
+
color: var(--text-secondary);
|
|
49
|
+
text-decoration: none;
|
|
50
|
+
padding: 8px 16px;
|
|
51
|
+
border-radius: 6px;
|
|
52
|
+
transition: all 0.2s;
|
|
53
|
+
}
|
|
54
|
+
nav a:hover, nav a.active {
|
|
55
|
+
color: var(--text-primary);
|
|
56
|
+
background: var(--bg-card);
|
|
57
|
+
}
|
|
58
|
+
.flash {
|
|
59
|
+
padding: 12px 20px;
|
|
60
|
+
border-radius: 6px;
|
|
61
|
+
margin-bottom: 20px;
|
|
62
|
+
}
|
|
63
|
+
.flash.notice { background: rgba(0, 210, 106, 0.2); border: 1px solid var(--success); }
|
|
64
|
+
.flash.alert { background: rgba(233, 69, 96, 0.2); border: 1px solid var(--accent); }
|
|
65
|
+
.grid { display: grid; gap: 20px; }
|
|
66
|
+
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
|
|
67
|
+
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
|
|
68
|
+
.grid-4 { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
|
|
69
|
+
.card {
|
|
70
|
+
background: var(--bg-card);
|
|
71
|
+
border-radius: 12px;
|
|
72
|
+
padding: 20px;
|
|
73
|
+
border: 1px solid var(--border);
|
|
74
|
+
}
|
|
75
|
+
.card h2, .card h3 {
|
|
76
|
+
margin-bottom: 15px;
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 10px;
|
|
80
|
+
}
|
|
81
|
+
.stat-card {
|
|
82
|
+
text-align: center;
|
|
83
|
+
padding: 25px;
|
|
84
|
+
}
|
|
85
|
+
.stat-card .value {
|
|
86
|
+
font-size: 2.5rem;
|
|
87
|
+
font-weight: bold;
|
|
88
|
+
color: var(--accent);
|
|
89
|
+
}
|
|
90
|
+
.stat-card .label {
|
|
91
|
+
color: var(--text-secondary);
|
|
92
|
+
font-size: 0.9rem;
|
|
93
|
+
margin-top: 5px;
|
|
94
|
+
}
|
|
95
|
+
.stat-card.success .value { color: var(--success); }
|
|
96
|
+
.stat-card.warning .value { color: var(--warning); }
|
|
97
|
+
.stat-card.info .value { color: var(--info); }
|
|
98
|
+
table {
|
|
99
|
+
width: 100%;
|
|
100
|
+
border-collapse: collapse;
|
|
101
|
+
}
|
|
102
|
+
th, td {
|
|
103
|
+
padding: 12px;
|
|
104
|
+
text-align: left;
|
|
105
|
+
border-bottom: 1px solid var(--border);
|
|
106
|
+
}
|
|
107
|
+
th {
|
|
108
|
+
color: var(--text-secondary);
|
|
109
|
+
font-weight: 500;
|
|
110
|
+
font-size: 0.85rem;
|
|
111
|
+
text-transform: uppercase;
|
|
112
|
+
}
|
|
113
|
+
tr:hover { background: rgba(255,255,255,0.02); }
|
|
114
|
+
.badge {
|
|
115
|
+
display: inline-block;
|
|
116
|
+
padding: 4px 10px;
|
|
117
|
+
border-radius: 20px;
|
|
118
|
+
font-size: 0.8rem;
|
|
119
|
+
font-weight: 500;
|
|
120
|
+
}
|
|
121
|
+
.badge-success { background: rgba(0,210,106,0.2); color: var(--success); }
|
|
122
|
+
.badge-warning { background: rgba(255,190,11,0.2); color: var(--warning); }
|
|
123
|
+
.badge-danger { background: rgba(233,69,96,0.2); color: var(--accent); }
|
|
124
|
+
.badge-info { background: rgba(0,180,216,0.2); color: var(--info); }
|
|
125
|
+
.badge-neutral { background: rgba(170,170,170,0.2); color: var(--text-secondary); }
|
|
126
|
+
.btn {
|
|
127
|
+
display: inline-block;
|
|
128
|
+
padding: 10px 20px;
|
|
129
|
+
border-radius: 6px;
|
|
130
|
+
border: none;
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
font-size: 0.9rem;
|
|
133
|
+
text-decoration: none;
|
|
134
|
+
transition: all 0.2s;
|
|
135
|
+
}
|
|
136
|
+
.btn-primary {
|
|
137
|
+
background: var(--accent);
|
|
138
|
+
color: white;
|
|
139
|
+
}
|
|
140
|
+
.btn-primary:hover { background: #d63654; }
|
|
141
|
+
.btn-secondary {
|
|
142
|
+
background: var(--bg-secondary);
|
|
143
|
+
color: var(--text-primary);
|
|
144
|
+
border: 1px solid var(--border);
|
|
145
|
+
}
|
|
146
|
+
.btn-secondary:hover { background: var(--bg-card); }
|
|
147
|
+
.btn-sm { padding: 6px 12px; font-size: 0.8rem; }
|
|
148
|
+
.progress-bar {
|
|
149
|
+
height: 8px;
|
|
150
|
+
background: var(--bg-secondary);
|
|
151
|
+
border-radius: 4px;
|
|
152
|
+
overflow: hidden;
|
|
153
|
+
margin-top: 8px;
|
|
154
|
+
}
|
|
155
|
+
.progress-bar .fill {
|
|
156
|
+
height: 100%;
|
|
157
|
+
border-radius: 4px;
|
|
158
|
+
transition: width 0.3s;
|
|
159
|
+
}
|
|
160
|
+
.text-muted { color: var(--text-secondary); }
|
|
161
|
+
.text-success { color: var(--success); }
|
|
162
|
+
.text-warning { color: var(--warning); }
|
|
163
|
+
.text-danger { color: var(--accent); }
|
|
164
|
+
.text-info { color: var(--info); }
|
|
165
|
+
.mb-1 { margin-bottom: 0.5rem; }
|
|
166
|
+
.mb-2 { margin-bottom: 1rem; }
|
|
167
|
+
.mb-3 { margin-bottom: 1.5rem; }
|
|
168
|
+
.mt-2 { margin-top: 1rem; }
|
|
169
|
+
.d-flex { display: flex; }
|
|
170
|
+
.justify-between { justify-content: space-between; }
|
|
171
|
+
.align-center { align-items: center; }
|
|
172
|
+
.gap-2 { gap: 1rem; }
|
|
173
|
+
.time-ago { font-size: 0.85rem; color: var(--text-secondary); }
|
|
174
|
+
</style>
|
|
175
|
+
</head>
|
|
176
|
+
<body>
|
|
177
|
+
<header>
|
|
178
|
+
<div class="container">
|
|
179
|
+
<h1>
|
|
180
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
181
|
+
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
|
182
|
+
<path d="M2 17l10 5 10-5"/>
|
|
183
|
+
<path d="M2 12l10 5 10-5"/>
|
|
184
|
+
</svg>
|
|
185
|
+
<span>Solid Queue</span> Autoscaler
|
|
186
|
+
</h1>
|
|
187
|
+
<nav>
|
|
188
|
+
<%= link_to 'Dashboard', main_app.respond_to?(:solid_queue_autoscaler_path) ? main_app.solid_queue_autoscaler_path : root_path, class: request.path == root_path ? 'active' : '' %>
|
|
189
|
+
<%= link_to 'Workers', workers_path, class: request.path.include?('/workers') ? 'active' : '' %>
|
|
190
|
+
<%= link_to 'Events', events_path, class: request.path.include?('/events') ? 'active' : '' %>
|
|
191
|
+
</nav>
|
|
192
|
+
</div>
|
|
193
|
+
</header>
|
|
194
|
+
|
|
195
|
+
<main class="container">
|
|
196
|
+
<% if notice %>
|
|
197
|
+
<div class="flash notice"><%= notice %></div>
|
|
198
|
+
<% end %>
|
|
199
|
+
<% if alert %>
|
|
200
|
+
<div class="flash alert"><%= alert %></div>
|
|
201
|
+
<% end %>
|
|
202
|
+
|
|
203
|
+
<%= yield %>
|
|
204
|
+
</main>
|
|
205
|
+
</body>
|
|
206
|
+
</html>
|
|
@@ -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_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_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>
|