sqs_web 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+ # To be removed once fake_sqs merges my PR changes
5
+ gem 'fake_sqs', git: 'https://github.com/nritholtz/fake_sqs.git', ref: '1b0d283'
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,9 @@
1
+ class ExceptionHandle
2
+ attr_reader :error_class, :error_message, :error_backtrace
3
+
4
+ def initialize(error)
5
+ @error_class = error.inspect.to_s
6
+ @error_message = error.message.to_s
7
+ @error_backtrace = error.backtrace.to_s
8
+ end
9
+ 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
+ })