roundhouse_ui 0.7.0 → 0.8.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 +31 -0
- data/app/controllers/concerns/roundhouse_ui/job_set_browsing.rb +20 -0
- data/app/controllers/roundhouse_ui/dead_controller.rb +12 -1
- data/app/controllers/roundhouse_ui/metrics_controller.rb +1 -0
- data/app/controllers/roundhouse_ui/retries_controller.rb +11 -1
- data/app/views/layouts/roundhouse_ui/application.html.erb +30 -2
- data/app/views/roundhouse_ui/dead/index.html.erb +8 -0
- data/app/views/roundhouse_ui/metrics/show.html.erb +21 -0
- data/app/views/roundhouse_ui/retries/index.html.erb +9 -0
- data/config/routes.rb +5 -3
- data/lib/roundhouse_ui/duration_collector.rb +53 -0
- data/lib/roundhouse_ui/version.rb +1 -1
- data/lib/roundhouse_ui.rb +8 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 11537f41a49c350dbd057961b82994fdc7524afdf3285961a28fa13701daaf57
|
|
4
|
+
data.tar.gz: aedef27c3202b3d2eaf25a2b9fefb9290df222c66d1fde4ae8414152f3a477cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4b22e1a716bbbdfdbab62cc5b0dcb10094a3cefbefd7ad1f1a24367e1f383f4c4c1467d1aa840600f860b1bc65b433fbdbdc56da5e1f1c1545f1756a99ed83c
|
|
7
|
+
data.tar.gz: cbe28c835c0e6413ee88658242e455e39a47cbd124a1e06ee0a9d40106ea9eb3ba49e2491f8dbd6f3a757ad71c0a24f5ead03b0b263b8a96237db3d2f8732b74
|
data/README.md
CHANGED
|
@@ -88,6 +88,14 @@ RoundhouseUi.configure do |c|
|
|
|
88
88
|
# e.g. when you run reliable fetch (super_fetch) instead of RoundhouseUi::Fetch.
|
|
89
89
|
# Default: true.
|
|
90
90
|
# c.pause_enabled = false
|
|
91
|
+
|
|
92
|
+
# Seconds between dashboard stat polls (default 5). Raise it if polling shows
|
|
93
|
+
# up in your traces — each poll re-runs the host's auth/routing on the mount.
|
|
94
|
+
# c.poll_interval = 10
|
|
95
|
+
|
|
96
|
+
# Show the "slowest job classes" table on the Metrics page. Requires the
|
|
97
|
+
# DurationCollector middleware (see below). Default: false.
|
|
98
|
+
# c.collect_durations = true
|
|
91
99
|
end
|
|
92
100
|
```
|
|
93
101
|
|
|
@@ -150,6 +158,29 @@ The **Busy** page's Cancel button flags a job's JID. A queued/scheduled/retrying
|
|
|
150
158
|
is then skipped when it would next run; a *currently running* job stops only if it
|
|
151
159
|
checks in — e.g. a long loop can `break if RoundhouseUi.cancelled?(jid)`.
|
|
152
160
|
|
|
161
|
+
## Slowest job classes
|
|
162
|
+
|
|
163
|
+
Sidekiq doesn't track per-class durations, so Roundhouse can record them itself.
|
|
164
|
+
Install the opt-in server middleware and set `collect_durations = true`; the
|
|
165
|
+
Metrics page then lists the slowest classes by total time (count + average).
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
# config/initializers/sidekiq.rb
|
|
169
|
+
Sidekiq.configure_server do |config|
|
|
170
|
+
config.server_middleware { |chain| chain.add RoundhouseUi::DurationCollector }
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
It's two cheap Redis writes per job (a counter + a summed-ms float) into a single
|
|
175
|
+
hash, and a job failure never propagates from the collector.
|
|
176
|
+
|
|
177
|
+
## Bulk actions on a filter
|
|
178
|
+
|
|
179
|
+
On **Retries** and **Dead**, searching narrows the set; with a filter active you
|
|
180
|
+
can retry or delete **every** matching job in one action (not just the visible
|
|
181
|
+
page), capped at 1,000 per run. Gated to when a filter is present so it can't
|
|
182
|
+
become "retry everything", `read_only`-aware, and audit-logged.
|
|
183
|
+
|
|
153
184
|
## Observability deep-links
|
|
154
185
|
|
|
155
186
|
The core depends on nothing — it asks the configured adapter for a URL and renders a link
|
|
@@ -5,6 +5,7 @@ module RoundhouseUi
|
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
7
|
PER_PAGE = 25
|
|
8
|
+
BULK_CAP = 1_000 # safety ceiling on a single match-set action
|
|
8
9
|
|
|
9
10
|
# Returns [entries_for_page, has_next?]. Scans only far enough to fill the
|
|
10
11
|
# requested page plus one (to know if a next page exists) — never loads the
|
|
@@ -32,6 +33,25 @@ module RoundhouseUi
|
|
|
32
33
|
[ jobs, has_next ]
|
|
33
34
|
end
|
|
34
35
|
|
|
36
|
+
# Apply an op ("retry"/"delete") to every entry matching the query, capped at
|
|
37
|
+
# BULK_CAP. Entries are collected first, then acted on — mutating a Sidekiq set
|
|
38
|
+
# mid-iteration skips entries. Returns [count_acted_on, capped?].
|
|
39
|
+
def bulk_apply(set, query, op, cap = BULK_CAP)
|
|
40
|
+
matches = []
|
|
41
|
+
capped = false
|
|
42
|
+
set.each do |entry|
|
|
43
|
+
next if query.present? && !entry_matches?(entry, query)
|
|
44
|
+
|
|
45
|
+
matches << entry
|
|
46
|
+
if matches.size >= cap
|
|
47
|
+
capped = true
|
|
48
|
+
break
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
matches.each { |entry| op == "delete" ? entry.delete : entry.retry }
|
|
52
|
+
[ matches.size, capped ]
|
|
53
|
+
end
|
|
54
|
+
|
|
35
55
|
def entry_matches?(entry, query)
|
|
36
56
|
needle = query.downcase
|
|
37
57
|
[ entry.klass, entry.jid, entry.item["error_class"], entry.item["error_message"], entry.args.to_s ]
|
|
@@ -2,7 +2,7 @@ module RoundhouseUi
|
|
|
2
2
|
class DeadController < ApplicationController
|
|
3
3
|
include JobSetBrowsing
|
|
4
4
|
|
|
5
|
-
before_action :require_writable!, only: %i[requeue destroy bulk]
|
|
5
|
+
before_action :require_writable!, only: %i[requeue destroy bulk bulk_all]
|
|
6
6
|
|
|
7
7
|
def index
|
|
8
8
|
@query = params[:q].to_s.strip
|
|
@@ -36,6 +36,17 @@ module RoundhouseUi
|
|
|
36
36
|
redirect_to dead_set_path, notice: "#{verb} #{count} job(s)."
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# Smart bulk: act on EVERY job matching the current filter (not just the
|
|
40
|
+
# selected/visible ones), capped for safety. Only offered when a filter is
|
|
41
|
+
# active, so it can't become "retry the entire dead set" by accident.
|
|
42
|
+
def bulk_all
|
|
43
|
+
count, capped = bulk_apply(Sidekiq::DeadSet.new, params[:q].to_s.strip, params[:op])
|
|
44
|
+
verb = params[:op] == "delete" ? "Deleted" : "Re-enqueued"
|
|
45
|
+
note = "#{verb} #{count} matching job(s)."
|
|
46
|
+
note += " Stopped at the #{JobSetBrowsing::BULK_CAP} cap — run again for more." if capped
|
|
47
|
+
redirect_to dead_set_path, notice: note
|
|
48
|
+
end
|
|
49
|
+
|
|
39
50
|
private
|
|
40
51
|
|
|
41
52
|
def require_writable!
|
|
@@ -2,7 +2,7 @@ module RoundhouseUi
|
|
|
2
2
|
class RetriesController < ApplicationController
|
|
3
3
|
include JobSetBrowsing
|
|
4
4
|
|
|
5
|
-
before_action :require_writable!, only: %i[requeue destroy]
|
|
5
|
+
before_action :require_writable!, only: %i[requeue destroy bulk_all]
|
|
6
6
|
|
|
7
7
|
def index
|
|
8
8
|
@query = params[:q].to_s.strip
|
|
@@ -24,6 +24,16 @@ module RoundhouseUi
|
|
|
24
24
|
redirect_to retries_path, notice: entry ? "Deleted #{params[:jid]}." : "Job is no longer in the retry set."
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Smart bulk: retry/delete EVERY job matching the current filter, capped for
|
|
28
|
+
# safety. Offered only when a filter is active.
|
|
29
|
+
def bulk_all
|
|
30
|
+
count, capped = bulk_apply(Sidekiq::RetrySet.new, params[:q].to_s.strip, params[:op])
|
|
31
|
+
verb = params[:op] == "delete" ? "Deleted" : "Re-enqueued"
|
|
32
|
+
note = "#{verb} #{count} matching job(s)."
|
|
33
|
+
note += " Stopped at the #{JobSetBrowsing::BULK_CAP} cap — run again for more." if capped
|
|
34
|
+
redirect_to retries_path, notice: note
|
|
35
|
+
end
|
|
36
|
+
|
|
27
37
|
private
|
|
28
38
|
|
|
29
39
|
def require_writable!
|
|
@@ -163,6 +163,27 @@
|
|
|
163
163
|
.rh-disclose summary::-webkit-details-marker { display:none; }
|
|
164
164
|
.rh-disclose summary::before { content:"▸ "; }
|
|
165
165
|
.rh-disclose[open] summary::before { content:"▾ "; }
|
|
166
|
+
|
|
167
|
+
/* connection state — poll failures surface here instead of silently going stale */
|
|
168
|
+
.rh-live.is-stale .rh-dot { background:var(--crit); }
|
|
169
|
+
.rh-live.is-stale .rh-dot::after { animation:none; }
|
|
170
|
+
|
|
171
|
+
/* keyboard focus — visible on every interactive control */
|
|
172
|
+
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible,
|
|
173
|
+
summary:focus-visible, .rh-nav:focus-visible { outline:2px solid var(--accent); outline-offset:2px; border-radius:6px; }
|
|
174
|
+
|
|
175
|
+
/* on-call / mobile: stack the rail on top as a horizontal nav, collapse the grid */
|
|
176
|
+
@media (max-width:760px) {
|
|
177
|
+
.rh-app { grid-template-columns:1fr; }
|
|
178
|
+
.rh-rail { position:static; height:auto; flex-direction:row; flex-wrap:wrap; gap:6px 14px; align-items:center; border-right:none; border-bottom:1px solid var(--line-soft); }
|
|
179
|
+
.rh-rail .rh-grp { flex-direction:row; flex-wrap:wrap; gap:4px; }
|
|
180
|
+
.rh-rail .rh-grp-label { display:none; }
|
|
181
|
+
.rh-brand { width:100%; }
|
|
182
|
+
.rh-main { padding:16px 16px 60px; }
|
|
183
|
+
.rh-cards { grid-template-columns:1fr 1fr; }
|
|
184
|
+
.rh-top { flex-wrap:wrap; }
|
|
185
|
+
}
|
|
186
|
+
@media (max-width:440px) { .rh-cards { grid-template-columns:1fr; } }
|
|
166
187
|
.rh-field { margin-bottom:16px; max-width:640px; }
|
|
167
188
|
.rh-field label { display:block; font-size:12px; color:var(--muted); margin-bottom:6px; }
|
|
168
189
|
.rh-field input, .rh-field textarea { width:100%; background:var(--panel); border:1px solid var(--line); border-radius:9px; padding:9px 12px; color:var(--text); font:13px var(--mono); }
|
|
@@ -254,6 +275,7 @@
|
|
|
254
275
|
fetch("<%= dashboard_stats_path %>", { headers: { Accept: "application/json" } })
|
|
255
276
|
.then(function (r) { return r.json(); })
|
|
256
277
|
.then(function (d) {
|
|
278
|
+
setConn(true);
|
|
257
279
|
apply(d);
|
|
258
280
|
var now = Date.now();
|
|
259
281
|
var backlog = d.enqueued + d.scheduled + d.retries;
|
|
@@ -285,7 +307,13 @@
|
|
|
285
307
|
}
|
|
286
308
|
lastProcessed = d.processed; lastFailed = d.failed; lastBacklog = backlog; lastT = now;
|
|
287
309
|
})
|
|
288
|
-
.catch(function () {});
|
|
310
|
+
.catch(function () { setConn(false); });
|
|
311
|
+
}
|
|
312
|
+
// Surface a failed poll instead of letting the numbers silently go stale.
|
|
313
|
+
function setConn(ok) {
|
|
314
|
+
var el = document.getElementById("rh-live"), lbl = document.getElementById("rh-live-label");
|
|
315
|
+
if (el) el.classList.toggle("is-stale", !ok);
|
|
316
|
+
if (lbl) lbl.textContent = ok ? "live" : "reconnecting…";
|
|
289
317
|
}
|
|
290
318
|
function startOnce() { if (started) return; started = true; poll(); setInterval(poll, POLL_MS); }
|
|
291
319
|
function wireChart() {
|
|
@@ -450,7 +478,7 @@
|
|
|
450
478
|
<h1><%= yield :title %></h1>
|
|
451
479
|
<span class="rh-crumb"><%= yield :crumb %></span>
|
|
452
480
|
<span class="rh-spacer"></span>
|
|
453
|
-
<span class="rh-live"><span class="rh-dot"></span> live · <span class="num" data-stat="rate">—</span>/min</span>
|
|
481
|
+
<span class="rh-live" id="rh-live"><span class="rh-dot"></span> <span id="rh-live-label">live</span> · <span class="num" data-stat="rate">—</span>/min</span>
|
|
454
482
|
<% if RoundhouseUi.read_only %><span class="rh-ro">read-only</span><% end %>
|
|
455
483
|
<button class="rh-kbd" id="rh-palette-open" type="button" title="Command palette (⌘K)">⌘K</button>
|
|
456
484
|
<button class="rh-iconbtn" id="rh-width" type="button" title="Toggle full width" aria-label="Toggle full width">⟷</button>
|
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
<h2 class="rh-h2">Dead set · <%= number_with_delimiter @total %> jobs</h2>
|
|
8
8
|
|
|
9
|
+
<% if @query.present? %>
|
|
10
|
+
<div class="rh-bulkbar">
|
|
11
|
+
<%= button_to "↻ Retry all matching", bulk_all_dead_path, method: :post, params: { op: "retry", q: @query }, class: "rh-btn", form_class: "rh-inline", data: { turbo_confirm: "Retry every dead job matching “#{@query}” (up to #{RoundhouseUi::JobSetBrowsing::BULK_CAP})?" } %>
|
|
12
|
+
<%= button_to "✕ Delete all matching", bulk_all_dead_path, method: :post, params: { op: "delete", q: @query }, class: "rh-btn rh-btn-danger", form_class: "rh-inline", data: { turbo_confirm: "Permanently delete every dead job matching “#{@query}” (up to #{RoundhouseUi::JobSetBrowsing::BULK_CAP})?" } %>
|
|
13
|
+
<span class="rh-sub">acts on every match for “<%= @query %>”, not just this page</span>
|
|
14
|
+
</div>
|
|
15
|
+
<% end %>
|
|
16
|
+
|
|
9
17
|
<%= form_with url: bulk_dead_path, method: :post, data: { turbo: false } do %>
|
|
10
18
|
<div class="rh-bulkbar">
|
|
11
19
|
<button type="submit" name="op" value="retry" class="rh-btn">↻ Retry selected</button>
|
|
@@ -47,3 +47,24 @@
|
|
|
47
47
|
<div class="d">backlog clears in, at current rate</div>
|
|
48
48
|
</div>
|
|
49
49
|
</div>
|
|
50
|
+
|
|
51
|
+
<% if RoundhouseUi.collect_durations %>
|
|
52
|
+
<h2 class="rh-h2">Slowest job classes <span class="hint">by total time · from RoundhouseUi::DurationCollector</span></h2>
|
|
53
|
+
<% if @durations.blank? %>
|
|
54
|
+
<div class="rh-panel"><div class="rh-empty">No durations recorded yet — jobs need to run with the collector middleware installed.</div></div>
|
|
55
|
+
<% else %>
|
|
56
|
+
<table class="rh-table">
|
|
57
|
+
<thead><tr><th>Job class</th><th class="r">Runs</th><th class="r">Avg</th><th class="r">Total time</th></tr></thead>
|
|
58
|
+
<tbody>
|
|
59
|
+
<% @durations.each do |d| %>
|
|
60
|
+
<tr>
|
|
61
|
+
<td class="rh-mono"><%= d[:klass] %></td>
|
|
62
|
+
<td class="r rh-mono"><%= number_with_delimiter d[:count] %></td>
|
|
63
|
+
<td class="r rh-mono <%= "rh-lat-warn" if d[:avg_ms] >= 1000 %>"><%= d[:avg_ms] >= 1000 ? "#{(d[:avg_ms] / 1000).round(1)}s" : "#{d[:avg_ms].round}ms" %></td>
|
|
64
|
+
<td class="r rh-mono"><%= d[:total_ms] >= 1000 ? "#{(d[:total_ms] / 1000).round(1)}s" : "#{d[:total_ms].round}ms" %></td>
|
|
65
|
+
</tr>
|
|
66
|
+
<% end %>
|
|
67
|
+
</tbody>
|
|
68
|
+
</table>
|
|
69
|
+
<% end %>
|
|
70
|
+
<% end %>
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
</form>
|
|
6
6
|
|
|
7
7
|
<h2 class="rh-h2">Retries · <%= number_with_delimiter @total %> jobs</h2>
|
|
8
|
+
|
|
9
|
+
<% if @query.present? %>
|
|
10
|
+
<div class="rh-bulkbar">
|
|
11
|
+
<%= button_to "↻ Run all matching now", bulk_all_retries_path, method: :post, params: { op: "retry", q: @query }, class: "rh-btn", form_class: "rh-inline", data: { turbo_confirm: "Run every retry matching “#{@query}” now (up to #{RoundhouseUi::JobSetBrowsing::BULK_CAP})?" } %>
|
|
12
|
+
<%= button_to "✕ Delete all matching", bulk_all_retries_path, method: :post, params: { op: "delete", q: @query }, class: "rh-btn rh-btn-danger", form_class: "rh-inline", data: { turbo_confirm: "Permanently delete every retry matching “#{@query}” (up to #{RoundhouseUi::JobSetBrowsing::BULK_CAP})?" } %>
|
|
13
|
+
<span class="rh-sub">acts on every match for “<%= @query %>”, not just this page</span>
|
|
14
|
+
</div>
|
|
15
|
+
<% end %>
|
|
16
|
+
|
|
8
17
|
<table class="rh-table">
|
|
9
18
|
<thead><tr><th>Job</th><th>Last error</th><th class="r">Attempt</th><th class="r">Next try</th><th class="r">Actions</th></tr></thead>
|
|
10
19
|
<tbody>
|
data/config/routes.rb
CHANGED
|
@@ -30,13 +30,15 @@ RoundhouseUi::Engine.routes.draw do
|
|
|
30
30
|
post "scheduled/:jid/delete" => "scheduled#destroy", as: :delete_scheduled
|
|
31
31
|
|
|
32
32
|
get "retries" => "retries#index", as: :retries
|
|
33
|
+
post "retries/bulk_all" => "retries#bulk_all", as: :bulk_all_retries
|
|
33
34
|
post "retries/:jid/run" => "retries#requeue", as: :run_retry
|
|
34
35
|
post "retries/:jid/delete" => "retries#destroy", as: :delete_retry
|
|
35
36
|
|
|
36
37
|
get "dead" => "dead#index", as: :dead_set
|
|
37
|
-
post "dead/bulk" => "dead#bulk",
|
|
38
|
-
post "dead
|
|
39
|
-
post "dead/:jid/
|
|
38
|
+
post "dead/bulk" => "dead#bulk", as: :bulk_dead
|
|
39
|
+
post "dead/bulk_all" => "dead#bulk_all", as: :bulk_all_dead
|
|
40
|
+
post "dead/:jid/retry" => "dead#requeue", as: :retry_dead_job
|
|
41
|
+
post "dead/:jid/delete" => "dead#destroy", as: :delete_dead_job
|
|
40
42
|
|
|
41
43
|
get "errors" => "errors#index", as: :errors
|
|
42
44
|
get "capsules" => "capsules#index", as: :capsules
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
# Opt-in server middleware that records per-class execution time, so the UI can
|
|
3
|
+
# answer "which job classes are slow?" — something Sidekiq doesn't track. Two
|
|
4
|
+
# cheap Redis writes per job (a counter + a summed-ms float). Off by default;
|
|
5
|
+
# enable in your Sidekiq server config:
|
|
6
|
+
#
|
|
7
|
+
# Sidekiq.configure_server do |config|
|
|
8
|
+
# config.server_middleware { |chain| chain.add RoundhouseUi::DurationCollector }
|
|
9
|
+
# end
|
|
10
|
+
class DurationCollector
|
|
11
|
+
KEY = "roundhouse:durations".freeze
|
|
12
|
+
|
|
13
|
+
def call(_worker, job, _queue)
|
|
14
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
15
|
+
yield
|
|
16
|
+
ensure
|
|
17
|
+
record(job["class"], (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000.0)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def record(klass, elapsed_ms)
|
|
23
|
+
return unless klass
|
|
24
|
+
|
|
25
|
+
Sidekiq.redis do |conn|
|
|
26
|
+
conn.call("HINCRBY", KEY, "#{klass}\x00count", 1)
|
|
27
|
+
conn.call("HINCRBYFLOAT", KEY, "#{klass}\x00ms", elapsed_ms)
|
|
28
|
+
end
|
|
29
|
+
rescue => e
|
|
30
|
+
# Metrics collection must never break a job.
|
|
31
|
+
Sidekiq.logger&.warn("[roundhouse] duration collect failed: #{e.message}")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# [{ klass:, count:, total_ms:, avg_ms: }], slowest (by total time) first.
|
|
35
|
+
def self.summary(limit: 20)
|
|
36
|
+
raw = Sidekiq.redis { |conn| conn.call("HGETALL", KEY) }
|
|
37
|
+
pairs = raw.is_a?(Hash) ? raw : raw.each_slice(2).to_a # redis-client: flat array; redis-rb: hash
|
|
38
|
+
|
|
39
|
+
by_class = Hash.new { |h, k| h[k] = { klass: k, count: 0, total_ms: 0.0 } }
|
|
40
|
+
pairs.each do |field, value|
|
|
41
|
+
klass, kind = field.split("\x00", 2)
|
|
42
|
+
next unless kind
|
|
43
|
+
|
|
44
|
+
kind == "count" ? by_class[klass][:count] = value.to_i : by_class[klass][:total_ms] = value.to_f
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
by_class.values
|
|
48
|
+
.map { |r| r.merge(avg_ms: r[:count].positive? ? r[:total_ms] / r[:count] : 0.0) }
|
|
49
|
+
.sort_by { |r| -r[:total_ms] }
|
|
50
|
+
.first(limit)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/roundhouse_ui.rb
CHANGED
|
@@ -12,6 +12,7 @@ require "roundhouse_ui/cancel_middleware"
|
|
|
12
12
|
require "roundhouse_ui/metrics"
|
|
13
13
|
require "roundhouse_ui/error_groups"
|
|
14
14
|
require "roundhouse_ui/health"
|
|
15
|
+
require "roundhouse_ui/duration_collector"
|
|
15
16
|
|
|
16
17
|
# Brand name is "Roundhouse"; the gem and Ruby namespace are RoundhouseUi
|
|
17
18
|
# (matching the published gem name `roundhouse_ui`).
|
|
@@ -59,6 +60,12 @@ module RoundhouseUi
|
|
|
59
60
|
# Sidekiq's retry/dead sets, so this is the only way to surface them here.
|
|
60
61
|
attr_accessor :show_sidekiq_failures
|
|
61
62
|
|
|
63
|
+
# Opt-in: record per-class job durations (via RoundhouseUi::DurationCollector
|
|
64
|
+
# server middleware) so the Metrics page can show the slowest job classes.
|
|
65
|
+
# Default false; reads/writes a single Redis hash. The flag gates the UI; the
|
|
66
|
+
# collection itself is enabled by installing the middleware.
|
|
67
|
+
attr_accessor :collect_durations
|
|
68
|
+
|
|
62
69
|
# Seconds between dashboard stat polls. Lower = livelier, but each poll also
|
|
63
70
|
# re-runs the host's auth/routing on the mount, so a busy console can add DB
|
|
64
71
|
# load. Default 5s; raise it if polling shows up in your traces.
|
|
@@ -93,4 +100,5 @@ module RoundhouseUi
|
|
|
93
100
|
self.show_sidekiq_failures = false
|
|
94
101
|
self.pause_enabled = true
|
|
95
102
|
self.poll_interval = 5
|
|
103
|
+
self.collect_durations = false
|
|
96
104
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: roundhouse_ui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- R.J. Robinson
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -96,6 +96,7 @@ files:
|
|
|
96
96
|
- lib/roundhouse_ui/audit.rb
|
|
97
97
|
- lib/roundhouse_ui/cancel_middleware.rb
|
|
98
98
|
- lib/roundhouse_ui/cancellation.rb
|
|
99
|
+
- lib/roundhouse_ui/duration_collector.rb
|
|
99
100
|
- lib/roundhouse_ui/engine.rb
|
|
100
101
|
- lib/roundhouse_ui/error_groups.rb
|
|
101
102
|
- lib/roundhouse_ui/fetch.rb
|