toro 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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: []