pgbus 0.1.4 → 0.1.6
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 +326 -11
- data/app/controllers/pgbus/api/insights_controller.rb +16 -0
- data/app/controllers/pgbus/insights_controller.rb +10 -0
- data/app/controllers/pgbus/locks_controller.rb +9 -0
- data/app/controllers/pgbus/outbox_controller.rb +10 -0
- data/app/controllers/pgbus/queues_controller.rb +10 -0
- data/app/helpers/pgbus/application_helper.rb +34 -0
- data/app/models/pgbus/job_lock.rb +82 -0
- data/app/models/pgbus/job_stat.rb +94 -0
- data/app/models/pgbus/outbox_entry.rb +10 -0
- data/app/models/pgbus/queue_state.rb +33 -0
- data/app/views/layouts/pgbus/application.html.erb +33 -8
- data/app/views/pgbus/dashboard/_stats_cards.html.erb +24 -18
- data/app/views/pgbus/insights/show.html.erb +161 -0
- data/app/views/pgbus/locks/index.html.erb +53 -0
- data/app/views/pgbus/outbox/index.html.erb +55 -0
- data/app/views/pgbus/queues/_queues_list.html.erb +15 -1
- data/config/routes.rb +7 -0
- data/lib/generators/pgbus/add_job_locks_generator.rb +52 -0
- data/lib/generators/pgbus/add_job_stats_generator.rb +52 -0
- data/lib/generators/pgbus/add_outbox_generator.rb +52 -0
- data/lib/generators/pgbus/add_queue_states_generator.rb +51 -0
- data/lib/generators/pgbus/add_recurring_generator.rb +1 -1
- data/lib/generators/pgbus/install_generator.rb +1 -1
- data/lib/generators/pgbus/templates/add_job_locks.rb.erb +21 -0
- data/lib/generators/pgbus/templates/add_job_stats.rb.erb +18 -0
- data/lib/generators/pgbus/templates/add_outbox.rb.erb +25 -0
- data/lib/generators/pgbus/templates/add_queue_states.rb.erb +16 -0
- data/lib/generators/pgbus/upgrade_pgmq_generator.rb +1 -1
- data/lib/pgbus/active_job/adapter.rb +64 -9
- data/lib/pgbus/active_job/executor.rb +67 -5
- data/lib/pgbus/circuit_breaker.rb +112 -0
- data/lib/pgbus/client.rb +127 -50
- data/lib/pgbus/configuration.rb +55 -1
- data/lib/pgbus/dedup_cache.rb +76 -0
- data/lib/pgbus/engine.rb +1 -0
- data/lib/pgbus/event_bus/handler.rb +13 -2
- data/lib/pgbus/outbox/poller.rb +117 -0
- data/lib/pgbus/outbox.rb +30 -0
- data/lib/pgbus/process/consumer_priority.rb +64 -0
- data/lib/pgbus/process/dispatcher.rb +75 -0
- data/lib/pgbus/process/heartbeat.rb +3 -1
- data/lib/pgbus/process/lifecycle.rb +111 -0
- data/lib/pgbus/process/queue_lock.rb +87 -0
- data/lib/pgbus/process/supervisor.rb +46 -6
- data/lib/pgbus/process/wake_signal.rb +53 -0
- data/lib/pgbus/process/worker.rb +117 -21
- data/lib/pgbus/queue_factory.rb +62 -0
- data/lib/pgbus/rate_counter.rb +81 -0
- data/lib/pgbus/recurring/schedule.rb +1 -1
- data/lib/pgbus/uniqueness.rb +169 -0
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +136 -2
- data/lib/pgbus.rb +9 -0
- metadata +31 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
class JobStat < Pgbus::ApplicationRecord
|
|
5
|
+
self.table_name = "pgbus_job_stats"
|
|
6
|
+
|
|
7
|
+
scope :since, ->(time) { where("created_at >= ?", time) }
|
|
8
|
+
scope :successful, -> { where(status: "success") }
|
|
9
|
+
scope :failed, -> { where(status: "failed") }
|
|
10
|
+
scope :dead_lettered, -> { where(status: "dead_lettered") }
|
|
11
|
+
|
|
12
|
+
# Record a job execution stat. Called by the executor after each job.
|
|
13
|
+
def self.record!(job_class:, queue_name:, status:, duration_ms:)
|
|
14
|
+
return unless table_exists?
|
|
15
|
+
|
|
16
|
+
create!(
|
|
17
|
+
job_class: job_class,
|
|
18
|
+
queue_name: queue_name,
|
|
19
|
+
status: status,
|
|
20
|
+
duration_ms: duration_ms
|
|
21
|
+
)
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
Pgbus.logger.debug { "[Pgbus] Failed to record job stat: #{e.message}" }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Memoized — intentionally never invalidated at runtime. If the
|
|
27
|
+
# pgbus_job_stats migration runs while the app is already running,
|
|
28
|
+
# a restart is required for stat recording to begin.
|
|
29
|
+
def self.table_exists?
|
|
30
|
+
return @table_exists if defined?(@table_exists)
|
|
31
|
+
|
|
32
|
+
@table_exists = connection.table_exists?(table_name)
|
|
33
|
+
rescue StandardError
|
|
34
|
+
@table_exists = false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Throughput: jobs per minute bucketed by minute for the last N minutes
|
|
38
|
+
def self.throughput(minutes: 60)
|
|
39
|
+
since(minutes.minutes.ago)
|
|
40
|
+
.group("date_trunc('minute', created_at)")
|
|
41
|
+
.order(Arel.sql("date_trunc('minute', created_at)"))
|
|
42
|
+
.count
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Average duration by job class
|
|
46
|
+
def self.avg_duration_by_class(minutes: 60)
|
|
47
|
+
since(minutes.minutes.ago)
|
|
48
|
+
.group(:job_class)
|
|
49
|
+
.order(Arel.sql("AVG(duration_ms) DESC"))
|
|
50
|
+
.average(:duration_ms)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Success/fail/DLQ counts
|
|
54
|
+
def self.status_counts(minutes: 60)
|
|
55
|
+
since(minutes.minutes.ago).group(:status).count
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Top N slowest job classes by average duration
|
|
59
|
+
def self.slowest_classes(limit: 10, minutes: 60)
|
|
60
|
+
since(minutes.minutes.ago)
|
|
61
|
+
.group(:job_class)
|
|
62
|
+
.order(Arel.sql("AVG(duration_ms) DESC"))
|
|
63
|
+
.limit(limit)
|
|
64
|
+
.pluck(:job_class, Arel.sql("COUNT(*)"), Arel.sql("ROUND(AVG(duration_ms))"), Arel.sql("MAX(duration_ms)"))
|
|
65
|
+
.map { |cls, count, avg, max| { job_class: cls, count: count.to_i, avg_ms: avg.to_i, max_ms: max.to_i } }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Single-query aggregate summary using conditional counts.
|
|
69
|
+
def self.summary(minutes: 60)
|
|
70
|
+
row = since(minutes.minutes.ago).pick(
|
|
71
|
+
Arel.sql("COUNT(*)"),
|
|
72
|
+
Arel.sql("COUNT(*) FILTER (WHERE status = 'success')"),
|
|
73
|
+
Arel.sql("COUNT(*) FILTER (WHERE status = 'failed')"),
|
|
74
|
+
Arel.sql("COUNT(*) FILTER (WHERE status = 'dead_lettered')"),
|
|
75
|
+
Arel.sql("ROUND(AVG(duration_ms)::numeric, 1)"),
|
|
76
|
+
Arel.sql("MAX(duration_ms)")
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
{
|
|
80
|
+
total: row[0].to_i,
|
|
81
|
+
success: row[1].to_i,
|
|
82
|
+
failed: row[2].to_i,
|
|
83
|
+
dead_lettered: row[3].to_i,
|
|
84
|
+
avg_duration_ms: row[4]&.to_f || 0,
|
|
85
|
+
max_duration_ms: row[5].to_i
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Cleanup old stats
|
|
90
|
+
def self.cleanup!(older_than:)
|
|
91
|
+
where("created_at < ?", older_than).delete_all
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
class OutboxEntry < Pgbus::ApplicationRecord
|
|
5
|
+
self.table_name = "pgbus_outbox_entries"
|
|
6
|
+
|
|
7
|
+
scope :unpublished, -> { where(published_at: nil) }
|
|
8
|
+
scope :published_before, ->(time) { where(published_at: ...time) }
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
class QueueState < Pgbus::ApplicationRecord
|
|
5
|
+
self.table_name = "pgbus_queue_states"
|
|
6
|
+
|
|
7
|
+
scope :paused, -> { where(paused: true) }
|
|
8
|
+
|
|
9
|
+
def self.paused?(queue_name)
|
|
10
|
+
where(queue_name: queue_name, paused: true).exists?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.pause!(queue_name, reason: nil)
|
|
14
|
+
record = find_or_initialize_by(queue_name: queue_name)
|
|
15
|
+
record.update!(paused: true, paused_reason: reason, paused_at: Time.current, circuit_breaker_resume_at: nil)
|
|
16
|
+
record
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.resume!(queue_name)
|
|
20
|
+
record = find_by(queue_name: queue_name)
|
|
21
|
+
return unless record
|
|
22
|
+
|
|
23
|
+
record.update!(
|
|
24
|
+
paused: false,
|
|
25
|
+
paused_reason: nil,
|
|
26
|
+
paused_at: nil,
|
|
27
|
+
circuit_breaker_trip_count: 0,
|
|
28
|
+
circuit_breaker_resume_at: nil
|
|
29
|
+
)
|
|
30
|
+
record
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en" class="h-full
|
|
2
|
+
<html lang="en" class="h-full">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Pgbus Dashboard</title>
|
|
7
7
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<script>
|
|
9
|
+
tailwind.config = { darkMode: 'class' };
|
|
10
|
+
// Restore dark mode preference
|
|
11
|
+
if (localStorage.getItem('pgbus-dark') === 'true' ||
|
|
12
|
+
(!localStorage.getItem('pgbus-dark') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
13
|
+
document.documentElement.classList.add('dark');
|
|
14
|
+
}
|
|
15
|
+
</script>
|
|
8
16
|
<script type="module">
|
|
9
17
|
import * as Turbo from "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8/dist/turbo.es2017.esm.js";
|
|
10
18
|
|
|
11
19
|
<% if Pgbus.configuration.web_live_updates %>
|
|
12
|
-
// Auto-refresh turbo frames, pausing when tab is hidden
|
|
13
20
|
const interval = <%= Pgbus.configuration.web_refresh_interval %>;
|
|
14
21
|
if (interval > 0) {
|
|
15
22
|
let timer;
|
|
@@ -24,12 +31,18 @@
|
|
|
24
31
|
start();
|
|
25
32
|
}
|
|
26
33
|
<% end %>
|
|
34
|
+
|
|
35
|
+
// Dark mode toggle
|
|
36
|
+
window.toggleDarkMode = function() {
|
|
37
|
+
const isDark = document.documentElement.classList.toggle('dark');
|
|
38
|
+
localStorage.setItem('pgbus-dark', isDark);
|
|
39
|
+
};
|
|
27
40
|
</script>
|
|
28
41
|
</head>
|
|
29
|
-
<body class="h-full">
|
|
42
|
+
<body class="h-full bg-gray-50 dark:bg-gray-950 transition-colors">
|
|
30
43
|
<div class="min-h-full">
|
|
31
44
|
<!-- Top nav -->
|
|
32
|
-
<nav class="bg-gray-900">
|
|
45
|
+
<nav class="bg-gray-900 dark:bg-gray-950 border-b border-gray-800">
|
|
33
46
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
34
47
|
<div class="flex h-14 items-center justify-between">
|
|
35
48
|
<div class="flex items-center space-x-8">
|
|
@@ -46,8 +59,20 @@
|
|
|
46
59
|
<%= pgbus_nav_link "Processes", pgbus.processes_path %>
|
|
47
60
|
<%= pgbus_nav_link "Events", pgbus.events_path %>
|
|
48
61
|
<%= pgbus_nav_link "DLQ", pgbus.dead_letter_index_path %>
|
|
62
|
+
<%= pgbus_nav_link "Outbox", pgbus.outbox_index_path %>
|
|
63
|
+
<%= pgbus_nav_link "Locks", pgbus.locks_path %>
|
|
64
|
+
<%= pgbus_nav_link "Insights", pgbus.insights_path %>
|
|
49
65
|
</div>
|
|
50
66
|
</div>
|
|
67
|
+
|
|
68
|
+
<button onclick="toggleDarkMode()" class="rounded-md p-2 text-gray-400 hover:text-white focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-900" aria-label="Toggle dark mode">
|
|
69
|
+
<svg class="h-5 w-5 hidden dark:block" fill="currentColor" viewBox="0 0 20 20">
|
|
70
|
+
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/>
|
|
71
|
+
</svg>
|
|
72
|
+
<svg class="h-5 w-5 block dark:hidden" fill="currentColor" viewBox="0 0 20 20">
|
|
73
|
+
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
|
|
74
|
+
</svg>
|
|
75
|
+
</button>
|
|
51
76
|
</div>
|
|
52
77
|
</div>
|
|
53
78
|
</nav>
|
|
@@ -55,15 +80,15 @@
|
|
|
55
80
|
<!-- Flash messages -->
|
|
56
81
|
<% if notice %>
|
|
57
82
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 mt-4">
|
|
58
|
-
<div class="rounded-md bg-green-50 p-3">
|
|
59
|
-
<p class="text-sm text-green-800"><%= notice %></p>
|
|
83
|
+
<div class="rounded-md bg-green-50 dark:bg-green-900/30 p-3">
|
|
84
|
+
<p class="text-sm text-green-800 dark:text-green-300"><%= notice %></p>
|
|
60
85
|
</div>
|
|
61
86
|
</div>
|
|
62
87
|
<% end %>
|
|
63
88
|
<% if alert %>
|
|
64
89
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 mt-4">
|
|
65
|
-
<div class="rounded-md bg-red-50 p-3">
|
|
66
|
-
<p class="text-sm text-red-800"><%= alert %></p>
|
|
90
|
+
<div class="rounded-md bg-red-50 dark:bg-red-900/30 p-3">
|
|
91
|
+
<p class="text-sm text-red-800 dark:text-red-300"><%= alert %></p>
|
|
67
92
|
</div>
|
|
68
93
|
</div>
|
|
69
94
|
<% end %>
|
|
@@ -1,36 +1,42 @@
|
|
|
1
1
|
<turbo-frame id="dashboard-stats" data-auto-refresh src="<%= pgbus.root_path(frame: 'stats') %>">
|
|
2
|
-
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-
|
|
3
|
-
<div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
|
|
4
|
-
<p class="text-sm font-medium text-gray-500">Queues</p>
|
|
5
|
-
<p class="mt-1 text-3xl font-semibold text-gray-900"><%= @stats[:total_queues] %></p>
|
|
2
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-6 mb-8">
|
|
3
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
4
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Queues</p>
|
|
5
|
+
<p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= @stats[:total_queues] %></p>
|
|
6
6
|
</div>
|
|
7
7
|
|
|
8
|
-
<div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
|
|
9
|
-
<p class="text-sm font-medium text-gray-500">Enqueued</p>
|
|
10
|
-
<p class="mt-1 text-3xl font-semibold text-gray-900"><%= pgbus_number(@stats[:total_depth]) %></p>
|
|
11
|
-
<p class="text-xs text-gray-400"><%= pgbus_number(@stats[:total_visible]) %> visible</p>
|
|
8
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
9
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Enqueued</p>
|
|
10
|
+
<p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= pgbus_number(@stats[:total_depth]) %></p>
|
|
11
|
+
<p class="text-xs text-gray-400 dark:text-gray-500"><%= pgbus_number(@stats[:total_visible]) %> visible</p>
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
|
-
<div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
|
|
15
|
-
<p class="text-sm font-medium text-gray-500">Processes</p>
|
|
16
|
-
<p class="mt-1 text-3xl font-semibold <%= @stats[:active_processes] > 0 ? 'text-green-600' : 'text-gray-400' %>">
|
|
14
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
15
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Processes</p>
|
|
16
|
+
<p class="mt-1 text-3xl font-semibold <%= @stats[:active_processes] > 0 ? 'text-green-600 dark:text-green-400' : 'text-gray-400' %>">
|
|
17
17
|
<%= @stats[:active_processes] %>
|
|
18
18
|
</p>
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
|
-
<div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
|
|
22
|
-
<p class="text-sm font-medium text-gray-500">Recurring</p>
|
|
23
|
-
<p class="mt-1 text-3xl font-semibold text-gray-900"><%= @stats[:recurring_count] %></p>
|
|
21
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
22
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Recurring</p>
|
|
23
|
+
<p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= @stats[:recurring_count] %></p>
|
|
24
24
|
<p class="text-xs text-gray-400">
|
|
25
|
-
<%= link_to "View tasks", pgbus.recurring_tasks_path, class: "text-blue-500 hover:text-blue-700" %>
|
|
25
|
+
<%= link_to "View tasks", pgbus.recurring_tasks_path, class: "text-blue-500 hover:text-blue-700 dark:text-blue-400" %>
|
|
26
26
|
</p>
|
|
27
27
|
</div>
|
|
28
28
|
|
|
29
|
-
<div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
|
|
30
|
-
<p class="text-sm font-medium text-gray-500">Failed / DLQ</p>
|
|
31
|
-
<p class="mt-1 text-3xl font-semibold <%= (@stats[:failed_count] + @stats[:dlq_depth]) > 0 ? 'text-red-600' : 'text-gray-900' %>">
|
|
29
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
30
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Failed / DLQ</p>
|
|
31
|
+
<p class="mt-1 text-3xl font-semibold <%= (@stats[:failed_count] + @stats[:dlq_depth]) > 0 ? 'text-red-600 dark:text-red-400' : 'text-gray-900 dark:text-white' %>">
|
|
32
32
|
<%= @stats[:failed_count] %> / <%= @stats[:dlq_depth] %>
|
|
33
33
|
</p>
|
|
34
34
|
</div>
|
|
35
|
+
|
|
36
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
37
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Throughput</p>
|
|
38
|
+
<p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= @stats[:throughput_rate] %></p>
|
|
39
|
+
<p class="text-xs text-gray-400">msgs/s</p>
|
|
40
|
+
</div>
|
|
35
41
|
</div>
|
|
36
42
|
</turbo-frame>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Insights</h1>
|
|
3
|
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Job performance metrics for the last hour</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<!-- Summary cards -->
|
|
7
|
+
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6 mb-8">
|
|
8
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
9
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Total Jobs</dt>
|
|
10
|
+
<dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_number(@summary[:total]) %></dd>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
13
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Succeeded</dt>
|
|
14
|
+
<dd class="mt-1 text-2xl font-semibold text-green-600 dark:text-green-400"><%= pgbus_number(@summary[:success]) %></dd>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
17
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Failed</dt>
|
|
18
|
+
<dd class="mt-1 text-2xl font-semibold text-red-600 dark:text-red-400"><%= pgbus_number(@summary[:failed]) %></dd>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
21
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Dead Lettered</dt>
|
|
22
|
+
<dd class="mt-1 text-2xl font-semibold text-orange-600 dark:text-orange-400"><%= pgbus_number(@summary[:dead_lettered]) %></dd>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
25
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Avg Duration</dt>
|
|
26
|
+
<dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_ms_duration(@summary[:avg_duration_ms]) %></dd>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
29
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Max Duration</dt>
|
|
30
|
+
<dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_ms_duration(@summary[:max_duration_ms]) %></dd>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- Charts -->
|
|
35
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
|
36
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
37
|
+
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-4">Throughput (jobs/min)</h3>
|
|
38
|
+
<div id="throughput-chart" style="height: 280px;"></div>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
41
|
+
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-4">Status Distribution</h3>
|
|
42
|
+
<div id="status-chart" style="height: 280px;"></div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<!-- Slowest job classes -->
|
|
47
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
48
|
+
<div class="px-5 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
49
|
+
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300">Slowest Job Classes (avg duration)</h3>
|
|
50
|
+
</div>
|
|
51
|
+
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
52
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
53
|
+
<tr>
|
|
54
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Job Class</th>
|
|
55
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Count</th>
|
|
56
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Avg</th>
|
|
57
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Max</th>
|
|
58
|
+
</tr>
|
|
59
|
+
</thead>
|
|
60
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
61
|
+
<% @slowest.each do |row| %>
|
|
62
|
+
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
63
|
+
<td class="px-4 py-3 text-sm font-medium text-gray-700 dark:text-gray-300"><%= row[:job_class] %></td>
|
|
64
|
+
<td class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(row[:count]) %></td>
|
|
65
|
+
<td class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_ms_duration(row[:avg_ms]) %></td>
|
|
66
|
+
<td class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_ms_duration(row[:max_ms]) %></td>
|
|
67
|
+
</tr>
|
|
68
|
+
<% end %>
|
|
69
|
+
<% if @slowest.empty? %>
|
|
70
|
+
<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500">No job stats yet</td></tr>
|
|
71
|
+
<% end %>
|
|
72
|
+
</tbody>
|
|
73
|
+
</table>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<script src="https://cdn.jsdelivr.net/npm/apexcharts@4"></script>
|
|
77
|
+
<script>
|
|
78
|
+
let throughputChart, statusChart;
|
|
79
|
+
|
|
80
|
+
function getThemeColors() {
|
|
81
|
+
const isDark = document.documentElement.classList.contains('dark');
|
|
82
|
+
return {
|
|
83
|
+
isDark,
|
|
84
|
+
text: isDark ? '#9ca3af' : '#6b7280',
|
|
85
|
+
grid: isDark ? '#374151' : '#e5e7eb',
|
|
86
|
+
tooltip: isDark ? 'dark' : 'light',
|
|
87
|
+
dataLabel: isDark ? '#fff' : '#000'
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderCharts(data) {
|
|
92
|
+
const t = getThemeColors();
|
|
93
|
+
|
|
94
|
+
// Destroy existing charts before re-rendering
|
|
95
|
+
if (throughputChart) throughputChart.destroy();
|
|
96
|
+
if (statusChart) statusChart.destroy();
|
|
97
|
+
|
|
98
|
+
// Throughput chart
|
|
99
|
+
const throughputData = data.throughput.map(p => ({
|
|
100
|
+
x: new Date(p.time).getTime(), y: p.count
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
throughputChart = new ApexCharts(document.querySelector('#throughput-chart'), {
|
|
104
|
+
series: [{ name: 'Jobs/min', data: throughputData }],
|
|
105
|
+
chart: { type: 'area', height: 280, toolbar: { show: false }, background: 'transparent', foreColor: t.text },
|
|
106
|
+
stroke: { curve: 'smooth', width: 2 },
|
|
107
|
+
fill: { type: 'gradient', gradient: { shadeIntensity: 1, opacityFrom: 0.4, opacityTo: 0.05, stops: [0, 100] } },
|
|
108
|
+
colors: ['#6366f1'],
|
|
109
|
+
xaxis: { type: 'datetime', labels: { style: { colors: t.text } } },
|
|
110
|
+
yaxis: { labels: { style: { colors: t.text } } },
|
|
111
|
+
grid: { borderColor: t.grid },
|
|
112
|
+
tooltip: { theme: t.tooltip },
|
|
113
|
+
dataLabels: { enabled: false }
|
|
114
|
+
});
|
|
115
|
+
throughputChart.render();
|
|
116
|
+
|
|
117
|
+
// Status distribution chart
|
|
118
|
+
const statusLabels = Object.keys(data.status_counts);
|
|
119
|
+
const statusValues = Object.values(data.status_counts);
|
|
120
|
+
const statusColors = statusLabels.map(s => {
|
|
121
|
+
if (s === 'success') return '#10b981';
|
|
122
|
+
if (s === 'failed') return '#ef4444';
|
|
123
|
+
if (s === 'dead_lettered') return '#f97316';
|
|
124
|
+
return '#6b7280';
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (statusLabels.length > 0) {
|
|
128
|
+
statusChart = new ApexCharts(document.querySelector('#status-chart'), {
|
|
129
|
+
series: statusValues, labels: statusLabels,
|
|
130
|
+
chart: { type: 'donut', height: 280, background: 'transparent', foreColor: t.text },
|
|
131
|
+
colors: statusColors,
|
|
132
|
+
legend: { position: 'bottom', labels: { colors: t.text } },
|
|
133
|
+
plotOptions: { pie: { donut: { size: '60%' } } },
|
|
134
|
+
dataLabels: { style: { colors: [t.dataLabel] } },
|
|
135
|
+
tooltip: { theme: t.tooltip }
|
|
136
|
+
});
|
|
137
|
+
statusChart.render();
|
|
138
|
+
} else {
|
|
139
|
+
document.querySelector('#status-chart').innerHTML =
|
|
140
|
+
'<p class="text-center text-sm text-gray-400 dark:text-gray-500 pt-24">No data yet</p>';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Fetch data and render
|
|
145
|
+
let chartData = null;
|
|
146
|
+
fetch('<%= pgbus.api_insights_path %>')
|
|
147
|
+
.then(r => r.json())
|
|
148
|
+
.then(data => { chartData = data; renderCharts(data); })
|
|
149
|
+
.catch(() => {
|
|
150
|
+
const msg = '<p class="text-center text-sm text-gray-400 dark:text-gray-500 pt-24">Failed to load chart data</p>';
|
|
151
|
+
document.querySelector('#throughput-chart').innerHTML = msg;
|
|
152
|
+
document.querySelector('#status-chart').innerHTML = msg;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Re-render charts when dark mode toggles
|
|
156
|
+
const origToggle = window.toggleDarkMode;
|
|
157
|
+
window.toggleDarkMode = function() {
|
|
158
|
+
origToggle();
|
|
159
|
+
if (chartData) renderCharts(chartData);
|
|
160
|
+
};
|
|
161
|
+
</script>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Job Locks</h1>
|
|
3
|
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Active uniqueness locks preventing duplicate job execution</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
7
|
+
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
8
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
9
|
+
<tr>
|
|
10
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Lock Key</th>
|
|
11
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Job Class</th>
|
|
12
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">State</th>
|
|
13
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Owner</th>
|
|
14
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Age</th>
|
|
15
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Expires</th>
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
19
|
+
<% @locks.each do |lock| %>
|
|
20
|
+
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
21
|
+
<td class="px-4 py-3 text-sm font-mono text-gray-700 dark:text-gray-300 max-w-xs truncate"><%= lock[:lock_key] %></td>
|
|
22
|
+
<td class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300"><%= lock[:job_class] %></td>
|
|
23
|
+
<td class="px-4 py-3 text-sm">
|
|
24
|
+
<% if lock[:state] == "executing" %>
|
|
25
|
+
<span class="inline-flex items-center rounded-full bg-blue-100 dark:bg-blue-900/50 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:text-blue-300">Executing</span>
|
|
26
|
+
<% else %>
|
|
27
|
+
<span class="inline-flex items-center rounded-full bg-yellow-100 dark:bg-yellow-900/50 px-2.5 py-0.5 text-xs font-medium text-yellow-800 dark:text-yellow-300">Queued</span>
|
|
28
|
+
<% end %>
|
|
29
|
+
</td>
|
|
30
|
+
<td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
|
|
31
|
+
<% if lock[:owner_pid] %>
|
|
32
|
+
<span class="font-mono"><%= lock[:owner_pid] %></span>
|
|
33
|
+
<% if lock[:owner_hostname] %>
|
|
34
|
+
<span class="text-xs text-gray-400 dark:text-gray-500">@<%= lock[:owner_hostname] %></span>
|
|
35
|
+
<% end %>
|
|
36
|
+
<% else %>
|
|
37
|
+
<span class="text-gray-400 dark:text-gray-500">—</span>
|
|
38
|
+
<% end %>
|
|
39
|
+
</td>
|
|
40
|
+
<td class="px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400">
|
|
41
|
+
<% if lock[:age_seconds] %>
|
|
42
|
+
<%= pgbus_duration(lock[:age_seconds]) %>
|
|
43
|
+
<% end %>
|
|
44
|
+
</td>
|
|
45
|
+
<td class="px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400"><%= pgbus_time_ago_future(lock[:expires_at]) %></td>
|
|
46
|
+
</tr>
|
|
47
|
+
<% end %>
|
|
48
|
+
<% if @locks.empty? %>
|
|
49
|
+
<tr><td colspan="6" class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500">No active locks</td></tr>
|
|
50
|
+
<% end %>
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
</div>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold text-gray-900">Outbox</h1>
|
|
3
|
+
<p class="mt-1 text-sm text-gray-500">Transactional outbox entries pending publication to PGMQ</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="mb-6 grid grid-cols-3 gap-4">
|
|
7
|
+
<div class="rounded-lg bg-white p-4 shadow ring-1 ring-gray-200">
|
|
8
|
+
<dt class="text-xs font-medium uppercase text-gray-500">Unpublished</dt>
|
|
9
|
+
<dd class="mt-1 text-2xl font-semibold text-gray-900"><%= pgbus_number(@stats[:unpublished]) %></dd>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="rounded-lg bg-white p-4 shadow ring-1 ring-gray-200">
|
|
12
|
+
<dt class="text-xs font-medium uppercase text-gray-500">Total</dt>
|
|
13
|
+
<dd class="mt-1 text-2xl font-semibold text-gray-900"><%= pgbus_number(@stats[:total]) %></dd>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="rounded-lg bg-white p-4 shadow ring-1 ring-gray-200">
|
|
16
|
+
<dt class="text-xs font-medium uppercase text-gray-500">Oldest Unpublished</dt>
|
|
17
|
+
<dd class="mt-1 text-2xl font-semibold text-gray-900"><%= @stats[:oldest_unpublished_age] ? "#{@stats[:oldest_unpublished_age]}s" : "—" %></dd>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="overflow-hidden rounded-lg bg-white shadow ring-1 ring-gray-200">
|
|
22
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
23
|
+
<thead class="bg-gray-50">
|
|
24
|
+
<tr>
|
|
25
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">ID</th>
|
|
26
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Queue / Topic</th>
|
|
27
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Payload</th>
|
|
28
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">Priority</th>
|
|
29
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">Status</th>
|
|
30
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">Created</th>
|
|
31
|
+
</tr>
|
|
32
|
+
</thead>
|
|
33
|
+
<tbody class="divide-y divide-gray-100">
|
|
34
|
+
<% @entries.each do |entry| %>
|
|
35
|
+
<tr class="hover:bg-gray-50">
|
|
36
|
+
<td class="px-4 py-3 text-sm font-mono text-gray-700"><%= entry.id %></td>
|
|
37
|
+
<td class="px-4 py-3 text-sm text-gray-700"><%= entry.routing_key || entry.queue_name %></td>
|
|
38
|
+
<td class="px-4 py-3 text-sm text-gray-500 max-w-xs truncate"><%= pgbus_json_preview(entry.payload) %></td>
|
|
39
|
+
<td class="px-4 py-3 text-sm text-right text-gray-500"><%= entry.priority %></td>
|
|
40
|
+
<td class="px-4 py-3 text-sm text-right">
|
|
41
|
+
<% if entry.published_at %>
|
|
42
|
+
<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">Published</span>
|
|
43
|
+
<% else %>
|
|
44
|
+
<span class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800">Pending</span>
|
|
45
|
+
<% end %>
|
|
46
|
+
</td>
|
|
47
|
+
<td class="px-4 py-3 text-sm text-right text-gray-500"><%= pgbus_time_ago(entry.created_at) %></td>
|
|
48
|
+
</tr>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% if @entries.empty? %>
|
|
51
|
+
<tr><td colspan="6" class="px-4 py-8 text-center text-sm text-gray-400">No outbox entries</td></tr>
|
|
52
|
+
<% end %>
|
|
53
|
+
</tbody>
|
|
54
|
+
</table>
|
|
55
|
+
</div>
|
|
@@ -18,13 +18,27 @@
|
|
|
18
18
|
<td class="px-4 py-3 text-sm">
|
|
19
19
|
<%= link_to q[:name], pgbus.queue_path(name: q[:name]), class: "font-medium text-indigo-600 hover:text-indigo-500", data: { turbo_frame: "_top" } %>
|
|
20
20
|
<%= pgbus_queue_badge(q[:name]) %>
|
|
21
|
+
<% if q[:paused] %>
|
|
22
|
+
<span class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800">Paused</span>
|
|
23
|
+
<% end %>
|
|
21
24
|
</td>
|
|
22
25
|
<td class="px-4 py-3 text-sm text-right font-mono text-gray-700"><%= pgbus_number(q[:queue_length]) %></td>
|
|
23
26
|
<td class="px-4 py-3 text-sm text-right font-mono text-gray-700"><%= pgbus_number(q[:queue_visible_length]) %></td>
|
|
24
27
|
<td class="px-4 py-3 text-sm text-right text-gray-500"><%= q[:oldest_msg_age_sec] || "—" %></td>
|
|
25
28
|
<td class="px-4 py-3 text-sm text-right text-gray-500"><%= q[:newest_msg_age_sec] || "—" %></td>
|
|
26
29
|
<td class="px-4 py-3 text-sm text-right text-gray-500"><%= pgbus_number(q[:total_messages]) %></td>
|
|
27
|
-
<td class="px-4 py-3 text-sm text-right">
|
|
30
|
+
<td class="px-4 py-3 text-sm text-right space-x-2">
|
|
31
|
+
<% if q[:paused] %>
|
|
32
|
+
<%= button_to "Resume", pgbus.resume_queue_path(name: q[:name]),
|
|
33
|
+
method: :post,
|
|
34
|
+
class: "text-xs text-green-600 hover:text-green-800 font-medium",
|
|
35
|
+
data: { turbo_frame: "_top" } %>
|
|
36
|
+
<% else %>
|
|
37
|
+
<%= button_to "Pause", pgbus.pause_queue_path(name: q[:name]),
|
|
38
|
+
method: :post,
|
|
39
|
+
class: "text-xs text-yellow-600 hover:text-yellow-800 font-medium",
|
|
40
|
+
data: { turbo_confirm: "Pause processing for #{q[:name]}?", turbo_frame: "_top" } %>
|
|
41
|
+
<% end %>
|
|
28
42
|
<%= button_to "Purge", pgbus.purge_queue_path(name: q[:name]),
|
|
29
43
|
method: :post,
|
|
30
44
|
class: "text-xs text-red-600 hover:text-red-800 font-medium",
|
data/config/routes.rb
CHANGED
|
@@ -6,6 +6,8 @@ Pgbus::Engine.routes.draw do
|
|
|
6
6
|
resources :queues, only: %i[index show], param: :name do
|
|
7
7
|
member do
|
|
8
8
|
post :purge
|
|
9
|
+
post :pause
|
|
10
|
+
post :resume
|
|
9
11
|
end
|
|
10
12
|
end
|
|
11
13
|
|
|
@@ -46,7 +48,12 @@ Pgbus::Engine.routes.draw do
|
|
|
46
48
|
end
|
|
47
49
|
end
|
|
48
50
|
|
|
51
|
+
resources :outbox, only: [:index], controller: "outbox"
|
|
52
|
+
resources :locks, only: [:index]
|
|
53
|
+
resource :insights, only: [:show], controller: "insights"
|
|
54
|
+
|
|
49
55
|
namespace :api do
|
|
50
56
|
get :stats, to: "stats#show"
|
|
57
|
+
get :insights, to: "insights#show"
|
|
51
58
|
end
|
|
52
59
|
end
|