Skip to content

Commit b263b03

Browse files
author
David Verba
committed
Add tab in sidekiq web ui for bus info
- copypasta from resque-bus for query construction - add/register custom tab and routes for bus info and unsubscribe - minimal styling of tables - bump version
1 parent d7d0e52 commit b263b03

File tree

3 files changed

+342
-0
lines changed

3 files changed

+342
-0
lines changed

lib/sidekiq-bus.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'queue-bus'
44
require 'sidekiq_bus/adapter'
55
require 'sidekiq_bus/version'
6+
require 'sidekiq_bus/server'
67
require 'sidekiq_bus/middleware/retry'
78

89
module SidekiqBus

lib/sidekiq_bus/server.rb

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
require 'sidekiq/web'
2+
3+
module SidekiqBus
4+
module Server
5+
def self.registered(app)
6+
app.get('/bus') do
7+
# you can define @instance_variables for passing into template
8+
# Sidekiq uses erb for its templates so you should do it aswell
9+
erb File.read(File.join(File.dirname(__FILE__), "server/views/bus.erb"))
10+
end
11+
12+
app.post '/bus/unsubscribe' do
13+
bus_app = ::QueueBus::Application.new(params[:name]).unsubscribe
14+
redirect "#{root_path}bus"
15+
end
16+
end
17+
18+
class Helpers
19+
class << self
20+
def parse_query(query_string)
21+
has_open_brace = query_string.include?("{")
22+
has_close_brace = query_string.include?("}")
23+
has_multiple_lines = query_string.include?("\n")
24+
has_colon = query_string.include?(":")
25+
has_comma = query_string.include?(",")
26+
has_quote = query_string.include?("\"")
27+
28+
exception = nil
29+
30+
# first let's see if it parses
31+
begin
32+
query_attributes = JSON.parse(query_string)
33+
raise "Not a JSON Object" unless query_attributes.is_a?(Hash)
34+
rescue Exception => e
35+
exception = e
36+
end
37+
return query_attributes unless exception
38+
39+
if query_attributes
40+
# it parsed but it's something else
41+
if query_attributes.is_a?(Array) && query_attributes.length == 1
42+
# maybe it's the thing from the queue
43+
json_string = query_attributes.first
44+
fixed = JSON.parse(json_string) rescue nil
45+
return fixed if fixed
46+
end
47+
48+
# something else?
49+
raise exception
50+
end
51+
52+
if !has_open_brace && !has_close_brace
53+
# maybe they just forgot the braces
54+
fixed = JSON.parse("{ #{query_string} }") rescue nil
55+
return fixed if fixed
56+
end
57+
58+
if !has_open_brace
59+
# maybe they just forgot the braces
60+
fixed = JSON.parse("{ #{query_string}") rescue nil
61+
return fixed if fixed
62+
end
63+
64+
if !has_close_brace
65+
# maybe they just forgot the braces
66+
fixed = JSON.parse("#{query_string} }") rescue nil
67+
return fixed if fixed
68+
end
69+
70+
if !has_multiple_lines && !has_colon && !has_open_brace && !has_close_brace
71+
# we say they just put a bus_event type here, so help them out
72+
return {"bus_event_type" => query_string, "more_here" => true}
73+
end
74+
75+
if has_colon && !has_quote
76+
# maybe it's some search syntax like this: field: value other: true, etc
77+
# maybe use something like this later: https://github.com/dxwcyber/search-query-parser
78+
79+
# quote all the strings, (simply) tries to avoid integers
80+
test_query = query_string.gsub(/([a-zA-z]\w*)/,'"\0"')
81+
if !has_comma
82+
test_query.gsub!("\n", ",\n")
83+
end
84+
if !has_open_brace && !has_close_brace
85+
test_query = "{ #{test_query} }"
86+
end
87+
88+
fixed = JSON.parse(test_query) rescue nil
89+
return fixed if fixed
90+
end
91+
92+
if has_open_brace && has_close_brace
93+
# maybe the whole thing is a hash output from a hash.inspect log
94+
ruby_hash_text = query_string.clone
95+
# https://stackoverflow.com/questions/1667630/how-do-i-convert-a-string-object-into-a-hash-object
96+
# Transform object string symbols to quoted strings
97+
ruby_hash_text.gsub!(/([{,]\s*):([^>\s]+)\s*=>/, '\1"\2"=>')
98+
# Transform object string numbers to quoted strings
99+
ruby_hash_text.gsub!(/([{,]\s*)([0-9]+\.?[0-9]*)\s*=>/, '\1"\2"=>')
100+
# Transform object value symbols to quotes strings
101+
ruby_hash_text.gsub!(/([{,]\s*)(".+?"|[0-9]+\.?[0-9]*)\s*=>\s*:([^,}\s]+\s*)/, '\1\2=>"\3"')
102+
# Transform array value symbols to quotes strings
103+
ruby_hash_text.gsub!(/([\[,]\s*):([^,\]\s]+)/, '\1"\2"')
104+
# fix up nil situation
105+
ruby_hash_text.gsub!(/=>nil/, '=>null')
106+
# Transform object string object value delimiter to colon delimiter
107+
ruby_hash_text.gsub!(/([{,]\s*)(".+?"|[0-9]+\.?[0-9]*)\s*=>/, '\1\2:')
108+
fixed = JSON.parse(ruby_hash_text) rescue nil
109+
return fixed if fixed
110+
end
111+
112+
raise exception
113+
end
114+
115+
def sort_query(query_attributes)
116+
query_attributes.each do |key, value|
117+
if value.is_a?(Hash)
118+
query_attributes[key] = sort_query(value)
119+
end
120+
end
121+
query_attributes.sort_by { |key| key }.to_h
122+
end
123+
124+
def query_subscriptions(app, query_attributes)
125+
# TODO: all of this can move to method in queue-bus to replace event_display_tuples
126+
if query_attributes
127+
subscriptions = app.subscription_matches(query_attributes)
128+
else
129+
subscriptions = app.send(:subscriptions).all
130+
end
131+
end
132+
end
133+
end
134+
end
135+
end
136+
137+
Sidekiq::Web.register SidekiqBus::Server
138+
Sidekiq::Web.locales << File.expand_path(File.dirname(__FILE__) + "/web/locales")
139+
Sidekiq::Web.tabs['Bus'] = 'bus'
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
2+
<script LANGUAGE="JavaScript">
3+
<!--
4+
function confirmSubmit()
5+
{
6+
var agree=confirm("Are you sure you wish to continue?");
7+
if (agree)
8+
return true ;
9+
else
10+
return false ;
11+
}
12+
13+
function setSample() {
14+
var text = document.getElementById("query_attributes").textContent;
15+
var textArea = document.getElementById('querytext');
16+
textArea.value = text;
17+
return false;
18+
}
19+
// -->
20+
</script>
21+
22+
<%
23+
app_hash = {}
24+
class_hash = {}
25+
event_hash = {}
26+
query_string = params[:query].to_s.strip
27+
28+
query_attributes = nil
29+
query_error = nil
30+
if query_string.length > 0
31+
begin
32+
query_attributes = ::SidekiqBus::Server::Helpers.parse_query(query_string)
33+
raise "Not a JSON Object" unless query_attributes.is_a?(Hash)
34+
rescue Exception => e
35+
query_attributes = nil
36+
query_error = e.message
37+
end
38+
39+
if query_attributes
40+
# sort keys for display
41+
query_attributes = ::SidekiqBus::Server::Helpers.sort_query(query_attributes)
42+
end
43+
end
44+
45+
# collect each differently
46+
::QueueBus::Application.all.each do |app|
47+
app_key = app.app_key
48+
49+
subscriptions = ::SidekiqBus::Server::Helpers.query_subscriptions(app, query_attributes)
50+
subscriptions.each do |sub|
51+
class_name = sub.class_name
52+
queue = sub.queue_name
53+
filters = sub.matcher.filters
54+
sub_key = sub.key
55+
56+
if filters["bus_event_type"]
57+
event = filters["bus_event_type"]
58+
else
59+
event = "see filter"
60+
end
61+
62+
app_hash[app_key] ||= []
63+
app_hash[app_key] << [sub_key, event, class_name, queue, filters]
64+
65+
class_hash[class_name] ||= []
66+
class_hash[class_name] << [app_key, sub_key, event, queue, filters]
67+
68+
event_hash[event] ||= []
69+
event_hash[event] << [app_key, sub_key, class_name, queue, filters]
70+
end
71+
end
72+
73+
# sort each list item by secondary label
74+
class_hash.each do |_, array|
75+
array.sort!{ |a,b| a.first <=> b.first }
76+
end
77+
event_hash.each do |_, array|
78+
array.sort!{ |a,b| a.first <=> b.first }
79+
end
80+
81+
# helper to display either
82+
def display_row(name, val, button=nil, first=false)
83+
form = ""
84+
if button
85+
text, url = button
86+
form = "<form method='POST' action='#{url}' style='float:left; padding:0 5px 0 0;margin:0;'>#{csrf_tag}<input type='submit' name='' value='#{h(text)}' style='padding:0;margin:0;' onclick=\"return confirmSubmit();\"/><input type='hidden' name='name' value='#{h(name)}' /></form>"
87+
end
88+
89+
if !val
90+
out = "<td>&nbsp;</td><td>&nbsp;</td>"
91+
else
92+
one, two, three, queue, filters = val
93+
out = "<td>#{h(one)}</td><td>#{h(two)}</td><td>#{h(three)}</td><td><a href='#{"queues/#{queue}"}'>#{h(queue)}</a></td>"
94+
out << "<td>#{h(::QueueBus::Util.encode(filters).gsub(/\"bus_special_value_(\w+)\"/){ "(#{$1})" }).gsub(" ", "&nbsp;").gsub('&quot;,&quot;', '&quot;, &quot;')}</td>"
95+
end
96+
97+
if first
98+
"<tr><td>#{h(name)}#{form}</td>#{out}</tr>\n"
99+
else
100+
"<tr><td>&nbsp;</td>#{out}</tr>\n"
101+
end
102+
end
103+
104+
def output_hash(hash, action=nil)
105+
out = ""
106+
hash.keys.sort.each do |item|
107+
display = hash[item]
108+
first = display.shift
109+
out << display_row(item, first, action, true)
110+
display.each do |val|
111+
out << display_row(item, val, action)
112+
end
113+
end
114+
out
115+
end
116+
%>
117+
118+
<h1 class='wi'>Sample Event</h1>
119+
<p class='intro'>Enter JSON of an event to see applicable subscriptions.</p>
120+
<div style="text-align: center;width:700px;">
121+
-<form method="GET" action="<%= "bus" %>" style="float:none;padding:0;margin:0;">
122+
<textarea id="querytext" name="query" style="padding: 10px;height:150px;width:700px;font-size:14px;font-family:monospace"><%=
123+
h(query_string)
124+
%></textarea>
125+
<br/>
126+
<button onclick="window.location.href = '<%= "bus" %>'; return false;">Clear</button>
127+
<input type="submit" name="" value="Filter results to this event"/>
128+
</form>
129+
</div>
130+
131+
<% if query_error %>
132+
<blockquote><pre style="text-align:left;font-family:monospace;margin:5px 0 5px 0;padding:10px;background:#f2dede;color:#a94442;"><code><%=
133+
h(query_error.strip)
134+
%></code></pre></blockquote>
135+
<% end %>
136+
<% if query_attributes %>
137+
<blockquote><pre style="text-align:left;font-family:monospace;margin:5px 0 5px 0;padding:10px;background:#dff0d8;color:#3c763d;"><code id="query_attributes"><%=
138+
h(JSON.pretty_generate(query_attributes).strip)
139+
%></code></pre></blockquote>
140+
<div style="text-align:right;">
141+
<button onclick="return setSample();">Set Sample</button>
142+
</div>
143+
<% end %>
144+
145+
<hr/>
146+
147+
<h1 class='wi'>Applications</h1>
148+
<p class='intro'>The apps below have registered the given classes and queues.</p>
149+
<table class='table-striped table-bordered'>
150+
<thead class="thead-dark">
151+
<tr>
152+
<th>App Key</th>
153+
<th>Subscription Key</th>
154+
<th>Event Type</th>
155+
<th>Class Name</th>
156+
<th>Queue</th>
157+
<th>Filters</th>
158+
</tr>
159+
</thead>
160+
161+
<%= output_hash(app_hash, query_attributes ? false : ["Unsubscribe", "bus/unsubscribe"]) %>
162+
</table>
163+
164+
<p>&nbsp;</p>
165+
166+
<h1 class='wi'>Events</h1>
167+
<p class='intro'>The events below have been registered by the given applications and queues.</p>
168+
169+
<table class='table-striped table-bordered'>
170+
<thead class="thead-dark">
171+
<tr>
172+
<th>Event Type</th>
173+
<th>App Key</th>
174+
<th>Subscription Key</th>
175+
<th>Class Name</th>
176+
<th>Queue</th>
177+
<th>Filters</th>
178+
</tr>
179+
</thead>
180+
<%= output_hash(event_hash, false) %>
181+
</table>
182+
183+
184+
185+
<p>&nbsp;</p>
186+
187+
<h1 class='wi'>Classes</h1>
188+
<p class='intro'>The classes below have been registered by the given applications and queues.</p>
189+
<table class='table-striped table-bordered'>
190+
<thead class="thead-dark">
191+
<tr>
192+
<th>Class Name</th>
193+
<th>App Key</th>
194+
<th>Subscription Key</th>
195+
<th>Event Type</th>
196+
<th>Queue</th>
197+
<th>Filters</th>
198+
</tr>
199+
</thead>
200+
<%= output_hash(class_hash, false) %>
201+
</table>
202+

0 commit comments

Comments
 (0)