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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b76009aa85c003ad62a2e6bacf8efbdf69767bda6e1b9b382de8a606ec3acb8b
4
- data.tar.gz: 4ae59af49806c3536b6f709f02146338ad69e716257d2223f1fbf5061995390a
3
+ metadata.gz: 73ff2f1bc8db03a06168ee39d18b1de518e1a373860e1e0faafdbd08370d5eab
4
+ data.tar.gz: e1bb6668de35f02e64c369d016a21af989984f604a6009efba39160bd05b5b6a
5
5
  SHA512:
6
- metadata.gz: 7df7cbc7dbdab13f4821500f1c5e3d8d9ea8357416530662d4e6cd0a38edd427f11eec13d2a7d22ff923b289a5f5dd10edc3ba405217967563bea6e55a7432d4
7
- data.tar.gz: e9ebfd3e8edf616ed69ae517f92ca2bdda61dd3065e83624d524b4f5d133bd7d395ef7e4cba7d6d477c92dd1547d5495c83fd68822d58a5136db279a64963268
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, series = [];
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
- // Points to display, from the window picker (24 pts 1m at the 2.5s poll).
170
- function chartWindow() { var s = document.getElementById("rh-chart-window"); return s ? parseInt(s.value, 10) : 24; }
171
- // Moving average sized to the window flattens the per-poll spikes into a
172
- // sustained line instead of a noisy sawtooth.
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
- var pts = smooth(series.slice(-chartWindow())), n = pts.length; if (n < 2) return;
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
- series.push(rate); if (series.length > 360) series.shift(); draw();
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, 2500); }
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 window picker — persist the choice and redraw the smoothed line.
321
- function restoreChartWindow() {
322
- var s = document.getElementById("rh-chart-window"); if (!s) return;
323
- try { var v = localStorage.getItem("rh-chart-win"); if (v) s.value = v; } catch (_) {}
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-window") return;
327
- try { localStorage.setItem("rh-chart-win", e.target.value); } catch (_) {}
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(); restoreChartWindow(); draw(); });
341
- document.addEventListener("DOMContentLoaded", function () { startOnce(); syncTheme(); syncWidth(); setActiveNav(); restoreChartWindow(); });
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 up">▲ <span class="num" data-stat="rate">—</span> / min</div>
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-window" aria-label="Throughput window" 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="24">1m</option><option value="120">5m</option><option value="360">15m</option></select></div>
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)
@@ -1,3 +1,3 @@
1
1
  module RoundhouseUi
2
- VERSION = "0.5.0"
2
+ VERSION = "0.6.0"
3
3
  end
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roundhouse_ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - R.J. Robinson