pgbus 0.17.1 → 0.17.2
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/CHANGELOG.md +38 -0
- data/README.md +48 -13
- data/app/controllers/concerns/pgbus/job_listing.rb +37 -0
- data/app/controllers/concerns/pgbus/process_listing.rb +17 -0
- data/app/controllers/pgbus/application_controller.rb +5 -2
- data/app/controllers/pgbus/batches_controller.rb +4 -1
- data/app/controllers/pgbus/dashboard_controller.rb +20 -6
- data/app/controllers/pgbus/dead_letter_controller.rb +27 -3
- data/app/controllers/pgbus/events_controller.rb +52 -3
- data/app/controllers/pgbus/insights_controller.rb +19 -1
- data/app/controllers/pgbus/jobs_controller.rb +45 -26
- data/app/controllers/pgbus/locks_controller.rb +7 -2
- data/app/controllers/pgbus/outbox_controller.rb +4 -1
- data/app/controllers/pgbus/processes_controller.rb +3 -1
- data/app/controllers/pgbus/queues_controller.rb +16 -4
- data/app/controllers/pgbus/recurring_tasks_controller.rb +5 -7
- data/app/frontend/pgbus/modules/charts.js +119 -77
- data/app/frontend/pgbus/style.css +1 -1
- data/app/frontend/pgbus/tailwind.css +18 -0
- data/app/helpers/pgbus/application_helper.rb +155 -113
- data/app/helpers/pgbus/button_helper.rb +88 -0
- data/app/helpers/pgbus/dashboard_helper.rb +157 -0
- data/app/helpers/pgbus/events_helper.rb +40 -0
- data/app/helpers/pgbus/insights_helper.rb +39 -0
- data/app/helpers/pgbus/processes_helper.rb +79 -0
- data/app/views/pgbus/batches/_batches_table.html.erb +16 -13
- data/app/views/pgbus/batches/show.html.erb +3 -4
- data/app/views/pgbus/dashboard/_processes_table.html.erb +11 -11
- data/app/views/pgbus/dashboard/_queue_health.html.erb +42 -51
- data/app/views/pgbus/dashboard/_queues_table.html.erb +18 -16
- data/app/views/pgbus/dashboard/_recent_failures.html.erb +16 -12
- data/app/views/pgbus/dashboard/_stats_cards.html.erb +70 -36
- data/app/views/pgbus/dashboard/show.html.erb +1 -1
- data/app/views/pgbus/dead_letter/_error.html.erb +13 -0
- data/app/views/pgbus/dead_letter/_messages_table.html.erb +37 -96
- data/app/views/pgbus/dead_letter/_row.html.erb +75 -0
- data/app/views/pgbus/dead_letter/index.html.erb +6 -9
- data/app/views/pgbus/dead_letter/show.html.erb +37 -21
- data/app/views/pgbus/events/_list.html.erb +55 -0
- data/app/views/pgbus/events/_processed_table.html.erb +45 -0
- data/app/views/pgbus/events/_replay.html.erb +10 -0
- data/app/views/pgbus/events/_row.html.erb +162 -0
- data/app/views/pgbus/events/_tabs.html.erb +13 -0
- data/app/views/pgbus/events/index.html.erb +27 -48
- data/app/views/pgbus/events/show.html.erb +25 -7
- data/app/views/pgbus/insights/_chart_card.html.erb +21 -0
- data/app/views/pgbus/insights/_latency_table.html.erb +20 -0
- data/app/views/pgbus/insights/_status_table.html.erb +19 -0
- data/app/views/pgbus/insights/_throughput_table.html.erb +18 -0
- data/app/views/pgbus/insights/show.html.erb +83 -100
- data/app/views/pgbus/jobs/_list.html.erb +64 -0
- data/app/views/pgbus/jobs/_row.html.erb +107 -0
- data/app/views/pgbus/jobs/_tabs.html.erb +14 -0
- data/app/views/pgbus/jobs/index.html.erb +10 -10
- data/app/views/pgbus/jobs/show.html.erb +18 -20
- data/app/views/pgbus/locks/_concurrency.html.erb +25 -23
- data/app/views/pgbus/locks/_uniqueness.html.erb +20 -22
- data/app/views/pgbus/outbox/index.html.erb +30 -27
- data/app/views/pgbus/processes/_processes_table.html.erb +6 -25
- data/app/views/pgbus/processes/_row.html.erb +48 -0
- data/app/views/pgbus/processes/_status.html.erb +4 -0
- data/app/views/pgbus/queues/_queues_list.html.erb +28 -32
- data/app/views/pgbus/queues/show.html.erb +69 -118
- data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +75 -76
- data/app/views/pgbus/recurring_tasks/index.html.erb +1 -1
- data/app/views/pgbus/recurring_tasks/show.html.erb +45 -51
- data/app/views/pgbus/shared/_empty_row.html.erb +2 -0
- data/app/views/pgbus/shared/_pager.html.erb +34 -0
- data/config/i18n-tasks.yml +11 -2
- data/config/locales/da.yml +405 -113
- data/config/locales/de.yml +405 -113
- data/config/locales/en.yml +405 -113
- data/config/locales/es.yml +404 -112
- data/config/locales/fi.yml +404 -112
- data/config/locales/fr.yml +406 -114
- data/config/locales/it.yml +405 -113
- data/config/locales/ja.yml +405 -113
- data/config/locales/nb.yml +405 -113
- data/config/locales/nl.yml +405 -113
- data/config/locales/pt.yml +406 -114
- data/config/locales/sv.yml +404 -112
- data/lib/generators/pgbus/templates/initializer.rb.erb +4 -0
- data/lib/pgbus/active_job/executor.rb +16 -6
- data/lib/pgbus/cli/dlq.rb +32 -5
- data/lib/pgbus/client.rb +4 -2
- data/lib/pgbus/configuration.rb +47 -2
- data/lib/pgbus/dead_letter_header.rb +129 -0
- data/lib/pgbus/doctor.rb +8 -4
- data/lib/pgbus/event_bus/handler.rb +18 -11
- data/lib/pgbus/event_bus/registry.rb +6 -0
- data/lib/pgbus/failed_event_recorder.rb +34 -0
- data/lib/pgbus/instrumentation.rb +2 -1
- data/lib/pgbus/mcp/base_tool.rb +25 -0
- data/lib/pgbus/mcp/tools/concurrency_tool.rb +23 -4
- data/lib/pgbus/mcp/tools/dlq_detail_tool.rb +4 -2
- data/lib/pgbus/mcp/tools/dlq_tool.rb +10 -4
- data/lib/pgbus/mcp/tools/locks_tool.rb +23 -5
- data/lib/pgbus/mcp/tools/recurring_tool.rb +33 -4
- data/lib/pgbus/outbox/poller.rb +1 -1
- data/lib/pgbus/process/consumer.rb +53 -3
- data/lib/pgbus/process/dispatcher.rb +1 -1
- data/lib/pgbus/process/supervisor.rb +32 -11
- data/lib/pgbus/process/worker.rb +45 -8
- data/lib/pgbus/recurring/scheduler.rb +2 -1
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source/dashboard_signals.rb +170 -0
- data/lib/pgbus/web/data_source/dead_letter.rb +142 -0
- data/lib/pgbus/web/data_source/event_list.rb +161 -0
- data/lib/pgbus/web/data_source/job_list.rb +323 -0
- data/lib/pgbus/web/data_source/list_counts.rb +80 -0
- data/lib/pgbus/web/data_source/process_list.rb +69 -0
- data/lib/pgbus/web/data_source/queue_summary.rb +122 -0
- data/lib/pgbus/web/data_source.rb +132 -254
- data/lib/pgbus/web/dead_letter_reason.rb +81 -0
- data/lib/pgbus/web/event_state.rb +71 -0
- data/lib/pgbus/web/health_app.rb +7 -3
- data/lib/pgbus/web/health_verdict.rb +78 -0
- data/lib/pgbus/web/insights_charts.rb +84 -0
- data/lib/pgbus/web/job_state.rb +111 -0
- data/lib/pgbus/web/process_activity.rb +97 -0
- data/lib/pgbus/web/process_state.rb +187 -0
- data/lib/pgbus/web/queue_summary.rb +104 -0
- data/lib/pgbus/web/time_format.rb +128 -0
- metadata +43 -4
- data/app/views/pgbus/events/_pending_table.html.erb +0 -169
- data/app/views/pgbus/jobs/_enqueued_table.html.erb +0 -118
- data/app/views/pgbus/jobs/_failed_table.html.erb +0 -76
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "time"
|
|
4
|
-
|
|
5
3
|
module Pgbus
|
|
6
4
|
module ApplicationHelper
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
# A moment as "5m ago" / "in 2h" / "now", with the exact value in the
|
|
6
|
+
# tooltip and the UTC value in datetime (issue #497). clock: true adds
|
|
7
|
+
# the wall-clock time: "in 2h (21:40)".
|
|
8
|
+
def pgbus_time(value, clock: false)
|
|
9
|
+
return Web::TimeFormat::NONE if value.blank?
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
relative = Web::TimeFormat.relative(value)
|
|
12
|
+
return value.to_s unless relative
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
text = clock ? "#{relative} (#{Web::TimeFormat.clock(value)})" : relative
|
|
15
|
+
tag.time(text, datetime: Web::TimeFormat.iso(value), title: Web::TimeFormat.absolute(value))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The exact moment, then the relative one: for expanded rows and show
|
|
19
|
+
# pages, where the absolute is what an operator debugs with.
|
|
20
|
+
def pgbus_timestamp(value)
|
|
21
|
+
return Web::TimeFormat::NONE if value.blank?
|
|
22
|
+
return value.to_s unless Web::TimeFormat.coerce(value)
|
|
23
|
+
|
|
24
|
+
safe_join([pgbus_absolute_time(value), " (#{Web::TimeFormat.relative(value)})"])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def pgbus_absolute_time(value)
|
|
28
|
+
return Web::TimeFormat::NONE if value.blank?
|
|
29
|
+
return value.to_s unless Web::TimeFormat.coerce(value)
|
|
30
|
+
|
|
31
|
+
tag.time(Web::TimeFormat.absolute(value), datetime: Web::TimeFormat.iso(value))
|
|
19
32
|
end
|
|
20
33
|
|
|
21
34
|
def pgbus_number(n)
|
|
@@ -29,43 +42,6 @@ module Pgbus
|
|
|
29
42
|
end
|
|
30
43
|
end
|
|
31
44
|
|
|
32
|
-
# Order the rate keys are rendered in, and their i18n label suffixes.
|
|
33
|
-
WORKER_RATE_KEYS = %w[processed failed dequeued].freeze
|
|
34
|
-
|
|
35
|
-
# Metadata keys rendered specially elsewhere (throughput badge, health
|
|
36
|
-
# status) and therefore excluded from the generic key/value dump.
|
|
37
|
-
WORKER_INTERNAL_METADATA_KEYS = %w[rates jobs_processed jobs_failed in_flight loop_tick_at].freeze
|
|
38
|
-
|
|
39
|
-
# The subset of a process's metadata to render as generic key/value badges:
|
|
40
|
-
# everything except the throughput/health keys that have dedicated rendering.
|
|
41
|
-
def pgbus_display_metadata(metadata)
|
|
42
|
-
return {} unless metadata.is_a?(Hash)
|
|
43
|
-
|
|
44
|
-
metadata.reject { |k, _| WORKER_INTERNAL_METADATA_KEYS.include?(k.to_s) }
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Render a worker's per-second throughput rates (from heartbeat metadata)
|
|
48
|
-
# as a compact human-readable string, e.g. "12.4/s processed · 0.2/s failed".
|
|
49
|
-
# Zero rates are omitted; returns nil when there are no non-zero rates so
|
|
50
|
-
# callers can fall back to the raw metadata rendering.
|
|
51
|
-
def pgbus_worker_rates(metadata)
|
|
52
|
-
return nil unless metadata.is_a?(Hash)
|
|
53
|
-
|
|
54
|
-
rates = metadata["rates"]
|
|
55
|
-
return nil unless rates.is_a?(Hash)
|
|
56
|
-
|
|
57
|
-
parts = WORKER_RATE_KEYS.filter_map do |key|
|
|
58
|
-
value = rates[key].to_f
|
|
59
|
-
next if value.zero?
|
|
60
|
-
|
|
61
|
-
label = I18n.t("pgbus.processes.processes_table.rates.#{key}", default: key)
|
|
62
|
-
"#{value}/s #{label}"
|
|
63
|
-
end
|
|
64
|
-
return nil if parts.empty?
|
|
65
|
-
|
|
66
|
-
parts.join(" · ")
|
|
67
|
-
end
|
|
68
|
-
|
|
69
45
|
def pgbus_status_badge(healthy_or_status)
|
|
70
46
|
status = case healthy_or_status
|
|
71
47
|
when true then :healthy
|
|
@@ -76,59 +52,45 @@ module Pgbus
|
|
|
76
52
|
case status
|
|
77
53
|
when :healthy
|
|
78
54
|
tag.span(I18n.t("pgbus.helpers.status_badge.healthy"),
|
|
79
|
-
class: "inline-flex items-center rounded-full
|
|
55
|
+
class: "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium " \
|
|
56
|
+
"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400")
|
|
80
57
|
when :stalled
|
|
81
58
|
tag.span(I18n.t("pgbus.helpers.status_badge.stalled"),
|
|
82
|
-
class: "inline-flex items-center rounded-full
|
|
59
|
+
class: "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium " \
|
|
60
|
+
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400")
|
|
83
61
|
else
|
|
84
62
|
tag.span(I18n.t("pgbus.helpers.status_badge.stale"),
|
|
85
|
-
class: "inline-flex items-center rounded-full
|
|
63
|
+
class: "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium " \
|
|
64
|
+
"bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400")
|
|
86
65
|
end
|
|
87
66
|
end
|
|
88
67
|
|
|
89
68
|
def pgbus_queue_badge(name)
|
|
90
69
|
if name.to_s.end_with?("_dlq")
|
|
91
70
|
tag.span(I18n.t("pgbus.helpers.queue_badge.dlq"),
|
|
92
|
-
class: "inline-flex items-center rounded-full
|
|
71
|
+
class: "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium " \
|
|
72
|
+
"bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400")
|
|
93
73
|
else
|
|
94
74
|
tag.span(I18n.t("pgbus.helpers.queue_badge.queue"),
|
|
95
|
-
class: "inline-flex items-center rounded-full
|
|
75
|
+
class: "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium " \
|
|
76
|
+
"bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400")
|
|
96
77
|
end
|
|
97
78
|
end
|
|
98
79
|
|
|
99
80
|
def pgbus_duration(seconds)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
seconds = seconds.to_i
|
|
103
|
-
if seconds < 60
|
|
104
|
-
"#{seconds}s"
|
|
105
|
-
elsif seconds < 3600
|
|
106
|
-
"#{seconds / 60}m #{seconds % 60}s"
|
|
107
|
-
elsif seconds < 86_400
|
|
108
|
-
"#{seconds / 3600}h #{(seconds % 3600) / 60}m"
|
|
109
|
-
else
|
|
110
|
-
"#{seconds / 86_400}d #{(seconds % 86_400) / 3600}h"
|
|
111
|
-
end
|
|
81
|
+
Web::TimeFormat.duration(seconds)
|
|
112
82
|
end
|
|
113
83
|
|
|
114
84
|
def pgbus_ms_duration(millis)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
millis = millis.to_i
|
|
118
|
-
if millis < 1000
|
|
119
|
-
"#{millis}ms"
|
|
120
|
-
elsif millis < 60_000
|
|
121
|
-
"#{(millis / 1000.0).round(1)}s"
|
|
122
|
-
else
|
|
123
|
-
"#{(millis / 60_000.0).round(1)}m"
|
|
124
|
-
end
|
|
85
|
+
Web::TimeFormat.ms_duration(millis)
|
|
125
86
|
end
|
|
126
87
|
|
|
127
88
|
def pgbus_paused_badge(paused)
|
|
128
89
|
return unless paused
|
|
129
90
|
|
|
130
91
|
tag.span(I18n.t("pgbus.helpers.paused_badge"),
|
|
131
|
-
class: "inline-flex items-center rounded-full
|
|
92
|
+
class: "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium " \
|
|
93
|
+
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400")
|
|
132
94
|
end
|
|
133
95
|
|
|
134
96
|
BATCH_BADGE_BASE = "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
|
|
@@ -143,6 +105,117 @@ module Pgbus
|
|
|
143
105
|
tag.span(I18n.t("pgbus.helpers.batch_status.#{status}", default: status), class: css)
|
|
144
106
|
end
|
|
145
107
|
|
|
108
|
+
JOB_STATE_BADGE_CSS = {
|
|
109
|
+
blue: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
|
|
110
|
+
gray: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",
|
|
111
|
+
indigo: "bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-300",
|
|
112
|
+
yellow: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400",
|
|
113
|
+
purple: "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400",
|
|
114
|
+
green: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
|
|
115
|
+
}.freeze
|
|
116
|
+
|
|
117
|
+
def pgbus_job_state_badge(state)
|
|
118
|
+
tone = Pgbus::Web::JobState::BADGE_TONES.fetch(state.to_s, :gray)
|
|
119
|
+
tag.span(I18n.t("pgbus.jobs.list.states.#{state}"), class: "#{BATCH_BADGE_BASE} #{JOB_STATE_BADGE_CSS[tone]}")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# A JobState::Result's reason in words. :time is a future moment
|
|
123
|
+
# ("in 2h (21:40)"), :ago a past one ("5m ago"), both as <time>. The
|
|
124
|
+
# _html key lets them through; translate escapes every other argument.
|
|
125
|
+
def pgbus_job_reason(result)
|
|
126
|
+
translate("pgbus.jobs.list.reasons.#{result.reason_key}_html", **pgbus_reason_args(result.reason_args))
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Reason arguments ready for an _html key: :time (future) and :ago (past)
|
|
130
|
+
# as <time>, everything else as given for translate to escape.
|
|
131
|
+
def pgbus_reason_args(args)
|
|
132
|
+
args.to_h do |key, value|
|
|
133
|
+
case key
|
|
134
|
+
when :time then [key, pgbus_time(value, clock: true)]
|
|
135
|
+
when :ago then [key, pgbus_time(value)]
|
|
136
|
+
else [key, value]
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# A QueueSummary::Line in words. :ago is a past moment and :time a future
|
|
142
|
+
# one, both as <time>; :age is a wait in seconds. The _html key lets the
|
|
143
|
+
# <time> elements through; translate escapes every other argument.
|
|
144
|
+
def pgbus_queue_summary_line(line)
|
|
145
|
+
args = line.args.to_h do |key, value|
|
|
146
|
+
case key
|
|
147
|
+
when :ago then [key, pgbus_time(value)]
|
|
148
|
+
when :time then [key, pgbus_time(value, clock: true)]
|
|
149
|
+
when :age then [key, pgbus_duration(value)]
|
|
150
|
+
else [key, value]
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
translate("pgbus.queues.show.summary.#{line.key}_html", **args)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
QUEUE_SUMMARY_TONE_CSS = {
|
|
157
|
+
yellow: "bg-yellow-50 text-yellow-800 ring-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-200 dark:ring-yellow-800",
|
|
158
|
+
red: "bg-red-50 text-red-800 ring-red-200 dark:bg-red-900/30 dark:text-red-200 dark:ring-red-800",
|
|
159
|
+
gray: "bg-gray-50 text-gray-700 ring-gray-200 dark:bg-gray-900/40 dark:text-gray-200 dark:ring-gray-700"
|
|
160
|
+
}.freeze
|
|
161
|
+
|
|
162
|
+
def pgbus_queue_summary_classes(tone)
|
|
163
|
+
QUEUE_SUMMARY_TONE_CSS.fetch(tone, QUEUE_SUMMARY_TONE_CSS[:gray])
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# "2/5" for a job's delivery attempts against max_retries; "—" when the
|
|
167
|
+
# row was never delivered (blocked).
|
|
168
|
+
def pgbus_job_attempts(row, max_retries)
|
|
169
|
+
return "—" if row[:read_ct].nil?
|
|
170
|
+
|
|
171
|
+
"#{row[:read_ct]}/#{max_retries}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# A DeadLetterReason::Result in words (issue #495). With filter_path
|
|
175
|
+
# (->(extra) { url }) the error class becomes a link that filters the list
|
|
176
|
+
# to it. The _html keys let that link through; translate escapes the
|
|
177
|
+
# error message and every other argument.
|
|
178
|
+
def pgbus_dead_letter_reason(result, filter_path: nil)
|
|
179
|
+
args = result.reason_args.dup
|
|
180
|
+
if args[:error_class] && filter_path
|
|
181
|
+
args[:error_class] = pgbus_link_to(
|
|
182
|
+
result.error_class, filter_path.call(error_class: result.error_class, page: nil),
|
|
183
|
+
title: t("pgbus.dead_letter.filters.filter_error_class", error_class: result.error_class),
|
|
184
|
+
data: { turbo_frame: "_top" }
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
sentence = translate("pgbus.dead_letter.reasons.#{result.reason_key}_html", **args)
|
|
188
|
+
return sentence unless result.retried_before.to_i.positive?
|
|
189
|
+
|
|
190
|
+
note = t("pgbus.dead_letter.reasons.retried_before", count: result.retried_before)
|
|
191
|
+
safe_join([sentence, tag.span(note, class: "mt-1 block text-xs text-gray-600 dark:text-gray-300")])
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def pgbus_dead_letter_attempts(result)
|
|
195
|
+
return "—" if result.attempts.nil?
|
|
196
|
+
|
|
197
|
+
"#{result.attempts}/#{result.max_retries}"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# The Job column: an ActiveJob's class, or a dead event's routing key.
|
|
201
|
+
# Reads the raw message like the Events page: both are identifiers, and
|
|
202
|
+
# PayloadFilter's default /_key$/ pattern would mask routing_key.
|
|
203
|
+
def pgbus_dead_letter_job(message)
|
|
204
|
+
payload = message.is_a?(String) ? JSON.parse(message) : message
|
|
205
|
+
return "—" unless payload.is_a?(Hash)
|
|
206
|
+
|
|
207
|
+
headers = payload["headers"]
|
|
208
|
+
payload["job_class"].presence || (headers["routing_key"].presence if headers.is_a?(Hash)) ||
|
|
209
|
+
payload["routing_key"].presence || "—"
|
|
210
|
+
rescue JSON::ParserError
|
|
211
|
+
"—"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# A tab's count, with "+" when a capped fragment made it a lower bound.
|
|
215
|
+
def pgbus_job_count(counts, state)
|
|
216
|
+
"#{number_with_delimiter(counts[state])}#{"+" if counts.capped?(state)}"
|
|
217
|
+
end
|
|
218
|
+
|
|
146
219
|
# Persisted Current attributes for a job payload, as
|
|
147
220
|
# { "Current" => { "tenant" => "gid://…", ... } } or nil (issue #430).
|
|
148
221
|
# Goes through pgbus_parse_message so PayloadFilter redaction applies.
|
|
@@ -175,45 +248,20 @@ module Pgbus
|
|
|
175
248
|
Pgbus.configuration.web_refresh_interval
|
|
176
249
|
end
|
|
177
250
|
|
|
178
|
-
def pgbus_time_ago_future(time)
|
|
179
|
-
return "—" unless time
|
|
180
|
-
|
|
181
|
-
time = Time.parse(time) if time.is_a?(String)
|
|
182
|
-
seconds = (time - Time.now).to_i
|
|
183
|
-
|
|
184
|
-
if seconds <= 0
|
|
185
|
-
"now"
|
|
186
|
-
elsif seconds < 60
|
|
187
|
-
"in #{seconds}s"
|
|
188
|
-
elsif seconds < 3600
|
|
189
|
-
"in #{seconds / 60}m"
|
|
190
|
-
elsif seconds < 86_400
|
|
191
|
-
"in #{seconds / 3600}h"
|
|
192
|
-
else
|
|
193
|
-
"in #{seconds / 86_400}d"
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
|
|
197
251
|
def pgbus_recurring_health_badge(task)
|
|
198
252
|
if task[:last_run_at].nil?
|
|
199
253
|
tag.span(I18n.t("pgbus.helpers.recurring_health.pending"),
|
|
200
|
-
class: "inline-flex items-center rounded-full
|
|
254
|
+
class: "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium " \
|
|
255
|
+
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400")
|
|
201
256
|
else
|
|
202
257
|
tag.span(I18n.t("pgbus.helpers.recurring_health.active"),
|
|
203
|
-
class: "inline-flex items-center rounded-full
|
|
258
|
+
class: "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium " \
|
|
259
|
+
"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400")
|
|
204
260
|
end
|
|
205
261
|
end
|
|
206
262
|
|
|
207
263
|
def pgbus_time_range_label(minutes)
|
|
208
|
-
minutes
|
|
209
|
-
|
|
210
|
-
if minutes > 1440 && (minutes % 1440).zero?
|
|
211
|
-
pgbus_pluralize_unit(minutes / 1440, "day")
|
|
212
|
-
elsif minutes >= 60 && (minutes % 60).zero?
|
|
213
|
-
pgbus_pluralize_unit(minutes / 60, "hour")
|
|
214
|
-
else
|
|
215
|
-
pgbus_pluralize_unit(minutes, "minute")
|
|
216
|
-
end
|
|
264
|
+
Web::TimeFormat.range_label(minutes)
|
|
217
265
|
end
|
|
218
266
|
|
|
219
267
|
def pgbus_nav_link(label, path)
|
|
@@ -272,11 +320,5 @@ module Pgbus
|
|
|
272
320
|
else "\u{1F310}"
|
|
273
321
|
end
|
|
274
322
|
end
|
|
275
|
-
|
|
276
|
-
private
|
|
277
|
-
|
|
278
|
-
def pgbus_pluralize_unit(count, unit)
|
|
279
|
-
count == 1 ? "1 #{unit}" : "#{count} #{unit}s"
|
|
280
|
-
end
|
|
281
323
|
end
|
|
282
324
|
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
# Every button and action link on the dashboard (issue #496): one class table
|
|
5
|
+
# with light and dark colours, a focus ring, and a 24 px minimum target
|
|
6
|
+
# (WCAG 2.5.8) in every size. Class lists are spelled out in full so Tailwind
|
|
7
|
+
# and the static specs (dark_mode_contrast_spec, compiled_css_coverage_spec)
|
|
8
|
+
# can see every token; never interpolate a colour name.
|
|
9
|
+
module ButtonHelper
|
|
10
|
+
BASE = "inline-flex items-center min-h-6 font-medium focus-visible:outline-2 focus-visible:outline-offset-2 " \
|
|
11
|
+
"focus-visible:outline-indigo-600 dark:focus-visible:outline-indigo-400"
|
|
12
|
+
|
|
13
|
+
VARIANTS = {
|
|
14
|
+
# Hover goes darker, never lighter: white on red-500 is 3.8:1, on
|
|
15
|
+
# green-600 3.3:1, both under 4.5:1 for these small labels.
|
|
16
|
+
primary: "bg-indigo-600 text-white hover:bg-indigo-700",
|
|
17
|
+
danger: "bg-red-600 text-white hover:bg-red-700",
|
|
18
|
+
# Permanent and irreversible (Delete queue), set apart from danger.
|
|
19
|
+
destructive: "bg-red-800 text-white hover:bg-red-900",
|
|
20
|
+
success: "bg-green-700 text-white hover:bg-green-800",
|
|
21
|
+
# White on yellow fails; yellow buttons carry dark text in both themes.
|
|
22
|
+
warning: "bg-yellow-400 text-yellow-950 hover:bg-yellow-300",
|
|
23
|
+
secondary: "bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600",
|
|
24
|
+
# Bordered, on the card's own surface (the pager).
|
|
25
|
+
outline: "border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 " \
|
|
26
|
+
"hover:bg-gray-50 dark:hover:bg-gray-700",
|
|
27
|
+
# A row in a dropdown menu (the Events reroute picker).
|
|
28
|
+
menu: "w-full justify-start text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700",
|
|
29
|
+
link: nil
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
# Link colours. Dark text is -300: 12 px row actions sit on the hovered row
|
|
33
|
+
# tint (dark:hover:bg-gray-700/50), where -400 drops below 4.5:1.
|
|
34
|
+
TONES = {
|
|
35
|
+
indigo: "text-indigo-600 dark:text-indigo-300 hover:text-indigo-800 dark:hover:text-indigo-200",
|
|
36
|
+
red: "text-red-600 dark:text-red-300 hover:text-red-800 dark:hover:text-red-200",
|
|
37
|
+
green: "text-green-700 dark:text-green-300 hover:text-green-900 dark:hover:text-green-200",
|
|
38
|
+
yellow: "text-yellow-700 dark:text-yellow-300 hover:text-yellow-900 dark:hover:text-yellow-200"
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
SIZES = {
|
|
42
|
+
sm: "rounded-md px-3 py-1.5 text-xs",
|
|
43
|
+
md: "rounded-md px-3 py-2 text-sm"
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
# Links keep their type size; the padding on sm gives the 24 px target.
|
|
47
|
+
LINK_SIZES = {
|
|
48
|
+
sm: "rounded px-1.5 py-1 text-xs",
|
|
49
|
+
md: "text-sm"
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
def pgbus_button_classes(variant, size = :md, tone: :indigo, extra: nil)
|
|
53
|
+
raise ArgumentError, "unknown button variant: #{variant.inspect}" unless VARIANTS.key?(variant)
|
|
54
|
+
raise ArgumentError, "unknown button size: #{size.inspect}" unless SIZES.key?(size)
|
|
55
|
+
|
|
56
|
+
colours = variant == :link ? TONES.fetch(tone) { raise ArgumentError, "unknown link tone: #{tone.inspect}" } : VARIANTS[variant]
|
|
57
|
+
sizing = variant == :link ? LINK_SIZES[size] : SIZES[size]
|
|
58
|
+
[BASE, sizing, colours, extra].compact.join(" ")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def pgbus_button_to(label, path, variant: :primary, size: :md, tone: :indigo, confirm: nil, **options)
|
|
62
|
+
options[:class] = pgbus_button_classes(variant, size, tone: tone, extra: options[:class])
|
|
63
|
+
options[:data] = pgbus_confirm_data(options[:data], confirm)
|
|
64
|
+
button_to(label, path, options)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def pgbus_link_to(label, path, variant: :link, size: :md, tone: :indigo, **options)
|
|
68
|
+
options[:class] = pgbus_button_classes(variant, size, tone: tone, extra: options[:class])
|
|
69
|
+
link_to(label, path, options)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# A submit button outside its form (form: "bulk-…"), for the bulk actions.
|
|
73
|
+
def pgbus_button_tag(label, variant: :primary, size: :md, tone: :indigo, confirm: nil, **options)
|
|
74
|
+
options[:class] = pgbus_button_classes(variant, size, tone: tone, extra: options[:class])
|
|
75
|
+
options[:data] = pgbus_confirm_data(options[:data], confirm)
|
|
76
|
+
content_tag(:button, label, type: "submit", **options)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# application.js's confirm dialog reads data-turbo-confirm.
|
|
82
|
+
def pgbus_confirm_data(data, confirm)
|
|
83
|
+
data = (data || {}).dup
|
|
84
|
+
data[:turbo_confirm] = confirm if confirm
|
|
85
|
+
data.presence
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
# The home page's words (issue #492): the health verdict strip, the card
|
|
5
|
+
# sentences built from DataSource#dashboard_signals, the queue rows'
|
|
6
|
+
# QueueSummary line and the Queue Health thresholds from HealthVerdict.
|
|
7
|
+
module DashboardHelper
|
|
8
|
+
PART_SEPARATOR = " · "
|
|
9
|
+
|
|
10
|
+
# Every home card is a link to the page that explains it.
|
|
11
|
+
CARD_CSS = "block rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700 " \
|
|
12
|
+
"hover:ring-gray-300 dark:hover:ring-gray-600"
|
|
13
|
+
CARD_NUMBER_CSS = "mt-1 text-3xl font-semibold text-gray-900 dark:text-white"
|
|
14
|
+
|
|
15
|
+
VERDICT_CSS = {
|
|
16
|
+
"OK" => "bg-green-50 text-green-800 ring-green-200 dark:bg-green-900/30 dark:text-green-200 dark:ring-green-800",
|
|
17
|
+
"DEGRADED" => "bg-yellow-50 text-yellow-800 ring-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-200 " \
|
|
18
|
+
"dark:ring-yellow-800",
|
|
19
|
+
"STALLED" => "bg-red-50 text-red-800 ring-red-200 dark:bg-red-900/30 dark:text-red-200 dark:ring-red-800"
|
|
20
|
+
}.freeze
|
|
21
|
+
VERDICT_UNKNOWN_CSS = "bg-gray-50 text-gray-700 ring-gray-200 dark:bg-gray-900/40 dark:text-gray-200 dark:ring-gray-700"
|
|
22
|
+
|
|
23
|
+
# Stat numbers are text-3xl, so the -600 shades meet the 3:1 large-text ratio.
|
|
24
|
+
HEALTH_NUMBER_CSS = {
|
|
25
|
+
ok: "mt-1 text-3xl font-semibold text-green-600 dark:text-green-400",
|
|
26
|
+
warn: "mt-1 text-3xl font-semibold text-amber-600 dark:text-amber-400",
|
|
27
|
+
critical: "mt-1 text-3xl font-semibold text-red-600 dark:text-red-400",
|
|
28
|
+
unknown: "mt-1 text-3xl font-semibold text-gray-900 dark:text-white"
|
|
29
|
+
}.freeze
|
|
30
|
+
HEALTH_WORDS_CSS = {
|
|
31
|
+
ok: "text-xs text-gray-600 dark:text-gray-300",
|
|
32
|
+
warn: "text-xs text-amber-700 dark:text-amber-300",
|
|
33
|
+
critical: "text-xs text-red-700 dark:text-red-300",
|
|
34
|
+
unknown: "text-xs text-gray-600 dark:text-gray-300"
|
|
35
|
+
}.freeze
|
|
36
|
+
QUEUE_STATUS_CSS = {
|
|
37
|
+
red: "text-red-700 dark:text-red-300",
|
|
38
|
+
yellow: "text-yellow-800 dark:text-yellow-200",
|
|
39
|
+
gray: "text-gray-600 dark:text-gray-300"
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
def pgbus_verdict_css(verdict)
|
|
43
|
+
VERDICT_CSS.fetch(verdict&.dig(:status).to_s, VERDICT_UNKNOWN_CSS)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Where the verdict's reasons are explained, from the signals rather than
|
|
47
|
+
# the (English, unlocalised) reason strings.
|
|
48
|
+
def pgbus_verdict_links(signals)
|
|
49
|
+
processes = signals[:processes]
|
|
50
|
+
links = []
|
|
51
|
+
if (processes[:stalled] + processes[:stale]).positive?
|
|
52
|
+
links << pgbus_link_to(t("pgbus.dashboard.verdict.links.processes"), pgbus.processes_path,
|
|
53
|
+
data: { turbo_frame: "_top" })
|
|
54
|
+
end
|
|
55
|
+
if signals[:dlq][:depth].positive?
|
|
56
|
+
links << pgbus_link_to(t("pgbus.dashboard.verdict.links.dlq"), pgbus.dead_letter_index_path, data: { turbo_frame: "_top" })
|
|
57
|
+
end
|
|
58
|
+
links
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def pgbus_queue_card_words(queues)
|
|
62
|
+
parts = [t("pgbus.dashboard.stats_cards.queues_detail", paused: queues[:paused], dlq: queues[:dlq])]
|
|
63
|
+
parts << t("pgbus.dashboard.stats_cards.queues_undrained", count: queues[:undrained]) if queues[:undrained].positive?
|
|
64
|
+
safe_join(parts, PART_SEPARATOR)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def pgbus_enqueued_card_words(queues)
|
|
68
|
+
parts = [t("pgbus.dashboard.stats_cards.enqueued_detail", claimable: number_with_delimiter(queues[:claimable]),
|
|
69
|
+
waiting: number_with_delimiter(queues[:waiting]))]
|
|
70
|
+
parts << t("pgbus.dashboard.stats_cards.enqueued_paused", count: queues[:paused_depth]) if queues[:paused_depth].positive?
|
|
71
|
+
safe_join(parts, PART_SEPARATOR)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def pgbus_process_card_words(processes)
|
|
75
|
+
return t("pgbus.dashboard.stats_cards.processes_none") if processes[:total].zero?
|
|
76
|
+
|
|
77
|
+
words = t("pgbus.dashboard.stats_cards.processes_detail", **processes.slice(:healthy, :stalled, :stale))
|
|
78
|
+
return words unless processes[:stale].positive?
|
|
79
|
+
|
|
80
|
+
safe_join([words, t("pgbus.dashboard.stats_cards.processes_stale")], " — ")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def pgbus_retrying_card_words(retrying)
|
|
84
|
+
return t("pgbus.dashboard.stats_cards.retrying_none") if retrying[:count].zero?
|
|
85
|
+
|
|
86
|
+
parts = []
|
|
87
|
+
if retrying[:next_attempt_at]
|
|
88
|
+
parts << t("pgbus.dashboard.stats_cards.retrying_next_html", time: pgbus_time(retrying[:next_attempt_at], clock: true))
|
|
89
|
+
end
|
|
90
|
+
if retrying[:due_now_count].positive?
|
|
91
|
+
# Past the per-queue cap the due count is a lower bound, like the total.
|
|
92
|
+
due = retrying[:capped] ? "#{number_with_delimiter(retrying[:due_now_count])}+" : retrying[:due_now_count]
|
|
93
|
+
parts << t("pgbus.dashboard.stats_cards.retrying_due", count: due)
|
|
94
|
+
end
|
|
95
|
+
parts << t("pgbus.dashboard.stats_cards.retrying_newest", error_class: retrying[:newest_error_class]) if retrying[:newest_error_class]
|
|
96
|
+
safe_join(parts, PART_SEPARATOR)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def pgbus_retrying_count(retrying)
|
|
100
|
+
"#{number_with_delimiter(retrying[:count])}#{"+" if retrying[:capped]}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The newest dead letter's reason, read as the DLQ page reads it (#495).
|
|
104
|
+
def pgbus_dlq_card_words(dlq)
|
|
105
|
+
return t("pgbus.dashboard.stats_cards.dlq_none") if dlq[:depth].zero?
|
|
106
|
+
return t("pgbus.dashboard.stats_cards.dlq_see_why") unless dlq[:newest]
|
|
107
|
+
|
|
108
|
+
reason = Web::DeadLetterReason.present(dlq[:newest])
|
|
109
|
+
args = { job: pgbus_dead_letter_job(dlq[:newest][:message]), ago: pgbus_time(reason.died_at) }
|
|
110
|
+
return t("pgbus.dashboard.stats_cards.dlq_newest_legacy_html", **args) if reason.legacy?
|
|
111
|
+
return t("pgbus.dashboard.stats_cards.dlq_newest_no_error_html", **args) unless reason.error_class
|
|
112
|
+
|
|
113
|
+
t("pgbus.dashboard.stats_cards.dlq_newest_html", error_class: reason.error_class, **args)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def pgbus_queue_status(line)
|
|
117
|
+
return Web::TimeFormat::NONE unless line
|
|
118
|
+
|
|
119
|
+
tag.span(pgbus_queue_summary_line(line), class: QUEUE_STATUS_CSS.fetch(line.tone, QUEUE_STATUS_CSS[:gray]))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# A HealthVerdict::Entry in words. Bloat thresholds are ratios, the
|
|
123
|
+
# vacuum and transaction ones seconds.
|
|
124
|
+
def pgbus_health_words(entry)
|
|
125
|
+
percent = entry.key.start_with?("bloat.")
|
|
126
|
+
args = entry.args.to_h do |key, value|
|
|
127
|
+
case key
|
|
128
|
+
when :table then [key, value]
|
|
129
|
+
when :count, :limit then [key, number_with_delimiter(value)]
|
|
130
|
+
else [key, percent ? pgbus_percent(value) : pgbus_duration(value)]
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
tag.p(t("pgbus.dashboard.queue_health.words.#{entry.key}", **args), class: HEALTH_WORDS_CSS.fetch(entry.level))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def pgbus_health_number_css(entry) = HEALTH_NUMBER_CSS.fetch(entry.level)
|
|
137
|
+
|
|
138
|
+
def pgbus_percent(ratio)
|
|
139
|
+
number_to_percentage(ratio.to_f * 100, precision: 1, strip_insignificant_zeros: true)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# "4 / 5" for a failed row: the attempt that failed against max_retries.
|
|
143
|
+
def pgbus_failure_attempt(row)
|
|
144
|
+
"#{row["retry_count"].to_i + 1} / #{Pgbus.configuration.max_retries}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The payload arrives as a JSON string or, when the column is typecast,
|
|
148
|
+
# an already-parsed Hash.
|
|
149
|
+
def pgbus_failure_job(row)
|
|
150
|
+
payload = row["payload"]
|
|
151
|
+
payload = JSON.parse(payload.to_s) unless payload.is_a?(Hash)
|
|
152
|
+
(payload["job_class"] if payload.is_a?(Hash)).presence || row["handler_class"].presence || Web::TimeFormat::NONE
|
|
153
|
+
rescue JSON::ParserError
|
|
154
|
+
row["handler_class"].presence || Web::TimeFormat::NONE
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
# The Events page's words (issue #494): EventState results as badges and
|
|
5
|
+
# sentences, in the jobs list's vocabulary and colours.
|
|
6
|
+
module EventsHelper
|
|
7
|
+
def pgbus_event_state_badge(state)
|
|
8
|
+
tone = Pgbus::Web::JobState::BADGE_TONES.fetch(state.to_s, :gray)
|
|
9
|
+
tag.span(t("pgbus.events.list.states.#{state}"),
|
|
10
|
+
class: "#{ApplicationHelper::BATCH_BADGE_BASE} #{ApplicationHelper::JOB_STATE_BADGE_CSS[tone]}")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# An EventState::Result in words. The _html key lets the <time> elements
|
|
14
|
+
# through; translate escapes the handler, pattern and error.
|
|
15
|
+
def pgbus_event_reason(result)
|
|
16
|
+
translate("pgbus.events.list.reasons.#{result.reason_key}_html", **pgbus_reason_args(result.reason_args))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def pgbus_processed_event_badge(result)
|
|
20
|
+
tag.span(t("pgbus.events.processed.states.#{result.state}"),
|
|
21
|
+
class: "#{ApplicationHelper::BATCH_BADGE_BASE} #{ApplicationHelper::JOB_STATE_BADGE_CSS[result.badge_tone]}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pgbus_processed_event_reason(result)
|
|
25
|
+
translate("pgbus.events.processed.reasons.#{result.reason_key}_html", **pgbus_reason_args(result.reason_args))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The routing key from the raw message: PayloadFilter's default /_key$/
|
|
29
|
+
# pattern would mask it in the parsed (filtered) payload.
|
|
30
|
+
def pgbus_event_routing_key(message)
|
|
31
|
+
payload = message.is_a?(String) ? JSON.parse(message) : message
|
|
32
|
+
return "—" unless payload.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
headers = payload["headers"]
|
|
35
|
+
(headers["routing_key"].presence if headers.is_a?(Hash)) || payload["routing_key"].presence || "—"
|
|
36
|
+
rescue JSON::ParserError, TypeError
|
|
37
|
+
"—"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
# The Insights charts' words (issue #492): one sentence per chart, used as
|
|
5
|
+
# its figcaption and as the chart's accessible name.
|
|
6
|
+
module InsightsHelper
|
|
7
|
+
def pgbus_job_stat_status(status)
|
|
8
|
+
t("pgbus.insights.show.statuses.#{status}", default: status.to_s.humanize)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def pgbus_throughput_caption(chart, minutes)
|
|
12
|
+
t("pgbus.insights.show.charts.captions.throughput_html",
|
|
13
|
+
total: number_with_delimiter(chart.total), range: pgbus_time_range_label(minutes),
|
|
14
|
+
peak: number_with_delimiter(chart.peak), at: pgbus_time(chart.peak_at, clock: true))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def pgbus_status_caption(chart)
|
|
18
|
+
t("pgbus.insights.show.charts.captions.status", success: number_with_delimiter(chart[:success]),
|
|
19
|
+
failed: number_with_delimiter(chart[:failed]),
|
|
20
|
+
dead_lettered: number_with_delimiter(chart[:dead_lettered]))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def pgbus_latency_caption(summary)
|
|
24
|
+
t("pgbus.insights.show.charts.captions.latency", avg: pgbus_ms_duration(summary[:avg_latency_ms]),
|
|
25
|
+
p95: pgbus_ms_duration(summary[:p95_latency_ms]))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The labels charts.js puts on the series and the donut legend.
|
|
29
|
+
def pgbus_chart_i18n
|
|
30
|
+
{
|
|
31
|
+
seriesName: t("pgbus.insights.show.charts.series_name"),
|
|
32
|
+
latencyAvg: t("pgbus.insights.show.charts.latency_avg"),
|
|
33
|
+
latencyP95: t("pgbus.insights.show.charts.latency_p95"),
|
|
34
|
+
milliseconds: t("pgbus.helpers.time.units.milliseconds", count: "%n"),
|
|
35
|
+
statuses: Web::InsightsCharts::STATUS_ORDER.index_with { |status| pgbus_job_stat_status(status) }
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|