cosmonats 0.7.0 → 0.9.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 +219 -113
- data/lib/cosmo/active_job/adapter.rb +1 -2
- data/lib/cosmo/api/counter.rb +17 -17
- data/lib/cosmo/api/cron/entry.rb +10 -11
- data/lib/cosmo/api/cron.rb +6 -3
- data/lib/cosmo/api/job.rb +1 -1
- data/lib/cosmo/api/kv.rb +3 -3
- data/lib/cosmo/api/registry.rb +1 -1
- data/lib/cosmo/api/stats/busy.rb +6 -1
- data/lib/cosmo/api/stats/metrics.rb +109 -0
- data/lib/cosmo/api/stats/processes.rb +14 -4
- data/lib/cosmo/api/stats/totals.rb +4 -12
- data/lib/cosmo/api/stats.rb +18 -2
- data/lib/cosmo/api/stream.rb +7 -2
- data/lib/cosmo/batch/counters.rb +19 -0
- data/lib/cosmo/batch.rb +6 -6
- data/lib/cosmo/cli.rb +21 -5
- data/lib/cosmo/client.rb +37 -14
- data/lib/cosmo/config/cosmo.yml +58 -0
- data/lib/cosmo/config/settings.rb +20 -0
- data/lib/cosmo/config.rb +144 -19
- data/lib/cosmo/heartbeat.rb +11 -0
- data/lib/cosmo/job/data.rb +17 -12
- data/lib/cosmo/job/enqueuer.rb +53 -0
- data/lib/cosmo/job/limit.rb +2 -2
- data/lib/cosmo/job/processor.rb +32 -58
- data/lib/cosmo/job/requeue.rb +20 -0
- data/lib/cosmo/job.rb +8 -22
- data/lib/cosmo/logger.rb +15 -2
- data/lib/cosmo/middleware/busy.rb +15 -0
- data/lib/cosmo/middleware/chain.rb +137 -0
- data/lib/cosmo/middleware/limit.rb +50 -0
- data/lib/cosmo/middleware/metrics.rb +39 -0
- data/lib/cosmo/{sentry/job_processor_middleware.rb → middleware/sentry.rb} +26 -30
- data/lib/cosmo/middleware/totals.rb +29 -0
- data/lib/cosmo/middleware.rb +7 -0
- data/lib/cosmo/processor.rb +11 -5
- data/lib/cosmo/publisher.rb +1 -13
- data/lib/cosmo/railtie.rb +2 -2
- data/lib/cosmo/services.rb +69 -0
- data/lib/cosmo/stream/processor.rb +7 -1
- data/lib/cosmo/utils/hash.rb +7 -8
- data/lib/cosmo/utils.rb +0 -1
- data/lib/cosmo/version.rb +1 -1
- data/lib/cosmo/web/assets/app.css +271 -571
- data/lib/cosmo/web/assets/app.js +158 -0
- data/lib/cosmo/web/assets/fonts/OFL.txt +92 -0
- data/lib/cosmo/web/assets/fonts/geist-mono.woff2 +0 -0
- data/lib/cosmo/web/assets/fonts/geist.woff2 +0 -0
- data/lib/cosmo/web/assets/logo.svg +10 -0
- data/lib/cosmo/web/cache.rb +75 -0
- data/lib/cosmo/web/chart.rb +122 -0
- data/lib/cosmo/web/circuit.rb +55 -0
- data/lib/cosmo/web/context.rb +0 -4
- data/lib/cosmo/web/controllers/application.rb +16 -0
- data/lib/cosmo/web/controllers/batches.rb +1 -0
- data/lib/cosmo/web/controllers/crons.rb +17 -2
- data/lib/cosmo/web/controllers/jobs.rb +59 -54
- data/lib/cosmo/web/controllers/metrics.rb +49 -0
- data/lib/cosmo/web/controllers/overview.rb +70 -0
- data/lib/cosmo/web/controllers/processes.rb +33 -0
- data/lib/cosmo/web/controllers/streams.rb +26 -28
- data/lib/cosmo/web/helpers/application.rb +67 -0
- data/lib/cosmo/web/renderer.rb +19 -6
- data/lib/cosmo/web/throughput.rb +104 -0
- data/lib/cosmo/web/views/batches/_table.erb +42 -43
- data/lib/cosmo/web/views/batches/index.erb +3 -10
- data/lib/cosmo/web/views/crons/_table.erb +32 -53
- data/lib/cosmo/web/views/crons/index.erb +2 -9
- data/lib/cosmo/web/views/jobs/_args.erb +5 -0
- data/lib/cosmo/web/views/jobs/_busy.erb +25 -121
- data/lib/cosmo/web/views/jobs/_dead.erb +42 -82
- data/lib/cosmo/web/views/jobs/_enqueued.erb +33 -113
- data/lib/cosmo/web/views/jobs/_panel.erb +29 -0
- data/lib/cosmo/web/views/jobs/_queue_row.erb +18 -0
- data/lib/cosmo/web/views/jobs/_queues.erb +20 -0
- data/lib/cosmo/web/views/jobs/_scheduled.erb +20 -54
- data/lib/cosmo/web/views/jobs/index.erb +4 -17
- data/lib/cosmo/web/views/layout.erb +73 -22
- data/lib/cosmo/web/views/metrics/_chart.erb +41 -0
- data/lib/cosmo/web/views/metrics/_panel.erb +13 -0
- data/lib/cosmo/web/views/metrics/_table.erb +25 -0
- data/lib/cosmo/web/views/metrics/index.erb +9 -0
- data/lib/cosmo/web/views/overview/_badges.erb +3 -0
- data/lib/cosmo/web/views/overview/_kpis.erb +19 -0
- data/lib/cosmo/web/views/overview/_running.erb +18 -0
- data/lib/cosmo/web/views/overview/_throughput.erb +50 -0
- data/lib/cosmo/web/views/overview/_workers.erb +25 -0
- data/lib/cosmo/web/views/overview/index.erb +28 -0
- data/lib/cosmo/web/views/processes/_table.erb +67 -0
- data/lib/cosmo/web/views/processes/index.erb +1 -0
- data/lib/cosmo/web/views/shared/_pager.erb +22 -0
- data/lib/cosmo/web/views/shared/unavailable.erb +7 -0
- data/lib/cosmo/web/views/streams/_info.erb +28 -86
- data/lib/cosmo/web/views/streams/_list.erb +7 -0
- data/lib/cosmo/web/views/streams/_pause_banner.erb +8 -16
- data/lib/cosmo/web/views/streams/_stream.erb +52 -0
- data/lib/cosmo/web/views/streams/index.erb +3 -11
- data/lib/cosmo/web/views/streams/info.erb +3 -11
- data/lib/cosmo/web.rb +67 -33
- data/lib/cosmo.rb +38 -0
- data/sig/cosmo/api/counter.rbs +2 -3
- data/sig/cosmo/api/cron/entry.rbs +2 -1
- data/sig/cosmo/api/stats/busy.rbs +3 -1
- data/sig/cosmo/api/stats/metrics.rbs +42 -0
- data/sig/cosmo/api/stats/processes.rbs +3 -0
- data/sig/cosmo/api/stats/totals.rbs +3 -2
- data/sig/cosmo/api/stats.rbs +4 -0
- data/sig/cosmo/api/stream.rbs +2 -0
- data/sig/cosmo/batch/counters.rbs +10 -0
- data/sig/cosmo/batch.rbs +3 -4
- data/sig/cosmo/cli.rbs +2 -0
- data/sig/cosmo/client.rbs +4 -1
- data/sig/cosmo/config/settings.rbs +31 -0
- data/sig/cosmo/config.rbs +59 -5
- data/sig/cosmo/heartbeat.rbs +2 -0
- data/sig/cosmo/job/data.rbs +5 -3
- data/sig/cosmo/job/enqueuer.rbs +11 -0
- data/sig/cosmo/job/processor.rbs +3 -5
- data/sig/cosmo/job/requeue.rbs +9 -0
- data/sig/cosmo/job.rbs +0 -2
- data/sig/cosmo/logger.rbs +6 -0
- data/sig/cosmo/middleware/chain.rbs +51 -0
- data/sig/cosmo/middleware/sentry.rbs +14 -0
- data/sig/cosmo/middleware.rbs +35 -0
- data/sig/cosmo/processor.rbs +2 -0
- data/sig/cosmo/publisher.rbs +1 -9
- data/sig/cosmo/services.rbs +18 -0
- data/sig/cosmo/stream/processor.rbs +2 -0
- data/sig/cosmo/utils/hash.rbs +2 -3
- data/sig/cosmo.rbs +11 -0
- metadata +60 -20
- data/lib/cosmo/sentry/auto.rb +0 -6
- data/lib/cosmo/utils/overrides.rb +0 -206
- data/lib/cosmo/utils/warnings.rb +0 -17
- data/lib/cosmo/web/controllers/actions.rb +0 -16
- data/lib/cosmo/web/views/actions/index.erb +0 -7
- data/lib/cosmo/web/views/jobs/_processes.erb +0 -131
- data/lib/cosmo/web/views/jobs/_stats.erb +0 -73
- data/lib/cosmo/web/views/jobs/_tabs.erb +0 -8
- data/lib/cosmo/web/views/jobs/busy.erb +0 -18
- data/lib/cosmo/web/views/jobs/dead.erb +0 -18
- data/lib/cosmo/web/views/jobs/enqueued.erb +0 -18
- data/lib/cosmo/web/views/jobs/scheduled.erb +0 -18
- data/lib/cosmo/web/views/streams/_stream_row.erb +0 -42
- data/lib/cosmo/web/views/streams/_table.erb +0 -25
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<div class="card-head">
|
|
2
|
+
<div class="card-title">Workers</div>
|
|
3
|
+
<div class="mono mute" style="font-size:11px"><%= @busy %> / <%= @threads %> busy</div>
|
|
4
|
+
</div>
|
|
5
|
+
<% if @processes.empty? -%>
|
|
6
|
+
<div class="empty"><b>No worker processes</b>Start one with <code>cosmo -C config/cosmo.yml</code>.</div>
|
|
7
|
+
<% else -%>
|
|
8
|
+
<div class="cells">
|
|
9
|
+
<% @cells.each do |stream| -%>
|
|
10
|
+
<i<% if stream %> style="background:<%= hue(stream) %>" title="<%= h(stream) %>"<% end %>></i>
|
|
11
|
+
<% end -%>
|
|
12
|
+
</div>
|
|
13
|
+
<% if @hidden.positive? -%><div class="small-mono">+<%= @hidden %> more threads</div><% end -%>
|
|
14
|
+
<div class="procs">
|
|
15
|
+
<% @processes.each do |process| -%>
|
|
16
|
+
<div class="proc">
|
|
17
|
+
<div class="ellip" title="<%= h(process[:cmdline]) %>"><%= h(process[:hostname]) %> · pid <%= h(process[:pid]) %></div>
|
|
18
|
+
<span class="mute"><%= h(process[:busy]) %>/<%= h(process[:concurrency]) %></span>
|
|
19
|
+
<span class="faint"><%= process[:rss] ? format_bytes(process[:rss].to_i * 1024) : '—' %></span>
|
|
20
|
+
<span style="color:<%= { 'running' => 'var(--ok)', 'quiet' => 'var(--warn)', 'stopping' => 'var(--err)' }.fetch(process[:state].to_s, 'var(--mute)') %>"><%= h(process[:state]) %></span>
|
|
21
|
+
</div>
|
|
22
|
+
<% end -%>
|
|
23
|
+
</div>
|
|
24
|
+
<a class="small-mono" href="<%= url_for('/processes') %>">All <%= @total %> process(es) →</a>
|
|
25
|
+
<% end -%>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<div hx-get="<%= url_for('/_kpis') %>" hx-trigger="load, cosmo:poll" data-poll="<%= poll_interval %>" hx-swap="innerHTML">
|
|
2
|
+
<div class="kpis"><div class="kpi"><div class="loading">Loading statistics…</div></div></div>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="row-wrap">
|
|
6
|
+
<% if @metrics -%>
|
|
7
|
+
<section class="card card-pad grow2" id="throughput" hx-get="<%= url_for('/_throughput') %>" hx-trigger="load" hx-swap="outerHTML">
|
|
8
|
+
<div class="card-head"><div class="card-title">Throughput</div></div>
|
|
9
|
+
<div class="loading">Loading metrics…</div>
|
|
10
|
+
</section>
|
|
11
|
+
<% end -%>
|
|
12
|
+
<section class="card card-pad <%= @metrics ? 'grow1' : 'grow2' %>"
|
|
13
|
+
hx-get="<%= url_for('/_workers') %>" hx-trigger="load, cosmo:poll" data-poll="<%= poll_interval %>" hx-swap="innerHTML">
|
|
14
|
+
<div class="card-head"><div class="card-title">Workers</div></div>
|
|
15
|
+
<div class="loading">Loading workers…</div>
|
|
16
|
+
</section>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="row-wrap">
|
|
20
|
+
<section class="card grow2" hx-get="<%= url_for('/jobs/_queues', compact: 1) %>" hx-trigger="load, cosmo:poll" data-poll="<%= poll_interval %>" hx-swap="innerHTML">
|
|
21
|
+
<div class="card-head bar"><div class="card-title">Queues</div></div>
|
|
22
|
+
<div class="loading">Loading queues…</div>
|
|
23
|
+
</section>
|
|
24
|
+
<section class="card grow1" hx-get="<%= url_for('/_running') %>" hx-trigger="load, cosmo:poll" data-poll="<%= poll_interval %>" hx-swap="innerHTML">
|
|
25
|
+
<div class="card-head bar ruled"><div class="card-title">Running now</div></div>
|
|
26
|
+
<div class="loading">Loading jobs…</div>
|
|
27
|
+
</section>
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<% states = { 'running' => 'var(--ok)', 'quiet' => 'var(--warn)', 'stopping' => 'var(--err)' } -%>
|
|
2
|
+
<% kinds = { 'jobs' => 'Jobs', 'streams' => 'Streams', 'jobs+streams' => 'Jobs & Streams', 'none' => 'No subscriptions' } -%>
|
|
3
|
+
<section class="card" id="processes"
|
|
4
|
+
<% if @polling -%>
|
|
5
|
+
hx-get="<%= url_for('/processes', @query.merge(page: @page)) %>" hx-trigger="cosmo:poll" data-poll="<%= poll_interval %>" hx-swap="outerHTML"
|
|
6
|
+
<% end -%>>
|
|
7
|
+
<div class="card-head bar ruled">
|
|
8
|
+
<div class="card-title"><%= @total %> process(es) alive</div>
|
|
9
|
+
<% if @polling -%>
|
|
10
|
+
<button type="button" class="btn quiet" hx-get="<%= url_for('/processes', @query.merge(page: @page, poll: 0)) %>"
|
|
11
|
+
hx-target="#processes" hx-swap="outerHTML" hx-indicator="#global-spinner">⏸ Stop polling</button>
|
|
12
|
+
<% else -%>
|
|
13
|
+
<button type="button" class="btn" hx-get="<%= url_for('/processes', @query.except(:poll).merge(page: @page)) %>"
|
|
14
|
+
hx-target="#processes" hx-swap="outerHTML" hx-indicator="#global-spinner">▶ Live poll</button>
|
|
15
|
+
<% end -%>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<% if @groups.empty? -%>
|
|
19
|
+
<div class="empty"><b>No worker processes</b>Start one with <code>cosmo -C config/cosmo.yml</code>.</div>
|
|
20
|
+
<% else -%>
|
|
21
|
+
<div class="scroll"><div style="min-width:1040px">
|
|
22
|
+
<div class="thead t-procs">
|
|
23
|
+
<span>Process</span><span>Command</span><span>Subscriptions</span><span class="r">Busy</span><span>State</span>
|
|
24
|
+
<span class="r">RTT</span><span class="r">Reconn.</span><span class="r">Memory</span><span class="r">Uptime</span><span class="r">Last beat</span>
|
|
25
|
+
</div>
|
|
26
|
+
<% @groups.each do |kind, group| -%>
|
|
27
|
+
<div class="trow group t-procs"><span class="full"><%= h(kinds.fetch(kind, kind)) %> (<%= group.size %>)</span></div>
|
|
28
|
+
<% group.each do |process| -%>
|
|
29
|
+
<% nats = Hash(process[:nats]) -%>
|
|
30
|
+
<% cmdline = process[:cmdline].to_s -%>
|
|
31
|
+
<div class="trow hover t-procs">
|
|
32
|
+
<div class="stack">
|
|
33
|
+
<span class="strong ellip"><%= h(process[:hostname]) %>:<%= h(process[:pid]) %></span>
|
|
34
|
+
<span class="small-mono ellip"><%= h(nats[:ip] || 'unknown IP') %><% if nats[:name] -%> · <%= h(nats[:name]) %><% end -%></span>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="stack">
|
|
37
|
+
<% if cmdline.length > 32 -%>
|
|
38
|
+
<details>
|
|
39
|
+
<summary class="mono ellip" style="font-size:11.5px" title="<%= h(cmdline) %>"><%= h(cmdline) %></summary>
|
|
40
|
+
<pre class="code-block"><%= h(cmdline) %></pre>
|
|
41
|
+
</details>
|
|
42
|
+
<% else -%>
|
|
43
|
+
<span class="mono" style="font-size:11.5px"><%= h(cmdline) %></span>
|
|
44
|
+
<% end -%>
|
|
45
|
+
<span class="small-mono">cosmo <%= h(process[:version]) %> · <%= h(process[:ruby]) %><% if process[:http_port] -%> · http :<%= h(process[:http_port]) %><% end -%></span>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="stack">
|
|
48
|
+
<% Hash(process[:subscriptions]).each do |type, names| -%>
|
|
49
|
+
<% next if Array(names).empty? -%>
|
|
50
|
+
<span class="mono ellip" style="font-size:11.5px" title="<%= h(Array(names).join(', ')) %>"><% if kind == 'jobs+streams' -%><span class="faint"><%= h(type) %>:</span> <% end -%><%= h(Array(names).join(', ')) %></span>
|
|
51
|
+
<% end -%>
|
|
52
|
+
</div>
|
|
53
|
+
<span class="mono r"><%= h(process[:busy]) %>/<%= h(process[:concurrency]) %></span>
|
|
54
|
+
<span class="pill" style="--tone:<%= states.fetch(process[:state].to_s, 'var(--acc)') %>"><%= h(process[:state]) %></span>
|
|
55
|
+
<span class="mono mute r"><%= nats[:rtt] ? "#{h(nats[:rtt])}ms" : '—' %></span>
|
|
56
|
+
<span class="mono mute r"><%= h(nats[:reconnects].to_i) %></span>
|
|
57
|
+
<span class="mono mute r"><%= process[:rss] ? format_bytes(process[:rss].to_i * 1024) : '—' %></span>
|
|
58
|
+
<span class="mono mute r"><%= elapsed(process[:started_at]) %></span>
|
|
59
|
+
<span class="mono mute r"><%= elapsed(process[:beat_at]) %> ago</span>
|
|
60
|
+
</div>
|
|
61
|
+
<% end -%>
|
|
62
|
+
<% end -%>
|
|
63
|
+
</div></div>
|
|
64
|
+
<% end -%>
|
|
65
|
+
|
|
66
|
+
<%= render('shared/_pager', { path: '/processes', query: @query, page: @page, total_pages: @total_pages, target: '#processes' }) %>
|
|
67
|
+
</section>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= @table %>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<% if @total_pages.to_i > 1 -%>
|
|
2
|
+
<% link = ->(page) { url_for(@path, Hash(@query).merge(page: page)) } -%>
|
|
3
|
+
<nav class="pager">
|
|
4
|
+
<% if @page > 1 -%>
|
|
5
|
+
<a class="btn quiet" hx-get="<%= link.(@page - 1) %>" hx-target="<%= @target %>" hx-swap="outerHTML" hx-indicator="#global-spinner">← Prev</a>
|
|
6
|
+
<% else -%>
|
|
7
|
+
<span class="btn quiet disabled">← Prev</span>
|
|
8
|
+
<% end -%>
|
|
9
|
+
<% pages(@page, @total_pages).each do |p| -%>
|
|
10
|
+
<% if p == :gap -%>
|
|
11
|
+
<span class="gap">…</span>
|
|
12
|
+
<% else -%>
|
|
13
|
+
<a class="btn<%= ' cur' if p == @page %>" hx-get="<%= link.(p) %>" hx-target="<%= @target %>" hx-swap="outerHTML" hx-indicator="#global-spinner"><%= p %></a>
|
|
14
|
+
<% end -%>
|
|
15
|
+
<% end -%>
|
|
16
|
+
<% if @page < @total_pages -%>
|
|
17
|
+
<a class="btn quiet" hx-get="<%= link.(@page + 1) %>" hx-target="<%= @target %>" hx-swap="outerHTML" hx-indicator="#global-spinner">Next →</a>
|
|
18
|
+
<% else -%>
|
|
19
|
+
<span class="btn quiet disabled">Next →</span>
|
|
20
|
+
<% end -%>
|
|
21
|
+
</nav>
|
|
22
|
+
<% end -%>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<section class="card" data-reload="<%= @retry %>">
|
|
2
|
+
<div class="empty">
|
|
3
|
+
<b>NATS is unreachable</b>
|
|
4
|
+
<span>Nothing was fetched, so this page shows no data. It tries again every <%= @retry %> seconds.</span>
|
|
5
|
+
<a class="btn" href="<%= h(@request.fullpath) %>">Retry now</a>
|
|
6
|
+
</div>
|
|
7
|
+
</section>
|
|
@@ -1,92 +1,34 @@
|
|
|
1
|
-
<div
|
|
2
|
-
<
|
|
3
|
-
<h3><%= format_numbers(@state.messages) %></h3>
|
|
4
|
-
<p>Messages</p>
|
|
5
|
-
</article>
|
|
6
|
-
<article class="stat-card">
|
|
7
|
-
<h3><%= format_bytes(@state.bytes) %></h3>
|
|
8
|
-
<p>Size</p>
|
|
9
|
-
</article>
|
|
10
|
-
<article class="stat-card">
|
|
11
|
-
<h3><%= @state.consumer_count %></h3>
|
|
12
|
-
<p>Consumers</p>
|
|
13
|
-
</article>
|
|
14
|
-
<article class="stat-card">
|
|
15
|
-
<h3><%= format_numbers(@state.first_seq) + " → " + format_numbers(@state.last_seq) %></h3>
|
|
16
|
-
<p>Sequence Range</p>
|
|
17
|
-
</article>
|
|
1
|
+
<div>
|
|
2
|
+
<a class="small-mono" href="<%= url_for('/streams') %>">← Back to streams</a>
|
|
18
3
|
</div>
|
|
19
4
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<div class="
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
hx-target="#content"
|
|
26
|
-
hx-push-url="true"
|
|
27
|
-
class="btn">Back to Streams</a>
|
|
5
|
+
<div class="kpis">
|
|
6
|
+
<div class="kpi" style="--hue:var(--c1)"><div class="kpi-label"><i></i>Messages</div><div class="kpi-val"><b><%= format_numbers(@state.messages) %></b></div></div>
|
|
7
|
+
<div class="kpi" style="--hue:var(--c2)"><div class="kpi-label"><i></i>Size</div><div class="kpi-val"><b><%= format_bytes(@state.bytes) %></b></div></div>
|
|
8
|
+
<div class="kpi" style="--hue:var(--c3)"><div class="kpi-label"><i></i>Consumers</div><div class="kpi-val"><b><%= @state.consumer_count %></b></div></div>
|
|
9
|
+
<div class="kpi" style="--hue:var(--c4)"><div class="kpi-label"><i></i>Sequence range</div><div class="kpi-val"><b style="font-size:22px"><%= format_numbers(@state.first_seq) %> → <%= format_numbers(@state.last_seq) %></b></div></div>
|
|
28
10
|
</div>
|
|
29
11
|
|
|
30
|
-
|
|
31
|
-
<div class="table-container">
|
|
32
|
-
<table>
|
|
33
|
-
<tbody>
|
|
34
|
-
<tr>
|
|
35
|
-
<th>Name</th>
|
|
36
|
-
<td>
|
|
37
|
-
<code><%= h(@name) %></code>
|
|
38
|
-
</td>
|
|
39
|
-
</tr>
|
|
40
|
-
<tr>
|
|
41
|
-
<th>Retention</th>
|
|
42
|
-
<td>
|
|
43
|
-
<code><%= h(@config.retention.to_s) %></code>
|
|
44
|
-
</td>
|
|
45
|
-
</tr>
|
|
46
|
-
<tr>
|
|
47
|
-
<th>Storage</th>
|
|
48
|
-
<td>
|
|
49
|
-
<code><%= h(@config.storage.to_s) %></code>
|
|
50
|
-
</td>
|
|
51
|
-
</tr>
|
|
52
|
-
<tr>
|
|
53
|
-
<th>Discard Policy</th>
|
|
54
|
-
<td>
|
|
55
|
-
<code><%= h(@config.discard.to_s) %></code>
|
|
56
|
-
</td>
|
|
57
|
-
</tr>
|
|
58
|
-
<tr>
|
|
59
|
-
<th>Max Consumers</th>
|
|
60
|
-
<td>
|
|
61
|
-
<code><%= @config.max_consumers || "Unlimited" %></code>
|
|
62
|
-
</td>
|
|
63
|
-
</tr>
|
|
64
|
-
<tr>
|
|
65
|
-
<th>Max Messages</th>
|
|
66
|
-
<td>
|
|
67
|
-
<code><%= @config.max_msgs || "Unlimited" %></code>
|
|
68
|
-
</td>
|
|
69
|
-
</tr>
|
|
70
|
-
<tr>
|
|
71
|
-
<th>Max Bytes</th>
|
|
72
|
-
<td>
|
|
73
|
-
<code><%= @config.max_bytes&.positive? ? format_bytes(@config.max_bytes) : "Unlimited" %></code>
|
|
74
|
-
</td>
|
|
75
|
-
</tr>
|
|
76
|
-
<tr>
|
|
77
|
-
<th>Max Age</th>
|
|
78
|
-
<td>
|
|
79
|
-
<code><%= @config.max_age&.positive? ? "#{@config.max_age}ns" : "Unlimited" %></code>
|
|
80
|
-
</td>
|
|
81
|
-
</tr>
|
|
82
|
-
</tbody>
|
|
83
|
-
</table>
|
|
84
|
-
</div>
|
|
12
|
+
<%= render('streams/_pause_banner', { name: @name, paused: @paused }) %>
|
|
85
13
|
|
|
86
|
-
<
|
|
87
|
-
<div class="
|
|
88
|
-
<%
|
|
89
|
-
|
|
14
|
+
<section class="card">
|
|
15
|
+
<div class="card-head bar ruled"><div class="card-title">Configuration</div></div>
|
|
16
|
+
<% rows = {
|
|
17
|
+
'Name' => @name,
|
|
18
|
+
'Retention' => @config.retention,
|
|
19
|
+
'Storage' => @config.storage,
|
|
20
|
+
'Replicas' => @config.num_replicas || 1,
|
|
21
|
+
'Discard policy' => @config.discard,
|
|
22
|
+
'Max consumers' => @config.max_consumers.to_i.positive? ? @config.max_consumers : 'Unlimited',
|
|
23
|
+
'Max messages' => @config.max_msgs.to_i.positive? ? format_numbers(@config.max_msgs) : 'Unlimited',
|
|
24
|
+
'Max bytes' => @config.max_bytes.to_i.positive? ? format_bytes(@config.max_bytes) : 'Unlimited',
|
|
25
|
+
'Max age' => @config.max_age.to_i.positive? ? "#{format_span(@config.max_age.to_i / 1_000_000_000)} (#{@config.max_age}ns)" : 'Unlimited'
|
|
26
|
+
} -%>
|
|
27
|
+
<% rows.each do |label, value| -%>
|
|
28
|
+
<div class="trow t-kv"><span class="eyebrow"><%= label %></span><code><%= h(value) %></code></div>
|
|
90
29
|
<% end -%>
|
|
91
|
-
|
|
92
|
-
|
|
30
|
+
<div class="trow t-kv">
|
|
31
|
+
<span class="eyebrow">Subjects</span>
|
|
32
|
+
<div class="subjects"><% Array(@config.subjects).each do |subject| -%><span class="subject"><%= h(subject) %></span><% end -%></div>
|
|
33
|
+
</div>
|
|
34
|
+
</section>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<% if @streams.empty? -%>
|
|
2
|
+
<section class="card"><div class="empty"><b>No streams</b>Run <code>cosmo --setup</code> to create them.</div></section>
|
|
3
|
+
<% else -%>
|
|
4
|
+
<% @streams.each do |stream| -%>
|
|
5
|
+
<%= render('streams/_stream', { stream: }) %>
|
|
6
|
+
<% end -%>
|
|
7
|
+
<% end -%>
|
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
<div
|
|
3
|
-
<span
|
|
4
|
-
<
|
|
5
|
-
hx-target="#pause-banner-<%= h(@name) %>"
|
|
6
|
-
hx-swap="outerHTML"
|
|
7
|
-
class="btn btn-success">▶ Resume Stream</a>
|
|
1
|
+
<section class="card banner" style="<%= 'border-color:color-mix(in oklch,var(--warn) 45%,var(--line))' if @paused %>">
|
|
2
|
+
<div class="stack">
|
|
3
|
+
<span class="strong"><%= @paused ? '⏸ Stream is paused' : '▶ Stream is active' %></span>
|
|
4
|
+
<span class="mute"><%= @paused ? 'Consumers are not fetching new messages.' : 'Consumers are fetching new messages.' %></span>
|
|
8
5
|
</div>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
hx-target="#pause-banner-<%= h(@name) %>"
|
|
14
|
-
hx-swap="outerHTML"
|
|
15
|
-
class="btn btn-warning">⏸ Pause Stream</a>
|
|
16
|
-
</div>
|
|
17
|
-
<% end -%>
|
|
6
|
+
<button type="button" class="btn<%= ' quiet' unless @paused %>"
|
|
7
|
+
hx-patch="<%= url_for(@paused ? '/streams/unpause' : '/streams/pause', { name: u(@name), view: 'banner' }) %>"
|
|
8
|
+
hx-target="closest section" hx-swap="outerHTML" hx-indicator="#global-spinner"><%= @paused ? '▶ Resume stream' : '⏸ Pause stream' %></button>
|
|
9
|
+
</section>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<% s = @stream -%>
|
|
2
|
+
<% state = s[:state] -%>
|
|
3
|
+
<% config = s[:config] -%>
|
|
4
|
+
<% max_age = config.max_age.to_i -%>
|
|
5
|
+
<section class="card">
|
|
6
|
+
<div class="stream-head">
|
|
7
|
+
<div class="stack" style="gap:6px">
|
|
8
|
+
<a class="stream-name" style="--hue:<%= s[:paused] ? 'var(--warn)' : hue(s[:name]) %>"
|
|
9
|
+
href="<%= url_for('/streams/info', name: u(s[:name])) %>"><i></i><%= h(s[:name]) %></a>
|
|
10
|
+
<div class="mono mute ellip" style="font-size:11.5px" title="<%= h(Array(config.subjects).join(', ')) %>"><%= h(Array(config.subjects).join(' · ')) %></div>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="stat"><span class="eyebrow">Messages</span><b><%= compact_number(state.messages) %></b></div>
|
|
13
|
+
<div class="stat"><span class="eyebrow">Size</span><b><%= format_bytes(state.bytes) %></b></div>
|
|
14
|
+
<div class="stat"><span class="eyebrow">Retention</span><b><%= h(config.retention) %><%= " · #{format_span(max_age / 1_000_000_000)}" if max_age.positive? %></b></div>
|
|
15
|
+
<div class="stat"><span class="eyebrow">Replicas</span><b>R<%= h(config.num_replicas || 1) %></b></div>
|
|
16
|
+
<div class="stat"><span class="eyebrow">Sequence</span><b class="ellip"><%= compact_number(state.first_seq) %> → <%= compact_number(state.last_seq) %></b></div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="stream-tools">
|
|
19
|
+
<% if s[:paused] -%><span class="paused-tag">paused · consumers are not fetching</span><% end -%>
|
|
20
|
+
<span class="spacer"></span>
|
|
21
|
+
<a class="btn quiet" href="<%= url_for('/streams/info', name: u(s[:name])) %>">Details</a>
|
|
22
|
+
<button type="button" class="btn<%= ' quiet' unless s[:paused] %>"
|
|
23
|
+
hx-patch="<%= url_for(s[:paused] ? '/streams/unpause' : '/streams/pause', name: u(s[:name])) %>"
|
|
24
|
+
hx-target="closest section" hx-swap="outerHTML" hx-indicator="#global-spinner"><%= s[:paused] ? '▶ Resume' : '⏸ Pause' %></button>
|
|
25
|
+
</div>
|
|
26
|
+
<% if s[:consumers].empty? -%>
|
|
27
|
+
<div class="thead top tint"><span>No consumers</span></div>
|
|
28
|
+
<% else -%>
|
|
29
|
+
<div class="scroll"><div style="min-width:760px">
|
|
30
|
+
<div class="thead top tint t-cons"><span>Consumer</span><span class="r">Pending</span><span class="r">Ack pending</span><span class="r">Ack wait</span><span class="r">Redeliv.</span><span>Lag</span></div>
|
|
31
|
+
<% s[:consumers].each do |consumer| -%>
|
|
32
|
+
<% cfg = Hash(consumer['config']) -%>
|
|
33
|
+
<% pending = consumer['num_pending'].to_i -%>
|
|
34
|
+
<% redelivered = consumer['num_redelivered'].to_i -%>
|
|
35
|
+
<% ratio = pending.fdiv([state.messages.to_i, 1].max).clamp(0, 1) -%>
|
|
36
|
+
<% tone = ratio > 0.7 ? 'var(--err)' : ratio > 0.3 ? 'var(--warn)' : 'var(--ok)' -%>
|
|
37
|
+
<% filters = Array(cfg['filter_subjects']).concat(Array(cfg['filter_subject'])).reject(&:empty?) -%>
|
|
38
|
+
<div class="trow t-cons">
|
|
39
|
+
<div class="stack">
|
|
40
|
+
<span class="strong ellip"><%= h(consumer['name']) %></span>
|
|
41
|
+
<span class="small-mono ellip" title="<%= h(filters.join(', ')) %>">deliver: <%= h(cfg['deliver_policy']) %><%= " · #{h(filters.join(', '))}" if filters.any? %></span>
|
|
42
|
+
</div>
|
|
43
|
+
<span class="r"><%= format_numbers(pending) %></span>
|
|
44
|
+
<span class="r mute"><%= format_numbers(consumer['num_ack_pending'].to_i) %></span>
|
|
45
|
+
<span class="r mute"><%= format_span(cfg['ack_wait'].to_i / 1_000_000_000) %></span>
|
|
46
|
+
<span class="r" style="color:<%= redelivered > 100 ? 'var(--warn)' : 'var(--mute)' %>"><%= format_numbers(redelivered) %></span>
|
|
47
|
+
<div class="lag"><div class="lag-track"><i style="width:<%= [ratio * 100, 2].max.round(1) %>%;background:<%= tone %>"></i></div><span style="color:<%= tone %>"><%= compact_number(pending) %></span></div>
|
|
48
|
+
</div>
|
|
49
|
+
<% end -%>
|
|
50
|
+
</div></div>
|
|
51
|
+
<% end -%>
|
|
52
|
+
</section>
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
<
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
<div id="content">
|
|
5
|
-
<div hx-get="<%= url_for('/streams/_table') %>"
|
|
6
|
-
hx-trigger="load, every <%= poll_interval %>s"
|
|
7
|
-
hx-swap="innerHTML">
|
|
8
|
-
<div class="alert alert-info">Loading streams…</div>
|
|
9
|
-
</div>
|
|
10
|
-
</div>
|
|
11
|
-
</section>
|
|
1
|
+
<div class="view" hx-get="<%= url_for('/streams') %>" hx-trigger="load, cosmo:poll" data-poll="<%= poll_interval %>" hx-swap="innerHTML">
|
|
2
|
+
<section class="card"><div class="loading">Loading streams…</div></section>
|
|
3
|
+
</div>
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
<
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
<div id="content">
|
|
5
|
-
<div hx-get="<%= url_for("/streams/_info", { name: u(@name) }) %>"
|
|
6
|
-
hx-trigger="load"
|
|
7
|
-
hx-swap="innerHTML">
|
|
8
|
-
<div class="alert alert-info">Loading stream info…</div>
|
|
9
|
-
</div>
|
|
10
|
-
</div>
|
|
11
|
-
</section>
|
|
1
|
+
<div class="view" hx-get="<%= url_for('/streams/info', { name: u(@name) }) %>" hx-trigger="load" hx-swap="innerHTML">
|
|
2
|
+
<section class="card"><div class="loading">Loading stream info…</div></section>
|
|
3
|
+
</div>
|
data/lib/cosmo/web.rb
CHANGED
|
@@ -3,68 +3,102 @@
|
|
|
3
3
|
require "rack"
|
|
4
4
|
require "json"
|
|
5
5
|
require "erb"
|
|
6
|
+
require "zlib"
|
|
7
|
+
require "cosmo/web/cache"
|
|
8
|
+
require "cosmo/web/circuit"
|
|
6
9
|
require "cosmo/web/context"
|
|
7
10
|
require "cosmo/web/renderer"
|
|
8
11
|
require "cosmo/web/controllers/application"
|
|
12
|
+
require "cosmo/web/controllers/overview"
|
|
9
13
|
require "cosmo/web/controllers/jobs"
|
|
14
|
+
require "cosmo/web/controllers/processes"
|
|
10
15
|
require "cosmo/web/controllers/streams"
|
|
11
|
-
require "cosmo/web/controllers/actions"
|
|
12
16
|
require "cosmo/web/controllers/crons"
|
|
13
17
|
require "cosmo/web/controllers/batches"
|
|
18
|
+
require "cosmo/web/controllers/metrics"
|
|
14
19
|
|
|
15
20
|
module Cosmo
|
|
16
21
|
class Web
|
|
17
22
|
include Renderer
|
|
18
23
|
|
|
24
|
+
FONT_HEADERS = { "cache-control" => "public, max-age=31536000, immutable" }.freeze
|
|
25
|
+
|
|
19
26
|
def self.call(env)
|
|
20
27
|
new.call(env)
|
|
21
28
|
end
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
# Serves the UI. An htmx refresh is shared through Cache with concurrent and recent identical ones; any other
|
|
31
|
+
# method than GET changes something, so it empties the cache. While Circuit is open, anything but an asset
|
|
32
|
+
# answers 503 at once instead of waiting on NATS.
|
|
33
|
+
def call(env) # rubocop:disable Metrics/AbcSize
|
|
24
34
|
@request = Rack::Request.new(env)
|
|
35
|
+
return route if @request.path_info.start_with?("/assets/", "/favicon.ico")
|
|
36
|
+
return guarded.tap { Cache.instance.clear } unless @request.get?
|
|
37
|
+
return guarded unless @request.get_header("HTTP_HX_REQUEST") == "true"
|
|
38
|
+
|
|
39
|
+
status, headers, body = Cache.instance.fetch(@request.fullpath) { guarded }
|
|
40
|
+
[status, headers.dup, body.dup]
|
|
41
|
+
rescue *Circuit::ERRORS
|
|
42
|
+
Circuit.instance.trip!
|
|
43
|
+
unavailable
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def route # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
25
49
|
path = @request.path_info
|
|
26
50
|
method = @request.request_method
|
|
27
51
|
|
|
28
52
|
response = case [method.downcase.to_sym, path]
|
|
29
|
-
in [:get, "/"]
|
|
30
|
-
in [:get, "/
|
|
31
|
-
in [:get, "/
|
|
32
|
-
in [:get, "/
|
|
33
|
-
in [:get, "/
|
|
34
|
-
in [:get, "/
|
|
35
|
-
in [:
|
|
36
|
-
in [:
|
|
37
|
-
in [:
|
|
38
|
-
in [:get, "/jobs/
|
|
39
|
-
in [:get, "/jobs/
|
|
40
|
-
in [:get, "/jobs/
|
|
41
|
-
in [:
|
|
42
|
-
in [:
|
|
43
|
-
in [:
|
|
44
|
-
in [:get, "/
|
|
45
|
-
in [:get, "/streams
|
|
46
|
-
in [:get, "/streams/
|
|
47
|
-
in [:
|
|
48
|
-
in [:patch, "/streams/
|
|
49
|
-
in [:
|
|
50
|
-
in [:get, "/crons"]
|
|
51
|
-
in [:
|
|
52
|
-
in [:
|
|
53
|
-
in [:
|
|
54
|
-
in [:get, "/
|
|
55
|
-
in [:get, "/batches
|
|
56
|
-
in [:get, "/
|
|
53
|
+
in [:get, "/" | ""] then [Controllers::Overview, :index]
|
|
54
|
+
in [:get, "/_kpis"] then [Controllers::Overview, :_kpis]
|
|
55
|
+
in [:get, "/_throughput"] then [Controllers::Overview, :_throughput]
|
|
56
|
+
in [:get, "/_workers"] then [Controllers::Overview, :_workers]
|
|
57
|
+
in [:get, "/_running"] then [Controllers::Overview, :_running]
|
|
58
|
+
in [:get, "/_badges"] then [Controllers::Overview, :_badges]
|
|
59
|
+
in [:get, "/jobs"] then [Controllers::Jobs, :index]
|
|
60
|
+
in [:get, "/jobs/busy"] then [Controllers::Jobs, :busy]
|
|
61
|
+
in [:get, "/jobs/enqueued"] then [Controllers::Jobs, :enqueued]
|
|
62
|
+
in [:get, "/jobs/scheduled"] then [Controllers::Jobs, :scheduled]
|
|
63
|
+
in [:get, "/jobs/dead"] then [Controllers::Jobs, :dead]
|
|
64
|
+
in [:get, "/jobs/_queues"] then [Controllers::Jobs, :_queues]
|
|
65
|
+
in [:patch, %r{/jobs/retry/\d+}] then [Controllers::Jobs, :retry]
|
|
66
|
+
in [:delete, %r{/jobs/delete/\d+}] then [Controllers::Jobs, :delete]
|
|
67
|
+
in [:delete, %r{/jobs/enqueued/\d+}] then [Controllers::Jobs, :delete_enqueued]
|
|
68
|
+
in [:get, "/processes"] then [Controllers::Processes, :index]
|
|
69
|
+
in [:get, "/streams"] then [Controllers::Streams, :index]
|
|
70
|
+
in [:get, "/streams/info"] then [Controllers::Streams, :info]
|
|
71
|
+
in [:patch, "/streams/pause"] then [Controllers::Streams, :pause]
|
|
72
|
+
in [:patch, "/streams/unpause"] then [Controllers::Streams, :unpause]
|
|
73
|
+
in [:get, "/crons"] then [Controllers::Crons, :index]
|
|
74
|
+
in [:get, "/crons/_table"] then [Controllers::Crons, :_table]
|
|
75
|
+
in [:delete, "/crons/delete"] then [Controllers::Crons, :delete]
|
|
76
|
+
in [:post, "/crons/run"] then [Controllers::Crons, :run_now]
|
|
77
|
+
in [:get, "/metrics"] then [Controllers::Metrics, :index]
|
|
78
|
+
in [:get, "/metrics/_panel"] then [Controllers::Metrics, :_panel]
|
|
79
|
+
in [:get, "/batches"] then [Controllers::Batches, :index]
|
|
80
|
+
in [:get, "/batches/_table"] then [Controllers::Batches, :_table]
|
|
57
81
|
in [:get, "/assets/htmx.min.js.gz"] then serve("htmx.2.0.8.min.js.gz",
|
|
58
82
|
"application/javascript",
|
|
59
83
|
{ "content-encoding" => "gzip" })
|
|
60
|
-
in [:get, "/assets/app.css"]
|
|
61
|
-
in [:get, "/
|
|
84
|
+
in [:get, "/assets/app.css"] then serve("app.css", "text/css")
|
|
85
|
+
in [:get, "/assets/app.js"] then serve("app.js", "application/javascript")
|
|
86
|
+
in [:get, "/assets/logo.svg"] then serve("logo.svg", "image/svg+xml")
|
|
87
|
+
in [:get, "/assets/fonts/geist.woff2"] then serve("fonts/geist.woff2", "font/woff2", FONT_HEADERS)
|
|
88
|
+
in [:get, "/assets/fonts/geist-mono.woff2"] then serve("fonts/geist-mono.woff2", "font/woff2", FONT_HEADERS)
|
|
89
|
+
in [:get, "/favicon.ico"] then redirect_to("/assets/logo.svg", 301)
|
|
62
90
|
else not_found
|
|
63
91
|
end
|
|
64
92
|
handler(response)
|
|
65
93
|
end
|
|
66
94
|
|
|
67
|
-
|
|
95
|
+
def guarded
|
|
96
|
+
Circuit.instance.open? ? unavailable : route
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def unavailable
|
|
100
|
+
Controllers::Application.new(@request).unavailable
|
|
101
|
+
end
|
|
68
102
|
|
|
69
103
|
def handler(response)
|
|
70
104
|
if response[0].is_a?(Class)
|
data/lib/cosmo.rb
CHANGED
|
@@ -14,6 +14,8 @@ require "cosmo/cli"
|
|
|
14
14
|
require "cosmo/heartbeat"
|
|
15
15
|
require "cosmo/engine"
|
|
16
16
|
require "cosmo/api"
|
|
17
|
+
require "cosmo/middleware"
|
|
18
|
+
require "cosmo/services"
|
|
17
19
|
require "cosmo/railtie" if defined?(Rails::Railtie)
|
|
18
20
|
|
|
19
21
|
module Cosmo
|
|
@@ -22,18 +24,54 @@ module Cosmo
|
|
|
22
24
|
# Optional, requires rack and webrick gems. Loaded only when an HTTP port is configured.
|
|
23
25
|
autoload :HTTPServer, "cosmo/http_server"
|
|
24
26
|
|
|
27
|
+
# Programmatic setup, typically from an initializer:
|
|
28
|
+
#
|
|
29
|
+
# Cosmo.configure do |config|
|
|
30
|
+
# config.logger = Rails.logger
|
|
31
|
+
# config.log_level = :debug
|
|
32
|
+
# config.server_middleware { |c| c.add MyMiddleware }
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# @yieldparam config [Config] the {Config} singleton
|
|
36
|
+
# @return [void]
|
|
37
|
+
def self.configure
|
|
38
|
+
yield Config.instance
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Hands +error+ to every handler in {Config#error_handlers}. A handler that raises is logged and skipped, so error
|
|
42
|
+
# reporting never breaks processing.
|
|
43
|
+
#
|
|
44
|
+
# @param error [Exception]
|
|
45
|
+
# @param context [Hash] where it happened, see {Config#error_handlers}
|
|
46
|
+
# @return [void]
|
|
47
|
+
def self.handle_error(error, context)
|
|
48
|
+
Config.error_handlers.each do |handler|
|
|
49
|
+
handler.call(error, context)
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
Logger.error "Error handler failed: #{e.class}: #{e.message}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
25
55
|
class Error < StandardError; end
|
|
26
56
|
|
|
27
57
|
class ArgumentError < Error; end
|
|
28
58
|
|
|
29
59
|
class NotImplementedError < Error; end
|
|
30
60
|
|
|
61
|
+
class ConfigError < Error; end
|
|
62
|
+
|
|
31
63
|
class ConfigNotFoundError < Error
|
|
32
64
|
def initialize(config_file)
|
|
33
65
|
super("No such file #{config_file}")
|
|
34
66
|
end
|
|
35
67
|
end
|
|
36
68
|
|
|
69
|
+
class SchedulingDisabledError < Error
|
|
70
|
+
def initialize
|
|
71
|
+
super("Scheduling is turned off (config.scheduled.enabled = false), so delayed jobs and crons are unavailable")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
37
75
|
class StreamNotFoundError < Error
|
|
38
76
|
def initialize(stream_name)
|
|
39
77
|
super("Missing stream `#{stream_name}`")
|