toro 0.2.1 → 0.2.2

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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +7 -0
  3. data/Appraisals +7 -0
  4. data/Gemfile +3 -0
  5. data/app/assets/javascripts/toro/monitor/abstract_jobs_table.js.coffee +188 -0
  6. data/app/assets/javascripts/toro/monitor/application.js.coffee +14 -0
  7. data/app/assets/javascripts/toro/monitor/chart.js.coffee +203 -0
  8. data/app/assets/javascripts/toro/monitor/initialize.js.coffee.erb +25 -0
  9. data/app/assets/javascripts/toro/monitor/jobs_table.js.coffee +63 -0
  10. data/app/assets/javascripts/toro/monitor/processes_table.js.coffee +96 -0
  11. data/app/assets/javascripts/toro/monitor/queue_jobs_table.js.coffee +91 -0
  12. data/app/assets/stylesheets/toro/monitor/application.css.sass +5 -0
  13. data/app/assets/stylesheets/toro/monitor/jobs_table.css.sass +69 -0
  14. data/app/assets/stylesheets/toro/monitor/layout.css.sass +5 -0
  15. data/app/controllers/toro/monitor/api/jobs_controller.rb +65 -0
  16. data/app/controllers/toro/monitor/api/processes_controller.rb +13 -0
  17. data/app/controllers/toro/monitor/api/queues_controller.rb +24 -0
  18. data/app/controllers/toro/monitor/base_controller.rb +11 -0
  19. data/app/controllers/toro/monitor/jobs_controller.rb +11 -0
  20. data/app/controllers/toro/monitor/processes_controller.rb +8 -0
  21. data/app/controllers/toro/monitor/queues_controller.rb +9 -0
  22. data/app/datatables/toro/monitor/abstract_datatable.rb +71 -0
  23. data/app/datatables/toro/monitor/jobs_datatable.rb +60 -0
  24. data/app/datatables/toro/monitor/processes_datatable.rb +60 -0
  25. data/app/helpers/toro/monitor/toro_monitor_helper.rb +13 -0
  26. data/app/views/toro/monitor/jobs/_jobs.slim +14 -0
  27. data/app/views/toro/monitor/jobs/chart.slim +1 -0
  28. data/app/views/toro/monitor/jobs/index.slim +1 -0
  29. data/app/views/toro/monitor/layouts/application.slim +32 -0
  30. data/app/views/toro/monitor/processes/index.slim +14 -0
  31. data/app/views/toro/monitor/queues/_jobs.slim +14 -0
  32. data/app/views/toro/monitor/queues/index.slim +5 -0
  33. data/config/routes.rb +16 -0
  34. data/examples/chart.png +0 -0
  35. data/examples/job.png +0 -0
  36. data/examples/jobs.png +0 -0
  37. data/examples/processes.png +0 -0
  38. data/examples/queues.png +0 -0
  39. data/gemfiles/rails_3.gemfile +7 -0
  40. data/gemfiles/rails_3.gemfile.lock +146 -0
  41. data/gemfiles/rails_4.gemfile +7 -0
  42. data/gemfiles/rails_4.gemfile.lock +147 -0
  43. data/lib/toro/version.rb +1 -1
  44. data/spec/config/database.yml.example +6 -0
  45. data/spec/lib/cli_spec.rb +28 -0
  46. data/spec/lib/client_spec.rb +26 -0
  47. data/spec/lib/fetcher_spec.rb +105 -0
  48. data/spec/lib/job_spec.rb +17 -0
  49. data/spec/lib/listener_spec.rb +72 -0
  50. data/spec/lib/manager_spec.rb +78 -0
  51. data/spec/lib/middleware/server/error_spec.rb +18 -0
  52. data/spec/lib/middleware/server/error_storage_spec.rb +22 -0
  53. data/spec/lib/middleware/server/properties_spec.rb +18 -0
  54. data/spec/lib/middleware/server/retry_spec.rb +38 -0
  55. data/spec/lib/processor_spec.rb +33 -0
  56. data/spec/lib/worker_spec.rb +50 -0
  57. data/spec/spec_helper.rb +31 -0
  58. data/spec/support/active_record_spec_helper.rb +18 -0
  59. data/spec/support/jobs_helper.rb +4 -0
  60. data/spec/support/workers_helper.rb +56 -0
  61. data/toro.gemspec +29 -0
  62. data/vendor/assets/javascripts/toro/monitor/bootstrap-select.min.js +1 -0
  63. data/vendor/assets/javascripts/toro/monitor/bootstrap.min.js +6 -0
  64. data/vendor/assets/javascripts/toro/monitor/d3.js +8795 -0
  65. data/vendor/assets/javascripts/toro/monitor/datatables.bootstrap.js +96 -0
  66. data/vendor/assets/javascripts/toro/monitor/datatables.js +256 -0
  67. data/vendor/assets/javascripts/toro/monitor/datatables.standing_redraw.js.coffee +5 -0
  68. data/vendor/assets/javascripts/toro/monitor/jquery.timeago.js +27 -0
  69. data/vendor/assets/stylesheets/toro/monitor/bootstrap-select.min.css +1 -0
  70. data/vendor/assets/stylesheets/toro/monitor/bootstrap.min.css +9 -0
  71. metadata +103 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 07ee787aba69a5ad1b58e57f9a6c261cb177d879
4
- data.tar.gz: e14fbe84cd89cacf59a332f943a84d9f00f87016
3
+ metadata.gz: d512053c857d51305148c1d6e40ef0bad33c67b7
4
+ data.tar.gz: d5f16d52befe81638484fa1b8dea32d8a7e6ecab
5
5
  SHA512:
6
- metadata.gz: 8b1df2c1919267187c1b497ae6ecc6abe742f2e703fe2376703377bd3c4df27f12770f35305dbbfeacb1da5343f8b669a812b74508e6d444d1b8b80d294080fb
7
- data.tar.gz: 06123458e653d8ac6d51ca486d0739ff27463eb5a9f91b8ed0afb373553a10dc1621a454edc85af0600dbc8ea73df2b3ee2a3346ca13a2bef29843567067f359
6
+ metadata.gz: 7f8e7812ff28d22e87f77e98eb11a15e3ce503671d9b6988f67f5e42896e1e0301c6dff2e0d16b535aabe51f58ce6c10485c8edbd96c1cafd161a5bc692a9a15
7
+ data.tar.gz: 59a44752ec748288e189d87f6f31f781173d07845e2a9a4ee00c0d3d5d58c090cf62d1431c04b8876f0614d1e17433f56b80835625b5a08fa5cfc70d62e59e91
@@ -0,0 +1,7 @@
1
+ .DS_Store
2
+ Gemfile.lock
3
+ spec/config/database.yml
4
+ *.gem
5
+ log/*.log
6
+ .bundle/
7
+ pkg/
@@ -0,0 +1,7 @@
1
+ appraise "rails-3" do
2
+ gem "rails", "3.2.17"
3
+ end
4
+
5
+ appraise "rails-4" do
6
+ gem "rails", "4.1.0"
7
+ end
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,188 @@
1
+ class ToroMonitor.AbstractJobsTable
2
+
3
+ constructor: ->
4
+ @api_params = {}
5
+ @initialize()
6
+
7
+ initialize_with_options: (options) =>
8
+ @options = options
9
+ @table = $(@options.table_selector)
10
+
11
+ @columns = @options.columns
12
+ @status_filter = null
13
+
14
+ @table.dataTable
15
+ bProcessing: true
16
+ bServerSide: true
17
+ sAjaxSource: @table.data('source')
18
+ iDisplayLength: 10
19
+ aaSorting: [[@columns.created_at, 'desc']]
20
+ sPaginationType: 'bootstrap'
21
+ aoColumns: @options.column_options
22
+ oLanguage:
23
+ sInfo: '_TOTAL_ jobs'
24
+ sInfoFiltered: ' (filtered from _MAX_)'
25
+ sLengthMenu: 'Per page: _MENU_'
26
+ sSearch: ''
27
+ fnRowCallback: (nRow, aData, iDisplayIndex) =>
28
+ $('.timeago', nRow).timeago() if nRow
29
+ fnInitComplete: () =>
30
+ filter_container = @table.siblings('.dataTables_filter')
31
+ filter_container.find('input').attr('placeholder', 'Search...')
32
+ $.getJSON ToroMonitor.settings.api_url('jobs/statuses'), (statuses) =>
33
+ status_filter_html = ''
34
+ $.each statuses, (key, status) =>
35
+ status_filter_html += """<button type="button" class="btn btn-small" data-value="#{status}">#{status}</button>"""
36
+ status_filter_html = """<div class="btn-group status-filter" data-toggle="buttons-radio">#{status_filter_html}</div>"""
37
+ filter_container.prepend status_filter_html
38
+ @status_filter = filter_container.find('.status-filter')
39
+ fnServerData: (sSource, aoData, fnCallback) =>
40
+ $.each @api_params, (key, value) =>
41
+ aoData.push
42
+ name: key
43
+ value: @api_params[key]
44
+ $.getJSON sSource, aoData, (json) -> fnCallback(json)
45
+
46
+ @table.parents('.dataTables_wrapper').addClass('jobs-table-wrapper')
47
+
48
+ @initialize_ui()
49
+
50
+ initialize_ui: =>
51
+
52
+ @table.on 'click', '.status-value', (e) =>
53
+ tr = $(e.target).parents('tr:first')[0]
54
+ job = @table.fnGetData(tr)
55
+ @show_job(job)
56
+ false
57
+ @table.on 'click', '.retry-job', (e) =>
58
+ id = $(e.target).attr('data-job-id')
59
+ $.getJSON ToroMonitor.settings.api_url('jobs/retry/'+id), =>
60
+ @reload_table()
61
+ false
62
+
63
+ $('body').on 'click', '.status-filter .btn', (e) =>
64
+ e.stopPropagation()
65
+ btn = $(e.target)
66
+ btn.siblings('.btn').removeClass('active')
67
+ btn.toggleClass('active')
68
+ value = @status_filter.find('.active:first').attr('data-value')
69
+ value = '' if !value?
70
+ @table.fnFilter(value, @columns.status - 1)
71
+
72
+ @start_polling()
73
+
74
+ show_job: (job) =>
75
+ return false if !job?
76
+
77
+ id = job[@columns.id]
78
+ started_by = job[@columns.started_by]
79
+ class_name = job[@columns.class_name]
80
+ name = job[@columns.name]
81
+ started_at = job[@columns.started_at]
82
+ duration = job[@columns.duration]
83
+ status = job[@columns.status]
84
+ properties = job[@columns.properties]
85
+ args = job[@columns.args]
86
+
87
+ # TODO: Make this cleaner; is there a way to grab the original value returned by the server?
88
+ status = $("<div>#{status}</div>").find('.status-value').text()
89
+
90
+ properties_html = ''
91
+ if properties?
92
+ rows_html = ''
93
+ if status == 'failed'
94
+ properties_html += """
95
+ <h4>Error</h4>
96
+ #{properties['error:message']}
97
+ <h5>Backtrace</h5>
98
+ <pre>
99
+ #{properties['error:backtrace'].join("\n")}
100
+ </pre>
101
+ """
102
+ for key, value of properties
103
+ rows_html += "<tr><td>#{key}</td><td>#{JSON.stringify(value, null, 2)}</td></tr>"
104
+ if rows_html
105
+ properties_html += """
106
+ <h4>Properties</h4>
107
+ <table class="table table-striped">
108
+ #{rows_html}
109
+ </table>
110
+ """
111
+
112
+ modal_html = """
113
+ <div class="modal hide fade job-modal" role="dialog">
114
+ <div class="modal-header">
115
+ <button type="button" class="close" data-dismiss="modal">×</button>
116
+ <h3>Job</h3>
117
+ </div>
118
+ <div class="modal-body">
119
+ <table class="table table-striped">
120
+ <tr>
121
+ <th>ID</th>
122
+ <td>#{id}</td>
123
+ </tr>
124
+ <tr>
125
+ <th>Class</th>
126
+ <td>#{class_name}</td>
127
+ </tr>
128
+ <tr>
129
+ <th>Name</th>
130
+ <td>#{name}</td>
131
+ </tr>
132
+ <tr>
133
+ <th>Args</th>
134
+ <td>#{JSON.stringify(args, null, 2)}</td>
135
+ </tr>
136
+ <tr>
137
+ <th>Started</th>
138
+ <td>#{started_at}</td>
139
+ </tr>
140
+ <tr>
141
+ <th>Duration</th>
142
+ <td>#{duration}</td>
143
+ </tr>
144
+ <tr>
145
+ <th>Process</th>
146
+ <td>#{started_by}</td>
147
+ </tr>
148
+ <tr>
149
+ <th>Status</th>
150
+ <td>#{status}</td>
151
+ </tr>
152
+ </table>
153
+ #{properties_html}
154
+ <div class="job-custom-views"></div>
155
+ </div>
156
+ </div>
157
+ """
158
+ $('.job-modal').modal('hide')
159
+ $('body').append(modal_html)
160
+ modal = $('.job-modal:last')
161
+ modal.modal
162
+ width: 480
163
+ $.getJSON ToroMonitor.settings.api_url("jobs/custom_views/#{id}"), (views) ->
164
+ html = ''
165
+ for view in views
166
+ html += """
167
+ <h4>#{view['name']}</h4>
168
+ #{view['html']}
169
+ """
170
+ $('.job-custom-views', modal).html(html)
171
+
172
+ on_poll: =>
173
+ if document.hasFocus()
174
+ @reload_table()
175
+
176
+ reload_table: =>
177
+ @table.dataTable().fnStandingRedraw()
178
+
179
+ start_polling: =>
180
+ setInterval =>
181
+ @on_poll()
182
+ , ToroMonitor.settings.poll_interval
183
+
184
+ format_time_ago: (time) =>
185
+ if time?
186
+ """<span class="timeago" title="#{time}">#{time}</span>"""
187
+ else
188
+ ""
@@ -0,0 +1,14 @@
1
+ #= require jquery
2
+ #= require toro/monitor/bootstrap.min
3
+ #= require toro/monitor/bootstrap-select.min
4
+ #= require toro/monitor/jquery.timeago
5
+ #= require toro/monitor/d3
6
+ #= require toro/monitor/datatables
7
+ #= require toro/monitor/datatables.bootstrap
8
+ #= require toro/monitor/datatables.standing_redraw
9
+ #= require toro/monitor/initialize
10
+ #= require toro/monitor/abstract_jobs_table
11
+ #= require toro/monitor/jobs_table
12
+ #= require toro/monitor/processes_table
13
+ #= require toro/monitor/queue_jobs_table
14
+ #= require toro/monitor/chart
@@ -0,0 +1,203 @@
1
+ class ToroMonitor.Chart
2
+
3
+ constructor: ->
4
+ options =
5
+ selector: '.chart'
6
+ poll_interval: ToroMonitor.settings.poll_interval
7
+ @initialize(options)
8
+
9
+ initialize: (options) =>
10
+ @options = options
11
+ @width = 960
12
+ @height = 580
13
+ @padding = [120, 50, 30, 20]
14
+
15
+ return null unless $(options.selector).length
16
+
17
+ # These correspond to Job.statuses: queued, running, complete, failed, [custom statuses]
18
+ colors = ['lightblue', 'blue', 'green', 'red', 'gray', 'purple', 'yellow']
19
+
20
+ @color_scale = d3.scale.ordinal().range(colors)
21
+
22
+ @x_scale = d3.scale.ordinal().rangeRoundBands([0, @width - @padding[1] - @padding[3]])
23
+ @y_scale = d3.scale.linear().range([0, @height - @padding[0] - @padding[2]])
24
+ @z_scale = d3.scale.ordinal().range(colors)
25
+
26
+ @render()
27
+ @start_polling()
28
+
29
+ start_polling: =>
30
+ setInterval =>
31
+ if document.hasFocus()
32
+ @render()
33
+ , @options.poll_interval
34
+
35
+ render: =>
36
+ d3.json ToroMonitor.settings.api_url('jobs/chart'), (data) =>
37
+
38
+ # Clear out the previously rendered chart
39
+ $(@options.selector).text('')
40
+
41
+ queues = data.queues_status_counts.map (queues_status_count) -> queues_status_count.queue
42
+ charts_config = ToroMonitor.settings.charts || { 'ALL': null }
43
+ charts_config = @standardize_charts_config(charts_config, queues)
44
+
45
+ for index, chart_config of charts_config
46
+ chart_config['show_legend'] = true if index == '0'
47
+ @render_chart(data, chart_config)
48
+
49
+ render_chart: (data, chart_config) =>
50
+ @svg = d3.select(@options.selector).append('svg:svg')
51
+ .attr('width', @width)
52
+ .attr('height', @height)
53
+ .append('svg:g')
54
+ .attr('transform', 'translate(' + @padding[3] + ',' + (@height - @padding[2]) + ')')
55
+
56
+ statuses = data.statuses
57
+ queues_status_counts = []
58
+ for queue_status_counts in data.queues_status_counts
59
+ if chart_config['queues'].indexOf(queue_status_counts.queue) > -1
60
+ queues_status_counts.push queue_status_counts
61
+ data = queues_status_counts
62
+
63
+ @color_scale.domain(statuses)
64
+
65
+ # Transpose the data into layers by queue
66
+ queues = d3.layout.stack()(statuses.map( (queue) =>
67
+ layers = []
68
+ for d in data
69
+ if d[queue]?
70
+ layers.push {x: d.queue, y: d[queue]+0}
71
+ else
72
+ layers.push {x: d.queue, y: 0}
73
+ layers
74
+ ))
75
+
76
+ # Compute the x-domain (by queue) and y-domain (by top).
77
+ @x_scale.domain(queues[0].map( (d) => d.x))
78
+ @y_scale.domain([0, d3.max(queues[queues.length - 1], (d) => d.y0 + d.y )])
79
+
80
+ # Groups for queues
81
+ queue = @svg.selectAll('g.queue')
82
+ .data(queues)
83
+ .enter().append('svg:g')
84
+ .attr('class', 'queue')
85
+ .style('fill', (d, i) => @z_scale(i) )
86
+ .style('stroke', (d, i) => d3.rgb(@z_scale(i)).darker() )
87
+
88
+ # Rects for queues
89
+ rect = queue.selectAll('rect')
90
+ .data(Object)
91
+ .enter().append('svg:rect')
92
+ .attr('x', (d) => @x_scale(d.x) )
93
+ .attr('y', (d) => -@y_scale(d.y0) - @y_scale(d.y) )
94
+ .attr('height', (d) => @y_scale(d.y) )
95
+ .attr('width', @x_scale.rangeBand())
96
+ .append('title')
97
+ .text((d) => if d.y == 1 then "#{d.y} job" else "#{d.y} jobs")
98
+
99
+ # Labels for queues
100
+ label = @svg.selectAll('text')
101
+ .data(@x_scale.domain())
102
+ .enter().append('foreignObject')
103
+ .attr('x', (d) => @x_scale(d) )
104
+ .attr('y', 6)
105
+ .attr('dy', '.71em')
106
+ .attr('height', 20)
107
+ .attr('width', @x_scale.rangeBand())
108
+ .append('xhtml:div')
109
+ .text((d) => d)
110
+ .attr('style', 'font-size: 11px; text-align: center; word-wrap:break-word; padding: 0 3px')
111
+
112
+ # Y-axis rules
113
+ rule = @svg.selectAll('g.rule')
114
+ .data(@y_scale.ticks(5))
115
+ .enter().append('svg:g')
116
+ .attr('class', 'rule')
117
+ .attr('transform', (d) => 'translate(0,' + -@y_scale(d) + ')' )
118
+
119
+ rule.append('svg:text')
120
+ .attr('x', @width - @padding[1] - @padding[3] + 6)
121
+ .attr('dy', '.35em')
122
+ .text(d3.format(',d'))
123
+
124
+ # Title
125
+ if chart_config['title']
126
+ label = @svg.selectAll('g.text').data(['Test 1']).enter()
127
+ .append('text')
128
+ .attr('x', 0)
129
+ .attr('y', 90 - @height)
130
+ .attr('height', 20)
131
+ .attr('font-size', '24px')
132
+ .attr('fill', '#777')
133
+ .text(chart_config['title'])
134
+
135
+ # Legend
136
+ if chart_config['show_legend']
137
+ legend_x = @width - 130
138
+ legend_y = 30 - @height
139
+ legend_box_width = 18
140
+ legend_box_height = 18
141
+
142
+ legend = @svg.selectAll('.legend')
143
+ .data(@color_scale.domain().reverse().slice())
144
+ .enter().append('g')
145
+ .attr('class', 'legend')
146
+ .attr('transform', (d, i) => 'translate(0,' + i * 20 + ')')
147
+
148
+ # White background for the legend
149
+ legend.append('rect')
150
+ .attr('x', legend_x - 10)
151
+ .attr('y', legend_y)
152
+ .attr('width', legend_box_width + 100)
153
+ .attr('height', legend_box_height + 10)
154
+ .style('fill', '#fff')
155
+
156
+ # Legend boxes
157
+ legend.append('rect')
158
+ .attr('x', legend_x)
159
+ .attr('y', legend_y)
160
+ .attr('width', legend_box_width)
161
+ .attr('height', legend_box_height)
162
+ .style('fill', @color_scale)
163
+
164
+ # Legend labels
165
+ legend.append('text')
166
+ .attr('x', legend_x + 25)
167
+ .attr('y', legend_y + 9)
168
+ .attr('dy', '.35em')
169
+ .text((d) => d)
170
+
171
+ standardize_charts_config: (chart_config, queues) =>
172
+ charts = []
173
+ matched_queues = []
174
+ for pattern, title of chart_config
175
+ chart = {
176
+ title: title
177
+ pattern: pattern
178
+ }
179
+ if pattern == 'OTHER' || pattern == 'ALL'
180
+ charts[pattern] = chart
181
+ continue
182
+ else
183
+ pattern_queues = []
184
+ for queue in queues
185
+ re = new RegExp(pattern, 'i')
186
+ if re.test(queue)
187
+ pattern_queues.push queue
188
+ matched_queues.push queue
189
+ chart['queues'] = pattern_queues
190
+ charts[pattern] = chart
191
+ remaining_queues = @array_difference(queues, matched_queues)
192
+ charts['OTHER']['queues'] = remaining_queues if charts['OTHER']
193
+ charts['ALL']['queues'] = queues if charts['ALL']
194
+ @object_values(charts)
195
+
196
+ array_difference: (array1, array2) =>
197
+ array1.filter (value) -> !(array2.indexOf(value) > -1)
198
+
199
+ object_values: (object) =>
200
+ value for key, value of object
201
+
202
+ $ ->
203
+ new ToroMonitor.Chart
@@ -0,0 +1,25 @@
1
+ window.ToroMonitor =
2
+ settings:
3
+ api_url: (path) ->
4
+ "<%= Toro::Monitor.root_path %>api/#{path}"
5
+ charts: <%= Toro::Monitor.options[:charts].to_json %>
6
+ poll_interval: <%= Toro::Monitor.options[:poll_interval] %>
7
+
8
+ $.timeago.settings.strings =
9
+ prefixAgo: null
10
+ prefixFromNow: null
11
+ suffixAgo: " ago"
12
+ suffixFromNow: " from now"
13
+ seconds: "1m"
14
+ minute: "1m"
15
+ minutes: "%dm"
16
+ hour: "1h"
17
+ hours: "%dh"
18
+ day: "1d"
19
+ days: "%dd"
20
+ month: "1mo"
21
+ months: "%dmo"
22
+ year: "1yr"
23
+ years: "%dyr"
24
+ wordSeparator: " "
25
+ numbers: []