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.
Files changed (131) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +21 -169
  3. data/README.md +296 -116
  4. data/lib/cosmo/active_job/adapter.rb +1 -2
  5. data/lib/cosmo/api/counter.rb +18 -15
  6. data/lib/cosmo/api/cron/entry.rb +23 -3
  7. data/lib/cosmo/api/cron.rb +25 -26
  8. data/lib/cosmo/api/job.rb +30 -0
  9. data/lib/cosmo/api/kv.rb +31 -11
  10. data/lib/cosmo/api/registry.rb +68 -0
  11. data/lib/cosmo/api/stats/busy.rb +63 -0
  12. data/lib/cosmo/api/stats/metrics.rb +109 -0
  13. data/lib/cosmo/api/stats/processes.rb +47 -0
  14. data/lib/cosmo/api/stats/totals.rb +27 -0
  15. data/lib/cosmo/api/stats.rb +8 -6
  16. data/lib/cosmo/api/stream.rb +1 -1
  17. data/lib/cosmo/api.rb +3 -2
  18. data/lib/cosmo/batch/counters.rb +19 -0
  19. data/lib/cosmo/batch.rb +15 -8
  20. data/lib/cosmo/cli.rb +46 -10
  21. data/lib/cosmo/client.rb +61 -7
  22. data/lib/cosmo/config/cosmo.yml +58 -0
  23. data/lib/cosmo/config/settings.rb +20 -0
  24. data/lib/cosmo/config.rb +144 -15
  25. data/lib/cosmo/engine.rb +103 -4
  26. data/lib/cosmo/heartbeat.rb +140 -0
  27. data/lib/cosmo/http_server/handler.rb +44 -0
  28. data/lib/cosmo/http_server/health.rb +39 -0
  29. data/lib/cosmo/http_server/ping.rb +23 -0
  30. data/lib/cosmo/http_server.rb +62 -0
  31. data/lib/cosmo/job/data.rb +17 -12
  32. data/lib/cosmo/job/enqueuer.rb +53 -0
  33. data/lib/cosmo/job/failure.rb +52 -0
  34. data/lib/cosmo/job/limit.rb +2 -2
  35. data/lib/cosmo/job/processor.rb +110 -73
  36. data/lib/cosmo/job/requeue.rb +20 -0
  37. data/lib/cosmo/job/stream_filter.rb +54 -0
  38. data/lib/cosmo/job.rb +10 -22
  39. data/lib/cosmo/logger.rb +15 -2
  40. data/lib/cosmo/middleware/busy.rb +15 -0
  41. data/lib/cosmo/middleware/chain.rb +137 -0
  42. data/lib/cosmo/middleware/limit.rb +50 -0
  43. data/lib/cosmo/middleware/metrics.rb +39 -0
  44. data/lib/cosmo/middleware/sentry.rb +61 -0
  45. data/lib/cosmo/middleware/totals.rb +29 -0
  46. data/lib/cosmo/middleware.rb +7 -0
  47. data/lib/cosmo/processor.rb +49 -8
  48. data/lib/cosmo/publisher.rb +1 -13
  49. data/lib/cosmo/railtie.rb +2 -2
  50. data/lib/cosmo/services.rb +69 -0
  51. data/lib/cosmo/stream/message.rb +2 -1
  52. data/lib/cosmo/stream/processor.rb +12 -1
  53. data/lib/cosmo/utils/hash.rb +7 -8
  54. data/lib/cosmo/utils/json.rb +1 -1
  55. data/lib/cosmo/utils/overrides.rb +16 -10
  56. data/lib/cosmo/utils/thread_pool.rb +5 -0
  57. data/lib/cosmo/version.rb +1 -1
  58. data/lib/cosmo/web/assets/app.css +79 -0
  59. data/lib/cosmo/web/chart.rb +98 -0
  60. data/lib/cosmo/web/controllers/crons.rb +8 -0
  61. data/lib/cosmo/web/controllers/jobs.rb +43 -5
  62. data/lib/cosmo/web/controllers/metrics.rb +48 -0
  63. data/lib/cosmo/web/helpers/application.rb +20 -1
  64. data/lib/cosmo/web/views/batches/_table.erb +11 -0
  65. data/lib/cosmo/web/views/batches/index.erb +1 -1
  66. data/lib/cosmo/web/views/jobs/_busy.erb +70 -4
  67. data/lib/cosmo/web/views/jobs/_dead.erb +29 -16
  68. data/lib/cosmo/web/views/jobs/_enqueued.erb +10 -0
  69. data/lib/cosmo/web/views/jobs/_processes.erb +145 -0
  70. data/lib/cosmo/web/views/jobs/_scheduled.erb +1 -1
  71. data/lib/cosmo/web/views/jobs/_stats.erb +4 -0
  72. data/lib/cosmo/web/views/jobs/_tabs.erb +6 -0
  73. data/lib/cosmo/web/views/jobs/busy.erb +1 -1
  74. data/lib/cosmo/web/views/jobs/dead.erb +2 -2
  75. data/lib/cosmo/web/views/jobs/enqueued.erb +1 -1
  76. data/lib/cosmo/web/views/jobs/index.erb +7 -1
  77. data/lib/cosmo/web/views/jobs/scheduled.erb +1 -1
  78. data/lib/cosmo/web/views/layout.erb +1 -1
  79. data/lib/cosmo/web/views/metrics/_chart.erb +36 -0
  80. data/lib/cosmo/web/views/metrics/_panel.erb +13 -0
  81. data/lib/cosmo/web/views/metrics/_table.erb +41 -0
  82. data/lib/cosmo/web/views/metrics/index.erb +17 -0
  83. data/lib/cosmo/web/views/streams/index.erb +1 -1
  84. data/lib/cosmo/web.rb +5 -0
  85. data/lib/cosmo.rb +51 -0
  86. data/sig/cosmo/api/counter.rbs +3 -6
  87. data/sig/cosmo/api/cron/entry.rbs +2 -0
  88. data/sig/cosmo/api/cron.rbs +4 -2
  89. data/sig/cosmo/api/job.rbs +12 -0
  90. data/sig/cosmo/api/kv.rbs +7 -1
  91. data/sig/cosmo/api/registry.rbs +30 -0
  92. data/sig/cosmo/api/stats/busy.rbs +31 -0
  93. data/sig/cosmo/api/stats/metrics.rbs +42 -0
  94. data/sig/cosmo/api/stats/processes.rbs +19 -0
  95. data/sig/cosmo/api/stats/totals.rbs +18 -0
  96. data/sig/cosmo/batch/counters.rbs +10 -0
  97. data/sig/cosmo/batch.rbs +3 -4
  98. data/sig/cosmo/cli.rbs +6 -0
  99. data/sig/cosmo/client.rbs +15 -2
  100. data/sig/cosmo/config/settings.rbs +31 -0
  101. data/sig/cosmo/config.rbs +59 -5
  102. data/sig/cosmo/engine.rbs +29 -0
  103. data/sig/cosmo/heartbeat.rbs +38 -0
  104. data/sig/cosmo/http_server/handler.rbs +17 -0
  105. data/sig/cosmo/http_server/health.rbs +21 -0
  106. data/sig/cosmo/http_server/ping.rbs +14 -0
  107. data/sig/cosmo/http_server.rbs +22 -0
  108. data/sig/cosmo/job/data.rbs +5 -3
  109. data/sig/cosmo/job/enqueuer.rbs +11 -0
  110. data/sig/cosmo/job/failure.rbs +28 -0
  111. data/sig/cosmo/job/processor.rbs +18 -4
  112. data/sig/cosmo/job/requeue.rbs +9 -0
  113. data/sig/cosmo/job/stream_filter.rbs +25 -0
  114. data/sig/cosmo/job.rbs +0 -2
  115. data/sig/cosmo/logger.rbs +6 -0
  116. data/sig/cosmo/middleware/chain.rbs +51 -0
  117. data/sig/cosmo/middleware/sentry.rbs +14 -0
  118. data/sig/cosmo/middleware.rbs +35 -0
  119. data/sig/cosmo/processor.rbs +14 -1
  120. data/sig/cosmo/publisher.rbs +1 -9
  121. data/sig/cosmo/services.rbs +18 -0
  122. data/sig/cosmo/stream/message.rbs +3 -1
  123. data/sig/cosmo/stream/processor.rbs +4 -0
  124. data/sig/cosmo/utils/hash.rbs +2 -3
  125. data/sig/cosmo/utils/thread_pool.rbs +3 -0
  126. data/sig/cosmo.rbs +15 -0
  127. metadata +62 -22
  128. data/lib/cosmo/api/busy.rb +0 -66
  129. data/lib/cosmo/sentry/auto.rb +0 -6
  130. data/lib/cosmo/sentry/job_processor_middleware.rb +0 -66
  131. 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">&#9209; 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">&#x25B6; 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] -%>&middot; <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]) %> &middot; <%= h(process[:ruby]) %>
80
+ <% if process[:http_port] -%>&middot; 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" : '&mdash;' %></td>
96
+ <td><%= h(nats[:reconnects].to_i) %></td>
97
+ <td><%= process[:rss] ? format_bytes(process[:rss].to_i * 1024) : '&mdash;' %></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">&#x2190; Prev</a>
116
+ <% else -%>
117
+ <span class="btn btn-disabled">&#x2190; Prev</span>
118
+ <% end -%>
119
+
120
+ <% pages(@page, @total_pages).each do |p| -%>
121
+ <% if p == :gap -%>
122
+ <span class="pagination-gap">&hellip;</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 &#x2192;</a>
140
+ <% else -%>
141
+ <span class="btn btn-disabled">Next &#x2192;</span>
142
+ <% end -%>
143
+ </div>
144
+ <% end -%>
145
+ </div>
@@ -1,6 +1,6 @@
1
1
  <div id="scheduled-poller"
2
2
  hx-get="<%= url_for('/jobs/_scheduled') %>"
3
- hx-trigger="every 5s"
3
+ hx-trigger="every <%= poll_interval %>s"
4
4
  hx-swap="outerHTML">
5
5
  <div class="pending">
6
6
  <span></span>
@@ -34,6 +34,7 @@
34
34
  class="<%= 'active' if referrer?('/jobs/enqueued') %>">&#x1F4EC; 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') %>">&#x23F0; 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') %>">&#x1F480; 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') %>">&#x1F4CB; Dashboard</a>
4
4
  <a href="<%= url_for('/batches') %>"
5
5
  class="<%= 'active' if path_prefix?('/batches') %>">&#x1F4E6; Batches</a>
6
+ <% if Cosmo::Config.scheduled.enabled -%>
6
7
  <a href="<%= url_for('/crons') %>"
7
8
  class="<%= 'active' if path_prefix?('/crons') %>">&#x23F0; Crons</a>
9
+ <% end -%>
10
+ <% if Cosmo::Config.metrics.enabled -%>
11
+ <a href="<%= url_for('/metrics') %>"
12
+ class="<%= 'active' if path_prefix?('/metrics') %>">&#x1F4CA; Metrics</a>
13
+ <% end -%>
8
14
  </nav>
@@ -2,7 +2,7 @@
2
2
  <header><%= render('jobs/_tabs') %></header>
3
3
 
4
4
  <div hx-get="<%= url_for('/jobs/_stats') %>"
5
- hx-trigger="load, every 5s"
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>
@@ -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 5s"
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-poller"
11
+ <div id="dead-jobs"
12
12
  hx-get="<%= url_for('/jobs/_dead') %>"
13
13
  hx-trigger="load"
14
14
  hx-swap="outerHTML">
@@ -2,7 +2,7 @@
2
2
  <header><%= render('jobs/_tabs') %></header>
3
3
 
4
4
  <div hx-get="<%= url_for('/jobs/_stats') %>"
5
- hx-trigger="load, every 5s"
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>
@@ -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 5s"
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&hellip;</div>
16
+ </div>
11
17
  </div>
12
18
  </section>
@@ -2,7 +2,7 @@
2
2
  <header><%= render('jobs/_tabs') %></header>
3
3
 
4
4
  <div hx-get="<%= url_for('/jobs/_stats') %>"
5
- hx-trigger="load, every 5s"
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>
@@ -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&hellip;</div>
16
+ </div>
17
+ </section>
@@ -3,7 +3,7 @@
3
3
 
4
4
  <div id="content">
5
5
  <div hx-get="<%= url_for('/streams/_table') %>"
6
- hx-trigger="load, every 5s"
6
+ hx-trigger="load, every <%= poll_interval %>s"
7
7
  hx-swap="innerHTML">
8
8
  <div class="alert alert-info">Loading streams…</div>
9
9
  </div>
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
@@ -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.instance: () -> Counter
6
+ def self.setup!: () -> void
10
7
 
11
- def initialize: (::String namespace) -> void
8
+ def self.stream_config: () -> Hash[Symbol, untyped]
12
9
 
13
- def with: () { () -> bool } -> void
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]
@@ -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 schedules_from_stream: (::String stream_name) -> ::Array[Hash[Symbol, untyped]]
21
- def build_from_nats: (::String stream_name, ::String subject) -> Hash[Symbol, untyped]?
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
@@ -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