sidekiq 8.1.0 → 8.1.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/Changes.md +29 -0
- data/README.md +1 -1
- data/bin/kiq +17 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +11 -8
- data/lib/generators/sidekiq/job_generator.rb +15 -3
- data/lib/sidekiq/api.rb +130 -76
- data/lib/sidekiq/client.rb +3 -1
- data/lib/sidekiq/component.rb +3 -0
- data/lib/sidekiq/launcher.rb +1 -1
- data/lib/sidekiq/manager.rb +1 -1
- data/lib/sidekiq/paginator.rb +6 -1
- data/lib/sidekiq/profiler.rb +1 -1
- data/lib/sidekiq/test_api.rb +331 -0
- data/lib/sidekiq/testing/inline.rb +2 -30
- data/lib/sidekiq/testing.rb +2 -334
- data/lib/sidekiq/tui/controls.rb +53 -0
- data/lib/sidekiq/tui/filtering.rb +53 -0
- data/lib/sidekiq/tui/tabs/base_tab.rb +187 -0
- data/lib/sidekiq/tui/tabs/busy.rb +118 -0
- data/lib/sidekiq/tui/tabs/dead.rb +19 -0
- data/lib/sidekiq/tui/tabs/home.rb +144 -0
- data/lib/sidekiq/tui/tabs/metrics.rb +131 -0
- data/lib/sidekiq/tui/tabs/queues.rb +95 -0
- data/lib/sidekiq/tui/tabs/retries.rb +19 -0
- data/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
- data/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
- data/lib/sidekiq/tui/tabs.rb +15 -0
- data/lib/sidekiq/tui.rb +380 -0
- data/lib/sidekiq/version.rb +1 -1
- data/lib/sidekiq/web/application.rb +2 -2
- data/lib/sidekiq/web/helpers.rb +11 -0
- data/lib/sidekiq.rb +7 -0
- data/sidekiq.gemspec +1 -1
- data/web/assets/javascripts/application.js +1 -1
- data/web/locales/zh-TW.yml +1 -1
- data/web/views/busy.html.erb +1 -1
- metadata +18 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Sidekiq
|
|
2
|
+
class TUI
|
|
3
|
+
module Controls
|
|
4
|
+
# Defines data for input handling and for displaying controls.
|
|
5
|
+
# :code is the key code for input handling.
|
|
6
|
+
# :display and :description are shown in the controls area, with different
|
|
7
|
+
# styling between them. If :display is omitted, :code is displayed instead.
|
|
8
|
+
# :action is a lambda to execute when the control is triggered.
|
|
9
|
+
# :refresh means the action requires immediate refreshing of data
|
|
10
|
+
#
|
|
11
|
+
# Conventions: dangerous/irreversible actions should use UPPERCASE codes.
|
|
12
|
+
# The Shift button means "I'm sure".
|
|
13
|
+
GLOBAL = [
|
|
14
|
+
{code: "?", display: "?", description: "Help", action: ->(tui, tab) { tui.show_help }},
|
|
15
|
+
{code: "left", display: "←/→", description: "Select Tab", action: ->(tui, tab) { tui.navigate(:left) }, refresh: true},
|
|
16
|
+
{code: "right", action: ->(tui, tab) { tui.navigate(:right) }, refresh: true},
|
|
17
|
+
{code: "q", description: "Quit", action: ->(tui, tab) { :quit }},
|
|
18
|
+
{code: "c", modifiers: ["ctrl"], action: ->(tui, tab) { :quit }}
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
SHARED = {
|
|
22
|
+
pageable: [
|
|
23
|
+
{code: "h", display: "h/l", description: "Prev/Next Page",
|
|
24
|
+
action: ->(tui, tab) { tab.prev_page }, refresh: true},
|
|
25
|
+
{code: "l", action: ->(tui, tab) { tab.next_page }, refresh: true}
|
|
26
|
+
],
|
|
27
|
+
selectable: [
|
|
28
|
+
{code: "k", display: "j/k", description: "Prev/Next Row",
|
|
29
|
+
action: ->(tui, tab) { tab.navigate_row(:up) }},
|
|
30
|
+
{code: "j", action: ->(tui, tab) { tab.navigate_row(:down) }},
|
|
31
|
+
{code: "x", description: "Select", action: ->(tui, tab) { tab.toggle_select }},
|
|
32
|
+
{code: "A", modifiers: ["shift"], display: "A", description: "Select All",
|
|
33
|
+
action: ->(tui, tab) { tab.toggle_select(:all) }}
|
|
34
|
+
],
|
|
35
|
+
filterable: [
|
|
36
|
+
{code: "/", display: "/", description: "Filter", action: ->(tui, tab) { tab.start_filtering }},
|
|
37
|
+
{code: "backspace", action: ->(tui, tab) { tab.remove_last_char_from_filter }, refresh: true},
|
|
38
|
+
{code: "enter", action: ->(tui, tab) { tab.stop_filtering }, refresh: true},
|
|
39
|
+
{code: "esc", action: ->(tui, tab) { tab.stop_and_clear_filtering }, refresh: true}
|
|
40
|
+
]
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
# Returns an array of symbols for functionality which this tab implements
|
|
44
|
+
def features
|
|
45
|
+
[]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def controls
|
|
49
|
+
GLOBAL + SHARED.slice(*features).values.flatten
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Sidekiq
|
|
2
|
+
class TUI
|
|
3
|
+
module Filtering
|
|
4
|
+
def filtering?
|
|
5
|
+
@data[:filtering]
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def current_filter
|
|
9
|
+
@data[:filter]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def start_filtering
|
|
13
|
+
@data[:filtering] = true
|
|
14
|
+
@data[:filter] = ""
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def stop_filtering
|
|
18
|
+
return unless @data[:filtering]
|
|
19
|
+
|
|
20
|
+
@data[:filtering] = false
|
|
21
|
+
@data[:selected] = []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stop_and_clear_filtering
|
|
25
|
+
return unless @data[:filtering]
|
|
26
|
+
|
|
27
|
+
@data[:filtering] = false
|
|
28
|
+
@data[:filter] = nil
|
|
29
|
+
@data[:selected] = []
|
|
30
|
+
on_filter_change
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def remove_last_char_from_filter
|
|
34
|
+
return unless @data[:filtering]
|
|
35
|
+
|
|
36
|
+
@data[:filter] = @data[:filter].empty? ? "" : @data[:filter][0..-2]
|
|
37
|
+
on_filter_change
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def append_to_filter(string)
|
|
41
|
+
return unless @data[:filtering]
|
|
42
|
+
|
|
43
|
+
@data[:filter] += string
|
|
44
|
+
@data[:selected] = []
|
|
45
|
+
on_filter_change
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def on_filter_change
|
|
49
|
+
# callback for subclasses
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
module Sidekiq
|
|
2
|
+
class TUI
|
|
3
|
+
class BaseTab
|
|
4
|
+
include Controls
|
|
5
|
+
|
|
6
|
+
attr_reader :name
|
|
7
|
+
attr_reader :data
|
|
8
|
+
|
|
9
|
+
def initialize(parent)
|
|
10
|
+
@parent = parent
|
|
11
|
+
@name = self.class.name.split("::").last
|
|
12
|
+
reset_data
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def t(*)
|
|
16
|
+
@parent.t(*)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def reset_data
|
|
20
|
+
@data = {selected: [], selected_row_index: 0}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def error
|
|
24
|
+
@data[:error]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def error=(e)
|
|
28
|
+
@data[:error] = e
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def selected?(entry)
|
|
32
|
+
@data[:selected].index(entry.id)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def filtering?
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def each_selection(unselect: true, &)
|
|
40
|
+
sel = @data[:selected]
|
|
41
|
+
finished = []
|
|
42
|
+
if !sel.empty?
|
|
43
|
+
sel.each do |id|
|
|
44
|
+
yield id
|
|
45
|
+
# When processing multiple items in bulk, we want to unselect
|
|
46
|
+
# each row if its operation succeeds so our UI will not
|
|
47
|
+
# re-process rows 1-3 if row 4 fails.
|
|
48
|
+
finished << id
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
ids = @data.dig(:table, :row_ids)
|
|
52
|
+
return if !ids || ids.empty?
|
|
53
|
+
yield ids[@data[:selected_row_index]]
|
|
54
|
+
end
|
|
55
|
+
ensure
|
|
56
|
+
@data[:selected] = sel - finished if unselect
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Navigate the row selection up or down in the current tab's table.
|
|
60
|
+
# @param direction [Symbol] :up or :down
|
|
61
|
+
def navigate_row(direction)
|
|
62
|
+
ids = @data.dig(:table, :row_ids)
|
|
63
|
+
return if !ids || ids.empty?
|
|
64
|
+
|
|
65
|
+
index_change = (direction == :down) ? 1 : -1
|
|
66
|
+
@data[:selected_row_index] = (@data[:selected_row_index] + index_change) % ids.count
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def prev_page
|
|
70
|
+
opts = @data.dig(:table, :pager)
|
|
71
|
+
return unless opts
|
|
72
|
+
return if opts.page < 2
|
|
73
|
+
|
|
74
|
+
@data[:table][:pager] = Sidekiq::TUI::PageOptions.new(opts.page - 1, opts.size)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def next_page
|
|
78
|
+
np = @data.dig(:table, :next_page)
|
|
79
|
+
return unless np
|
|
80
|
+
opts = @data.dig(:table, :pager)
|
|
81
|
+
return unless opts
|
|
82
|
+
|
|
83
|
+
@data[:table][:pager] = Sidekiq::TUI::PageOptions.new(np, opts.size)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def toggle_select(which = :current)
|
|
87
|
+
sel = @data[:selected]
|
|
88
|
+
# log(which, sel)
|
|
89
|
+
if which == :current
|
|
90
|
+
x = @data[:table][:row_ids][@data[:selected_row_index]]
|
|
91
|
+
if sel.index(x)
|
|
92
|
+
# already checked, uncheck it
|
|
93
|
+
sel.delete(x)
|
|
94
|
+
else
|
|
95
|
+
sel << x
|
|
96
|
+
end
|
|
97
|
+
elsif sel.empty?
|
|
98
|
+
@data[:selected] = @data[:table][:row_ids]
|
|
99
|
+
else
|
|
100
|
+
sel.clear
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def refresh_data_for_stats
|
|
105
|
+
stats = Sidekiq::Stats.new
|
|
106
|
+
@data[:stats] = {
|
|
107
|
+
processed: stats.processed,
|
|
108
|
+
failed: stats.failed,
|
|
109
|
+
busy: stats.workers_size,
|
|
110
|
+
enqueued: stats.enqueued,
|
|
111
|
+
retries: stats.retry_size,
|
|
112
|
+
scheduled: stats.scheduled_size,
|
|
113
|
+
dead: stats.dead_size
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def render_table(tui, frame, area)
|
|
118
|
+
page = @data.dig(:table, :current_page) || 1
|
|
119
|
+
rows = @data.dig(:table, :rows) || []
|
|
120
|
+
total = @data.dig(:table, :total) || 0
|
|
121
|
+
footer = ["", "Page: #{page}", "Count: #{rows.size}", "Total: #{total}"]
|
|
122
|
+
footer << "Selected: #{@data[:selected].size}" unless @data[:selected].empty?
|
|
123
|
+
|
|
124
|
+
defaults = {
|
|
125
|
+
title: "TableName",
|
|
126
|
+
footer: footer
|
|
127
|
+
}
|
|
128
|
+
if features.include?(:selectable)
|
|
129
|
+
defaults.merge!({
|
|
130
|
+
highlight_symbol: "➡️",
|
|
131
|
+
selected_row: @data[:selected_row_index],
|
|
132
|
+
row_highlight_style: tui.style(fg: :white, bg: :blue)
|
|
133
|
+
})
|
|
134
|
+
end
|
|
135
|
+
hash = defaults.merge(yield)
|
|
136
|
+
hash[:block] ||= tui.block(title: hash.delete(:title), borders: :all)
|
|
137
|
+
table = tui.table(**hash)
|
|
138
|
+
frame.render_widget(table, area)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def render_stats_section(tui, frame, area)
|
|
142
|
+
stats = @data[:stats]
|
|
143
|
+
|
|
144
|
+
keys = ["Processed", "Failed", "Busy", "Enqueued", "Retries", "Scheduled", "Dead"]
|
|
145
|
+
values = [
|
|
146
|
+
stats[:processed],
|
|
147
|
+
stats[:failed],
|
|
148
|
+
stats[:busy],
|
|
149
|
+
stats[:enqueued],
|
|
150
|
+
stats[:retries],
|
|
151
|
+
stats[:scheduled],
|
|
152
|
+
stats[:dead]
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
# Format keys and values with spacing
|
|
156
|
+
keys_line = keys.map { |k| t(k).to_s.ljust(12) }.join(" ")
|
|
157
|
+
values_line = values.map { |v| v.to_s.ljust(12) }.join(" ")
|
|
158
|
+
|
|
159
|
+
frame.render_widget(
|
|
160
|
+
tui.paragraph(
|
|
161
|
+
text: [keys_line, values_line],
|
|
162
|
+
block: tui.block(title: "Statistics", borders: [:all])
|
|
163
|
+
),
|
|
164
|
+
area
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# TODO Implement I18n delimiter
|
|
169
|
+
def number_with_delimiter(number, options = {})
|
|
170
|
+
precision = options[:precision] || 0
|
|
171
|
+
number.round(precision)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def format_memory(rss_kb)
|
|
175
|
+
return "0" if rss_kb.nil? || rss_kb == 0
|
|
176
|
+
|
|
177
|
+
if rss_kb < 100_000
|
|
178
|
+
"#{number_with_delimiter(rss_kb)} KB"
|
|
179
|
+
elsif rss_kb < 10_000_000
|
|
180
|
+
"#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
|
|
181
|
+
else
|
|
182
|
+
"#{number_with_delimiter(rss_kb / (1024.0 * 1024.0), precision: 1)} GB"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
require_relative "base_tab"
|
|
2
|
+
|
|
3
|
+
module Sidekiq
|
|
4
|
+
class TUI
|
|
5
|
+
module Tabs
|
|
6
|
+
class Busy < BaseTab
|
|
7
|
+
def features
|
|
8
|
+
%i[selectable]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def controls
|
|
12
|
+
@controls ||= super + [
|
|
13
|
+
{code: "T", modifiers: ["shift"], description: "Terminate", action: ->(tui, tab) { tab.terminate! }},
|
|
14
|
+
{code: "Q", modifiers: ["shift"], description: "Quiet", action: ->(tui, tab) { tab.quiet! }}
|
|
15
|
+
]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def quiet!
|
|
19
|
+
each_selection do |id|
|
|
20
|
+
Sidekiq::Process.new("identity" => id).quiet!
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def terminate!
|
|
25
|
+
each_selection do |id|
|
|
26
|
+
Sidekiq::Process.new("identity" => id).stop!
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def refresh_data
|
|
31
|
+
refresh_data_for_stats
|
|
32
|
+
|
|
33
|
+
busy = []
|
|
34
|
+
table_row_ids = []
|
|
35
|
+
|
|
36
|
+
Sidekiq::ProcessSet.new.each do |p|
|
|
37
|
+
name = "#{p["hostname"]}:#{p["pid"]}"
|
|
38
|
+
name += " ⭐️" if p.leader?
|
|
39
|
+
name += " 🛑" if p.stopping?
|
|
40
|
+
busy << [
|
|
41
|
+
selected?(p) ? "✅" : "",
|
|
42
|
+
name,
|
|
43
|
+
Time.at(p["started_at"]).utc,
|
|
44
|
+
format_memory(p["rss"].to_i),
|
|
45
|
+
number_with_delimiter(p["concurrency"]),
|
|
46
|
+
number_with_delimiter(p["busy"])
|
|
47
|
+
]
|
|
48
|
+
table_row_ids << p.identity
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@data[:busy] = busy
|
|
52
|
+
@data[:table] = {row_ids: table_row_ids}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def render(tui, frame, area)
|
|
56
|
+
chunks = tui.layout_split(
|
|
57
|
+
area,
|
|
58
|
+
direction: :vertical,
|
|
59
|
+
constraints: [
|
|
60
|
+
tui.constraint_length(4), # Stats
|
|
61
|
+
tui.constraint_length(4), # Status
|
|
62
|
+
tui.constraint_fill(1) # Graph
|
|
63
|
+
]
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
render_stats_section(tui, frame, chunks[0])
|
|
67
|
+
render_status_section(tui, frame, chunks[1])
|
|
68
|
+
render_table(tui, frame, chunks[2]) do
|
|
69
|
+
{
|
|
70
|
+
title: t("Processes"),
|
|
71
|
+
header: ["☑️", "Name", "Started", "RSS", "Threads", "Busy"].map { |x| t(x) },
|
|
72
|
+
widths: [
|
|
73
|
+
tui.constraint_length(5),
|
|
74
|
+
tui.constraint_fill(1),
|
|
75
|
+
tui.constraint_length(24),
|
|
76
|
+
tui.constraint_length(10),
|
|
77
|
+
tui.constraint_length(6),
|
|
78
|
+
tui.constraint_length(6)
|
|
79
|
+
],
|
|
80
|
+
rows: @data[:busy].map.with_index { |cells, idx|
|
|
81
|
+
tui.table_row(
|
|
82
|
+
cells:,
|
|
83
|
+
style: idx.even? ? nil : tui.style(bg: :dark_gray)
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def render_status_section(tui, frame, area)
|
|
91
|
+
values = []
|
|
92
|
+
processes = Sidekiq::ProcessSet.new
|
|
93
|
+
workset = Sidekiq::WorkSet.new
|
|
94
|
+
ws = workset.size
|
|
95
|
+
values << (s = processes.size
|
|
96
|
+
number_with_delimiter(s))
|
|
97
|
+
values << (x = processes.total_concurrency
|
|
98
|
+
number_with_delimiter(x))
|
|
99
|
+
values << number_with_delimiter(ws)
|
|
100
|
+
values << "#{(x == 0) ? 0 : ((ws / x.to_f) * 100).round(0)}%"
|
|
101
|
+
values << format_memory(processes.total_rss)
|
|
102
|
+
|
|
103
|
+
keys = %w[Processes Threads Busy Utilization RSS]
|
|
104
|
+
keys_line = keys.map { |k| t(k).to_s.ljust(12) }.join(" ")
|
|
105
|
+
values_line = values.map { |v| v.to_s.ljust(12) }.join(" ")
|
|
106
|
+
|
|
107
|
+
frame.render_widget(
|
|
108
|
+
tui.paragraph(
|
|
109
|
+
text: [keys_line, values_line],
|
|
110
|
+
block: tui.block(title: t("Status"), borders: [:all])
|
|
111
|
+
),
|
|
112
|
+
area
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require_relative "base_tab"
|
|
2
|
+
require_relative "set_tab"
|
|
3
|
+
|
|
4
|
+
module Sidekiq
|
|
5
|
+
class TUI
|
|
6
|
+
module Tabs
|
|
7
|
+
class Dead < BaseTab
|
|
8
|
+
include SetTab
|
|
9
|
+
|
|
10
|
+
def set_class = Sidekiq::DeadSet
|
|
11
|
+
|
|
12
|
+
def refresh_data
|
|
13
|
+
refresh_data_for_stats
|
|
14
|
+
refresh_data_for_set
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
require_relative "base_tab"
|
|
2
|
+
|
|
3
|
+
module Sidekiq
|
|
4
|
+
class TUI
|
|
5
|
+
module Tabs
|
|
6
|
+
class Home < BaseTab
|
|
7
|
+
def refresh_data
|
|
8
|
+
refresh_data_for_stats
|
|
9
|
+
|
|
10
|
+
stats = Sidekiq::Stats.new
|
|
11
|
+
@data[:chart] ||= {
|
|
12
|
+
previous_stats: {
|
|
13
|
+
processed: stats.processed,
|
|
14
|
+
failed: stats.failed
|
|
15
|
+
},
|
|
16
|
+
deltas: {
|
|
17
|
+
processed: Array.new(50, 0),
|
|
18
|
+
failed: Array.new(50, 0)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
processed_delta = stats.processed - @data[:chart][:previous_stats][:processed]
|
|
23
|
+
failed_delta = stats.failed - @data[:chart][:previous_stats][:failed]
|
|
24
|
+
|
|
25
|
+
@data[:chart][:deltas][:processed].shift
|
|
26
|
+
@data[:chart][:deltas][:processed].push(processed_delta)
|
|
27
|
+
@data[:chart][:deltas][:failed].shift
|
|
28
|
+
@data[:chart][:deltas][:failed].push(failed_delta)
|
|
29
|
+
|
|
30
|
+
@data[:chart][:previous_stats] = {
|
|
31
|
+
processed: stats.processed,
|
|
32
|
+
failed: stats.failed
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
redis_info = Sidekiq.default_configuration.redis_info
|
|
36
|
+
|
|
37
|
+
@data[:redis_info] = {
|
|
38
|
+
version: redis_info["redis_version"] || "N/A",
|
|
39
|
+
uptime_days: redis_info["uptime_in_days"] || "N/A",
|
|
40
|
+
connected_clients: redis_info["connected_clients"] || "N/A",
|
|
41
|
+
used_memory: redis_info["used_memory_human"] || "N/A",
|
|
42
|
+
peak_memory: redis_info["used_memory_peak_human"] || "N/A"
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def render(tui, frame, area)
|
|
47
|
+
chunks = tui.layout_split(
|
|
48
|
+
area,
|
|
49
|
+
direction: :vertical,
|
|
50
|
+
constraints: [
|
|
51
|
+
tui.constraint_length(4), # Stats
|
|
52
|
+
tui.constraint_fill(1), # Graph
|
|
53
|
+
tui.constraint_length(4) # Redis
|
|
54
|
+
]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
render_stats_section(tui, frame, chunks[0])
|
|
58
|
+
render_chart_section(tui, frame, chunks[1])
|
|
59
|
+
render_redis_info_section(tui, frame, chunks[2])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render_chart_section(tui, frame, area)
|
|
63
|
+
max_value = [@data[:chart][:deltas][:processed].max, @data[:chart][:deltas][:failed].max, 1].max
|
|
64
|
+
y_max = [max_value, 5].max
|
|
65
|
+
|
|
66
|
+
processed_data = @data[:chart][:deltas][:processed].each_with_index.map { |value, idx| [idx.to_f, value.to_f] }
|
|
67
|
+
failed_data = @data[:chart][:deltas][:failed].each_with_index.map { |value, idx| [idx.to_f, value.to_f] }
|
|
68
|
+
|
|
69
|
+
datasets = [
|
|
70
|
+
tui.dataset(
|
|
71
|
+
name: "",
|
|
72
|
+
data: processed_data,
|
|
73
|
+
style: tui.style(fg: :green),
|
|
74
|
+
marker: :dot,
|
|
75
|
+
graph_type: :line
|
|
76
|
+
),
|
|
77
|
+
tui.dataset(
|
|
78
|
+
name: "",
|
|
79
|
+
data: failed_data,
|
|
80
|
+
style: tui.style(fg: :red),
|
|
81
|
+
marker: :dot,
|
|
82
|
+
graph_type: :line
|
|
83
|
+
)
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
num_labels = 5
|
|
87
|
+
y_labels = (0...num_labels).map do |i|
|
|
88
|
+
value = ((y_max * i) / (num_labels - 1)).round
|
|
89
|
+
value.to_s
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
beacon_pulse = (Time.now.to_i % 2 == 0) ? "●" : " "
|
|
93
|
+
|
|
94
|
+
chart = tui.chart(
|
|
95
|
+
datasets: datasets,
|
|
96
|
+
x_axis: tui.axis(
|
|
97
|
+
bounds: [0.0, 49.0],
|
|
98
|
+
labels: [],
|
|
99
|
+
style: tui.style(fg: :white)
|
|
100
|
+
),
|
|
101
|
+
y_axis: tui.axis(
|
|
102
|
+
bounds: [0.0, y_max.to_f],
|
|
103
|
+
labels: y_labels,
|
|
104
|
+
style: tui.style(fg: :white)
|
|
105
|
+
),
|
|
106
|
+
block: tui.block(
|
|
107
|
+
title: "Dashboard #{beacon_pulse}",
|
|
108
|
+
borders: [:all]
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
frame.render_widget(chart, area)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def render_redis_info_section(tui, frame, area)
|
|
116
|
+
redis_info = @data[:redis_info]
|
|
117
|
+
|
|
118
|
+
uptime_value = (redis_info[:uptime_days] == "N/A") ? "N/A" : "#{redis_info[:uptime_days]} days"
|
|
119
|
+
|
|
120
|
+
keys = ["Version", "Uptime", "Connected Clients", "Memory Usage", "Peak Memory"]
|
|
121
|
+
values = [
|
|
122
|
+
redis_info[:version].to_s,
|
|
123
|
+
uptime_value,
|
|
124
|
+
redis_info[:connected_clients].to_s,
|
|
125
|
+
redis_info[:used_memory].to_s,
|
|
126
|
+
redis_info[:peak_memory].to_s
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
# Format keys and values with spacing
|
|
130
|
+
keys_line = keys.map { |k| t(k).ljust(18) }.join(" ")
|
|
131
|
+
values_line = values.map { |v| v.ljust(18) }.join(" ")
|
|
132
|
+
|
|
133
|
+
frame.render_widget(
|
|
134
|
+
tui.paragraph(
|
|
135
|
+
text: [keys_line, values_line],
|
|
136
|
+
block: tui.block(title: "Redis Information", borders: [:all])
|
|
137
|
+
),
|
|
138
|
+
area
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
require_relative "base_tab"
|
|
2
|
+
|
|
3
|
+
module Sidekiq
|
|
4
|
+
class TUI
|
|
5
|
+
module Tabs
|
|
6
|
+
class Metrics < BaseTab
|
|
7
|
+
include Filtering
|
|
8
|
+
|
|
9
|
+
COLORS = %i[light_blue light_cyan light_yellow light_red light_green white gray]
|
|
10
|
+
|
|
11
|
+
def features
|
|
12
|
+
%i[filterable]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def on_filter_change
|
|
16
|
+
@data[:metrics_refresh] = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def regexp
|
|
20
|
+
filtering? ? Regexp.new(Regexp.escape(current_filter), Regexp::IGNORECASE) : nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def refresh_data
|
|
24
|
+
refresh_data_for_stats
|
|
25
|
+
|
|
26
|
+
# only need to refresh every 60 seconds
|
|
27
|
+
if !@data[:metrics_refresh] || @data[:metrics_refresh] < Time.now
|
|
28
|
+
q = Sidekiq::Metrics::Query.new
|
|
29
|
+
query_result = q.top_jobs(class_filter: regexp, minutes: 60)
|
|
30
|
+
@data[:metrics] = query_result
|
|
31
|
+
@data[:metrics_refresh] = Time.now + 60
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def render(tui, frame, area)
|
|
36
|
+
chunks = tui.layout_split(
|
|
37
|
+
area,
|
|
38
|
+
direction: :vertical,
|
|
39
|
+
constraints: [
|
|
40
|
+
tui.constraint_length(4), # Stats
|
|
41
|
+
tui.constraint_fill(1) # Chart
|
|
42
|
+
# TOOD Table
|
|
43
|
+
]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
render_stats_section(tui, frame, chunks[0])
|
|
47
|
+
render_metrics_chart(tui, frame, chunks[1])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Run to generate metrics data:
|
|
51
|
+
# cd myapp && bundle install
|
|
52
|
+
# bundle exec rake seed_jobs
|
|
53
|
+
# bundle exec sidekiq
|
|
54
|
+
def render_metrics_chart(tui, frame, area)
|
|
55
|
+
y_max = 5
|
|
56
|
+
csize = COLORS.size
|
|
57
|
+
q = @data[:metrics]
|
|
58
|
+
job_results = q.job_results.sort_by { |(kls, jr)| jr.totals["s"] }.reverse.first(COLORS.size)
|
|
59
|
+
# visible_kls = job_results.first(5).map(&:first)
|
|
60
|
+
# chart_data = {
|
|
61
|
+
# series: job_results.map { |(kls, jr)| [kls, jr.dig("series", "s")] }.to_h,
|
|
62
|
+
# marks: query_result.marks.map { |m| [m.bucket, m.label] },
|
|
63
|
+
# starts_at: query_result.starts_at.iso8601,
|
|
64
|
+
# ends_at: query_result.ends_at.iso8601,
|
|
65
|
+
# visibleKls: visible_kls,
|
|
66
|
+
# yLabel: 'TotalExecutionTime',
|
|
67
|
+
# units: 'seconds',
|
|
68
|
+
# markLabel: '*',
|
|
69
|
+
# }
|
|
70
|
+
|
|
71
|
+
datasets = job_results.map.with_index do |(kls, data), idx|
|
|
72
|
+
# log kls, data, idx
|
|
73
|
+
hrdata = data.dig("series", "s")
|
|
74
|
+
tm = Time.now
|
|
75
|
+
tmi = tm.to_i
|
|
76
|
+
tm = Time.at(tmi - (tmi % 60)).utc
|
|
77
|
+
data = Array.new(60) { |idx| idx }.map do |bucket_idx|
|
|
78
|
+
jumpback = bucket_idx * 60
|
|
79
|
+
value = hrdata[(tm - jumpback).iso8601] || 0
|
|
80
|
+
y_max = value if value > y_max
|
|
81
|
+
# we have 60 data points, newest data should be
|
|
82
|
+
# at highest indexes so we have to rejigger the index
|
|
83
|
+
# here
|
|
84
|
+
[59 - bucket_idx, value]
|
|
85
|
+
end
|
|
86
|
+
# log data
|
|
87
|
+
|
|
88
|
+
# log(data)
|
|
89
|
+
tui.dataset(name: kls,
|
|
90
|
+
data: data,
|
|
91
|
+
style: tui.style(fg: COLORS[idx % csize]),
|
|
92
|
+
marker: :dot,
|
|
93
|
+
graph_type: :line)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
num_labels = 5
|
|
97
|
+
y_labels = (0...num_labels).map do |i|
|
|
98
|
+
value = ((y_max * i) / (num_labels - 1)).round
|
|
99
|
+
value.to_s
|
|
100
|
+
end
|
|
101
|
+
xlabels = [
|
|
102
|
+
q.starts_at.iso8601[11..15],
|
|
103
|
+
q.ends_at.iso8601[11..15]
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
# beacon_pulse = (Time.now.to_i % 2 == 0) ? "●" : " "
|
|
107
|
+
|
|
108
|
+
chart = tui.chart(
|
|
109
|
+
datasets: datasets,
|
|
110
|
+
x_axis: tui.axis(
|
|
111
|
+
bounds: [0.0, 60.0],
|
|
112
|
+
labels: xlabels,
|
|
113
|
+
style: tui.style(fg: :white)
|
|
114
|
+
),
|
|
115
|
+
y_axis: tui.axis(
|
|
116
|
+
bounds: [0.0, y_max.to_f],
|
|
117
|
+
labels: y_labels,
|
|
118
|
+
style: tui.style(fg: :white)
|
|
119
|
+
),
|
|
120
|
+
block: tui.block(
|
|
121
|
+
title: t(name),
|
|
122
|
+
borders: [:all]
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
frame.render_widget(chart, area)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|