pgbus 0.9.4 → 0.9.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 +115 -0
- data/Rakefile +56 -3
- data/app/controllers/pgbus/dead_letter_controller.rb +5 -1
- data/app/helpers/pgbus/application_helper.rb +12 -2
- data/app/views/pgbus/dashboard/_processes_table.html.erb +1 -1
- data/app/views/pgbus/dashboard/_recent_failures.html.erb +1 -1
- data/app/views/pgbus/dashboard/_stats_cards.html.erb +1 -0
- data/app/views/pgbus/dead_letter/_messages_table.html.erb +33 -1
- data/app/views/pgbus/insights/show.html.erb +1 -1
- data/app/views/pgbus/jobs/_failed_table.html.erb +1 -1
- data/app/views/pgbus/processes/_processes_table.html.erb +1 -1
- data/app/views/pgbus/queues/show.html.erb +3 -0
- data/config/locales/da.yml +8 -0
- data/config/locales/de.yml +8 -0
- data/config/locales/en.yml +8 -0
- data/config/locales/es.yml +8 -0
- data/config/locales/fi.yml +8 -0
- data/config/locales/fr.yml +8 -0
- data/config/locales/it.yml +8 -0
- data/config/locales/ja.yml +8 -0
- data/config/locales/nb.yml +8 -0
- data/config/locales/nl.yml +8 -0
- data/config/locales/pt.yml +8 -0
- data/config/locales/sv.yml +8 -0
- data/lib/pgbus/cli.rb +16 -0
- data/lib/pgbus/client.rb +34 -7
- data/lib/pgbus/configuration.rb +17 -1
- data/lib/pgbus/integrations/appsignal/dashboard.json +353 -0
- data/lib/pgbus/integrations/appsignal/dashboards/pgbus_health.json +224 -85
- data/lib/pgbus/integrations/appsignal/dashboards/pgbus_streams.json +154 -63
- data/lib/pgbus/integrations/appsignal/dashboards/pgbus_throughput.json +218 -79
- data/lib/pgbus/integrations/appsignal/probe.rb +17 -2
- data/lib/pgbus/integrations/appsignal.rb +17 -0
- data/lib/pgbus/mcp/base_tool.rb +79 -0
- data/lib/pgbus/mcp/health_analyzer.rb +159 -0
- data/lib/pgbus/mcp/rack_app.rb +97 -0
- data/lib/pgbus/mcp/redactor.rb +72 -0
- data/lib/pgbus/mcp/runner.rb +73 -0
- data/lib/pgbus/mcp/server.rb +62 -0
- data/lib/pgbus/mcp/tools/dlq_detail_tool.rb +72 -0
- data/lib/pgbus/mcp/tools/dlq_tool.rb +48 -0
- data/lib/pgbus/mcp/tools/health_tool.rb +33 -0
- data/lib/pgbus/mcp/tools/job_detail_tool.rb +42 -0
- data/lib/pgbus/mcp/tools/jobs_tool.rb +56 -0
- data/lib/pgbus/mcp/tools/locks_tool.rb +28 -0
- data/lib/pgbus/mcp/tools/processes_tool.rb +32 -0
- data/lib/pgbus/mcp/tools/queue_detail_tool.rb +46 -0
- data/lib/pgbus/mcp/tools/queues_tool.rb +28 -0
- data/lib/pgbus/mcp/tools/recurring_tool.rb +27 -0
- data/lib/pgbus/mcp/tools/stats_tool.rb +40 -0
- data/lib/pgbus/mcp/tools/throughput_tool.rb +35 -0
- data/lib/pgbus/mcp.rb +55 -0
- data/lib/pgbus/process/heartbeat.rb +8 -2
- data/lib/pgbus/process/supervisor.rb +44 -0
- data/lib/pgbus/process/worker.rb +12 -1
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +40 -2
- data/lib/pgbus.rb +15 -1
- metadata +21 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 71ef7f4e4fa290e422b8ce72402833f6a31fd7cb3d081aef8a4bfcc09fc467eb
|
|
4
|
+
data.tar.gz: 31a6522c809392316d28125b0fe8e972c6e18ca8b48510d8f891135b75c3e63c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7554038df6c2a70435a615a2594425ff05ce102953b445a7ad5f47ee3d69a7e81e32445f9e790d987d71b5b17aa807250ebadae6d50f191da3eb23c0676b8f98
|
|
7
|
+
data.tar.gz: fadb07428ddbaead22200de2ff1031ff924d75e4fa5cc7ea48307ab7a22e8cf84b7528ea3a69f25baab402732a7804046005e6c1b1fd33fced91823de4702896
|
data/README.md
CHANGED
|
@@ -838,6 +838,121 @@ When `config.metrics_enabled = true` (default), the dashboard exposes Prometheus
|
|
|
838
838
|
| `pgbus_worker_pool_busy` | Currently busy worker threads |
|
|
839
839
|
| `pgbus_worker_pool_utilization` | Busy / capacity ratio |
|
|
840
840
|
|
|
841
|
+
### MCP diagnostic server (read-only)
|
|
842
|
+
|
|
843
|
+
Pgbus ships an optional, **read-only** [MCP](https://modelcontextprotocol.io) server so an AI agent (or any MCP client) can diagnose pgbus directly — "are queues backed up?", "is `read_ct` advancing?", "are workers heart-beating but not claiming?" — instead of hand-writing `pgmq` / `pg_stat_activity` SQL against production. It is a thin adapter over the same read layer the dashboard uses, so it adds no new database access path.
|
|
844
|
+
|
|
845
|
+
Add the optional `mcp` gem to your `Gemfile` first (`gem "mcp"`); both entry points below tell you if it's missing.
|
|
846
|
+
|
|
847
|
+
#### Choosing a deployment
|
|
848
|
+
|
|
849
|
+
There are two ways to run it, depending on **who connects and from where**:
|
|
850
|
+
|
|
851
|
+
| | **stdio** (`pgbus mcp`) | **HTTP** (mount in Rails) |
|
|
852
|
+
|---|---|---|
|
|
853
|
+
| Who connects | A local operator (Claude Desktop / Claude Code on your machine) | A remote agent or alerting system |
|
|
854
|
+
| Process model | The MCP client **spawns** a short-lived process on demand | Runs **inside your existing Rails server** — no second process |
|
|
855
|
+
| Reaches production? | Only if run where prod DB creds are available | Yes — co-located with the app, same credentials |
|
|
856
|
+
| Use it for | Hands-on, interactive debugging | Automated/remote diagnostics & alerting |
|
|
857
|
+
|
|
858
|
+
> **Don't start a second `bin/pgbus` instance for HTTP.** The HTTP transport is a Rack app — mount it in the Rails app you already deploy.
|
|
859
|
+
|
|
860
|
+
##### stdio (local operator)
|
|
861
|
+
|
|
862
|
+
```bash
|
|
863
|
+
bundle exec pgbus mcp # speaks MCP over stdio
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
##### HTTP (mount in Rails)
|
|
867
|
+
|
|
868
|
+
```ruby
|
|
869
|
+
# config/routes.rb
|
|
870
|
+
Rails.application.routes.draw do
|
|
871
|
+
# ... your routes ...
|
|
872
|
+
mount Pgbus::MCP.rack_app(token: ENV["PGBUS_MCP_TOKEN"]) => "/pgbus/mcp"
|
|
873
|
+
end
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
`Pgbus::MCP.rack_app` returns a gated Rack app. It runs the transport in **stateless + JSON-response mode**, so every request is a self-contained POST with no in-memory session — safe behind multiple Puma/Falcon workers (any worker can answer any request). Keep the endpoint on an internal network / behind your VPN; it is not meant to be internet-exposed.
|
|
877
|
+
|
|
878
|
+
Options:
|
|
879
|
+
|
|
880
|
+
| Option | Default | Meaning |
|
|
881
|
+
|--------|---------|---------|
|
|
882
|
+
| `token:` | `nil` | Shared secret. When set, requests must send `Authorization: Bearer <token>` (constant-time compared). |
|
|
883
|
+
| `auth:` | `nil` | A callable `->(rack_request) { ... }` returning truthy to allow — mirrors `config.web_auth`. Wins over `token:`. |
|
|
884
|
+
| `allow_payloads:` | `false` | When true, tools honor a per-call `include_payloads` flag (see Security). |
|
|
885
|
+
|
|
886
|
+
If you set neither `token:` nor `auth:`, pgbus logs a warning — an unauthenticated diagnostic endpoint exposes operational metadata to anyone who can reach it.
|
|
887
|
+
|
|
888
|
+
> Clients must send `Accept: application/json` and `Content-Type: application/json` on every POST, or the transport replies `406 Not Acceptable`. MCP clients do this automatically.
|
|
889
|
+
|
|
890
|
+
Need a **standalone HTTP pod** instead of mounting in your main app? The same Rack app works under any Rack server, e.g. a one-line `config.ru`:
|
|
891
|
+
|
|
892
|
+
```ruby
|
|
893
|
+
require "pgbus"
|
|
894
|
+
Pgbus::MCP.load!
|
|
895
|
+
run Pgbus::MCP.rack_app(token: ENV["PGBUS_MCP_TOKEN"])
|
|
896
|
+
# rackup -p 9293 (run it in a container that shares the app's DB config)
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
#### Tools
|
|
900
|
+
|
|
901
|
+
All tools are read-only — no tool mutates state, and there is no raw-SQL passthrough.
|
|
902
|
+
|
|
903
|
+
| Tool | Purpose |
|
|
904
|
+
|------|---------|
|
|
905
|
+
| `pgbus_health` | One-call verdict: `OK` / `DEGRADED` / `STALLED`. STALLED is the silent-worker-wedge signal (visible backlog while workers heart-beat but don't claim). Suitable for automated alerting. |
|
|
906
|
+
| `pgbus_queues` | All queues: depth, visible count, oldest-message age, paused state. |
|
|
907
|
+
| `pgbus_queue_detail` | Per-queue metrics + paused state + table health (dead tuples, bloat, vacuum age). |
|
|
908
|
+
| `pgbus_processes` | Every process with kind, pid, heartbeat age, and `healthy`/`stale`/`stalled` status. |
|
|
909
|
+
| `pgbus_jobs` / `pgbus_job_detail` | Inspect enqueued messages (`read_ct`, `vt`, `enqueued_at`). Paginated. |
|
|
910
|
+
| `pgbus_dlq` / `pgbus_dlq_detail` | Dead-letter inspection. Paginated. |
|
|
911
|
+
| `pgbus_locks` | Active uniqueness locks (the leaked-lock diagnostic). |
|
|
912
|
+
| `pgbus_throughput` / `pgbus_stats` | Recent throughput time series and status counts. |
|
|
913
|
+
| `pgbus_recurring` | Recurring task schedule + last/next run times. |
|
|
914
|
+
|
|
915
|
+
#### Security
|
|
916
|
+
|
|
917
|
+
The server is built to be safe against a production datastore:
|
|
918
|
+
|
|
919
|
+
- **Read-only by default.** No tool mutates state and no arbitrary-query tool exists.
|
|
920
|
+
- **Payloads redacted.** Message bodies, headers, and job arguments are replaced with `[redacted]` unless payloads are explicitly allowed **and** `include_payloads: true` is passed on the call. Both gates must be open. Allow payloads with `PGBUS_MCP_ALLOW_PAYLOADS=1` (stdio) or `Pgbus::MCP.rack_app(allow_payloads: true)` (HTTP).
|
|
921
|
+
- **Bounded queries.** Every list tool paginates with a row cap (`pgbus_jobs` / `pgbus_dlq` cap at 100 rows/page; time windows cap at 1440 minutes).
|
|
922
|
+
- **Reuses your DB credentials.** No new privileged path — it reads through the app's existing connection config.
|
|
923
|
+
- **Authentication.**
|
|
924
|
+
- *HTTP:* set `token:` (clients send `Authorization: Bearer <token>`) or a custom `auth:` callable; unauthenticated requests get `401`.
|
|
925
|
+
- *stdio:* the channel is local (the client spawns the process), so the gate is a boot-time precondition — set `PGBUS_MCP_TOKEN` and the server refuses to start unless `PGBUS_MCP_AUTH_TOKEN` matches (constant-time compare).
|
|
926
|
+
|
|
927
|
+
#### Client configuration
|
|
928
|
+
|
|
929
|
+
For the **stdio** transport, a minimal MCP client config (Claude Code / Claude Desktop style):
|
|
930
|
+
|
|
931
|
+
```json
|
|
932
|
+
{
|
|
933
|
+
"mcpServers": {
|
|
934
|
+
"pgbus": {
|
|
935
|
+
"command": "bundle",
|
|
936
|
+
"args": ["exec", "pgbus", "mcp"],
|
|
937
|
+
"env": { "RAILS_ENV": "production" }
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
For the **HTTP** transport, point the client at the mounted URL with a streamable-HTTP server config, sending the bearer token, e.g.:
|
|
944
|
+
|
|
945
|
+
```json
|
|
946
|
+
{
|
|
947
|
+
"mcpServers": {
|
|
948
|
+
"pgbus": {
|
|
949
|
+
"url": "https://your-app.internal/pgbus/mcp",
|
|
950
|
+
"headers": { "Authorization": "Bearer ${PGBUS_MCP_TOKEN}" }
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
```
|
|
955
|
+
|
|
841
956
|
## Real-time broadcasts (turbo-streams replacement)
|
|
842
957
|
|
|
843
958
|
Pgbus ships a drop-in replacement for turbo-rails' `turbo_stream_from` helper that fixes several well-known ActionCable correctness bugs by using PGMQ message IDs as a replay cursor. Same API as turbo-rails. No Redis. No ActionCable. No lost messages on reconnect.
|
data/Rakefile
CHANGED
|
@@ -11,6 +11,19 @@ require "rubocop/rake_task"
|
|
|
11
11
|
RuboCop::RakeTask.new
|
|
12
12
|
|
|
13
13
|
namespace :bench do
|
|
14
|
+
bench_dir = "benchmarks"
|
|
15
|
+
# Benches that need a real PostgreSQL/PGMQ (or boot Puma) — excluded from the
|
|
16
|
+
# no-DB unit suite that bench:all runs in CI.
|
|
17
|
+
db_benches = %w[connection_pool_bench integration_bench streams_bench].freeze
|
|
18
|
+
# The unit suite is every *_bench.rb that doesn't need a database, derived
|
|
19
|
+
# from the directory so a new unit bench is picked up automatically (kept in
|
|
20
|
+
# sync with bench:one, which globs the same files).
|
|
21
|
+
unit_benches = Dir["#{bench_dir}/*_bench.rb"]
|
|
22
|
+
.map { |f| File.basename(f, ".rb") }
|
|
23
|
+
.reject { |name| db_benches.include?(name) }
|
|
24
|
+
.sort
|
|
25
|
+
.freeze
|
|
26
|
+
|
|
14
27
|
desc "Run serialization benchmarks"
|
|
15
28
|
task :serialization do
|
|
16
29
|
ruby "benchmarks/serialization_bench.rb"
|
|
@@ -41,11 +54,51 @@ namespace :bench do
|
|
|
41
54
|
ruby "benchmarks/streams_bench.rb"
|
|
42
55
|
end
|
|
43
56
|
|
|
44
|
-
desc "Run
|
|
45
|
-
task
|
|
57
|
+
desc "Run a single benchmark: rake bench:one[client_bench]"
|
|
58
|
+
task :one, [:name] do |_t, args|
|
|
59
|
+
name = args[:name] or abort "Usage: rake bench:one[serialization_bench|client_bench|...]"
|
|
60
|
+
available = Dir["#{bench_dir}/*_bench.rb"].map { |f| File.basename(f, ".rb") }
|
|
61
|
+
abort "No such benchmark: #{name}. Available: #{available.sort.join(", ")}" unless available.include?(name)
|
|
62
|
+
ruby "#{bench_dir}/#{name}.rb"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
desc "Run all unit-level benchmarks, save report to tmp/benchmarks/"
|
|
66
|
+
task :all do
|
|
67
|
+
require "fileutils"
|
|
68
|
+
require "open3"
|
|
69
|
+
require "rbconfig"
|
|
70
|
+
FileUtils.mkdir_p("tmp/benchmarks")
|
|
71
|
+
|
|
72
|
+
# Run each bench under the SAME Ruby + gemset as this Rake process, not a
|
|
73
|
+
# bare `ruby` from PATH — otherwise before/after numbers are unreliable and
|
|
74
|
+
# gem loading can break under a different interpreter.
|
|
75
|
+
ruby = RbConfig.ruby
|
|
76
|
+
failed = []
|
|
77
|
+
|
|
78
|
+
File.open("tmp/benchmarks/unit.txt", "w") do |report|
|
|
79
|
+
unit_benches.each do |name|
|
|
80
|
+
file = "#{bench_dir}/#{name}.rb"
|
|
81
|
+
header = "\n### #{file} ###"
|
|
82
|
+
puts "\e[1;35m#{header}\e[0m"
|
|
83
|
+
report.puts header
|
|
84
|
+
|
|
85
|
+
result, status = Open3.capture2e(ruby, file)
|
|
86
|
+
puts result
|
|
87
|
+
report.puts result.gsub(/\e\[[0-9;]*m/, "")
|
|
88
|
+
|
|
89
|
+
unless status.success?
|
|
90
|
+
failed << file
|
|
91
|
+
report.puts "!!! FAILED (exit #{status.exitstatus})"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
puts "\nSaved report to tmp/benchmarks/unit.txt"
|
|
97
|
+
abort "\nBenchmark(s) failed: #{failed.join(", ")}" if failed.any?
|
|
98
|
+
end
|
|
46
99
|
end
|
|
47
100
|
|
|
48
|
-
desc "Run all benchmarks"
|
|
101
|
+
desc "Run all benchmarks (alias for bench:all)"
|
|
49
102
|
task bench: "bench:all"
|
|
50
103
|
|
|
51
104
|
desc "Build gem and verify contents"
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
module Pgbus
|
|
4
4
|
class DeadLetterController < ApplicationController
|
|
5
5
|
def index
|
|
6
|
-
@
|
|
6
|
+
@page = page_param
|
|
7
|
+
@per_page = per_page
|
|
8
|
+
@messages = data_source.dlq_messages(page: @page, per_page: @per_page)
|
|
9
|
+
@total_count = data_source.dlq_total_count
|
|
10
|
+
@total_pages = (@total_count.to_f / @per_page).ceil
|
|
7
11
|
render_frame("pgbus/dead_letter/messages_table") if params[:frame] == "list"
|
|
8
12
|
end
|
|
9
13
|
|
|
@@ -29,10 +29,20 @@ module Pgbus
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
def pgbus_status_badge(
|
|
33
|
-
|
|
32
|
+
def pgbus_status_badge(healthy_or_status)
|
|
33
|
+
status = case healthy_or_status
|
|
34
|
+
when true then :healthy
|
|
35
|
+
when Symbol, String then healthy_or_status.to_sym
|
|
36
|
+
else :stale
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
case status
|
|
40
|
+
when :healthy
|
|
34
41
|
tag.span(I18n.t("pgbus.helpers.status_badge.healthy"),
|
|
35
42
|
class: "inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800")
|
|
43
|
+
when :stalled
|
|
44
|
+
tag.span(I18n.t("pgbus.helpers.status_badge.stalled"),
|
|
45
|
+
class: "inline-flex items-center rounded-full bg-yellow-100 px-2.5 py-0.5 text-xs font-medium text-yellow-800")
|
|
36
46
|
else
|
|
37
47
|
tag.span(I18n.t("pgbus.helpers.status_badge.stale"),
|
|
38
48
|
class: "inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800")
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<td data-label="Kind" class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white"><%= p[:kind] %></td>
|
|
18
18
|
<td data-label="Host" class="px-4 py-3 text-sm text-gray-500"><%= p[:hostname] %></td>
|
|
19
19
|
<td data-label="PID" class="px-4 py-3 text-sm text-gray-500"><%= p[:pid] %></td>
|
|
20
|
-
<td data-label="Status" class="px-4 py-3 text-sm"><%= pgbus_status_badge(p[:healthy]) %></td>
|
|
20
|
+
<td data-label="Status" class="px-4 py-3 text-sm"><%= pgbus_status_badge(p[:status] || p[:healthy]) %></td>
|
|
21
21
|
</tr>
|
|
22
22
|
<% end %>
|
|
23
23
|
<% if @processes.empty? %>
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<% @recent_failures.each do |f| %>
|
|
20
20
|
<tr>
|
|
21
21
|
<td data-label="Queue" class="px-4 py-3 text-sm text-gray-700"><%= f["queue_name"] %></td>
|
|
22
|
-
<td data-label="Error" class="px-4 py-3 text-sm text-red-600 truncate max-w-xs"><%= f["error_class"] %>: <%= truncate(f["error_message"].to_s, length: 60) %></td>
|
|
22
|
+
<td data-label="Error" class="px-4 py-3 text-sm text-red-600 truncate max-w-xs" title="<%= f["error_class"] %>: <%= f["error_message"] %>"><%= f["error_class"] %>: <%= truncate(f["error_message"].to_s, length: 60) %></td>
|
|
23
23
|
<td data-label="Time" class="px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(f["failed_at"]) %></td>
|
|
24
24
|
</tr>
|
|
25
25
|
<% end %>
|
|
@@ -31,6 +31,7 @@
|
|
|
31
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
|
+
<p class="text-xs text-gray-400 dark:text-gray-500"><%= t("pgbus.dashboard.stats_cards.failed_dlq_labels") %></p>
|
|
34
35
|
</div>
|
|
35
36
|
|
|
36
37
|
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
<turbo-frame id="dlq-messages" data-auto-refresh data-src="<%= pgbus.dead_letter_index_path(frame: 'list') %>">
|
|
1
|
+
<turbo-frame id="dlq-messages" data-auto-refresh data-src="<%= pgbus.dead_letter_index_path(frame: 'list', page: @page) %>">
|
|
2
|
+
<%# Count badge lives inside the frame so it refreshes in lockstep with the table. %>
|
|
3
|
+
<% if @total_count && @total_count > 0 %>
|
|
4
|
+
<p class="mb-3 text-sm text-gray-500 dark:text-gray-400">
|
|
5
|
+
<span class="inline-flex items-center rounded-full bg-red-100 dark:bg-red-900 px-2.5 py-0.5 text-sm font-medium text-red-800 dark:text-red-200"><%= @total_count %></span>
|
|
6
|
+
<%= t("pgbus.dead_letter.messages_table.total_label") %>
|
|
7
|
+
</p>
|
|
8
|
+
<% end %>
|
|
2
9
|
<%# Bulk form outside table to avoid nested form issues %>
|
|
3
10
|
<form id="bulk-discard-dlq-form" action="<%= pgbus.discard_selected_dead_letter_index_path %>" method="post" data-turbo-frame="_top" class="hidden">
|
|
4
11
|
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
|
|
@@ -91,5 +98,30 @@
|
|
|
91
98
|
<% end %>
|
|
92
99
|
</tbody>
|
|
93
100
|
</table>
|
|
101
|
+
<% if @total_count && @total_count > 0 %>
|
|
102
|
+
<div class="flex items-center justify-between border-t border-gray-200 dark:border-gray-700 px-4 py-3">
|
|
103
|
+
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
104
|
+
<% from = (@page - 1) * @per_page + 1 %>
|
|
105
|
+
<% to = [from + @messages.size - 1, @total_count].min %>
|
|
106
|
+
<%= t("pgbus.helpers.pagination.showing", from: from, to: to, total: @total_count) %>
|
|
107
|
+
</p>
|
|
108
|
+
<% if @total_pages > 1 %>
|
|
109
|
+
<nav class="inline-flex rounded-lg shadow-sm" role="navigation" aria-label="Pagination">
|
|
110
|
+
<% if @page > 1 %>
|
|
111
|
+
<%= link_to t("pgbus.helpers.pagination.previous"), pgbus.dead_letter_index_path(page: @page - 1),
|
|
112
|
+
class: "px-3 py-1.5 text-xs font-medium border border-gray-300 dark:border-gray-600 rounded-l-lg bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700" %>
|
|
113
|
+
<% else %>
|
|
114
|
+
<span class="px-3 py-1.5 text-xs font-medium border border-gray-300 dark:border-gray-600 rounded-l-lg bg-gray-100 dark:bg-gray-900 text-gray-400 dark:text-gray-600 cursor-not-allowed"><%= t("pgbus.helpers.pagination.previous") %></span>
|
|
115
|
+
<% end %>
|
|
116
|
+
<% if @page < @total_pages %>
|
|
117
|
+
<%= link_to t("pgbus.helpers.pagination.next"), pgbus.dead_letter_index_path(page: @page + 1),
|
|
118
|
+
class: "-ml-px px-3 py-1.5 text-xs font-medium border border-gray-300 dark:border-gray-600 rounded-r-lg bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700" %>
|
|
119
|
+
<% else %>
|
|
120
|
+
<span class="-ml-px px-3 py-1.5 text-xs font-medium border border-gray-300 dark:border-gray-600 rounded-r-lg bg-gray-100 dark:bg-gray-900 text-gray-400 dark:text-gray-600 cursor-not-allowed"><%= t("pgbus.helpers.pagination.next") %></span>
|
|
121
|
+
<% end %>
|
|
122
|
+
</nav>
|
|
123
|
+
<% end %>
|
|
124
|
+
</div>
|
|
125
|
+
<% end %>
|
|
94
126
|
</div>
|
|
95
127
|
</turbo-frame>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.description_html", range: pgbus_time_range_label(@minutes)) %></p>
|
|
5
5
|
</div>
|
|
6
6
|
<nav class="inline-flex rounded-lg shadow-sm" role="group" aria-label="Time range">
|
|
7
|
-
<% ranges = [["1h", 60], ["24h", 1440], ["7d", 10_080], ["30d", 43_200]] %>
|
|
7
|
+
<% ranges = [["1h", 60], ["4h", 240], ["24h", 1440], ["7d", 10_080], ["30d", 43_200]] %>
|
|
8
8
|
<% ranges.each_with_index do |(label, mins), i| %>
|
|
9
9
|
<%
|
|
10
10
|
active = @minutes == mins
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
<%= link_to f["id"], pgbus.job_path(f["id"]), class: "text-indigo-600 hover:text-indigo-500", data: { turbo_frame: "_top" } %>
|
|
52
52
|
</td>
|
|
53
53
|
<td data-label="Queue" class="px-4 py-3 text-sm text-gray-700"><%= f["queue_name"] %></td>
|
|
54
|
-
<td data-label="Error" class="px-4 py-3 text-sm text-red-600 max-w-sm truncate">
|
|
54
|
+
<td data-label="Error" class="px-4 py-3 text-sm text-red-600 max-w-sm truncate" title="<%= f["error_class"] %>: <%= f["error_message"] %>">
|
|
55
55
|
<span class="font-medium"><%= f["error_class"] %></span>: <%= truncate(f["error_message"].to_s, length: 80) %>
|
|
56
56
|
</td>
|
|
57
57
|
<td data-label="Retries" class="px-4 py-3 text-sm text-gray-500"><%= f["retry_count"] %></td>
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<td data-label="Kind" class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white"><%= p[:kind] %></td>
|
|
18
18
|
<td data-label="Host" class="px-4 py-3 text-sm text-gray-700"><%= p[:hostname] %></td>
|
|
19
19
|
<td data-label="PID" class="px-4 py-3 text-sm font-mono text-gray-700"><%= p[:pid] %></td>
|
|
20
|
-
<td data-label="Status" class="px-4 py-3 text-sm"><%= pgbus_status_badge(p[:healthy]) %></td>
|
|
20
|
+
<td data-label="Status" class="px-4 py-3 text-sm"><%= pgbus_status_badge(p[:status] || p[:healthy]) %></td>
|
|
21
21
|
<td data-label="Last Heartbeat" class="px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(p[:last_heartbeat_at]) %></td>
|
|
22
22
|
<td data-label="Metadata" class="px-4 py-3 text-sm text-gray-500 font-mono text-xs max-w-xs truncate">
|
|
23
23
|
<% if p[:metadata].is_a?(Hash) %>
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
<div class="mb-3">
|
|
2
|
+
<%= link_to "← #{t("pgbus.queues.show.back")}", pgbus.queues_path, class: "text-sm text-indigo-600 hover:text-indigo-500 dark:text-indigo-400" %>
|
|
3
|
+
</div>
|
|
1
4
|
<div class="flex items-center justify-between mb-6">
|
|
2
5
|
<div>
|
|
3
6
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
data/config/locales/da.yml
CHANGED
|
@@ -84,6 +84,7 @@ da:
|
|
|
84
84
|
stats_cards:
|
|
85
85
|
enqueued: Sat i kø
|
|
86
86
|
failed_dlq: Mislykkedes / DLQ
|
|
87
|
+
failed_dlq_labels: Fejlet / Dead Lettered
|
|
87
88
|
processes: Processer
|
|
88
89
|
queues: Køer
|
|
89
90
|
recurring: Gentagende
|
|
@@ -126,6 +127,7 @@ da:
|
|
|
126
127
|
queue: 'Kø:'
|
|
127
128
|
visible_at: 'Synlig ved:'
|
|
128
129
|
retry: Forsøg igen
|
|
130
|
+
total_label: beskeder i dead letter-kø
|
|
129
131
|
show:
|
|
130
132
|
back: Tilbage til DLQ
|
|
131
133
|
discard: Kassér
|
|
@@ -225,6 +227,10 @@ da:
|
|
|
225
227
|
bulk_select_all: Vælg alle
|
|
226
228
|
bulk_select_row: Vælg %{id}
|
|
227
229
|
bulk_selected: valgt
|
|
230
|
+
pagination:
|
|
231
|
+
next: Næste
|
|
232
|
+
previous: Forrige
|
|
233
|
+
showing: Viser %{from}–%{to} af %{total}
|
|
228
234
|
paused_badge: Pause
|
|
229
235
|
queue_badge:
|
|
230
236
|
dlq: DLQ
|
|
@@ -235,6 +241,7 @@ da:
|
|
|
235
241
|
status_badge:
|
|
236
242
|
healthy: Sund
|
|
237
243
|
stale: Forældet
|
|
244
|
+
stalled: Gået i stå
|
|
238
245
|
insights:
|
|
239
246
|
show:
|
|
240
247
|
charts:
|
|
@@ -476,6 +483,7 @@ da:
|
|
|
476
483
|
resume: Genoptag
|
|
477
484
|
show:
|
|
478
485
|
arguments: Argumenter
|
|
486
|
+
back: Tilbage til køer
|
|
479
487
|
delete_confirm: Slet denne kø permanent? Dette kan ikke fortrydes.
|
|
480
488
|
delete_queue: Slet kø
|
|
481
489
|
depth: 'Dybde:'
|
data/config/locales/de.yml
CHANGED
|
@@ -84,6 +84,7 @@ de:
|
|
|
84
84
|
stats_cards:
|
|
85
85
|
enqueued: Eingereiht
|
|
86
86
|
failed_dlq: Fehlgeschlagen / DLQ
|
|
87
|
+
failed_dlq_labels: Fehlgeschlagen / Dead Lettered
|
|
87
88
|
processes: Prozesse
|
|
88
89
|
queues: Warteschlangen
|
|
89
90
|
recurring: Wiederkehrend
|
|
@@ -126,6 +127,7 @@ de:
|
|
|
126
127
|
queue: 'Warteschlange:'
|
|
127
128
|
visible_at: 'Sichtbar um:'
|
|
128
129
|
retry: Erneut versuchen
|
|
130
|
+
total_label: Nachrichten in der Dead-Letter-Warteschlange
|
|
129
131
|
show:
|
|
130
132
|
back: Zurück zur DLQ
|
|
131
133
|
discard: Verwerfen
|
|
@@ -225,6 +227,10 @@ de:
|
|
|
225
227
|
bulk_select_all: Alle auswählen
|
|
226
228
|
bulk_select_row: "%{id} auswählen"
|
|
227
229
|
bulk_selected: ausgewählt
|
|
230
|
+
pagination:
|
|
231
|
+
next: Weiter
|
|
232
|
+
previous: Zurück
|
|
233
|
+
showing: Zeige %{from}–%{to} von %{total}
|
|
228
234
|
paused_badge: Pausiert
|
|
229
235
|
queue_badge:
|
|
230
236
|
dlq: DLQ
|
|
@@ -235,6 +241,7 @@ de:
|
|
|
235
241
|
status_badge:
|
|
236
242
|
healthy: Gesund
|
|
237
243
|
stale: Veraltet
|
|
244
|
+
stalled: Blockiert
|
|
238
245
|
insights:
|
|
239
246
|
show:
|
|
240
247
|
charts:
|
|
@@ -476,6 +483,7 @@ de:
|
|
|
476
483
|
resume: Fortsetzen
|
|
477
484
|
show:
|
|
478
485
|
arguments: Argumente
|
|
486
|
+
back: Zurück zu Warteschlangen
|
|
479
487
|
delete_confirm: Diese Warteschlange dauerhaft löschen? Dies kann nicht rückgängig gemacht werden.
|
|
480
488
|
delete_queue: Warteschlange löschen
|
|
481
489
|
depth: 'Tiefe:'
|
data/config/locales/en.yml
CHANGED
|
@@ -84,6 +84,7 @@ en:
|
|
|
84
84
|
stats_cards:
|
|
85
85
|
enqueued: Enqueued
|
|
86
86
|
failed_dlq: Failed / DLQ
|
|
87
|
+
failed_dlq_labels: Failed / Dead Lettered
|
|
87
88
|
processes: Processes
|
|
88
89
|
queues: Queues
|
|
89
90
|
recurring: Recurring
|
|
@@ -126,6 +127,7 @@ en:
|
|
|
126
127
|
queue: 'Queue:'
|
|
127
128
|
visible_at: 'Visible at:'
|
|
128
129
|
retry: Retry
|
|
130
|
+
total_label: messages in dead letter queue
|
|
129
131
|
show:
|
|
130
132
|
back: Back to DLQ
|
|
131
133
|
discard: Discard
|
|
@@ -225,6 +227,10 @@ en:
|
|
|
225
227
|
bulk_select_all: Select all
|
|
226
228
|
bulk_select_row: Select %{id}
|
|
227
229
|
bulk_selected: selected
|
|
230
|
+
pagination:
|
|
231
|
+
next: Next
|
|
232
|
+
previous: Previous
|
|
233
|
+
showing: Showing %{from}–%{to} of %{total}
|
|
228
234
|
paused_badge: Paused
|
|
229
235
|
queue_badge:
|
|
230
236
|
dlq: DLQ
|
|
@@ -235,6 +241,7 @@ en:
|
|
|
235
241
|
status_badge:
|
|
236
242
|
healthy: Healthy
|
|
237
243
|
stale: Stale
|
|
244
|
+
stalled: Stalled
|
|
238
245
|
insights:
|
|
239
246
|
show:
|
|
240
247
|
charts:
|
|
@@ -476,6 +483,7 @@ en:
|
|
|
476
483
|
resume: Resume
|
|
477
484
|
show:
|
|
478
485
|
arguments: Arguments
|
|
486
|
+
back: Back to Queues
|
|
479
487
|
delete_confirm: Permanently delete this queue? This cannot be undone.
|
|
480
488
|
delete_queue: Delete Queue
|
|
481
489
|
depth: 'Depth:'
|
data/config/locales/es.yml
CHANGED
|
@@ -84,6 +84,7 @@ es:
|
|
|
84
84
|
stats_cards:
|
|
85
85
|
enqueued: Encolado
|
|
86
86
|
failed_dlq: Fallidos / DLQ
|
|
87
|
+
failed_dlq_labels: Fallidos / Dead Lettered
|
|
87
88
|
processes: Procesos
|
|
88
89
|
queues: Colas
|
|
89
90
|
recurring: Recurrente
|
|
@@ -126,6 +127,7 @@ es:
|
|
|
126
127
|
queue: 'Cola:'
|
|
127
128
|
visible_at: 'Visible en:'
|
|
128
129
|
retry: Reintentar
|
|
130
|
+
total_label: mensajes en la cola de mensajes fallidos
|
|
129
131
|
show:
|
|
130
132
|
back: Volver a DLQ
|
|
131
133
|
discard: Descartar
|
|
@@ -225,6 +227,10 @@ es:
|
|
|
225
227
|
bulk_select_all: Seleccionar todo
|
|
226
228
|
bulk_select_row: Seleccionar %{id}
|
|
227
229
|
bulk_selected: seleccionado
|
|
230
|
+
pagination:
|
|
231
|
+
next: Siguiente
|
|
232
|
+
previous: Anterior
|
|
233
|
+
showing: Mostrando %{from}–%{to} de %{total}
|
|
228
234
|
paused_badge: Pausado
|
|
229
235
|
queue_badge:
|
|
230
236
|
dlq: DLQ
|
|
@@ -235,6 +241,7 @@ es:
|
|
|
235
241
|
status_badge:
|
|
236
242
|
healthy: Saludable
|
|
237
243
|
stale: Obsoleto
|
|
244
|
+
stalled: Bloqueado
|
|
238
245
|
insights:
|
|
239
246
|
show:
|
|
240
247
|
charts:
|
|
@@ -476,6 +483,7 @@ es:
|
|
|
476
483
|
resume: Reanudar
|
|
477
484
|
show:
|
|
478
485
|
arguments: Argumentos
|
|
486
|
+
back: Volver a colas
|
|
479
487
|
delete_confirm: "¿Eliminar permanentemente esta cola? Esto no se puede deshacer."
|
|
480
488
|
delete_queue: Eliminar cola
|
|
481
489
|
depth: 'Profundidad:'
|
data/config/locales/fi.yml
CHANGED
|
@@ -84,6 +84,7 @@ fi:
|
|
|
84
84
|
stats_cards:
|
|
85
85
|
enqueued: Lisätty jonoon
|
|
86
86
|
failed_dlq: Epäonnistuneet / DLQ
|
|
87
|
+
failed_dlq_labels: Epäonnistuneet / Dead Lettered
|
|
87
88
|
processes: Prosessit
|
|
88
89
|
queues: Jonot
|
|
89
90
|
recurring: Toistuva
|
|
@@ -126,6 +127,7 @@ fi:
|
|
|
126
127
|
queue: 'Jono:'
|
|
127
128
|
visible_at: 'Näkyvissä klo:'
|
|
128
129
|
retry: Yritä uudelleen
|
|
130
|
+
total_label: viestiä dead letter -jonossa
|
|
129
131
|
show:
|
|
130
132
|
back: Takaisin DLQ:hon
|
|
131
133
|
discard: Hylkää
|
|
@@ -225,6 +227,10 @@ fi:
|
|
|
225
227
|
bulk_select_all: Valitse kaikki
|
|
226
228
|
bulk_select_row: Valitse %{id}
|
|
227
229
|
bulk_selected: valittu
|
|
230
|
+
pagination:
|
|
231
|
+
next: Seuraava
|
|
232
|
+
previous: Edellinen
|
|
233
|
+
showing: Näytetään %{from}–%{to} / %{total}
|
|
228
234
|
paused_badge: Tauolla
|
|
229
235
|
queue_badge:
|
|
230
236
|
dlq: DLQ
|
|
@@ -235,6 +241,7 @@ fi:
|
|
|
235
241
|
status_badge:
|
|
236
242
|
healthy: Toimiva
|
|
237
243
|
stale: Vanha
|
|
244
|
+
stalled: Juuttunut
|
|
238
245
|
insights:
|
|
239
246
|
show:
|
|
240
247
|
charts:
|
|
@@ -476,6 +483,7 @@ fi:
|
|
|
476
483
|
resume: Jatka
|
|
477
484
|
show:
|
|
478
485
|
arguments: Argumentit
|
|
486
|
+
back: Takaisin jonoihin
|
|
479
487
|
delete_confirm: Poista tämä jono pysyvästi? Tätä ei voi kumota.
|
|
480
488
|
delete_queue: Poista jono
|
|
481
489
|
depth: 'Syvyys:'
|
data/config/locales/fr.yml
CHANGED
|
@@ -84,6 +84,7 @@ fr:
|
|
|
84
84
|
stats_cards:
|
|
85
85
|
enqueued: Enregistré
|
|
86
86
|
failed_dlq: Échoué / DLQ
|
|
87
|
+
failed_dlq_labels: Échoués / Dead Lettered
|
|
87
88
|
processes: Processus
|
|
88
89
|
queues: Files d'attente
|
|
89
90
|
recurring: Récurrent
|
|
@@ -126,6 +127,7 @@ fr:
|
|
|
126
127
|
queue: 'File d''attente :'
|
|
127
128
|
visible_at: 'Visible à :'
|
|
128
129
|
retry: Réessayer
|
|
130
|
+
total_label: messages dans la file de lettres mortes
|
|
129
131
|
show:
|
|
130
132
|
back: Retour à la DLQ
|
|
131
133
|
discard: Ignorer
|
|
@@ -225,6 +227,10 @@ fr:
|
|
|
225
227
|
bulk_select_all: Tout sélectionner
|
|
226
228
|
bulk_select_row: Sélectionner %{id}
|
|
227
229
|
bulk_selected: sélectionné
|
|
230
|
+
pagination:
|
|
231
|
+
next: Suivant
|
|
232
|
+
previous: Précédent
|
|
233
|
+
showing: Affichage de %{from}–%{to} sur %{total}
|
|
228
234
|
paused_badge: En pause
|
|
229
235
|
queue_badge:
|
|
230
236
|
dlq: DLQ
|
|
@@ -235,6 +241,7 @@ fr:
|
|
|
235
241
|
status_badge:
|
|
236
242
|
healthy: Sain
|
|
237
243
|
stale: Obsolète
|
|
244
|
+
stalled: Bloqué
|
|
238
245
|
insights:
|
|
239
246
|
show:
|
|
240
247
|
charts:
|
|
@@ -476,6 +483,7 @@ fr:
|
|
|
476
483
|
resume: Reprendre
|
|
477
484
|
show:
|
|
478
485
|
arguments: Arguments
|
|
486
|
+
back: Retour aux files d'attente
|
|
479
487
|
delete_confirm: Supprimer définitivement cette file d'attente ? Cette action est irréversible.
|
|
480
488
|
delete_queue: Supprimer la file d'attente
|
|
481
489
|
depth: 'Profondeur :'
|
data/config/locales/it.yml
CHANGED
|
@@ -84,6 +84,7 @@ it:
|
|
|
84
84
|
stats_cards:
|
|
85
85
|
enqueued: In coda
|
|
86
86
|
failed_dlq: Falliti / DLQ
|
|
87
|
+
failed_dlq_labels: Falliti / Dead Lettered
|
|
87
88
|
processes: Processi
|
|
88
89
|
queues: Code
|
|
89
90
|
recurring: Ricorrente
|
|
@@ -126,6 +127,7 @@ it:
|
|
|
126
127
|
queue: 'Coda:'
|
|
127
128
|
visible_at: 'Visibile alle:'
|
|
128
129
|
retry: Riprova
|
|
130
|
+
total_label: messaggi nella coda dei messaggi non recapitati
|
|
129
131
|
show:
|
|
130
132
|
back: Torna a DLQ
|
|
131
133
|
discard: Scarta
|
|
@@ -225,6 +227,10 @@ it:
|
|
|
225
227
|
bulk_select_all: Seleziona tutto
|
|
226
228
|
bulk_select_row: Seleziona %{id}
|
|
227
229
|
bulk_selected: selezionato
|
|
230
|
+
pagination:
|
|
231
|
+
next: Avanti
|
|
232
|
+
previous: Indietro
|
|
233
|
+
showing: Visualizzazione %{from}–%{to} di %{total}
|
|
228
234
|
paused_badge: In pausa
|
|
229
235
|
queue_badge:
|
|
230
236
|
dlq: DLQ
|
|
@@ -235,6 +241,7 @@ it:
|
|
|
235
241
|
status_badge:
|
|
236
242
|
healthy: Sano
|
|
237
243
|
stale: Obsoleto
|
|
244
|
+
stalled: Bloccato
|
|
238
245
|
insights:
|
|
239
246
|
show:
|
|
240
247
|
charts:
|
|
@@ -476,6 +483,7 @@ it:
|
|
|
476
483
|
resume: Riprendi
|
|
477
484
|
show:
|
|
478
485
|
arguments: Argomenti
|
|
486
|
+
back: Torna alle code
|
|
479
487
|
delete_confirm: Eliminare permanentemente questa coda? Questa azione non può essere annullata.
|
|
480
488
|
delete_queue: Elimina coda
|
|
481
489
|
depth: 'Profondità:'
|