sqs_web 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/nritholtz/sqs_web.svg?branch=master)](https://travis-ci.org/nritholtz/sqs_web)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/nritholtz/sqs_web/badges/gpa.svg)](https://codeclimate.com/github/nritholtz/sqs_web)
|
5
|
+
[![Test Coverage](https://codeclimate.com/github/nritholtz/sqs_web/badges/coverage.svg)](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
|
+
![Screen shot](https://www.dropbox.com/s/j14s0aqthj8w3d2/sqs_web_dashboard.png?dl=1)
|
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
|
+
})
|