cosmonats 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE.txt +21 -169
- data/README.md +296 -116
- data/lib/cosmo/active_job/adapter.rb +1 -2
- data/lib/cosmo/api/counter.rb +18 -15
- data/lib/cosmo/api/cron/entry.rb +23 -3
- data/lib/cosmo/api/cron.rb +25 -26
- data/lib/cosmo/api/job.rb +30 -0
- data/lib/cosmo/api/kv.rb +31 -11
- data/lib/cosmo/api/registry.rb +68 -0
- data/lib/cosmo/api/stats/busy.rb +63 -0
- data/lib/cosmo/api/stats/metrics.rb +109 -0
- data/lib/cosmo/api/stats/processes.rb +47 -0
- data/lib/cosmo/api/stats/totals.rb +27 -0
- data/lib/cosmo/api/stats.rb +8 -6
- data/lib/cosmo/api/stream.rb +1 -1
- data/lib/cosmo/api.rb +3 -2
- data/lib/cosmo/batch/counters.rb +19 -0
- data/lib/cosmo/batch.rb +15 -8
- data/lib/cosmo/cli.rb +46 -10
- data/lib/cosmo/client.rb +61 -7
- data/lib/cosmo/config/cosmo.yml +58 -0
- data/lib/cosmo/config/settings.rb +20 -0
- data/lib/cosmo/config.rb +144 -15
- data/lib/cosmo/engine.rb +103 -4
- data/lib/cosmo/heartbeat.rb +140 -0
- data/lib/cosmo/http_server/handler.rb +44 -0
- data/lib/cosmo/http_server/health.rb +39 -0
- data/lib/cosmo/http_server/ping.rb +23 -0
- data/lib/cosmo/http_server.rb +62 -0
- data/lib/cosmo/job/data.rb +17 -12
- data/lib/cosmo/job/enqueuer.rb +53 -0
- data/lib/cosmo/job/failure.rb +52 -0
- data/lib/cosmo/job/limit.rb +2 -2
- data/lib/cosmo/job/processor.rb +110 -73
- data/lib/cosmo/job/requeue.rb +20 -0
- data/lib/cosmo/job/stream_filter.rb +54 -0
- data/lib/cosmo/job.rb +10 -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/middleware/sentry.rb +61 -0
- data/lib/cosmo/middleware/totals.rb +29 -0
- data/lib/cosmo/middleware.rb +7 -0
- data/lib/cosmo/processor.rb +49 -8
- 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/message.rb +2 -1
- data/lib/cosmo/stream/processor.rb +12 -1
- data/lib/cosmo/utils/hash.rb +7 -8
- data/lib/cosmo/utils/json.rb +1 -1
- data/lib/cosmo/utils/overrides.rb +16 -10
- data/lib/cosmo/utils/thread_pool.rb +5 -0
- data/lib/cosmo/version.rb +1 -1
- data/lib/cosmo/web/assets/app.css +79 -0
- data/lib/cosmo/web/chart.rb +98 -0
- data/lib/cosmo/web/controllers/crons.rb +8 -0
- data/lib/cosmo/web/controllers/jobs.rb +43 -5
- data/lib/cosmo/web/controllers/metrics.rb +48 -0
- data/lib/cosmo/web/helpers/application.rb +20 -1
- data/lib/cosmo/web/views/batches/_table.erb +11 -0
- data/lib/cosmo/web/views/batches/index.erb +1 -1
- data/lib/cosmo/web/views/jobs/_busy.erb +70 -4
- data/lib/cosmo/web/views/jobs/_dead.erb +29 -16
- data/lib/cosmo/web/views/jobs/_enqueued.erb +10 -0
- data/lib/cosmo/web/views/jobs/_processes.erb +145 -0
- data/lib/cosmo/web/views/jobs/_scheduled.erb +1 -1
- data/lib/cosmo/web/views/jobs/_stats.erb +4 -0
- data/lib/cosmo/web/views/jobs/_tabs.erb +6 -0
- data/lib/cosmo/web/views/jobs/busy.erb +1 -1
- data/lib/cosmo/web/views/jobs/dead.erb +2 -2
- data/lib/cosmo/web/views/jobs/enqueued.erb +1 -1
- data/lib/cosmo/web/views/jobs/index.erb +7 -1
- data/lib/cosmo/web/views/jobs/scheduled.erb +1 -1
- data/lib/cosmo/web/views/layout.erb +1 -1
- data/lib/cosmo/web/views/metrics/_chart.erb +36 -0
- data/lib/cosmo/web/views/metrics/_panel.erb +13 -0
- data/lib/cosmo/web/views/metrics/_table.erb +41 -0
- data/lib/cosmo/web/views/metrics/index.erb +17 -0
- data/lib/cosmo/web/views/streams/index.erb +1 -1
- data/lib/cosmo/web.rb +5 -0
- data/lib/cosmo.rb +51 -0
- data/sig/cosmo/api/counter.rbs +3 -6
- data/sig/cosmo/api/cron/entry.rbs +2 -0
- data/sig/cosmo/api/cron.rbs +4 -2
- data/sig/cosmo/api/job.rbs +12 -0
- data/sig/cosmo/api/kv.rbs +7 -1
- data/sig/cosmo/api/registry.rbs +30 -0
- data/sig/cosmo/api/stats/busy.rbs +31 -0
- data/sig/cosmo/api/stats/metrics.rbs +42 -0
- data/sig/cosmo/api/stats/processes.rbs +19 -0
- data/sig/cosmo/api/stats/totals.rbs +18 -0
- data/sig/cosmo/batch/counters.rbs +10 -0
- data/sig/cosmo/batch.rbs +3 -4
- data/sig/cosmo/cli.rbs +6 -0
- data/sig/cosmo/client.rbs +15 -2
- data/sig/cosmo/config/settings.rbs +31 -0
- data/sig/cosmo/config.rbs +59 -5
- data/sig/cosmo/engine.rbs +29 -0
- data/sig/cosmo/heartbeat.rbs +38 -0
- data/sig/cosmo/http_server/handler.rbs +17 -0
- data/sig/cosmo/http_server/health.rbs +21 -0
- data/sig/cosmo/http_server/ping.rbs +14 -0
- data/sig/cosmo/http_server.rbs +22 -0
- data/sig/cosmo/job/data.rbs +5 -3
- data/sig/cosmo/job/enqueuer.rbs +11 -0
- data/sig/cosmo/job/failure.rbs +28 -0
- data/sig/cosmo/job/processor.rbs +18 -4
- data/sig/cosmo/job/requeue.rbs +9 -0
- data/sig/cosmo/job/stream_filter.rbs +25 -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 +14 -1
- data/sig/cosmo/publisher.rbs +1 -9
- data/sig/cosmo/services.rbs +18 -0
- data/sig/cosmo/stream/message.rbs +3 -1
- data/sig/cosmo/stream/processor.rbs +4 -0
- data/sig/cosmo/utils/hash.rbs +2 -3
- data/sig/cosmo/utils/thread_pool.rbs +3 -0
- data/sig/cosmo.rbs +15 -0
- metadata +62 -22
- data/lib/cosmo/api/busy.rb +0 -66
- data/lib/cosmo/sentry/auto.rb +0 -6
- data/lib/cosmo/sentry/job_processor_middleware.rb +0 -66
- data/sig/cosmo/api/busy.rbs +0 -35
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<% nav = @polling ? { limit: @limit } : { limit: @limit, poll: 0 } -%>
|
|
2
|
+
<% states = { "running" => "badge-success", "quiet" => "badge-warning", "stopping" => "badge-danger" } -%>
|
|
3
|
+
<% kinds = { "jobs" => "Jobs", "streams" => "Streams", "jobs+streams" => "Jobs & Streams", "none" => "No subscriptions" } -%>
|
|
4
|
+
<div id="processes-poller"
|
|
5
|
+
<% if @polling -%>
|
|
6
|
+
hx-get="<%= url_for('/jobs/_processes', page: @page, limit: @limit) %>"
|
|
7
|
+
hx-trigger="every <%= poll_interval %>s"
|
|
8
|
+
hx-swap="outerHTML"
|
|
9
|
+
<% end -%>
|
|
10
|
+
>
|
|
11
|
+
<div class="pending">
|
|
12
|
+
<span></span>
|
|
13
|
+
<span class="poll-status">
|
|
14
|
+
<span class="text-muted"><%= @total %> process(es) alive</span>
|
|
15
|
+
<% if @polling -%>
|
|
16
|
+
<a class="btn btn-sm"
|
|
17
|
+
hx-get="<%= url_for('/jobs/_processes', nav.merge(page: @page, poll: 0)) %>"
|
|
18
|
+
hx-target="#processes-poller"
|
|
19
|
+
hx-swap="outerHTML"
|
|
20
|
+
hx-indicator="#global-spinner">⏹ Stop Polling</a>
|
|
21
|
+
<% else -%>
|
|
22
|
+
<a class="btn btn-sm btn-primary"
|
|
23
|
+
hx-get="<%= url_for('/jobs/_processes', page: @page, limit: @limit) %>"
|
|
24
|
+
hx-target="#processes-poller"
|
|
25
|
+
hx-swap="outerHTML"
|
|
26
|
+
hx-indicator="#global-spinner">▶ Live Poll</a>
|
|
27
|
+
<% end -%>
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<% if @groups.empty? -%>
|
|
32
|
+
<div class="alert alert-warning">No worker processes are running</div>
|
|
33
|
+
<% else -%>
|
|
34
|
+
<div class="table-container">
|
|
35
|
+
<table>
|
|
36
|
+
<thead>
|
|
37
|
+
<tr>
|
|
38
|
+
<th>Process</th>
|
|
39
|
+
<th>Command</th>
|
|
40
|
+
<th>Subscriptions</th>
|
|
41
|
+
<th>Busy</th>
|
|
42
|
+
<th>State</th>
|
|
43
|
+
<th>RTT</th>
|
|
44
|
+
<th>Reconnects</th>
|
|
45
|
+
<th>Memory</th>
|
|
46
|
+
<th>Uptime</th>
|
|
47
|
+
<th>Last beat</th>
|
|
48
|
+
</tr>
|
|
49
|
+
</thead>
|
|
50
|
+
<tbody>
|
|
51
|
+
<% @groups.each do |kind, group| -%>
|
|
52
|
+
<tr class="group-row">
|
|
53
|
+
<td colspan="10"><%= h(kinds.fetch(kind, kind)) %> <span class="text-muted">(<%= group.size %>)</span></td>
|
|
54
|
+
</tr>
|
|
55
|
+
<% group.each do |process| -%>
|
|
56
|
+
<% nats = Hash(process[:nats]) -%>
|
|
57
|
+
<tr>
|
|
58
|
+
<td>
|
|
59
|
+
<div class="job-class"><%= h(process[:hostname]) %>:<%= h(process[:pid]) %></div>
|
|
60
|
+
<div class="text-muted" style="font-size: var(--font-size-small);">
|
|
61
|
+
<%= h(nats[:ip] || "unknown IP") %>
|
|
62
|
+
<% if nats[:name] -%>· <code><%= h(nats[:name]) %></code><% end -%>
|
|
63
|
+
</div>
|
|
64
|
+
</td>
|
|
65
|
+
<td>
|
|
66
|
+
<% cmdline = process[:cmdline].to_s -%>
|
|
67
|
+
<div class="job-id">
|
|
68
|
+
<% if cmdline.length > 32 -%>
|
|
69
|
+
<details>
|
|
70
|
+
<summary>
|
|
71
|
+
<code class="truncate" title="<%= h(cmdline) %>"><%= h(cmdline) %></code>
|
|
72
|
+
</summary>
|
|
73
|
+
<code style="font-size: var(--font-size-small);"><%= h(cmdline) %></code>
|
|
74
|
+
</details>
|
|
75
|
+
<% else -%>
|
|
76
|
+
<code style="font-size: var(--font-size-small);"><%= h(cmdline) %></code>
|
|
77
|
+
<% end -%>
|
|
78
|
+
<div class="text-muted" style="font-size: var(--font-size-small);">
|
|
79
|
+
cosmo <%= h(process[:version]) %> · <%= h(process[:ruby]) %>
|
|
80
|
+
<% if process[:http_port] -%>· http :<%= h(process[:http_port]) %><% end -%>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</td>
|
|
84
|
+
<td>
|
|
85
|
+
<% Hash(process[:subscriptions]).each do |type, names| -%>
|
|
86
|
+
<% next if Array(names).empty? -%>
|
|
87
|
+
<div>
|
|
88
|
+
<% if kind == "jobs+streams" -%><span class="text-muted"><%= h(type) %>:</span><% end -%>
|
|
89
|
+
<code><%= h(Array(names).join(", ")) %></code>
|
|
90
|
+
</div>
|
|
91
|
+
<% end -%>
|
|
92
|
+
</td>
|
|
93
|
+
<td><%= h(process[:busy]) %> / <%= h(process[:concurrency]) %></td>
|
|
94
|
+
<td><span class="badge <%= states.fetch(process[:state].to_s, 'badge-info') %>"><%= h(process[:state]) %></span></td>
|
|
95
|
+
<td><%= nats[:rtt] ? "#{h(nats[:rtt])}ms" : '—' %></td>
|
|
96
|
+
<td><%= h(nats[:reconnects].to_i) %></td>
|
|
97
|
+
<td><%= process[:rss] ? format_bytes(process[:rss].to_i * 1024) : '—' %></td>
|
|
98
|
+
<td><%= elapsed(process[:started_at]) %></td>
|
|
99
|
+
<td><%= elapsed(process[:beat_at]) %> ago</td>
|
|
100
|
+
</tr>
|
|
101
|
+
<% end -%>
|
|
102
|
+
<% end -%>
|
|
103
|
+
</tbody>
|
|
104
|
+
</table>
|
|
105
|
+
</div>
|
|
106
|
+
<% end -%>
|
|
107
|
+
|
|
108
|
+
<% if @total_pages.to_i > 1 -%>
|
|
109
|
+
<div class="pagination">
|
|
110
|
+
<% if @page > 1 -%>
|
|
111
|
+
<a hx-get="<%= url_for('/jobs/_processes', nav.merge(page: @page - 1)) %>"
|
|
112
|
+
hx-target="#processes-poller"
|
|
113
|
+
hx-swap="outerHTML"
|
|
114
|
+
hx-indicator="#global-spinner"
|
|
115
|
+
class="btn">← Prev</a>
|
|
116
|
+
<% else -%>
|
|
117
|
+
<span class="btn btn-disabled">← Prev</span>
|
|
118
|
+
<% end -%>
|
|
119
|
+
|
|
120
|
+
<% pages(@page, @total_pages).each do |p| -%>
|
|
121
|
+
<% if p == :gap -%>
|
|
122
|
+
<span class="pagination-gap">…</span>
|
|
123
|
+
<% elsif p == @page -%>
|
|
124
|
+
<span class="btn btn-primary btn-disabled"><%= p %></span>
|
|
125
|
+
<% else -%>
|
|
126
|
+
<a hx-get="<%= url_for('/jobs/_processes', nav.merge(page: p)) %>"
|
|
127
|
+
hx-target="#processes-poller"
|
|
128
|
+
hx-swap="outerHTML"
|
|
129
|
+
hx-indicator="#global-spinner"
|
|
130
|
+
class="btn"><%= p %></a>
|
|
131
|
+
<% end -%>
|
|
132
|
+
<% end -%>
|
|
133
|
+
|
|
134
|
+
<% if @page < @total_pages -%>
|
|
135
|
+
<a hx-get="<%= url_for('/jobs/_processes', nav.merge(page: @page + 1)) %>"
|
|
136
|
+
hx-target="#processes-poller"
|
|
137
|
+
hx-swap="outerHTML"
|
|
138
|
+
hx-indicator="#global-spinner"
|
|
139
|
+
class="btn">Next →</a>
|
|
140
|
+
<% else -%>
|
|
141
|
+
<span class="btn btn-disabled">Next →</span>
|
|
142
|
+
<% end -%>
|
|
143
|
+
</div>
|
|
144
|
+
<% end -%>
|
|
145
|
+
</div>
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
class="<%= 'active' if referrer?('/jobs/enqueued') %>">📬 Enqueued</a>
|
|
35
35
|
</p>
|
|
36
36
|
</article>
|
|
37
|
+
<% if Cosmo::Config.scheduled.enabled -%>
|
|
37
38
|
<article class="stat-card">
|
|
38
39
|
<h3><%= format_numbers(@scheduled) %></h3>
|
|
39
40
|
<p>
|
|
@@ -45,6 +46,8 @@
|
|
|
45
46
|
class="<%= 'active' if referrer?('/jobs/scheduled') %>">⏰ Scheduled</a>
|
|
46
47
|
</p>
|
|
47
48
|
</article>
|
|
49
|
+
<% end -%>
|
|
50
|
+
<% if Cosmo::Config.dead.enabled -%>
|
|
48
51
|
<article class="stat-card">
|
|
49
52
|
<h3><%= format_numbers(@dead) %></h3>
|
|
50
53
|
<p>
|
|
@@ -56,6 +59,7 @@
|
|
|
56
59
|
class="<%= 'active' if referrer?('/jobs/dead') %>">💀 Dead</a>
|
|
57
60
|
</p>
|
|
58
61
|
</article>
|
|
62
|
+
<% end -%>
|
|
59
63
|
</div>
|
|
60
64
|
|
|
61
65
|
<script>
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
class="<%= 'active' if path_prefix?('/jobs') %>">📋 Dashboard</a>
|
|
4
4
|
<a href="<%= url_for('/batches') %>"
|
|
5
5
|
class="<%= 'active' if path_prefix?('/batches') %>">📦 Batches</a>
|
|
6
|
+
<% if Cosmo::Config.scheduled.enabled -%>
|
|
6
7
|
<a href="<%= url_for('/crons') %>"
|
|
7
8
|
class="<%= 'active' if path_prefix?('/crons') %>">⏰ Crons</a>
|
|
9
|
+
<% end -%>
|
|
10
|
+
<% if Cosmo::Config.metrics.enabled -%>
|
|
11
|
+
<a href="<%= url_for('/metrics') %>"
|
|
12
|
+
class="<%= 'active' if path_prefix?('/metrics') %>">📊 Metrics</a>
|
|
13
|
+
<% end -%>
|
|
8
14
|
</nav>
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
<header><%= render('jobs/_tabs') %></header>
|
|
3
3
|
|
|
4
4
|
<div hx-get="<%= url_for('/jobs/_stats') %>"
|
|
5
|
-
hx-trigger="load, every
|
|
5
|
+
hx-trigger="load, every <%= poll_interval %>s"
|
|
6
6
|
hx-swap="innerHTML">
|
|
7
7
|
<div class="alert alert-info">Loading statistics…</div>
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
10
|
<div id="content">
|
|
11
|
-
<div id="dead-
|
|
11
|
+
<div id="dead-jobs"
|
|
12
12
|
hx-get="<%= url_for('/jobs/_dead') %>"
|
|
13
13
|
hx-trigger="load"
|
|
14
14
|
hx-swap="outerHTML">
|
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
<header><%= render('jobs/_tabs') %></header>
|
|
3
3
|
|
|
4
4
|
<div hx-get="<%= url_for('/jobs/_stats') %>"
|
|
5
|
-
hx-trigger="load, every
|
|
5
|
+
hx-trigger="load, every <%= poll_interval %>s"
|
|
6
6
|
hx-swap="innerHTML">
|
|
7
7
|
<div class="alert alert-info">Loading statistics…</div>
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
10
|
<div id="content">
|
|
11
|
+
<div id="processes-poller"
|
|
12
|
+
hx-get="<%= url_for('/jobs/_processes', (@processes_query unless @processes_query.empty?)) %>"
|
|
13
|
+
hx-trigger="load"
|
|
14
|
+
hx-swap="outerHTML">
|
|
15
|
+
<div class="alert alert-info">Loading processes…</div>
|
|
16
|
+
</div>
|
|
11
17
|
</div>
|
|
12
18
|
</section>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<a href="<%= url_for('/jobs') %>" class="navbar-brand">🚀 Cosmo</a>
|
|
15
15
|
<ul class="nav-list">
|
|
16
16
|
<li>
|
|
17
|
-
<a href="<%= url_for('/jobs') %>" class="<%= "active" if path_prefix?('/jobs', '/crons', '/batches') %>">Jobs</a>
|
|
17
|
+
<a href="<%= url_for('/jobs') %>" class="<%= "active" if path_prefix?('/jobs', '/crons', '/batches', '/metrics') %>">Jobs</a>
|
|
18
18
|
</li>
|
|
19
19
|
<li>
|
|
20
20
|
<a href="<%= url_for('/streams') %>" class="<%= "active" if current_page?('/streams') %>">Streams</a>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<div class="metrics-chart">
|
|
2
|
+
<div class="metrics-chart-header">
|
|
3
|
+
<span><code><%= h(@job) %></code> <span class="text-muted">last <%= @days %> days</span></span>
|
|
4
|
+
<span class="metrics-legend">
|
|
5
|
+
<span class="metrics-legend-runs">runs per day</span>
|
|
6
|
+
<span class="metrics-legend-exec">avg execution (ms)</span>
|
|
7
|
+
</span>
|
|
8
|
+
</div>
|
|
9
|
+
<% if @chart.empty? -%>
|
|
10
|
+
<div class="alert alert-info">No runs of <code><%= h(@job) %></code> in this period.</div>
|
|
11
|
+
<% else -%>
|
|
12
|
+
<svg viewBox="0 0 <%= Cosmo::Web::Chart::WIDTH %> <%= Cosmo::Web::Chart::HEIGHT %>" role="img"
|
|
13
|
+
aria-label="Runs per day and average execution time of <%= h(@job) %>">
|
|
14
|
+
<% @chart.ticks.each do |tick| -%>
|
|
15
|
+
<line class="chart-grid" x1="<%= Cosmo::Web::Chart::LEFT %>" x2="<%= Cosmo::Web::Chart::RIGHT %>"
|
|
16
|
+
y1="<%= tick[:y] %>" y2="<%= tick[:y] %>"/>
|
|
17
|
+
<text class="chart-axis" x="<%= Cosmo::Web::Chart::LEFT - 6 %>" y="<%= tick[:y] + 4 %>"
|
|
18
|
+
text-anchor="end"><%= format_numbers(tick[:count]) %></text>
|
|
19
|
+
<text class="chart-axis chart-axis-exec" x="<%= Cosmo::Web::Chart::RIGHT + 6 %>" y="<%= tick[:y] + 4 %>"
|
|
20
|
+
text-anchor="start"><%= format_numbers(tick[:exec_ms]) %></text>
|
|
21
|
+
<% end -%>
|
|
22
|
+
<% @chart.bars.each do |bar| -%>
|
|
23
|
+
<rect class="chart-bar" x="<%= bar[:x] %>" y="<%= bar[:y] %>" width="<%= bar[:width] %>"
|
|
24
|
+
height="<%= bar[:height] %>"><title><%= h(bar[:title]) %></title></rect>
|
|
25
|
+
<% end -%>
|
|
26
|
+
<polyline class="chart-line" points="<%= @chart.line %>"/>
|
|
27
|
+
<% @chart.points.each do |point| -%>
|
|
28
|
+
<circle class="chart-point" cx="<%= point[:x] %>" cy="<%= point[:y] %>" r="4"><title><%= h(point[:title]) %></title></circle>
|
|
29
|
+
<% end -%>
|
|
30
|
+
<% @chart.labels.each do |label| -%>
|
|
31
|
+
<text class="chart-axis" x="<%= label[:x] %>" y="<%= Cosmo::Web::Chart::BOTTOM + 18 %>"
|
|
32
|
+
text-anchor="middle"><%= label[:text] %></text>
|
|
33
|
+
<% end -%>
|
|
34
|
+
</svg>
|
|
35
|
+
<% end -%>
|
|
36
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div id="metrics-panel"
|
|
2
|
+
hx-get="<%= url_for('/metrics/_panel', { days: @days, job: u(@job) }) %>"
|
|
3
|
+
hx-trigger="every 30s"
|
|
4
|
+
hx-swap="outerHTML">
|
|
5
|
+
<% if @rows.empty? -%>
|
|
6
|
+
<div class="alert alert-success">No metrics recorded in this period.</div>
|
|
7
|
+
<% else -%>
|
|
8
|
+
<% if @chart -%>
|
|
9
|
+
<%= render('metrics/_chart', { job: @job, days: @days, chart: @chart }) %>
|
|
10
|
+
<% end -%>
|
|
11
|
+
<%= render('metrics/_table', { rows: @rows, job: @job, days: @days }) %>
|
|
12
|
+
<% end -%>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<% selectable = @days > 1 -%>
|
|
2
|
+
<% if selectable -%>
|
|
3
|
+
<p class="metrics-hint">Click a job to chart it.</p>
|
|
4
|
+
<% end -%>
|
|
5
|
+
<div class="table-container">
|
|
6
|
+
<table class="metrics-table">
|
|
7
|
+
<thead>
|
|
8
|
+
<tr>
|
|
9
|
+
<th>Job</th>
|
|
10
|
+
<th>Runs</th>
|
|
11
|
+
<th>Failed</th>
|
|
12
|
+
<th>Avg execution</th>
|
|
13
|
+
<th>Avg wait</th>
|
|
14
|
+
</tr>
|
|
15
|
+
</thead>
|
|
16
|
+
<tbody>
|
|
17
|
+
<% @rows.each do |row| -%>
|
|
18
|
+
<% selected = selectable && row[:job] == @job -%>
|
|
19
|
+
<tr<% if selectable -%>
|
|
20
|
+
class="selectable<%= ' selected' if selected %>"
|
|
21
|
+
hx-get="<%= url_for('/metrics/_panel', { days: @days, job: u(row[:job]) }) %>"
|
|
22
|
+
hx-target="#metrics-panel"
|
|
23
|
+
hx-swap="outerHTML"
|
|
24
|
+
<% if selected -%>aria-selected="true"<% end -%>
|
|
25
|
+
<% end -%>>
|
|
26
|
+
<td><code><%= h(row[:job]) %></code></td>
|
|
27
|
+
<td><%= format_numbers(row[:count]) %></td>
|
|
28
|
+
<td>
|
|
29
|
+
<% if row[:failed].positive? -%>
|
|
30
|
+
<span class="badge badge-danger"><%= format_numbers(row[:failed]) %></span>
|
|
31
|
+
<% else -%>
|
|
32
|
+
<span class="text-muted">0</span>
|
|
33
|
+
<% end -%>
|
|
34
|
+
</td>
|
|
35
|
+
<td><%= duration(row[:exec_ms]) %></td>
|
|
36
|
+
<td><%= duration(row[:wait_ms]) %></td>
|
|
37
|
+
</tr>
|
|
38
|
+
<% end -%>
|
|
39
|
+
</tbody>
|
|
40
|
+
</table>
|
|
41
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<section>
|
|
2
|
+
<header><%= render('jobs/_tabs') %></header>
|
|
3
|
+
|
|
4
|
+
<div class="nav">
|
|
5
|
+
<% @ranges.each do |range, name| -%>
|
|
6
|
+
<a href="<%= url_for('/metrics', { days: range }) %>"
|
|
7
|
+
class="btn <%= 'btn-primary' if @days == range %>"><%= h(name) %></a>
|
|
8
|
+
<% end -%>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div id="metrics-panel"
|
|
12
|
+
hx-get="<%= url_for('/metrics/_panel', { days: @days }) %>"
|
|
13
|
+
hx-trigger="load"
|
|
14
|
+
hx-swap="outerHTML">
|
|
15
|
+
<div class="alert alert-info">Loading metrics…</div>
|
|
16
|
+
</div>
|
|
17
|
+
</section>
|
data/lib/cosmo/web.rb
CHANGED
|
@@ -11,6 +11,7 @@ require "cosmo/web/controllers/streams"
|
|
|
11
11
|
require "cosmo/web/controllers/actions"
|
|
12
12
|
require "cosmo/web/controllers/crons"
|
|
13
13
|
require "cosmo/web/controllers/batches"
|
|
14
|
+
require "cosmo/web/controllers/metrics"
|
|
14
15
|
|
|
15
16
|
module Cosmo
|
|
16
17
|
class Web
|
|
@@ -34,10 +35,12 @@ module Cosmo
|
|
|
34
35
|
in [:get, "/jobs/enqueued"] then [Controllers::Jobs, :enqueued]
|
|
35
36
|
in [:patch, %r{/jobs/retry/\d+}] then [Controllers::Jobs, :retry]
|
|
36
37
|
in [:delete, %r{/jobs/delete/\d+}] then [Controllers::Jobs, :delete]
|
|
38
|
+
in [:delete, %r{/jobs/enqueued/\d+}] then [Controllers::Jobs, :delete_enqueued]
|
|
37
39
|
in [:get, "/jobs/_stats"] then [Controllers::Jobs, :_stats]
|
|
38
40
|
in [:get, "/jobs/_scheduled"] then [Controllers::Jobs, :_scheduled]
|
|
39
41
|
in [:get, "/jobs/_dead"] then [Controllers::Jobs, :_dead]
|
|
40
42
|
in [:get, "/jobs/_busy"] then [Controllers::Jobs, :_busy]
|
|
43
|
+
in [:get, "/jobs/_processes"] then [Controllers::Jobs, :_processes]
|
|
41
44
|
in [:get, "/jobs/_enqueued"] then [Controllers::Jobs, :_enqueued]
|
|
42
45
|
in [:get, "/streams"] then [Controllers::Streams, :index]
|
|
43
46
|
in [:get, "/streams/info"] then [Controllers::Streams, :info]
|
|
@@ -49,6 +52,8 @@ module Cosmo
|
|
|
49
52
|
in [:get, "/crons/_table"] then [Controllers::Crons, :_table]
|
|
50
53
|
in [:delete, "/crons/delete"] then [Controllers::Crons, :delete]
|
|
51
54
|
in [:post, "/crons/run"] then [Controllers::Crons, :run_now]
|
|
55
|
+
in [:get, "/metrics"] then [Controllers::Metrics, :index]
|
|
56
|
+
in [:get, "/metrics/_panel"] then [Controllers::Metrics, :_panel]
|
|
52
57
|
in [:get, "/batches"] then [Controllers::Batches, :index]
|
|
53
58
|
in [:get, "/batches/_table"] then [Controllers::Batches, :_table]
|
|
54
59
|
in [:get, "/actions"] then [Controllers::Actions, :index]
|
data/lib/cosmo.rb
CHANGED
|
@@ -11,26 +11,77 @@ require "cosmo/job"
|
|
|
11
11
|
require "cosmo/batch"
|
|
12
12
|
require "cosmo/stream"
|
|
13
13
|
require "cosmo/cli"
|
|
14
|
+
require "cosmo/heartbeat"
|
|
14
15
|
require "cosmo/engine"
|
|
15
16
|
require "cosmo/api"
|
|
17
|
+
require "cosmo/middleware"
|
|
18
|
+
require "cosmo/services"
|
|
16
19
|
require "cosmo/railtie" if defined?(Rails::Railtie)
|
|
17
20
|
|
|
18
21
|
module Cosmo
|
|
22
|
+
# Optional, requires rack
|
|
23
|
+
autoload :Web, "cosmo/web"
|
|
24
|
+
# Optional, requires rack and webrick gems. Loaded only when an HTTP port is configured.
|
|
25
|
+
autoload :HTTPServer, "cosmo/http_server"
|
|
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
|
+
|
|
19
55
|
class Error < StandardError; end
|
|
20
56
|
|
|
21
57
|
class ArgumentError < Error; end
|
|
22
58
|
|
|
23
59
|
class NotImplementedError < Error; end
|
|
24
60
|
|
|
61
|
+
class ConfigError < Error; end
|
|
62
|
+
|
|
25
63
|
class ConfigNotFoundError < Error
|
|
26
64
|
def initialize(config_file)
|
|
27
65
|
super("No such file #{config_file}")
|
|
28
66
|
end
|
|
29
67
|
end
|
|
30
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
|
+
|
|
31
75
|
class StreamNotFoundError < Error
|
|
32
76
|
def initialize(stream_name)
|
|
33
77
|
super("Missing stream `#{stream_name}`")
|
|
34
78
|
end
|
|
35
79
|
end
|
|
80
|
+
|
|
81
|
+
class UnknownJobStreamError < Error
|
|
82
|
+
def initialize(names, configured)
|
|
83
|
+
super("Unknown job stream#{"s" if names.size > 1} #{names.map { "`#{_1}`" }.join(", ")}; " \
|
|
84
|
+
"configured: #{configured.empty? ? "none" : configured.map { "`#{_1}`" }.join(", ")}")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
36
87
|
end
|
data/sig/cosmo/api/counter.rbs
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
module Cosmo
|
|
2
2
|
module API
|
|
3
3
|
class Counter
|
|
4
|
-
STREAM_NAME: ::String
|
|
5
|
-
|
|
6
|
-
self.@instance: Counter
|
|
7
4
|
@namespace: ::String
|
|
8
5
|
|
|
9
|
-
def self.
|
|
6
|
+
def self.setup!: () -> void
|
|
10
7
|
|
|
11
|
-
def
|
|
8
|
+
def self.stream_config: () -> Hash[Symbol, untyped]
|
|
12
9
|
|
|
13
|
-
def
|
|
10
|
+
def initialize: (::String namespace) -> void
|
|
14
11
|
|
|
15
12
|
def increment: (Symbol key, ?by: ::Integer) -> ::Integer
|
|
16
13
|
alias incr increment
|
|
@@ -18,6 +18,8 @@ module Cosmo
|
|
|
18
18
|
|
|
19
19
|
def schedule_subject: () -> ::String
|
|
20
20
|
def target_subject: () -> ::String
|
|
21
|
+
def dispatch_subject: () -> ::String
|
|
22
|
+
def schedule_headers: () -> Hash[::String, ::String]
|
|
21
23
|
def job_name: () -> ::String
|
|
22
24
|
def job_payload: () -> ::String
|
|
23
25
|
def as_json: () -> Hash[Symbol, untyped]
|
data/sig/cosmo/api/cron.rbs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module Cosmo
|
|
2
2
|
module API
|
|
3
3
|
class Cron
|
|
4
|
+
STREAM: ::String
|
|
5
|
+
|
|
4
6
|
def all: () -> ::Array[Hash[Symbol, untyped]]
|
|
5
7
|
|
|
6
8
|
def upsert!: (?Cosmo::API::Cron::Entry schedule_obj,
|
|
@@ -17,8 +19,8 @@ module Cosmo
|
|
|
17
19
|
|
|
18
20
|
private
|
|
19
21
|
|
|
20
|
-
def
|
|
21
|
-
def build_from_nats: (::String
|
|
22
|
+
def schedules: () -> ::Array[Hash[Symbol, untyped]]
|
|
23
|
+
def build_from_nats: (::String subject) -> Hash[Symbol, untyped]?
|
|
22
24
|
def name_from_subject: (::String subject) -> ::String?
|
|
23
25
|
end
|
|
24
26
|
end
|
data/sig/cosmo/api/job.rbs
CHANGED
|
@@ -23,9 +23,21 @@ module Cosmo
|
|
|
23
23
|
|
|
24
24
|
def subject: () -> ::String
|
|
25
25
|
|
|
26
|
+
def error_class: () -> ::String?
|
|
27
|
+
|
|
28
|
+
def error_message: () -> ::String?
|
|
29
|
+
|
|
30
|
+
def error_backtrace: () -> ::String?
|
|
31
|
+
|
|
32
|
+
def error_backtrace_lines: () -> Array[::String]
|
|
33
|
+
|
|
34
|
+
def error?: () -> bool
|
|
35
|
+
|
|
26
36
|
def stream: () -> ::String
|
|
27
37
|
|
|
28
38
|
def timestamp: () -> ::String?
|
|
39
|
+
|
|
40
|
+
def stored_at: () -> ::Time?
|
|
29
41
|
end
|
|
30
42
|
end
|
|
31
43
|
end
|
data/sig/cosmo/api/kv.rbs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module Cosmo
|
|
2
2
|
module API
|
|
3
3
|
class KV
|
|
4
|
+
LIMIT: ::Integer
|
|
5
|
+
|
|
4
6
|
@name: ::String
|
|
5
7
|
@options: Hash[Symbol, untyped]
|
|
6
8
|
@kv: untyped
|
|
@@ -15,7 +17,9 @@ module Cosmo
|
|
|
15
17
|
|
|
16
18
|
def delete: (::String | ::Integer key) -> untyped
|
|
17
19
|
|
|
18
|
-
def keys: (?::String? subject, ?limit: ::Integer) -> Array[::String]
|
|
20
|
+
def keys: (?::String? subject, ?limit: ::Integer?, ?offset: ::Integer) -> Array[::String]
|
|
21
|
+
|
|
22
|
+
def entries: (?::String? subject, ?limit: ::Integer?, ?offset: ::Integer) -> Array[[::String, ::String]]
|
|
19
23
|
|
|
20
24
|
def purge: (::String | ::Integer key) -> untyped
|
|
21
25
|
|
|
@@ -28,6 +32,8 @@ module Cosmo
|
|
|
28
32
|
|
|
29
33
|
private
|
|
30
34
|
|
|
35
|
+
def walk: (::String? subject, limit: ::Integer?, offset: ::Integer, meta_only: bool) -> Array[[::String, ::String]]
|
|
36
|
+
|
|
31
37
|
def publish_cas: (::String | ::Integer key, ::String value, ::Integer? ttl, last_seq: ::Integer) -> untyped
|
|
32
38
|
|
|
33
39
|
def client: () -> Client
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Cosmo
|
|
2
|
+
module API
|
|
3
|
+
class Registry
|
|
4
|
+
LIMIT: ::Integer
|
|
5
|
+
|
|
6
|
+
self.@instance: Registry
|
|
7
|
+
@kv: KV
|
|
8
|
+
|
|
9
|
+
def self.instance: () -> instance
|
|
10
|
+
|
|
11
|
+
def initialize: () -> void
|
|
12
|
+
|
|
13
|
+
def put: (::String | ::Integer key, Hash[Symbol, untyped] value) -> void
|
|
14
|
+
|
|
15
|
+
def remove: (::String | ::Integer key) -> void
|
|
16
|
+
|
|
17
|
+
def list: (?page: ::Integer?, ?limit: ::Integer) -> Array[Hash[Symbol, untyped]]
|
|
18
|
+
|
|
19
|
+
def all: () -> Array[Hash[Symbol, untyped]]
|
|
20
|
+
|
|
21
|
+
def size: () -> ::Integer
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def bucket: () -> KV
|
|
26
|
+
|
|
27
|
+
def sanitize: (::String | ::Integer key) -> ::String
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|