roundhouse_ui 0.9.1 → 0.11.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 +620 -46
- data/app/controllers/concerns/roundhouse_ui/job_set_browsing.rb +246 -12
- data/app/controllers/roundhouse_ui/application_controller.rb +74 -0
- data/app/controllers/roundhouse_ui/audit_controller.rb +1 -0
- data/app/controllers/roundhouse_ui/busy_controller.rb +25 -5
- data/app/controllers/roundhouse_ui/dashboard_controller.rb +27 -3
- data/app/controllers/roundhouse_ui/dead_controller.rb +27 -10
- data/app/controllers/roundhouse_ui/errors_controller.rb +60 -1
- data/app/controllers/roundhouse_ui/jobs_controller.rb +5 -1
- data/app/controllers/roundhouse_ui/queues_controller.rb +57 -6
- data/app/controllers/roundhouse_ui/recurring_controller.rb +12 -0
- data/app/controllers/roundhouse_ui/retries_controller.rb +34 -10
- data/app/controllers/roundhouse_ui/scheduled_controller.rb +4 -7
- data/app/controllers/roundhouse_ui/settings_controller.rb +15 -0
- data/app/controllers/roundhouse_ui/snapshots_controller.rb +5 -6
- data/app/controllers/roundhouse_ui/workers_controller.rb +2 -7
- data/app/helpers/roundhouse_ui/application_helper.rb +293 -0
- data/app/helpers/roundhouse_ui/nav_helper.rb +3 -3
- data/app/helpers/roundhouse_ui/observability_helper.rb +121 -12
- data/app/helpers/roundhouse_ui/tags_helper.rb +128 -0
- data/app/views/layouts/roundhouse_ui/application.html.erb +1277 -84
- data/app/views/roundhouse_ui/busy/index.html.erb +30 -6
- data/app/views/roundhouse_ui/dashboard/show.html.erb +62 -17
- data/app/views/roundhouse_ui/dead/index.html.erb +54 -32
- data/app/views/roundhouse_ui/errors/index.html.erb +51 -11
- data/app/views/roundhouse_ui/jobs/show.html.erb +8 -3
- data/app/views/roundhouse_ui/metrics/show.html.erb +26 -3
- data/app/views/roundhouse_ui/queues/index.html.erb +54 -10
- data/app/views/roundhouse_ui/queues/show.html.erb +55 -0
- data/app/views/roundhouse_ui/recurring/index.html.erb +71 -0
- data/app/views/roundhouse_ui/retries/index.html.erb +30 -17
- data/app/views/roundhouse_ui/scheduled/index.html.erb +17 -11
- data/app/views/roundhouse_ui/settings/show.html.erb +91 -0
- data/app/views/roundhouse_ui/shared/_filter_error.html.erb +23 -0
- data/app/views/roundhouse_ui/shared/_pager.html.erb +2 -2
- data/app/views/roundhouse_ui/shared/_search_bar.html.erb +55 -0
- data/app/views/roundhouse_ui/shared/_search_help.html.erb +37 -0
- data/app/views/roundhouse_ui/shared/_tag_filter.html.erb +28 -0
- data/app/views/roundhouse_ui/shared/bulk_preview.html.erb +69 -0
- data/config/routes.rb +9 -0
- data/lib/roundhouse_ui/backends/sidekiq.rb +124 -5
- data/lib/roundhouse_ui/backends/solid_queue.rb +54 -3
- data/lib/roundhouse_ui/demo.rb +130 -0
- data/lib/roundhouse_ui/error_groups.rb +25 -4
- data/lib/roundhouse_ui/filter_query.rb +504 -0
- data/lib/roundhouse_ui/health.rb +43 -4
- data/lib/roundhouse_ui/history.rb +54 -0
- data/lib/roundhouse_ui/icons.rb +104 -0
- data/lib/roundhouse_ui/marks/datadog-lockup-white.svg +30 -0
- data/lib/roundhouse_ui/marks/datadog-lockup.svg +41 -0
- data/lib/roundhouse_ui/observability.rb +55 -7
- data/lib/roundhouse_ui/pause.rb +6 -11
- data/lib/roundhouse_ui/queue_summary.rb +11 -0
- data/lib/roundhouse_ui/recurring.rb +137 -0
- data/lib/roundhouse_ui/runbooks.rb +75 -0
- data/lib/roundhouse_ui/snapshots.rb +13 -2
- data/lib/roundhouse_ui/tags.rb +112 -0
- data/lib/roundhouse_ui/theme.rb +298 -0
- data/lib/roundhouse_ui/version.rb +1 -1
- data/lib/roundhouse_ui.rb +235 -5
- data/lib/tasks/roundhouse_ui_tasks.rake +84 -4
- metadata +32 -7
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
# Icons as inline SVG. No font, no request, no CSP change, and identical on
|
|
3
|
+
# every platform — which the Unicode glyphs these replace were not: `⛁` and
|
|
4
|
+
# `☷` have no glyph at all on older Windows, `⚙`/`☾`/`⚠` get substituted for
|
|
5
|
+
# emoji on some platforms, and `▦` and `▤` are indistinguishable at 16px.
|
|
6
|
+
#
|
|
7
|
+
# Hosts that already ship an icon font can use it instead:
|
|
8
|
+
#
|
|
9
|
+
# RoundhouseUi.icons = :font_awesome
|
|
10
|
+
# RoundhouseUi.icons = { dashboard: "fa-solid fa-gauge-high", ... }
|
|
11
|
+
#
|
|
12
|
+
# Roundhouse never loads a font itself in either case. It emits class names and
|
|
13
|
+
# the host's existing pipeline supplies the glyphs, which is what keeps the
|
|
14
|
+
# self-contained CSP intact.
|
|
15
|
+
module Icons
|
|
16
|
+
VIEWBOX = "0 0 24 24".freeze
|
|
17
|
+
# Geometric on purpose: an operations console is not the place for
|
|
18
|
+
# personality, and a 1.9 stroke stays legible at 15px.
|
|
19
|
+
ATTRS = %(viewBox="#{VIEWBOX}" width="15" height="15" fill="none" stroke="currentColor" ) +
|
|
20
|
+
%(stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false")
|
|
21
|
+
|
|
22
|
+
PATHS = {
|
|
23
|
+
dashboard: '<rect x="3" y="3" width="7.5" height="7.5" rx="1.4"/><rect x="13.5" y="3" width="7.5" height="7.5" rx="1.4"/><rect x="3" y="13.5" width="7.5" height="7.5" rx="1.4"/><rect x="13.5" y="13.5" width="7.5" height="7.5" rx="1.4"/>',
|
|
24
|
+
queues: '<path d="M4 7h16M4 12h16M4 17h10"/>',
|
|
25
|
+
retries: '<path d="M20 11a8 8 0 1 0-2.5 6.9"/><path d="M20 5v6h-6"/>',
|
|
26
|
+
dead: '<circle cx="12" cy="12" r="8.4"/><path d="M9 9l6 6M15 9l-6 6"/>',
|
|
27
|
+
errors: '<path d="M12 4.2 2.8 20h18.4L12 4.2Z"/><path d="M12 10v4"/><circle cx="12" cy="17" r=".6" fill="currentColor"/>',
|
|
28
|
+
busy: '<circle cx="12" cy="12" r="8.4"/><path d="M12 3.6a8.4 8.4 0 0 1 0 16.8Z" fill="currentColor" stroke="none"/>',
|
|
29
|
+
metrics: '<path d="M4 20V13M9.3 20V8M14.7 20v-6M20 20V4"/>',
|
|
30
|
+
snapshots: '<ellipse cx="12" cy="6" rx="8" ry="3"/><path d="M4 6v6c0 1.7 3.6 3 8 3s8-1.3 8-3V6"/><path d="M4 12v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6"/>',
|
|
31
|
+
audit: '<path d="M4 6h16M4 11h16M4 16h9"/><path d="M15.5 18.5l1.8 1.8 3.2-3.6"/>',
|
|
32
|
+
settings: '<path d="M4 7h9M17 7h3M4 17h3M11 17h9"/><circle cx="15" cy="7" r="2.1"/><circle cx="7" cy="17" r="2.1"/>',
|
|
33
|
+
scheduled: '<circle cx="12" cy="12" r="8.4"/><path d="M12 7.6V12l3.2 2"/>',
|
|
34
|
+
workers: '<rect x="6.5" y="6.5" width="11" height="11" rx="1.6"/><path d="M10 3v3M14 3v3M10 18v3M14 18v3M3 10h3M3 14h3M18 10h3M18 14h3"/>',
|
|
35
|
+
capsules: '<rect x="3.5" y="3.5" width="7" height="7" rx="1.3"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.3"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.3"/><path d="M17 14v6M14 17h6"/>',
|
|
36
|
+
redis: '<path d="M3.6 7.5 12 4l8.4 3.5L12 11 3.6 7.5Z"/><path d="M3.6 12 12 15.5 20.4 12"/><path d="M3.6 16.5 12 20l8.4-3.5"/>',
|
|
37
|
+
enqueue: '<path d="M12 5v14M5 12h14"/>',
|
|
38
|
+
width: '<path d="M3 12h18"/><path d="M7 8l-4 4 4 4M17 8l4 4-4 4"/>',
|
|
39
|
+
theme: '<path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a6.8 6.8 0 0 0 10.5 10.5Z"/>',
|
|
40
|
+
pause: '<path d="M9.5 5v14M14.5 5v14"/>',
|
|
41
|
+
delete: '<path d="M5 7h14"/><path d="M9 7V4.6h6V7"/><path d="M6.5 7l1 12.4h9L18 7"/>',
|
|
42
|
+
ok: '<path d="M4.5 12.5l5 5L20 6.5"/>',
|
|
43
|
+
warn: '<path d="M12 4.2 2.8 20h18.4L12 4.2Z"/><path d="M12 10v4"/><circle cx="12" cy="17" r=".6" fill="currentColor"/>',
|
|
44
|
+
stalled: '<circle cx="12" cy="12" r="8.4"/><path d="M8.5 12h7"/>',
|
|
45
|
+
search: '<circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.4 15.4 21 21"/>',
|
|
46
|
+
# A funnel, for "find more like this". Deliberately not the magnifier: the
|
|
47
|
+
# search box is one control and this is another, and giving them the same
|
|
48
|
+
# glyph is what made the row control ambiguous. A funnel also cannot be
|
|
49
|
+
# misread as "duplicate", which two stacked shapes would be with Edit
|
|
50
|
+
# sitting directly beside it.
|
|
51
|
+
filter: '<path d="M4 5h16l-6.2 7.3v5.9L10.2 21v-8.7L4 5Z"/>',
|
|
52
|
+
runbook: '<path d="M5 4h9l5 5v11H5Z"/><path d="M14 4v5h5"/><path d="M8.5 13h7M8.5 16.5h4.5"/>',
|
|
53
|
+
# Default trace glyph: an external-link arrow. Roundhouse ships no vendor
|
|
54
|
+
# logo — see Observability adapters, which may supply their own mark.
|
|
55
|
+
trace_out: '<path d="M14 4h6v6"/><path d="M20 4 11 13"/><path d="M18 14v4.5A1.5 1.5 0 0 1 16.5 20h-11A1.5 1.5 0 0 1 4 18.5v-11A1.5 1.5 0 0 1 5.5 6H10"/>'
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
58
|
+
# For hosts that already load FontAwesome Free. Names chosen from the free
|
|
59
|
+
# set so nothing here needs a Pro licence.
|
|
60
|
+
FONT_AWESOME = {
|
|
61
|
+
dashboard: "fa-solid fa-gauge-high", queues: "fa-solid fa-layer-group",
|
|
62
|
+
retries: "fa-solid fa-rotate-right", dead: "fa-solid fa-circle-xmark",
|
|
63
|
+
errors: "fa-solid fa-triangle-exclamation", busy: "fa-solid fa-circle-half-stroke",
|
|
64
|
+
metrics: "fa-solid fa-chart-simple", snapshots: "fa-solid fa-database",
|
|
65
|
+
audit: "fa-solid fa-list-check", settings: "fa-solid fa-sliders",
|
|
66
|
+
scheduled: "fa-regular fa-clock", workers: "fa-solid fa-microchip",
|
|
67
|
+
capsules: "fa-solid fa-table-cells-large", redis: "fa-solid fa-cubes-stacked",
|
|
68
|
+
enqueue: "fa-solid fa-plus", width: "fa-solid fa-arrows-left-right",
|
|
69
|
+
theme: "fa-solid fa-moon", pause: "fa-solid fa-pause",
|
|
70
|
+
delete: "fa-solid fa-trash-can", ok: "fa-solid fa-check",
|
|
71
|
+
warn: "fa-solid fa-triangle-exclamation", stalled: "fa-solid fa-minus",
|
|
72
|
+
search: "fa-solid fa-magnifying-glass", runbook: "fa-regular fa-file-lines",
|
|
73
|
+
filter: "fa-solid fa-filter",
|
|
74
|
+
trace_out: "fa-solid fa-arrow-up-right-from-square"
|
|
75
|
+
}.freeze
|
|
76
|
+
|
|
77
|
+
module_function
|
|
78
|
+
|
|
79
|
+
# An icon's markup, or nil when the name is unknown — an unknown name draws
|
|
80
|
+
# nothing rather than raising, so a typo in a host's mapping cannot take a
|
|
81
|
+
# page down.
|
|
82
|
+
def markup(name, config = RoundhouseUi.icons)
|
|
83
|
+
key = name.to_s.to_sym
|
|
84
|
+
case config
|
|
85
|
+
when :font_awesome then class_name(FONT_AWESOME[key])
|
|
86
|
+
when Hash then class_name(config[key] || config[name.to_s])
|
|
87
|
+
else svg(key)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def svg(key)
|
|
92
|
+
path = PATHS[key]
|
|
93
|
+
return nil unless path
|
|
94
|
+
|
|
95
|
+
%(<svg #{ATTRS}>#{path}</svg>)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Host-supplied, so it is never marked html_safe — the caller escapes it into
|
|
99
|
+
# a class attribute.
|
|
100
|
+
def class_name(value)
|
|
101
|
+
value.presence
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800.5 196.2" role="img" aria-hidden="true" focusable="false" class="rh-mark-dd rh-mark-on-dark"><g><g><path fill="#FFFFFF" d="M167.2,98.4l-54.9,10c-1.4,1.8-4.7,4.8-6.3,5.6c-6.9,3.5-11.6,2.5-15.7,1.4c-2.6-0.7-4.1-1-6.3-2l-13.4,1.8
|
|
2
|
+
l8.1,67.9l94.1-17L167.2,98.4z M83.4,176.7l-0.8-7.4L98,145.9l17.4,5.1l15-25.1l18,11.9l13.7-28.7l4.9,52.2L83.4,176.7z M79.5,110
|
|
3
|
+
c-6.8-4.4-13.8-10.6-16.9-14.1c-0.5-0.4-0.4-2.1-0.4-2.1c2.8,2.2,14.3,10.4,26.4,14.1c4.3,1.3,10.9,1.8,16.6-1.4
|
|
4
|
+
c4.4-2.5,9.6-6.8,12.7-11.2l0.6,1c0,0.7-1.5,3.9-2.2,5.2c1.3,0.8,2.3,1,3.8,1.4l10.1-1.6c3.6-5.8,6.2-15.2,3.4-24.2
|
|
5
|
+
c-1.6-5.1-9.8-15.4-10.4-15.9c-2.1-2,0.3-9.6-3.7-18c-4.3-8.8-15.6-18-20.5-22.1c1.4,1,10.3,4.6,14.4,9.6c0.4-0.5,0.6-3.2,0.9-3.9
|
|
6
|
+
c-3.6-4.6-3.8-12.9-3.8-15.1c0-4.1-2.1-8.7-2.1-8.7s3.6,2.8,4.5,7.7c1.1,5.7,3.4,10.3,6.5,14.1c5.8,7.2,11,10.9,13.7,8.2
|
|
7
|
+
c3.2-3.2-3.2-17.5-11.3-25.5c-9.5-9.3-12-8.1-17.5-6.1c-4.4,1.5-6.8,14.2-18.4,13.9c-2-0.2-7-0.4-9.5-0.3c1.3-1.8,2.4-3.2,2.4-3.2
|
|
8
|
+
s-3.9,1.6-7.2,3.6l-0.3-0.4c1.1-2.4,2.3-3.8,2.3-3.8s-3.1,1.9-5.9,4.1c0.5-2.8,2.5-4.6,2.5-4.6s-3.9,0.7-8.9,6.2
|
|
9
|
+
c-5.7,1.5-7,2.6-11.6,4.6c-7.4-1.6-10.8-4.2-14.1-9c-2.5-3.6-7-4.2-11.6-2.3c-6.7,2.8-15.1,6.5-15.1,6.5s2.8-0.1,5.6,0
|
|
10
|
+
c-3.9,1.5-7.7,3.5-7.7,3.5s1.8-0.1,4.1,0c-1.6,1.3-2.4,1.9-3.9,2.9c-3.6,2.6-6.5,5.6-6.5,5.6s2.4-1.1,4.6-1.7
|
|
11
|
+
c-1.5,3.5-4.6,6.1-4,10.4c0.5,3.9,5.3,12,11.6,17c0.5,0.4,9,8.3,15.4,5.1c6.4-3.2,8.9-6.1,10-10.5c1.2-5.1,0.5-8.9-2.1-19.9
|
|
12
|
+
c-0.9-3.6-3.1-11.1-4.2-14.6l0.2-0.2c2.1,4.4,7.4,16.1,9.6,23.9c3.4,12.2,2.3,18.4,0.8,20.7c-4.7,6.8-16.6,7.8-22,4
|
|
13
|
+
c-0.8,13.1,2.1,18.9,3.1,21.8c-0.5,3.3,1.7,9.6,1.7,9.6s0.2-2.8,1.2-4.3c0.3,3.3,1.9,7.3,1.9,7.3s-0.1-2.4,0.7-4.5
|
|
14
|
+
c1.1,1.8,1.9,2.2,2.9,3.6c1,3.6,3,6.2,3,6.2s-0.3-1.9-0.2-3.9c5,4.8,5.8,11.8,6.3,17.1c1.4,14.7-23.2,26.4-28,35.6
|
|
15
|
+
c-3.6,5.4-5.8,14.1,0.3,19.2c14.8,12.3,9.1,15.7,16.5,21.1c10.2,7.4,22.9,4.1,27.2-1.9c6-8.4,4.5-16.3,2.2-23.7
|
|
16
|
+
c-1.8-5.8-6.5-15.4-12.4-19.1c-6-3.8-11.9-4.5-16.8-4l0.5-0.5c7.1-1.4,14.6-0.6,20,2.8c6.1,3.9,11.7,10.7,14.6,21
|
|
17
|
+
c3.3-0.5,3.8-0.7,6.8-1.1L65,111.6L79.5,110z M113.8,43.3c6.3,2.9,5.5,8.5,5.6,11.8c0.1,0.9,0,1.6-0.1,2c-0.9-0.5-2.2-0.8-4.4-0.7
|
|
18
|
+
c-0.6,0-1.3,0.1-1.9,0.2c-2.3-1.2-3.7-3.7-5-6.3c-0.1-0.2-0.2-0.5-0.3-0.7c0-0.1-0.1-0.2-0.1-0.3c0,0,0-0.1,0-0.1
|
|
19
|
+
c-0.7-2.2-0.2-2.7,0.1-3.3s1.4-1.3-0.2-1.8c-0.1,0-0.3-0.1-0.5-0.1C108.1,42.6,111.4,42.2,113.8,43.3z M106,79.9
|
|
20
|
+
c1.2-0.9,6.8-2.7,12-3.4c2.7-0.3,6.6-0.5,7.4,0c1.6,0.9,1.6,3.7,0.5,6.3c-1.6,3.8-3.9,7.9-6.6,8.2c-4.3,0.6-8.3-1.7-12.9-5.2
|
|
21
|
+
C104.8,84.6,103.8,81.6,106,79.9z M65.6,51.4c6.8-4.6,15.5-2.8,14-1.4c-2.8,2.7,0.9,1.9,1.3,6.8c0.3,3.6-0.9,5.6-1.9,6.7
|
|
22
|
+
c-2.1,0.3-4.7,0.8-7.8,1.8c-1.8,0.6-3.4,1.2-4.8,1.9c-0.4-0.2-0.8-0.5-1.2-0.9C60.5,62.1,60.7,54.8,65.6,51.4z"/></g><g><path fill="#FFFFFF" fill-rule="evenodd" clip-rule="evenodd" d="M249.2,141.8H211v-88h38.2c27.5,0,41.3,13.9,41.3,41.6C290.5,126.3,276.8,141.8,249.2,141.8z M227.4,127.6
|
|
23
|
+
h19.4c18.3,0,27.4-10.7,27.4-32.2c0-18.3-9.1-27.4-27.4-27.4h-19.4V127.6L227.4,127.6z"/><polygon fill="#FFFFFF" fill-rule="evenodd" clip-rule="evenodd" points="307.6,141.8 290.9,141.8 328.3,53.8 345.9,53.8 384.1,141.8 366.5,141.8 355.4,117.8 327.2,117.8
|
|
24
|
+
332.8,103.7 351.1,103.7 336.7,70.6 "/><polygon fill="#FFFFFF" fill-rule="evenodd" clip-rule="evenodd" points="374.8,53.8 441.7,53.8 441.7,68 416.5,68 416.5,141.8 400.1,141.8 400.1,68 374.8,68 "/><polygon fill="#FFFFFF" fill-rule="evenodd" clip-rule="evenodd" points="450.1,141.8 433.3,141.8 470.8,53.8 488.3,53.8 526.5,141.8 509,141.8 497.9,117.8 469.6,117.8
|
|
25
|
+
475.2,103.7 493.5,103.7 479.1,70.6 "/><path fill="#FFFFFF" fill-rule="evenodd" clip-rule="evenodd" d="M575.6,141.8h-38.2v-88h38.2c27.5,0,41.3,13.9,41.3,41.6C616.9,126.3,603.1,141.8,575.6,141.8z M553.7,127.6
|
|
26
|
+
h19.4c18.3,0,27.4-10.7,27.4-32.2c0-18.3-9.2-27.4-27.4-27.4h-19.4V127.6z"/><path fill="#FFFFFF" fill-rule="evenodd" clip-rule="evenodd" d="M628,97.9c0-29.8,14.8-44.7,44.3-44.7c29,0,43.6,14.9,43.6,44.7c0,29.7-14.5,44.5-43.6,44.5
|
|
27
|
+
C644,142.4,629.3,127.6,628,97.9z M672.2,128.2c17.7,0,26.6-10.2,26.6-30.7c0-20.2-8.9-30.2-26.6-30.2
|
|
28
|
+
c-18.2,0-27.3,10.1-27.3,30.2C644.9,118,654,128.2,672.2,128.2z"/><path fill="#FFFFFF" fill-rule="evenodd" clip-rule="evenodd" d="M783.9,106.2v20.6c-3.8,1-7.1,1.5-10.1,1.5c-20,0-30-10.6-30-31.7c0-19.5,10.6-29.2,31.7-29.2
|
|
29
|
+
c8.8,0,17,1.6,24.7,4.9V57.5c-7.6-2.9-16.2-4.3-25.9-4.3c-31.6,0-47.5,14.4-47.5,43.4c0,30.6,15.5,45.8,46.7,45.8
|
|
30
|
+
c10.7,0,19.6-1.6,26.7-4.7v-46h-26.4l-5.5,14.5H783.9z"/></g></g></svg>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800.5 203.19" role="img" aria-hidden="true" focusable="false" class="rh-mark-dd rh-mark-on-light"><g><g><path fill="#632CA6" fill-rule="evenodd" clip-rule="evenodd" d="M260.87,144.65h-37.4v-86.1h37.4c26.94,0,40.43,13.57,40.43,40.7C301.29,129.51,287.81,144.65,260.87,144.65z
|
|
2
|
+
M239.45,130.79h19c17.9,0,26.84-10.51,26.84-31.55c0-17.91-8.95-26.87-26.84-26.87h-19L239.45,130.79L239.45,130.79z"/><polygon fill="#632CA6" fill-rule="evenodd" clip-rule="evenodd" points="318.04,144.65 301.62,144.65 338.25,58.55 355.44,58.55 392.85,144.65 375.66,144.65 364.8,121.17
|
|
3
|
+
337.17,121.17 342.66,107.32 360.58,107.32 346.46,74.98 "/><polygon fill="#632CA6" fill-rule="evenodd" clip-rule="evenodd" points="383.82,58.55 449.28,58.55 449.28,72.39 424.55,72.39 424.55,144.65 408.57,144.65 408.57,72.39
|
|
4
|
+
383.82,72.39 "/><polygon fill="#632CA6" fill-rule="evenodd" clip-rule="evenodd" points="457.5,144.65 441.08,144.65 477.71,58.55 494.9,58.55 532.31,144.65 515.1,144.65 504.24,121.17
|
|
5
|
+
476.61,121.17 482.1,107.32 500.02,107.32 485.91,74.98 "/><path fill="#632CA6" fill-rule="evenodd" clip-rule="evenodd" d="M580.32,144.65h-37.4v-86.1h37.4c26.96,0,40.43,13.57,40.43,40.7C620.75,129.51,607.28,144.65,580.32,144.65z
|
|
6
|
+
M558.91,130.79h19c17.89,0,26.86-10.51,26.86-31.55c0-17.91-8.96-26.87-26.86-26.87h-19V130.79z"/><path fill="#632CA6" fill-rule="evenodd" clip-rule="evenodd" d="M631.58,101.72c0-29.2,14.45-43.79,43.33-43.79c28.44,0,42.64,14.59,42.64,43.79
|
|
7
|
+
c0,29.03-14.21,43.55-42.64,43.55C647.31,145.27,632.87,130.75,631.58,101.72z M674.91,131.39c17.36,0,26.05-10.01,26.05-30.05
|
|
8
|
+
c0-19.72-8.69-29.59-26.05-29.59c-17.82,0-26.73,9.87-26.73,29.59C648.18,121.38,657.09,131.39,674.91,131.39z"/><path fill="#632CA6" fill-rule="evenodd" clip-rule="evenodd" d="M784.26,109.81v20.16c-3.69,0.96-6.99,1.44-9.9,1.44c-19.55,0-29.31-10.34-29.31-31.01
|
|
9
|
+
c0-19.09,10.36-28.62,31.07-28.62c8.65,0,16.69,1.61,24.13,4.82V62.14c-7.44-2.8-15.89-4.21-25.34-4.21
|
|
10
|
+
c-30.97,0-46.46,14.15-46.46,42.47c0,29.9,15.22,44.87,45.67,44.87c10.47,0,19.17-1.52,26.13-4.58V95.64h-25.82l-5.4,14.16
|
|
11
|
+
L784.26,109.81L784.26,109.81z"/></g><g><g><path fill="#632CA6" fill-rule="evenodd" clip-rule="evenodd" d="M158.87,144.16L142,133.04l-14.07,23.5l-16.36-4.78l-14.41,21.99l0.74,6.92l78.33-14.43l-4.55-48.94
|
|
12
|
+
L158.87,144.16z M85.82,123.07l12.57-1.73c2.03,0.91,3.45,1.26,5.89,1.88c3.8,0.99,8.19,1.94,14.7-1.34
|
|
13
|
+
c1.51-0.75,4.67-3.64,5.94-5.28l51.49-9.34l5.25,63.57l-88.21,15.9L85.82,123.07z M181.46,100.16l-5.08,0.97L166.62,0.25
|
|
14
|
+
L0.25,19.54l20.5,166.33l19.47-2.83c-1.55-2.22-3.98-4.91-8.11-8.35c-5.74-4.76-3.71-12.86-0.32-17.97
|
|
15
|
+
c4.47-8.63,27.54-19.61,26.23-33.41c-0.47-5.02-1.27-11.55-5.93-16.03c-0.17,1.86,0.14,3.65,0.14,3.65s-1.91-2.44-2.87-5.77
|
|
16
|
+
c-0.95-1.28-1.69-1.68-2.7-3.39c-0.72,1.97-0.62,4.26-0.62,4.26s-1.56-3.7-1.82-6.82c-0.93,1.4-1.16,4.05-1.16,4.05
|
|
17
|
+
s-2.03-5.83-1.57-8.97c-0.93-2.73-3.68-8.15-2.9-20.47c5.08,3.56,16.26,2.71,20.61-3.71c1.45-2.13,2.44-7.93-0.72-19.36
|
|
18
|
+
c-2.03-7.33-7.05-18.25-9.01-22.4l-0.23,0.17c1.03,3.34,3.16,10.33,3.98,13.73c2.47,10.29,3.13,13.87,1.97,18.61
|
|
19
|
+
c-0.99,4.12-3.35,6.82-9.35,9.84c-6,3.03-13.96-4.34-14.47-4.74c-5.83-4.64-10.34-12.22-10.84-15.9
|
|
20
|
+
c-0.52-4.03,2.32-6.45,3.76-9.74c-2.05,0.59-4.34,1.63-4.34,1.63s2.73-2.83,6.1-5.27c1.4-0.92,2.21-1.51,3.68-2.73
|
|
21
|
+
c-2.13-0.03-3.86,0.02-3.86,0.02s3.55-1.92,7.23-3.31c-2.69-0.12-5.27-0.02-5.27-0.02S35.75,27.1,42,24.5
|
|
22
|
+
c4.3-1.76,8.5-1.24,10.86,2.17c3.1,4.47,6.35,6.9,13.25,8.41c4.24-1.88,5.52-2.84,10.84-4.29c4.68-5.15,8.36-5.82,8.36-5.82
|
|
23
|
+
s-1.82,1.67-2.31,4.3c2.66-2.09,5.57-3.84,5.57-3.84s-1.13,1.39-2.18,3.6l0.24,0.36c3.1-1.86,6.74-3.32,6.74-3.32
|
|
24
|
+
s-1.04,1.32-2.26,3.02c2.34-0.02,7.08,0.1,8.91,0.31c10.86,0.24,13.11-11.6,17.28-13.08c5.22-1.86,7.55-2.99,16.44,5.74
|
|
25
|
+
c7.63,7.5,13.59,20.91,10.63,23.92c-2.48,2.49-7.38-0.97-12.8-7.74c-2.87-3.58-5.03-7.81-6.05-13.19
|
|
26
|
+
c-0.86-4.54-4.19-7.17-4.19-7.17s1.93,4.31,1.93,8.11c0,2.08,0.26,9.84,3.59,14.19c-0.33,0.64-0.48,3.15-0.85,3.63
|
|
27
|
+
c-3.87-4.68-12.19-8.03-13.54-9.02c4.59,3.76,15.14,12.4,19.19,20.68c3.83,7.83,1.57,15.01,3.51,16.87
|
|
28
|
+
c0.55,0.53,8.24,10.11,9.72,14.93c2.58,8.39,0.15,17.21-3.22,22.68l-9.43,1.47c-1.38-0.38-2.31-0.58-3.55-1.29
|
|
29
|
+
c0.68-1.21,2.04-4.22,2.05-4.84l-0.53-0.93c-2.94,4.16-7.85,8.2-11.94,10.52c-5.35,3.03-11.51,2.56-15.52,1.32
|
|
30
|
+
c-11.39-3.51-22.16-11.21-24.75-13.23c0,0-0.08,1.61,0.41,1.98c2.87,3.24,9.45,9.1,15.81,13.18l-13.55,1.49l6.41,49.89
|
|
31
|
+
c-2.84,0.41-3.28,0.61-6.39,1.05c-2.74-9.68-7.98-16.01-13.71-19.69c-5.05-3.25-12.02-3.98-18.7-2.66l-0.43,0.5
|
|
32
|
+
c4.64-0.48,10.12,0.19,15.74,3.75c5.52,3.49,9.97,12.51,11.61,17.94c2.1,6.94,3.55,14.36-2.1,22.23
|
|
33
|
+
c-4.02,5.59-15.74,8.68-25.22,2c2.53,4.07,5.95,7.4,10.55,8.02c6.84,0.93,13.33-0.26,17.79-4.84c3.81-3.92,5.84-12.12,5.3-20.75
|
|
34
|
+
l6.03-0.87l2.18,15.49l99.88-12.03L181.46,100.16z M120.69,58.08c-0.28,0.64-0.72,1.05-0.06,3.12l0.04,0.12l0.1,0.27l0.27,0.62
|
|
35
|
+
c1.19,2.42,2.49,4.71,4.66,5.88c0.56-0.09,1.15-0.16,1.75-0.19c2.04-0.09,3.33,0.23,4.15,0.68c0.07-0.41,0.09-1,0.04-1.88
|
|
36
|
+
c-0.16-3.07,0.61-8.29-5.29-11.04c-2.23-1.03-5.35-0.72-6.39,0.58c0.19,0.02,0.36,0.06,0.49,0.11
|
|
37
|
+
C122.04,56.89,120.98,57.43,120.69,58.08 M137.23,86.73c-0.77-0.43-4.39-0.26-6.93,0.04c-4.84,0.57-10.07,2.25-11.22,3.14
|
|
38
|
+
c-2.08,1.61-1.14,4.42,0.4,5.57c4.32,3.22,8.1,5.39,12.09,4.86c2.45-0.32,4.61-4.2,6.14-7.73
|
|
39
|
+
C138.77,90.19,138.77,87.58,137.23,86.73 M94.36,61.88c1.37-1.3-6.8-3-13.14,1.32c-4.67,3.19-4.82,10.03-0.35,13.9
|
|
40
|
+
c0.45,0.38,0.82,0.66,1.16,0.88c1.31-0.62,2.8-1.24,4.51-1.79c2.9-0.94,5.3-1.43,7.28-1.68c0.95-1.06,2.05-2.92,1.77-6.29
|
|
41
|
+
C95.22,63.63,91.75,64.36,94.36,61.88"/></g></g></g></svg>
|
|
@@ -5,9 +5,16 @@ module RoundhouseUi
|
|
|
5
5
|
# never depends on Datadog (or anything) — it asks the configured adapter for
|
|
6
6
|
# a URL and renders a link only if one comes back.
|
|
7
7
|
#
|
|
8
|
-
# RoundhouseUi.observability = RoundhouseUi::Observability::DatadogAdapter.new(service: "
|
|
8
|
+
# RoundhouseUi.observability = RoundhouseUi::Observability::DatadogAdapter.new(service: "sidekiq")
|
|
9
9
|
#
|
|
10
|
-
#
|
|
10
|
+
# `service:` is the service your *Sidekiq spans* carry, which is frequently not
|
|
11
|
+
# your app name — apps commonly set `c.tracing.instrument :sidekiq, service_name:
|
|
12
|
+
# "sidekiq"`, and dd-trace has no default of its own. Passing the app name when
|
|
13
|
+
# your spans say something else silently matches nothing. Omit it if unsure; the
|
|
14
|
+
# term is left out entirely when nil.
|
|
15
|
+
#
|
|
16
|
+
# Write your own (Honeycomb, Sentry, …) by duck-typing job_url and label.
|
|
17
|
+
# error_url, icon and wordmark? are optional; queue_url is unused by the UI.
|
|
11
18
|
module Observability
|
|
12
19
|
# Default: no links anywhere.
|
|
13
20
|
class NullAdapter
|
|
@@ -26,22 +33,63 @@ module RoundhouseUi
|
|
|
26
33
|
|
|
27
34
|
def label = "Datadog"
|
|
28
35
|
|
|
36
|
+
# Datadog's official horizontal lockup, from datadoghq.com/about/resources
|
|
37
|
+
# (Logo_Assets.zip). Their mark, their trademark, shipped unmodified in
|
|
38
|
+
# geometry and aspect ratio.
|
|
39
|
+
#
|
|
40
|
+
# Both variants ship, because Datadog's white lockup is different artwork
|
|
41
|
+
# rather than the purple one recoloured: a different viewBox (196.2 tall
|
|
42
|
+
# against 203.19) and a different fill-rule mix. Forcing white onto the
|
|
43
|
+
# purple paths renders Bits as a solid tile with the dog knocked out of
|
|
44
|
+
# him. The only edit is moving their <style> block's declarations onto the
|
|
45
|
+
# elements that carried each class — per element, since the white asset
|
|
46
|
+
# mixes nonzero and evenodd — because an inline <style> inside the SVG
|
|
47
|
+
# would need a CSP nonce an adapter cannot see.
|
|
48
|
+
#
|
|
49
|
+
# The lockup rather than the icon-only mark, because Bits is drawn for
|
|
50
|
+
# large use: at the 12-16px a table row allows he renders as a smudge,
|
|
51
|
+
# while the wordmark stays legible to about 11px.
|
|
52
|
+
MARKS_DIR = File.expand_path("marks", __dir__)
|
|
53
|
+
MARK_ON_LIGHT = File.read(File.join(MARKS_DIR, "datadog-lockup.svg")).freeze
|
|
54
|
+
MARK_ON_DARK = File.read(File.join(MARKS_DIR, "datadog-lockup-white.svg")).freeze
|
|
55
|
+
|
|
56
|
+
# Both go in the markup and CSS picks one, rather than the UI guessing the
|
|
57
|
+
# ground it will be painted on. A viewer on the system default has stamped
|
|
58
|
+
# no theme at all, so there is nothing server-side to branch on.
|
|
59
|
+
def icon = MARK_ON_LIGHT + MARK_ON_DARK
|
|
60
|
+
|
|
61
|
+
# The mark already spells "Datadog", so the UI must not print the word
|
|
62
|
+
# again beside it. Datadog's usage rules also forbid encasing the logo in
|
|
63
|
+
# a box, which is why this affordance stays borderless where its sibling
|
|
64
|
+
# controls do not.
|
|
65
|
+
def wordmark? = true
|
|
66
|
+
|
|
67
|
+
# Tag names come from dd-trace-rb's Sidekiq integration: sidekiq.job.id and
|
|
68
|
+
# sidekiq.job.queue (Datadog::Tracing::Contrib::Sidekiq::Ext). There is no
|
|
69
|
+
# `sidekiq.jid` or `sidekiq.queue` facet — querying those matches nothing.
|
|
70
|
+
#
|
|
71
|
+
# operation_name pins the job span: a JID also appears on the enqueueing
|
|
72
|
+
# push span, and the two can sit under different service names.
|
|
73
|
+
#
|
|
74
|
+
# Values are quoted because ":" is a Datadog query operator — an unquoted
|
|
75
|
+
# namespaced class is not merely unmatched, it fails to parse.
|
|
29
76
|
def job_url(klass:, jid:, queue: nil)
|
|
30
|
-
terms = [
|
|
77
|
+
terms = [ %(@sidekiq.job.id:"#{jid}"), "operation_name:sidekiq.job" ]
|
|
31
78
|
terms << "service:#{@service}" if @service
|
|
32
79
|
terms << @extra_query if @extra_query
|
|
33
80
|
traces_url(terms)
|
|
34
81
|
end
|
|
35
82
|
|
|
36
83
|
def queue_url(name)
|
|
37
|
-
traces_url([
|
|
84
|
+
traces_url([ %(@sidekiq.job.queue:"#{name}") ])
|
|
38
85
|
end
|
|
39
86
|
|
|
40
87
|
# Grouped Errors rows have no single JID, so link to a class-wide search.
|
|
41
|
-
#
|
|
42
|
-
#
|
|
88
|
+
# dd-trace sets span.resource to the job class — unwrapped, for ActiveJob —
|
|
89
|
+
# so callers must pass the real class, not the adapter's wrapper. Tune via
|
|
90
|
+
# `extra_query` if your spans are tagged differently.
|
|
43
91
|
def error_url(klass:, error: nil)
|
|
44
|
-
terms = [ "
|
|
92
|
+
terms = [ %(resource_name:"#{klass}") ]
|
|
45
93
|
terms << "service:#{@service}" if @service
|
|
46
94
|
terms << @extra_query if @extra_query
|
|
47
95
|
traces_url(terms)
|
data/lib/roundhouse_ui/pause.rb
CHANGED
|
@@ -9,10 +9,8 @@ module RoundhouseUi
|
|
|
9
9
|
# consumed without stopping the worker process.
|
|
10
10
|
#
|
|
11
11
|
# When Sidekiq Pro is loaded we defer to *its* registry instead (see .native?).
|
|
12
|
-
# Pro
|
|
13
|
-
#
|
|
14
|
-
# Roundhouse fetcher installed — and Pro's key ("paused") is not ours
|
|
15
|
-
# ("roundhouse:paused"), so writing our own set there would do nothing.
|
|
12
|
+
# Under Pro we defer to its own pause API (see .native?), so pausing is enforced
|
|
13
|
+
# with no Roundhouse fetcher installed. Pro's key is not ours.
|
|
16
14
|
module Pause
|
|
17
15
|
KEY = "roundhouse:paused"
|
|
18
16
|
FETCH_FLAG = "roundhouse:fetch_alive" # liveness beacon set by the fetcher
|
|
@@ -28,10 +26,8 @@ module RoundhouseUi
|
|
|
28
26
|
defined?(::Sidekiq::Queue) && ::Sidekiq::Queue.method_defined?(:pause!)
|
|
29
27
|
end
|
|
30
28
|
|
|
31
|
-
# Under Pro, go through Sidekiq::Queue#pause!
|
|
32
|
-
#
|
|
33
|
-
# update on the "pro:config" pubsub message that pause! publishes. A bare
|
|
34
|
-
# SADD would leave running workers pulling the queue until they restarted.
|
|
29
|
+
# Under Pro, always go through Sidekiq::Queue#pause! — writing PRO_KEY directly
|
|
30
|
+
# does not reach already-running workers.
|
|
35
31
|
def pause!(queue)
|
|
36
32
|
return ::Sidekiq::Queue.new(queue.to_s).pause! if native?
|
|
37
33
|
|
|
@@ -82,9 +78,8 @@ module RoundhouseUi
|
|
|
82
78
|
# True when a RoundhouseUi::Fetch has reported in recently — i.e. pausing
|
|
83
79
|
# will take effect. When false, the UI warns instead of pretending.
|
|
84
80
|
#
|
|
85
|
-
# Under Pro no beacon is needed: Pro
|
|
86
|
-
#
|
|
87
|
-
# enforces pauses whether or not our fetcher is installed.
|
|
81
|
+
# Under Pro no beacon is needed: any Pro worker enforces pauses whether or not
|
|
82
|
+
# our fetcher is installed.
|
|
88
83
|
def fetch_installed?
|
|
89
84
|
return true if native?
|
|
90
85
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
# One row of the Queues page: name, depth, and how long the oldest waiting job
|
|
3
|
+
# has been waiting. A plain value object rather than a backend's own Queue,
|
|
4
|
+
# because the backends compute these in one batch — Sidekiq in a single
|
|
5
|
+
# pipeline, Solid Queue in a single grouped query — and handing back live
|
|
6
|
+
# objects invites the caller to re-read per row and undo that.
|
|
7
|
+
#
|
|
8
|
+
# `latency` is seconds. Actions (pause, purge, snapshot) still go through
|
|
9
|
+
# backend.queue(name); this carries no behaviour.
|
|
10
|
+
QueueSummary = Struct.new(:name, :size, :latency, keyword_init: true)
|
|
11
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
# Periodic work, normalised across the four things that schedule it.
|
|
3
|
+
#
|
|
4
|
+
# There is no shared API: sidekiq-cron, sidekiq-scheduler, Sidekiq Enterprise's
|
|
5
|
+
# periodic loops and Solid Queue's recurring tasks each expose something
|
|
6
|
+
# different. So this detects what is loaded and adapts, the same way the
|
|
7
|
+
# backend port does — nothing to configure, and the nav item hides when nothing
|
|
8
|
+
# is present. See #62.
|
|
9
|
+
#
|
|
10
|
+
# Reading only. A UI that lets someone silently change a production schedule is
|
|
11
|
+
# a different risk conversation, and the schedule belongs in the code that
|
|
12
|
+
# defines it.
|
|
13
|
+
module Recurring
|
|
14
|
+
Task = Struct.new(:name, :schedule, :klass, :queue, :last_run, :enabled, :source,
|
|
15
|
+
keyword_init: true) do
|
|
16
|
+
def enabled? = enabled != false
|
|
17
|
+
|
|
18
|
+
# The interesting question is not "what is the crontab" — it is "this says
|
|
19
|
+
# hourly and has not run in three days". That needs the schedule's expected
|
|
20
|
+
# interval, which needs a cron parser.
|
|
21
|
+
#
|
|
22
|
+
# Fugit ships with both sidekiq-cron and sidekiq-scheduler, so it is present
|
|
23
|
+
# wherever this feature is. Where it is not, staleness is simply unknown
|
|
24
|
+
# rather than guessed.
|
|
25
|
+
def expected_interval
|
|
26
|
+
return nil unless defined?(::Fugit)
|
|
27
|
+
|
|
28
|
+
parsed = ::Fugit.parse(schedule.to_s)
|
|
29
|
+
return nil unless parsed.respond_to?(:next_time)
|
|
30
|
+
|
|
31
|
+
a = parsed.next_time(Time.now)
|
|
32
|
+
b = parsed.next_time(a.to_t + 1)
|
|
33
|
+
(b.to_t - a.to_t).to_f
|
|
34
|
+
rescue StandardError
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Overdue by more than one whole interval. One interval of slack on purpose:
|
|
39
|
+
# a job due at :00 and running at :00:07 is not late, and a view that says it
|
|
40
|
+
# is gets ignored.
|
|
41
|
+
def overdue?
|
|
42
|
+
interval = expected_interval
|
|
43
|
+
return false if interval.nil? || last_run.nil? || !enabled?
|
|
44
|
+
|
|
45
|
+
Time.now - last_run > interval * 2
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def status
|
|
49
|
+
return :paused unless enabled?
|
|
50
|
+
return :unknown if last_run.nil?
|
|
51
|
+
|
|
52
|
+
overdue? ? :overdue : :ok
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
module_function
|
|
56
|
+
|
|
57
|
+
# Every source that is loaded, merged. An app can genuinely run two — a
|
|
58
|
+
# sidekiq-cron schedule inherited from an old migration alongside Solid
|
|
59
|
+
# Queue's own — and hiding one of them would be worse than showing both.
|
|
60
|
+
# Overdue first, then by name — the order is information, same rule as
|
|
61
|
+
# everywhere else, and a stable tiebreak so it does not shuffle.
|
|
62
|
+
def tasks
|
|
63
|
+
SOURCES.flat_map { |name, reader| detected?(name) ? safely(name, &reader) : [] }
|
|
64
|
+
.sort_by { |t| [ t.overdue? ? 0 : 1, t.name.to_s ] }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def any? = SOURCES.keys.any? { |n| detected?(n) }
|
|
68
|
+
|
|
69
|
+
def detected?(name)
|
|
70
|
+
case name
|
|
71
|
+
when :sidekiq_cron then defined?(::Sidekiq::Cron::Job)
|
|
72
|
+
when :sidekiq_scheduler then defined?(::SidekiqScheduler::Schedule) || defined?(::Sidekiq::Scheduler)
|
|
73
|
+
when :sidekiq_ent then defined?(::Sidekiq::Periodic::LoopSet)
|
|
74
|
+
when :solid_queue then defined?(::SolidQueue::RecurringTask)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A broken or half-configured scheduler must not take the page down, and it
|
|
79
|
+
# must not hide the schedulers that ARE working.
|
|
80
|
+
def safely(name)
|
|
81
|
+
result = yield
|
|
82
|
+
# NOT Array(): a Struct responds to to_a, so Array(task) splats a single
|
|
83
|
+
# task into its seven field values instead of wrapping it.
|
|
84
|
+
result.is_a?(Array) ? result : [ result ].compact
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
Rails.logger&.warn("[roundhouse] recurring source #{name} failed: #{e.message}") if defined?(Rails)
|
|
87
|
+
[]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
SOURCES = {
|
|
91
|
+
sidekiq_cron: lambda do
|
|
92
|
+
::Sidekiq::Cron::Job.all.map do |j|
|
|
93
|
+
Task.new(name: j.name, schedule: j.cron, klass: j.klass, queue: j.queue,
|
|
94
|
+
last_run: j.last_enqueue_time, enabled: j.status != "disabled",
|
|
95
|
+
source: "sidekiq-cron")
|
|
96
|
+
end
|
|
97
|
+
end,
|
|
98
|
+
|
|
99
|
+
sidekiq_scheduler: lambda do
|
|
100
|
+
(::Sidekiq.schedule || {}).map do |name, cfg|
|
|
101
|
+
Task.new(name: name,
|
|
102
|
+
schedule: cfg["cron"] || cfg["every"] || cfg["interval"] || cfg[:cron],
|
|
103
|
+
klass: cfg["class"] || cfg[:class], queue: cfg["queue"] || cfg[:queue],
|
|
104
|
+
last_run: scheduler_last_run(name),
|
|
105
|
+
enabled: cfg["enabled"] != false,
|
|
106
|
+
source: "sidekiq-scheduler")
|
|
107
|
+
end
|
|
108
|
+
end,
|
|
109
|
+
|
|
110
|
+
sidekiq_ent: lambda do
|
|
111
|
+
::Sidekiq::Periodic::LoopSet.new.map do |loop_|
|
|
112
|
+
Task.new(name: loop_.klass, schedule: loop_.schedule, klass: loop_.klass,
|
|
113
|
+
queue: loop_.options&.dig("queue"),
|
|
114
|
+
last_run: (Time.at(loop_.last_enqueue_time.to_i) if loop_.last_enqueue_time),
|
|
115
|
+
enabled: true, source: "sidekiq-ent")
|
|
116
|
+
end
|
|
117
|
+
end,
|
|
118
|
+
|
|
119
|
+
solid_queue: lambda do
|
|
120
|
+
::SolidQueue::RecurringTask.all.map do |t|
|
|
121
|
+
Task.new(name: t.key, schedule: t.schedule, klass: t.class_name, queue: t.queue_name,
|
|
122
|
+
last_run: t.recurring_executions.maximum(:run_at),
|
|
123
|
+
enabled: true, source: "solid-queue")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
}.freeze
|
|
127
|
+
|
|
128
|
+
def scheduler_last_run(name)
|
|
129
|
+
return nil unless defined?(::SidekiqScheduler::RedisManager)
|
|
130
|
+
|
|
131
|
+
raw = ::SidekiqScheduler::RedisManager.get_job_last_time(name)
|
|
132
|
+
raw && Time.parse(raw.to_s)
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
# Resolves a runbook for a job class at read time, the same way Tags resolves
|
|
3
|
+
# owners (ADR 0002) — no middleware, no storage, applies retroactively to jobs
|
|
4
|
+
# already in the sets.
|
|
5
|
+
#
|
|
6
|
+
# RoundhouseUi.job_runbooks = RoundhouseUi::Runbooks.from_constant(:RUNBOOK)
|
|
7
|
+
# RoundhouseUi.job_runbooks = { "Billing::SyncWorker" => "https://wiki/…" }
|
|
8
|
+
# RoundhouseUi.job_runbooks = ->(klass:, item:) { "https://wiki/#{klass}" }
|
|
9
|
+
#
|
|
10
|
+
# Whoever wrote the job knows what to do when it fails; the person paged at
|
|
11
|
+
# 3am usually does not. This is the cheapest possible bridge between them.
|
|
12
|
+
module Runbooks
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# A runbook URL for one job, or nil. Resolvers always see the real job
|
|
16
|
+
# class — the ActiveJob wrapper is unwrapped first, so a runbook declared on
|
|
17
|
+
# a mailer is found when that mailer fails.
|
|
18
|
+
#
|
|
19
|
+
# Only http(s) URLs are returned. A runbook lands in an href, so `javascript:`
|
|
20
|
+
# and `data:` are refused rather than escaped: a host that misconfigures this
|
|
21
|
+
# should get no link, not a link that runs.
|
|
22
|
+
def for(klass, item = nil, cache: nil)
|
|
23
|
+
source = RoundhouseUi.job_runbooks
|
|
24
|
+
return nil unless source
|
|
25
|
+
|
|
26
|
+
effective = RoundhouseUi.unwrapped_class(klass, item)
|
|
27
|
+
return nil unless effective.present?
|
|
28
|
+
return resolve(source, effective, item) unless cache
|
|
29
|
+
|
|
30
|
+
# `item` is withheld once we are caching by class — the same rule Tags
|
|
31
|
+
# follows, and for the same reason: a resolver that reads the payload but
|
|
32
|
+
# is cached per class would hand every job of that class the *first* job's
|
|
33
|
+
# runbook. Withholding makes that misuse resolve to nil deterministically
|
|
34
|
+
# instead of returning something plausible and wrong. A runbook describes a
|
|
35
|
+
# job class, so reading the payload is a misuse rather than a use case.
|
|
36
|
+
cache.key?(effective) ? cache[effective] : cache[effective] = resolve(source, effective, nil)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The class-constant convention, matching Tags.from_constant. Inherited
|
|
40
|
+
# constants count, so a base class can carry the runbook for a whole family.
|
|
41
|
+
def from_constant(const_name = :RUNBOOK)
|
|
42
|
+
lambda do |klass:, item:|
|
|
43
|
+
_ = item
|
|
44
|
+
k = RoundhouseUi.job_class(klass)
|
|
45
|
+
k.const_get(const_name) if k&.const_defined?(const_name)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# -- internals ----------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
def resolve(source, klass, item)
|
|
52
|
+
raw = if source.respond_to?(:call)
|
|
53
|
+
source.call(klass: klass, item: item)
|
|
54
|
+
elsif source.respond_to?(:[])
|
|
55
|
+
source[klass]
|
|
56
|
+
end
|
|
57
|
+
safe_url(raw)
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
warn_once("job_runbooks resolver failed for #{klass}: #{e.message}")
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def safe_url(raw)
|
|
64
|
+
url = raw.to_s.strip
|
|
65
|
+
# Capped like theme values are: this is interpolated into markup on every
|
|
66
|
+
# matching row, and a runaway string from a misconfigured resolver should
|
|
67
|
+
# not become the page.
|
|
68
|
+
return nil if url.empty? || url.length > 2_000
|
|
69
|
+
|
|
70
|
+
url if url.match?(%r{\Ahttps?://\S+\z}i)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def warn_once(message) = RoundhouseUi.warn_once(message)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -47,8 +47,19 @@ module RoundhouseUi
|
|
|
47
47
|
return 0 unless raw
|
|
48
48
|
|
|
49
49
|
data = JSON.parse(raw)
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
queue = data["queue"]
|
|
51
|
+
|
|
52
|
+
Sidekiq.redis do |conn|
|
|
53
|
+
# Re-register the queue, not just its jobs. Sidekiq::Queue#clear removes
|
|
54
|
+
# the name from the `queues` set as well as deleting the list, and that
|
|
55
|
+
# set is what Sidekiq::Queue.all reads — so a restore that only pushes the
|
|
56
|
+
# payloads back leaves a queue Roundhouse cannot see. The operator purges
|
|
57
|
+
# production, restores, and every page says nothing came back. Sidekiq's
|
|
58
|
+
# own client does this SADD on every push.
|
|
59
|
+
conn.call("SADD", "queues", queue)
|
|
60
|
+
data["jobs"].each { |payload| conn.call("RPUSH", "queue:#{queue}", payload) }
|
|
61
|
+
end
|
|
62
|
+
|
|
52
63
|
data["jobs"].size
|
|
53
64
|
end
|
|
54
65
|
|