sqs_web 0.0.1
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 +7 -0
- data/Gemfile +5 -0
- data/README.markdown +121 -0
- data/Rakefile +27 -0
- data/bin/sqs_web +10 -0
- data/lib/sqs_web/application/app.rb +89 -0
- data/lib/sqs_web/application/controller_action.rb +16 -0
- data/lib/sqs_web/application/exception_handle.rb +9 -0
- data/lib/sqs_web/application/flash_message.rb +15 -0
- data/lib/sqs_web/application/navigation.rb +42 -0
- data/lib/sqs_web/application/poller_action.rb +41 -0
- data/lib/sqs_web/application/public/javascripts/application.js +58 -0
- data/lib/sqs_web/application/public/javascripts/jquery-1.7.1.min.js +4 -0
- data/lib/sqs_web/application/public/javascripts/jquery.relatize_date.js +117 -0
- data/lib/sqs_web/application/public/stylesheets/reset.css +44 -0
- data/lib/sqs_web/application/public/stylesheets/style.css +58 -0
- data/lib/sqs_web/application/sqs_action.rb +77 -0
- data/lib/sqs_web/application/views/dlq_console.erb +21 -0
- data/lib/sqs_web/application/views/error.erb +3 -0
- data/lib/sqs_web/application/views/layout.erb +33 -0
- data/lib/sqs_web/application/views/message.erb +47 -0
- data/lib/sqs_web/application/views/overview.erb +17 -0
- data/lib/sqs_web/application/views/queue_stats.erb +14 -0
- data/lib/sqs_web/application/views/working.erb +13 -0
- data/lib/sqs_web/railtie.rb +7 -0
- data/lib/sqs_web.rb +9 -0
- data/spec/integration/app_spec.rb +369 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/fake_sqs.rb +19 -0
- data/spec/support/rails_app.rb +12 -0
- data/sqs_web.gemspec +37 -0
- metadata +191 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 402d02606f9f97d50fa51892ee6b372b34d13cea
|
4
|
+
data.tar.gz: 85af922a688dbbe657cd445030827184203fb5eb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fbe157d2c9d2342154a0eb92f043c50664e27afc1dabe04133c93d60ac70cd5371cabb021a880c2b66c2ab77abdd91734e4952d1a7dacc3cd90c58da59a2bb04
|
7
|
+
data.tar.gz: 34d78f607cfaae8ab4d007d2aa698eff188ad41829007b38c319b9c99cb0f34f8a3f64b1255dbce512d8287ffb10f7b8000993b49c9136585cbd3a350a68754f
|
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
sqs_web
|
2
|
+
===============
|
3
|
+
[](https://travis-ci.org/nritholtz/sqs_web)
|
4
|
+
[](https://codeclimate.com/github/nritholtz/sqs_web)
|
5
|
+
[](https://codeclimate.com/github/nritholtz/sqs_web/coverage)
|
6
|
+
|
7
|
+
A [delayed_job_web](https://github.com/ejschmitt/delayed_job_web) inspired (read: stolen) interface for SQS.
|
8
|
+
This gem was written to work anchored within rails 3 and 4 applications.
|
9
|
+
|
10
|
+
Some features:
|
11
|
+
|
12
|
+
* Easily view messages visible and in-flight in your configured SQS queues
|
13
|
+
* Move any single enqueued message, or all enqueued messages, from a DLQ to the corresponding active queue
|
14
|
+
* Remove an enqueued message, or easily remove all enqueued messages from a DLQ
|
15
|
+
* View overview stats for the queues
|
16
|
+
|
17
|
+
The interface:
|
18
|
+
|
19
|
+

|
20
|
+
|
21
|
+
|
22
|
+
Quick Start For Rails 3 and 4 Applications
|
23
|
+
------------------------------------
|
24
|
+
|
25
|
+
Add the dependency to your Gemfile
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
gem "sqs_web"
|
29
|
+
```
|
30
|
+
|
31
|
+
Install it...
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
bundle
|
35
|
+
```
|
36
|
+
|
37
|
+
Add the following route to your application for accessing the interface,
|
38
|
+
and actions related to the messages.
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
match "/sqs" => SqsWeb, :anchor => false, via: [:get, :post]
|
42
|
+
```
|
43
|
+
|
44
|
+
You probably want to password protect the interface, an easy way is to add something like this your config.ru file
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
if Rails.env.production?
|
48
|
+
SqsWeb.use Rack::Auth::Basic do |username, password|
|
49
|
+
username == 'username' && password == 'password'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
`sqs_web` runs as a Sinatra application within the rails application. Visit it at `/sqs`.
|
55
|
+
|
56
|
+
## Supported SqsWeb configuration options
|
57
|
+
`SqsWeb.options[:aws]` supports the following hash key/value pairs:
|
58
|
+
- `access_key_id` : AWS Access Key. If not set will default to environment variable `AWS_ACCESS_KEY_ID` or instance profile credentials
|
59
|
+
- `secret_access_key` : AWS Secret Access Key. If not set will default to environment variable `AWS_SECRET_ACCESS_KEY` or instance profile credentials.
|
60
|
+
- `region`: AWS region for the SQS queue. If not set will default to environment variable `AWS_REGION` or else to `us-east-1`.
|
61
|
+
|
62
|
+
`SqsWeb.options[:queues]` supports an array of strings for the SQS queue names.
|
63
|
+
```ruby
|
64
|
+
SqsWeb.options[:queues] = ["TestSourceQueue", "TestSourceQueueDLQ"]
|
65
|
+
```
|
66
|
+
|
67
|
+
## Notes
|
68
|
+
Currently, this was written in mind for being only used for DLQ management. The [AWS SQS Management Console](https://aws.amazon.com/blogs/aws/aws-management-console-now-supports-the-simple-queue-service-sqs/) should have most of the functionality that you would want out of the box, this plugin is **not meant as a replacement for the AWS SQS Management Console**, but rather as a supplement. There are some features that are not implemented yet (e.g. moving a message from a DLQ back to the source queue) in the AWS Console, and there are some additional benefits for the management screen to live within the application.
|
69
|
+
|
70
|
+
This is not to say there are some features that may be duplicated or added to this plugin as it advances. In addition, our internal applications are using [Shoryuken](https://github.com/phstc/shoryuken) which uses `message attributes` (e.g. *ApproximateReceiveCount*) that become "invalid" once you pick up the message from an active queue. There is greater freedom when managing a DLQ, since this plugin is assuming that the management console (or the AWS SQS Management Console) are the only consumers of the DLQ, which solves the complexity of these `message attributes`.
|
71
|
+
|
72
|
+
## Serving static assets
|
73
|
+
|
74
|
+
If you mount the app on another route, you may encounter the CSS not working anymore. To work around this you can leverage a special HTTP header. Install it, activate it and configure it -- see below.
|
75
|
+
|
76
|
+
### Apache
|
77
|
+
|
78
|
+
XSendFile On
|
79
|
+
XSendFilePath /path/to/shared/bundle
|
80
|
+
|
81
|
+
[`XSendFilePath`](https://tn123.org/mod_xsendfile/) white-lists a directory from which static files are allowed to be served. This should be at least the path to where delayed_job_web is installed.
|
82
|
+
|
83
|
+
Using Rails you'll have to set `config.action_dispatch.x_sendfile_header = "X-Sendfile"`.
|
84
|
+
|
85
|
+
### Nginx
|
86
|
+
|
87
|
+
Nginx uses an equivalent that's called `X-Accel-Redirect`, further instructions can be found [in their wiki](http://wiki.nginx.org/XSendfile).
|
88
|
+
|
89
|
+
Rails' will need to be configured to `config.action_dispatch.x_sendfile_header = "X-Accel-Redirect"`.
|
90
|
+
|
91
|
+
### Lighttpd
|
92
|
+
|
93
|
+
Lighty is more `X-Sendfile`, like [outlined](http://redmine.lighttpd.net/projects/1/wiki/X-LIGHTTPD-send-file) in their wiki.
|
94
|
+
|
95
|
+
|
96
|
+
Contributing
|
97
|
+
------------
|
98
|
+
|
99
|
+
* To bootstrap a `fake_sqs` environment for development purposes, run the following:
|
100
|
+
```ruby
|
101
|
+
bundle exec ruby scripts/bootstrap_queues.rb
|
102
|
+
```
|
103
|
+
|
104
|
+
1. Fork
|
105
|
+
2. Hack
|
106
|
+
3. `rake test`
|
107
|
+
4. Send a pull request
|
108
|
+
|
109
|
+
|
110
|
+
Releasing a new version
|
111
|
+
-----------------------
|
112
|
+
|
113
|
+
1. Update the version in `sqs_web.gemspec`
|
114
|
+
2. `git commit sqs_web.gemspec` with the following message format:
|
115
|
+
|
116
|
+
Version x.x.x
|
117
|
+
|
118
|
+
Changelog:
|
119
|
+
* Some new feature
|
120
|
+
* Some new bug fix
|
121
|
+
3. `rake release`
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'rdoc/task'
|
4
|
+
|
5
|
+
namespace :spec do
|
6
|
+
desc "Run specs with in-memory database"
|
7
|
+
RSpec::Core::RakeTask.new(:memory) do |t|
|
8
|
+
ENV["SQS_DATABASE"] = ":memory:"
|
9
|
+
t.pattern = "spec/integration/*"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Run spec suite with in-memory"
|
14
|
+
task :spec => ["spec:memory"]
|
15
|
+
|
16
|
+
task :default => :spec
|
17
|
+
|
18
|
+
Rake::RDocTask.new do |rdoc|
|
19
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
20
|
+
|
21
|
+
rdoc.rdoc_dir = 'rdoc'
|
22
|
+
rdoc.title = "sqs_web #{version}"
|
23
|
+
rdoc.rdoc_files.include('README*')
|
24
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
25
|
+
end
|
26
|
+
|
27
|
+
Bundler::GemHelper.install_tasks
|
data/bin/sqs_web
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
begin
|
4
|
+
require 'sqs_web/application/app.rb'
|
5
|
+
rescue LoadError => e
|
6
|
+
require 'rubygems'
|
7
|
+
path = File.expand_path '../../lib', __FILE__
|
8
|
+
$:.unshift(path) if File.directory?(path) && !$:.include?(path)
|
9
|
+
require 'sqs_web/application/app.rb'
|
10
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'active_support'
|
3
|
+
require 'aws-sdk'
|
4
|
+
|
5
|
+
class SqsWeb < Sinatra::Base
|
6
|
+
include Navigation
|
7
|
+
include ControllerAction
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def options
|
11
|
+
@@options ||= {queues: [], aws: {region: ENV['AWS_REGION'] || 'us-east-1'}}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
helpers do
|
16
|
+
def flash_message
|
17
|
+
@flash_message ||= FlashMessage.new(session)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
set :root, File.dirname(__FILE__)
|
22
|
+
set :static, true
|
23
|
+
set :public_folder, File.expand_path('../public', __FILE__)
|
24
|
+
set :views, File.expand_path('../views', __FILE__)
|
25
|
+
set :show_exceptions, false
|
26
|
+
|
27
|
+
error Exception do |exception|
|
28
|
+
@error = ExceptionHandle.new(exception)
|
29
|
+
erb :error
|
30
|
+
end
|
31
|
+
|
32
|
+
# Enable sessions so we can use CSRF protection
|
33
|
+
enable :sessions
|
34
|
+
|
35
|
+
set :protection,
|
36
|
+
# Various session protections
|
37
|
+
:session => true,
|
38
|
+
# Various non-default Rack::Protection options
|
39
|
+
:use => [
|
40
|
+
# Prevent destructive actions without a valid CSRF auth token
|
41
|
+
:authenticity_token,
|
42
|
+
# Prevent destructive actions with remote referrers
|
43
|
+
:remote_referrer
|
44
|
+
],
|
45
|
+
# Deny the request, don't clear the session
|
46
|
+
:reaction => :deny
|
47
|
+
|
48
|
+
get '/overview' do
|
49
|
+
@stats = queue_stats
|
50
|
+
erb :overview
|
51
|
+
end
|
52
|
+
|
53
|
+
get "/dlq_console" do
|
54
|
+
@messages = messages
|
55
|
+
erb :dlq_console
|
56
|
+
end
|
57
|
+
|
58
|
+
%w(remove requeue).each do |action|
|
59
|
+
post "/#{action}/:queue_name/:message_id" do
|
60
|
+
process_page_single_request(action: action, params: params)
|
61
|
+
redirect back
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
%w(bulk_remove bulk_requeue).each do |action|
|
66
|
+
post "/#{action}" do
|
67
|
+
process_page_bulk_request(action: action.split('bulk_')[1], params: params) if params["message_collection"]
|
68
|
+
redirect back
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
get "/?" do
|
73
|
+
redirect u(:overview)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
def queue_stats
|
78
|
+
@queues ||= SqsAction.load_queue_urls
|
79
|
+
SqsAction.get_queue_stats(@queues)
|
80
|
+
end
|
81
|
+
|
82
|
+
def messages(options={})
|
83
|
+
@queues ||= SqsAction.load_queue_urls
|
84
|
+
@queues.select{|queue| queue[:source_url]}.each_with_object([]) do |queue, messages_result|
|
85
|
+
PollerAction.poll_by_queue_and_result_and_options(queue, messages_result, options.merge(flash_message: flash_message))
|
86
|
+
SqsAction.expire_messages(messages_result.reject{|c| c[:deleted]})
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ControllerAction
|
2
|
+
def process_page_single_request(options={})
|
3
|
+
result = messages({action: options[:action].to_sym, message_id: options[:params][:message_id], queue_name: options[:params][:queue_name]})
|
4
|
+
flash_message.message = "Message ID: #{options[:params][:message_id]} in Queue #{options[:params][:queue_name]} has already been deleted or is not visible." if result.empty?
|
5
|
+
end
|
6
|
+
|
7
|
+
def process_page_bulk_request(options={})
|
8
|
+
options[:params]["message_collection"].map!{|c| {message_id: c.split('/', 2)[0], queue_name: c.split('/', 2)[1]}}
|
9
|
+
result = messages({action: options[:action].to_sym, messages: options[:params]["message_collection"], bulk_action: true})
|
10
|
+
flash_message.message = if result.select{|c| c[:deleted]}.size != options[:params]["message_collection"].size
|
11
|
+
"One or more messages may have already been #{options[:action]}d or is not visible."
|
12
|
+
else
|
13
|
+
"Selected messages have been #{options[:action]}d successfully."
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class FlashMessage
|
2
|
+
def initialize(session)
|
3
|
+
@session ||= session
|
4
|
+
end
|
5
|
+
|
6
|
+
def message=(message)
|
7
|
+
@session[:flash_message] = message
|
8
|
+
end
|
9
|
+
|
10
|
+
def message
|
11
|
+
message = @session[:flash_message] #tmp get the value
|
12
|
+
@session[:flash_message] = nil # unset the value
|
13
|
+
message # display the value
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Navigation
|
2
|
+
def url_path(*path_parts)
|
3
|
+
[ path_prefix, path_parts ].join("/").squeeze('/')
|
4
|
+
end
|
5
|
+
|
6
|
+
alias_method :u, :url_path
|
7
|
+
|
8
|
+
def h(text)
|
9
|
+
Rack::Utils.escape_html(text)
|
10
|
+
end
|
11
|
+
|
12
|
+
def path_prefix
|
13
|
+
request.env['SCRIPT_NAME']
|
14
|
+
end
|
15
|
+
|
16
|
+
def tabs
|
17
|
+
[
|
18
|
+
{:name => 'Overview', :path => '/overview'},
|
19
|
+
{:name => 'DLQ Console', :path => '/dlq_console'}
|
20
|
+
]
|
21
|
+
end
|
22
|
+
|
23
|
+
def csrf_token
|
24
|
+
# Set up by Rack::Protection
|
25
|
+
session[:csrf]
|
26
|
+
end
|
27
|
+
|
28
|
+
def csrf_token_tag
|
29
|
+
# If csrf_token is nil, and we submit a blank string authenticity_token
|
30
|
+
# param, Rack::Protection will fail.
|
31
|
+
if csrf_token
|
32
|
+
"<input type='hidden' name='authenticity_token' value='#{h csrf_token}'>"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def partial(template, local_vars = {})
|
37
|
+
@partial = true
|
38
|
+
erb(template.to_sym, {:layout => false}, local_vars)
|
39
|
+
ensure
|
40
|
+
@partial = false
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class PollerAction
|
2
|
+
|
3
|
+
protected
|
4
|
+
def self.poll_by_queue_and_result_and_options(queue, result, options)
|
5
|
+
poller = Aws::SQS::QueuePoller.new(queue[:url], {client: SqsAction.sqs, skip_delete: true, idle_timeout: 0.2,
|
6
|
+
wait_time_seconds: 0, visibility_timeout: options[:visibility_timeout] || 5 })
|
7
|
+
Timeout::timeout(30) do
|
8
|
+
poller.poll do |message|
|
9
|
+
process_message_by_result_and_options(result,
|
10
|
+
options.merge({ poller: poller, message: message, queue: queue})
|
11
|
+
)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.process_message_by_result_and_options(result, options)
|
17
|
+
if message_match(options)
|
18
|
+
SqsAction.process_action_by_message(options[:message], options)
|
19
|
+
signal_deleted_message(result, options)
|
20
|
+
else
|
21
|
+
result << {message: options[:message], queue: options[:queue]}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.message_match(options)
|
26
|
+
if options[:bulk_action]
|
27
|
+
options[:messages].find{|message| match_message_by_options(message[:message_id], message[:queue_name], options)}
|
28
|
+
else
|
29
|
+
options[:action] && match_message_by_options(options[:message_id], options[:queue_name], options)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.match_message_by_options(message_id, queue_name, options)
|
34
|
+
message_id == options[:message].message_id && queue_name == options[:queue][:name]
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.signal_deleted_message(result, options)
|
38
|
+
result << {message: options[:message], queue: options[:queue], deleted: true}
|
39
|
+
throw :stop_polling unless (options[:messages] && result.select{|c| c[:deleted]}.size < options[:messages].size)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
$(function() {
|
2
|
+
var poll_interval = 3;
|
3
|
+
|
4
|
+
var relatizer = function(){
|
5
|
+
var dt = $(this).text(), relatized = $.relatizeDate(this)
|
6
|
+
if ($(this).parents("a").length > 0 || $(this).is("a")) {
|
7
|
+
$(this).relatizeDate()
|
8
|
+
if (!$(this).attr('title')) {
|
9
|
+
$(this).attr('title', dt)
|
10
|
+
}
|
11
|
+
} else {
|
12
|
+
$(this)
|
13
|
+
.text('')
|
14
|
+
.append( $('<a href="#" class="toggle_format" title="' + dt + '" />')
|
15
|
+
.append('<span class="date_time">' + dt +
|
16
|
+
'</span><span class="relatized_time">' +
|
17
|
+
relatized + '</span>') )
|
18
|
+
}
|
19
|
+
};
|
20
|
+
|
21
|
+
$('.time').each(relatizer);
|
22
|
+
|
23
|
+
$('.time a.toggle_format .date_time').hide();
|
24
|
+
|
25
|
+
var format_toggler = function(){
|
26
|
+
$('.time a.toggle_format span').toggle();
|
27
|
+
$(this).attr('title', $('span:hidden',this).text());
|
28
|
+
return false;
|
29
|
+
};
|
30
|
+
|
31
|
+
$('.time a.toggle_format').click(format_toggler);
|
32
|
+
|
33
|
+
$('ul li.job').hover(function() {
|
34
|
+
$(this).addClass('hover');
|
35
|
+
}, function() {
|
36
|
+
$(this).removeClass('hover');
|
37
|
+
})
|
38
|
+
|
39
|
+
$('a.backtrace').click(function (e) {
|
40
|
+
e.preventDefault();
|
41
|
+
if($(this).prev('div.backtrace:visible').length > 0) {
|
42
|
+
$(this).next('div.backtrace').show();
|
43
|
+
$(this).prev('div.backtrace').hide();
|
44
|
+
} else {
|
45
|
+
$(this).next('div.backtrace').hide();
|
46
|
+
$(this).prev('div.backtrace').show();
|
47
|
+
}
|
48
|
+
});
|
49
|
+
|
50
|
+
$("#select_all").click(function (e) {
|
51
|
+
var checked = $(this).is(':checked');
|
52
|
+
$('.bulk_check').attr('checked', checked);
|
53
|
+
});
|
54
|
+
|
55
|
+
$("#bulk_action_submit input").click(function() {
|
56
|
+
$('#bulk_action_form').attr("action", $(this).attr("action"));
|
57
|
+
});
|
58
|
+
})
|