roundhouse_ui 0.5.0 → 0.6.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/app/helpers/roundhouse_ui/observability_helper.rb +12 -0
- data/app/views/layouts/roundhouse_ui/application.html.erb +31 -26
- data/app/views/roundhouse_ui/dashboard/show.html.erb +2 -2
- data/app/views/roundhouse_ui/errors/index.html.erb +1 -1
- data/lib/roundhouse_ui/observability.rb +11 -0
- data/lib/roundhouse_ui/version.rb +1 -1
- data/lib/roundhouse_ui.rb +6 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 73ff2f1bc8db03a06168ee39d18b1de518e1a373860e1e0faafdbd08370d5eab
|
|
4
|
+
data.tar.gz: e1bb6668de35f02e64c369d016a21af989984f604a6009efba39160bd05b5b6a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a27aa6ed4d0a91982e1997a6dcb6492a734edbfcbe3c4cf1c914366b4019b8ba49e922c7bfe59a45da662493d12b196c1f5bcd5ac47f9486248458877f42bb7f
|
|
7
|
+
data.tar.gz: faa3b64f3da583b3e361b431b8e6fa7b89b4c077a07cc981d54dfd2b20f248144c2798a1497aae4220de86fe385aca7f335b15d6b63c9d9648b22aa05f4acde5
|
|
@@ -9,5 +9,17 @@ module RoundhouseUi
|
|
|
9
9
|
|
|
10
10
|
link_to "↗ #{adapter.label}", url, target: "_blank", rel: "noopener", class: "rh-trace"
|
|
11
11
|
end
|
|
12
|
+
|
|
13
|
+
# Deep-link for a grouped error row (no single JID) — a class-wide search.
|
|
14
|
+
# respond_to? keeps older/custom adapters that lack error_url working.
|
|
15
|
+
def error_trace_link(klass:, error: nil)
|
|
16
|
+
adapter = RoundhouseUi.observability
|
|
17
|
+
return unless adapter.respond_to?(:error_url)
|
|
18
|
+
|
|
19
|
+
url = adapter.error_url(klass: klass, error: error)
|
|
20
|
+
return unless url
|
|
21
|
+
|
|
22
|
+
link_to "↗ #{adapter.label}", url, target: "_blank", rel: "noopener", class: "rh-trace"
|
|
23
|
+
end
|
|
12
24
|
end
|
|
13
25
|
end
|
|
@@ -157,7 +157,9 @@
|
|
|
157
157
|
<script nonce="<%= content_nonce %>">
|
|
158
158
|
(function () {
|
|
159
159
|
var fmt = function (n) { return Number(n).toLocaleString(); };
|
|
160
|
-
var started = false, lastProcessed = null, lastFailed = null, lastBacklog = null, lastT = null
|
|
160
|
+
var started = false, lastProcessed = null, lastFailed = null, lastBacklog = null, lastT = null;
|
|
161
|
+
var POLL_MS = <%= (RoundhouseUi.poll_interval.to_f * 1000).round %>;
|
|
162
|
+
var samples = [], buckets = [], bucketStart = null; // samples = current bucket; buckets = finalized per-interval averages
|
|
161
163
|
function setText(id, t) { var el = document.getElementById(id); if (el) el.textContent = t; }
|
|
162
164
|
function humanizeEta(s) {
|
|
163
165
|
if (s < 60) return "~" + Math.round(s) + "s";
|
|
@@ -166,24 +168,18 @@
|
|
|
166
168
|
return "~" + (s / 86400).toFixed(1) + "d";
|
|
167
169
|
}
|
|
168
170
|
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
function smooth(arr) {
|
|
174
|
-
var k = Math.max(2, Math.round(arr.length / 12)), out = [];
|
|
175
|
-
for (var i = 0; i < arr.length; i++) {
|
|
176
|
-
var a = Math.max(0, i - k + 1), sum = 0, c = 0;
|
|
177
|
-
for (var j = a; j <= i; j++) { sum += arr[j]; c++; }
|
|
178
|
-
out.push(sum / c);
|
|
179
|
-
}
|
|
180
|
-
return out;
|
|
181
|
-
}
|
|
171
|
+
// Bucket interval in seconds, from the picker — a new chart point lands
|
|
172
|
+
// once per interval, so "per 1m" advances every minute (not every poll).
|
|
173
|
+
function chartInterval() { var s = document.getElementById("rh-chart-interval"); return s ? parseInt(s.value, 10) : 30; }
|
|
174
|
+
function avg(a) { return a.length ? a.reduce(function (s, v) { return s + v; }, 0) / a.length : 0; }
|
|
182
175
|
function draw() {
|
|
183
176
|
var cv = document.getElementById("rh-chart"); if (!cv) return;
|
|
184
177
|
var ctx = cv.getContext("2d"), w = cv.width, h = cv.height, pad = 6;
|
|
185
178
|
ctx.clearRect(0, 0, w, h);
|
|
186
|
-
|
|
179
|
+
// finalized buckets + the in-progress bucket as a provisional last point,
|
|
180
|
+
// so the chart shows data immediately instead of waiting a full interval.
|
|
181
|
+
var pts = buckets.slice(-60); if (samples.length) pts = pts.concat([ avg(samples) ]);
|
|
182
|
+
var n = pts.length; if (n < 2) return;
|
|
187
183
|
var max = Math.max.apply(null, pts) * 1.25 || 1;
|
|
188
184
|
var x = function (i) { return i / (n - 1) * w; }, y = function (v) { return h - pad - v / max * (h - pad * 2); };
|
|
189
185
|
var g = ctx.createLinearGradient(0, 0, 0, h); g.addColorStop(0, "rgba(110,139,255,.30)"); g.addColorStop(1, "rgba(110,139,255,0)");
|
|
@@ -210,7 +206,15 @@
|
|
|
210
206
|
var rate = dt > 0 ? Math.max(0, dp / dt) : 0;
|
|
211
207
|
var re = document.querySelector('[data-stat="rate"]'); if (re) re.textContent = fmt(Math.round(rate * 60));
|
|
212
208
|
var ne = document.getElementById("rh-chart-now"); if (ne) ne.textContent = Math.round(rate);
|
|
213
|
-
|
|
209
|
+
// Accumulate into the current bucket; finalize it (one averaged point)
|
|
210
|
+
// once the chosen interval has elapsed. Live "/s" still updates each poll.
|
|
211
|
+
samples.push(rate);
|
|
212
|
+
if (bucketStart == null) bucketStart = now;
|
|
213
|
+
if ((now - bucketStart) / 1000 >= chartInterval()) {
|
|
214
|
+
buckets.push(avg(samples)); if (buckets.length > 240) buckets.shift();
|
|
215
|
+
samples = []; bucketStart = now;
|
|
216
|
+
}
|
|
217
|
+
draw();
|
|
214
218
|
|
|
215
219
|
// Metrics tab live rates (these elements exist only on that page).
|
|
216
220
|
setText("rh-m-throughput", Math.round(rate) + "/s");
|
|
@@ -226,7 +230,7 @@
|
|
|
226
230
|
})
|
|
227
231
|
.catch(function () {});
|
|
228
232
|
}
|
|
229
|
-
function startOnce() { if (started) return; started = true; poll(); setInterval(poll,
|
|
233
|
+
function startOnce() { if (started) return; started = true; poll(); setInterval(poll, POLL_MS); }
|
|
230
234
|
function syncTheme() { var b = document.getElementById("rh-theme"); if (b) b.textContent = document.documentElement.getAttribute("data-theme") === "light" ? "☀" : "☾"; }
|
|
231
235
|
function syncWidth() { var b = document.getElementById("rh-width"); if (b) b.classList.toggle("is-on", document.documentElement.getAttribute("data-width") === "full"); }
|
|
232
236
|
|
|
@@ -317,15 +321,16 @@
|
|
|
317
321
|
if (e.target && e.target.id === "rh-palette") { palClose(); return; }
|
|
318
322
|
if (e.target.closest && e.target.closest("#rh-palette-open")) { e.preventDefault(); palOpen(); }
|
|
319
323
|
});
|
|
320
|
-
// Throughput
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
+
// Throughput interval picker — persist the choice; changing it re-buckets
|
|
325
|
+
// from here on, so reset the in-progress bucket and redraw.
|
|
326
|
+
function restoreChartInterval() {
|
|
327
|
+
var s = document.getElementById("rh-chart-interval"); if (!s) return;
|
|
328
|
+
try { var v = localStorage.getItem("rh-chart-iv"); if (v) s.value = v; } catch (_) {}
|
|
324
329
|
}
|
|
325
330
|
document.addEventListener("change", function (e) {
|
|
326
|
-
if (!e.target || e.target.id !== "rh-chart-
|
|
327
|
-
try { localStorage.setItem("rh-chart-
|
|
328
|
-
draw();
|
|
331
|
+
if (!e.target || e.target.id !== "rh-chart-interval") return;
|
|
332
|
+
try { localStorage.setItem("rh-chart-iv", e.target.value); } catch (_) {}
|
|
333
|
+
samples = []; bucketStart = null; draw();
|
|
329
334
|
});
|
|
330
335
|
|
|
331
336
|
document.addEventListener("keydown", function (e) {
|
|
@@ -337,8 +342,8 @@
|
|
|
337
342
|
else if (e.key === "Enter") { e.preventDefault(); if (palFiltered[palSel]) palRun(palFiltered[palSel]); }
|
|
338
343
|
});
|
|
339
344
|
|
|
340
|
-
document.addEventListener("turbo:load", function () { startOnce(); syncTheme(); syncWidth(); setActiveNav();
|
|
341
|
-
document.addEventListener("DOMContentLoaded", function () { startOnce(); syncTheme(); syncWidth(); setActiveNav();
|
|
345
|
+
document.addEventListener("turbo:load", function () { startOnce(); syncTheme(); syncWidth(); setActiveNav(); restoreChartInterval(); draw(); });
|
|
346
|
+
document.addEventListener("DOMContentLoaded", function () { startOnce(); syncTheme(); syncWidth(); setActiveNav(); restoreChartInterval(); });
|
|
342
347
|
document.addEventListener("visibilitychange", function () { if (!document.hidden) poll(); });
|
|
343
348
|
})();
|
|
344
349
|
</script>
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
<div class="rh-card">
|
|
28
28
|
<div class="k">Processed</div>
|
|
29
29
|
<div class="v num" data-stat="processed"><%= number_with_delimiter @stats.processed %></div>
|
|
30
|
-
<div class="d
|
|
30
|
+
<div class="d"><span class="num" data-stat="rate">—</span> / min</div>
|
|
31
31
|
</div>
|
|
32
32
|
<div class="rh-card">
|
|
33
33
|
<div class="k">Failed · total</div>
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
</div>
|
|
43
43
|
|
|
44
44
|
<div class="rh-chart-wrap">
|
|
45
|
-
<div class="top"><h3>Throughput</h3><span class="rh-sub">jobs / sec · live</span><span class="now"><span id="rh-chart-now">—</span>/s</span><select id="rh-chart-
|
|
45
|
+
<div class="top"><h3>Throughput</h3><span class="rh-sub">jobs / sec · live</span><span class="now"><span id="rh-chart-now">—</span>/s</span><select id="rh-chart-interval" aria-label="Throughput interval" style="margin-left:12px;background:var(--panel-2);color:var(--muted);border:1px solid var(--line);border-radius:7px;padding:3px 6px;font:12px var(--sans)"><option value="10">per 10s</option><option value="30" selected>per 30s</option><option value="60">per 1m</option><option value="300">per 5m</option></select></div>
|
|
46
46
|
<canvas id="rh-chart" width="1100" height="180"></canvas>
|
|
47
47
|
</div>
|
|
48
48
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<% else %>
|
|
14
14
|
<% @groups.each do |g| %>
|
|
15
15
|
<tr>
|
|
16
|
-
<td><%= g[:klass] %><br><span class="rh-err"><%= g[:error] %></span></td>
|
|
16
|
+
<td><%= g[:klass] %> <%= error_trace_link(klass: g[:klass], error: g[:error]) %><br><span class="rh-err"><%= g[:error] %></span></td>
|
|
17
17
|
<td class="r"><%= number_with_delimiter g[:count] %></td>
|
|
18
18
|
<td><span class="rh-sub"><%= g[:sources].join(", ") %></span></td>
|
|
19
19
|
<td><span class="rh-sub"><%= g[:queues].join(", ") %></span></td>
|
|
@@ -14,6 +14,7 @@ module RoundhouseUi
|
|
|
14
14
|
def label = "trace"
|
|
15
15
|
def job_url(**) = nil
|
|
16
16
|
def queue_url(_name) = nil
|
|
17
|
+
def error_url(**) = nil
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
class DatadogAdapter
|
|
@@ -36,6 +37,16 @@ module RoundhouseUi
|
|
|
36
37
|
traces_url([ "@sidekiq.queue:#{name}" ])
|
|
37
38
|
end
|
|
38
39
|
|
|
40
|
+
# Grouped Errors rows have no single JID, so link to a class-wide search.
|
|
41
|
+
# The exact facet depends on your Datadog tagging; tune via `extra_query`
|
|
42
|
+
# if `resource_name` isn't how your Sidekiq spans are tagged.
|
|
43
|
+
def error_url(klass:, error: nil)
|
|
44
|
+
terms = [ "resource_name:#{klass}" ]
|
|
45
|
+
terms << "service:#{@service}" if @service
|
|
46
|
+
terms << @extra_query if @extra_query
|
|
47
|
+
traces_url(terms)
|
|
48
|
+
end
|
|
49
|
+
|
|
39
50
|
private
|
|
40
51
|
|
|
41
52
|
def traces_url(terms)
|
data/lib/roundhouse_ui.rb
CHANGED
|
@@ -57,6 +57,11 @@ module RoundhouseUi
|
|
|
57
57
|
# Sidekiq's retry/dead sets, so this is the only way to surface them here.
|
|
58
58
|
attr_accessor :show_sidekiq_failures
|
|
59
59
|
|
|
60
|
+
# Seconds between dashboard stat polls. Lower = livelier, but each poll also
|
|
61
|
+
# re-runs the host's auth/routing on the mount, so a busy console can add DB
|
|
62
|
+
# load. Default 5s; raise it if polling shows up in your traces.
|
|
63
|
+
attr_accessor :poll_interval
|
|
64
|
+
|
|
60
65
|
# Queue pause/resume is only enforced when RoundhouseUi::Fetch is installed
|
|
61
66
|
# as the server's fetch strategy. If you run reliable fetch (Sidekiq
|
|
62
67
|
# Pro/Enterprise super_fetch) you can't also run our fetcher, so pause can't
|
|
@@ -85,4 +90,5 @@ module RoundhouseUi
|
|
|
85
90
|
self.redact_args = []
|
|
86
91
|
self.show_sidekiq_failures = false
|
|
87
92
|
self.pause_enabled = true
|
|
93
|
+
self.poll_interval = 5
|
|
88
94
|
end
|