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 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
+ })