resque-job-stats 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,11 +1,12 @@
1
- source "http://rubygems.org"
1
+ source 'http://rubygems.org'
2
2
 
3
3
  gem 'resque', '~> 1.17'
4
4
 
5
5
  group :development do
6
- gem "minitest", ">= 0"
7
- gem "bundler", "~> 1.0.0"
8
- gem "jeweler", "~> 1.6.4"
9
- gem "rcov", ">= 0"
6
+ gem 'minitest', '>= 0'
7
+ gem 'bundler', '~> 1.1.0'
8
+ gem 'jeweler', '~> 1.8.3'
10
9
  gem 'mynyml-redgreen', '~> 0.7.1'
10
+ gem 'timecop'
11
+ gem 'rack-test'
11
12
  end
data/README.rdoc CHANGED
@@ -9,10 +9,13 @@ Stats are tracked per Job type (class/module) in addition to the worker based st
9
9
  Stats tracked are:
10
10
 
11
11
  * Jobs performed
12
+ * Jobs enqueued
12
13
  * Jobs failed
13
14
  * Duration of last x jobs completed
14
15
  * Average job duration over last 100 jobs completed
15
16
  * Longest job duration over last 100 jobs completed
17
+ * Jobs enqueued as timeseries data (minute, hourly)
18
+ * Jobs performed as timeseries data (minute, hourly)
16
19
 
17
20
  This information can be used to help track performance and diagnose specific bottlenecks.
18
21
 
@@ -34,7 +37,7 @@ Simply extend your class
34
37
  extend Resque::Plugins::JobStats
35
38
 
36
39
  @queue = :my_job
37
- def self.perform(*payload)
40
+ def self.perform(*args)
38
41
  # ..
39
42
  end
40
43
  end
@@ -45,11 +48,14 @@ Alternatively you can include just the metric you wish to record.
45
48
 
46
49
  class MyVerboseJob
47
50
  extend Resque::Plugins::JobStats::Performed
51
+ extend Resque::Plugins::JobStats::Enqueued
48
52
  extend Resque::Plugins::JobStats::Failed
49
53
  extend Resque::Plugins::JobStats::Duration
54
+ extend Resque::Plugins::JobStats::Timeseries::Enqueued
55
+ extend Resque::Plugins::JobStats::Timeseries::Performed
50
56
 
51
57
  @queue = :my_job
52
- def self.perform(*payload)
58
+ def self.perform(*args)
53
59
  # ...
54
60
  end
55
61
  end
@@ -76,9 +82,33 @@ You may set the number of jobs to include by setting the @durations_recorded var
76
82
  end
77
83
  end
78
84
 
85
+ === Timeseries module
86
+
87
+ The timeseries module provides timeseries counts of jobs performed. The metrics are rolling and kept for a period of time before being expired.
88
+ The timestamp used for the timeseries data is UTC.
89
+
90
+ == Resque Web Tab
91
+
92
+ The Resque web page for showing the statistics will only display jobs that extend Resque::Plugins::JobStats (in other words, just
93
+ the jobs that include all of the metrics):
94
+
95
+ class MyJob
96
+ extend Resque::Plugins::JobStats
97
+ ...
98
+ end
99
+
100
+ The interface can be included in your app like this:
101
+
102
+ require 'resque-job-status/server'
103
+
104
+ If you wish to display only certain metrics, you can filter the metrics accordingly. The default metrics can be found in Resque::Plugins::JobStats::Statistic.
105
+
106
+ Resque::Server.job_stats_to_display = [:jobs_enqueued, :job_rolling_avg]
107
+
79
108
  == TODO
80
109
 
81
- * Add ui for stats in resque-web
110
+ * Find clean way to add queue wait time stats.
111
+ * Screen shot of interface with lots of stats
82
112
 
83
113
  == Contributing to resque-job-stats
84
114
 
@@ -90,7 +120,12 @@ You may set the number of jobs to include by setting the @durations_recorded var
90
120
  * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
91
121
  * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
92
122
 
123
+ == Contributers
124
+
125
+ * {damonmorgan}[https://github.com/damonmorgan]
126
+ * {unclebilly}[https://github.com/unclebilly]
127
+
93
128
  == Copyright
94
129
 
95
- Copyright (c) 2011 Alan Peabody. See LICENSE.txt for further details.
130
+ Copyright (c) 2011-2012 Alan Peabody. See LICENSE.txt for further details.
96
131
 
data/Rakefile CHANGED
@@ -32,17 +32,9 @@ Rake::TestTask.new(:test) do |test|
32
32
  test.verbose = true
33
33
  end
34
34
 
35
- require 'rcov/rcovtask'
36
- Rcov::RcovTask.new do |test|
37
- test.libs << 'test'
38
- test.pattern = 'test/**/test_*.rb'
39
- test.verbose = true
40
- test.rcov_opts << '--exclude "gems/*"'
41
- end
42
-
43
35
  task :default => :test
44
36
 
45
- require 'rake/rdoctask'
37
+ require 'rdoc/task'
46
38
  Rake::RDocTask.new do |rdoc|
47
39
  version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
40
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.2
1
+ 0.3.0
@@ -0,0 +1,68 @@
1
+ require 'resque/server'
2
+
3
+ module Resque
4
+ module Plugins
5
+ module JobStats
6
+ module Server
7
+ VIEW_PATH = File.join(File.dirname(__FILE__), 'server', 'views')
8
+
9
+ def job_stats_to_display
10
+ @job_stats_to_display ||= Resque::Plugins::JobStats::Statistic::DEFAULT_STATS
11
+ end
12
+
13
+ # Set this to an array of the public accessor names in Resque::Plugins::JobStats::Statistic
14
+ # that you wish to display. The default is [:jobs_enqueued, :jobs_performed, :jobs_failed, :job_rolling_avg, :longest_job]
15
+ # Examples:
16
+ # Resque::Server.job_stats_to_display = [:jobs_performed, :job_rolling_avg]
17
+ def job_stats_to_display=(stats)
18
+ @job_stats_to_display = stats
19
+ end
20
+
21
+ module Helpers
22
+ def display_stat?(stat_name)
23
+ self.class.job_stats_to_display == :all ||
24
+ [self.class.job_stats_to_display].flatten.map(&:to_sym).include?(stat_name.to_sym)
25
+ end
26
+
27
+ def time_display(float)
28
+ float.zero? ? "" : ("%.2f" % float.to_s) + "s"
29
+ end
30
+
31
+ def number_display(num)
32
+ num.zero? ? "" : num
33
+ end
34
+
35
+ def stat_header(stat_name)
36
+ if(display_stat?(stat_name))
37
+ "<th>" + stat_name.to_s.gsub(/_/,' ').capitalize + "</th>"
38
+ end
39
+ end
40
+
41
+ def display_stat(stat, stat_name, format)
42
+ if(display_stat?(stat_name))
43
+ formatted_stat = self.send(format, stat.send(stat_name))
44
+ "<td>#{formatted_stat}</td>"
45
+ end
46
+ end
47
+ end
48
+
49
+ class << self
50
+ def registered(app)
51
+ app.get '/job_stats' do
52
+ @jobs = Resque::Plugins::JobStats::Statistic.find_all(self.class.job_stats_to_display).sort
53
+ erb(File.read(File.join(VIEW_PATH, 'job_stats.erb')))
54
+ end
55
+ # We have little choice in using this funky name - Resque
56
+ # already has a "Stats" tab, and it doesn't like
57
+ # tab names with spaces in it (it translates the url as job%20stats)
58
+ app.tabs << "Job_Stats"
59
+
60
+ app.helpers(Helpers)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ Resque::Server.register Resque::Plugins::JobStats::Server
@@ -0,0 +1,26 @@
1
+ <h1>Resque Job Stats</h1>
2
+
3
+ <p class="intro">
4
+ This page displays statistics about jobs that have been executed.
5
+ </p>
6
+
7
+ <table>
8
+ <tr>
9
+ <th>Name</th>
10
+ <%= stat_header(:jobs_enqueued) %>
11
+ <%= stat_header(:jobs_performed) %>
12
+ <%= stat_header(:jobs_failed) %>
13
+ <%= stat_header(:job_rolling_avg) %>
14
+ <%= stat_header(:longest_job) %>
15
+ </tr>
16
+ <% @jobs.each do |job| %>
17
+ <tr>
18
+ <td><%= job.name %></td>
19
+ <%= display_stat(job, :jobs_enqueued, :number_display) %>
20
+ <%= display_stat(job, :jobs_performed, :number_display) %>
21
+ <%= display_stat(job, :jobs_failed, :number_display) %>
22
+ <%= display_stat(job, :job_rolling_avg, :time_display) %>
23
+ <%= display_stat(job, :longest_job, :time_display) %>
24
+ </tr>
25
+ <% end %>
26
+ </table>
@@ -1,14 +1,28 @@
1
1
  require 'resque'
2
2
  require 'resque/plugins/job_stats/performed'
3
+ require 'resque/plugins/job_stats/enqueued'
3
4
  require 'resque/plugins/job_stats/failed'
4
5
  require 'resque/plugins/job_stats/duration'
6
+ require 'resque/plugins/job_stats/timeseries'
7
+ require 'resque/plugins/job_stats/statistic'
5
8
 
6
9
  module Resque
7
10
  module Plugins
8
11
  module JobStats
9
12
  include Resque::Plugins::JobStats::Performed
13
+ include Resque::Plugins::JobStats::Enqueued
10
14
  include Resque::Plugins::JobStats::Failed
11
15
  include Resque::Plugins::JobStats::Duration
16
+ include Resque::Plugins::JobStats::Timeseries::Enqueued
17
+ include Resque::Plugins::JobStats::Timeseries::Performed
18
+
19
+ def self.extended(base)
20
+ self.measured_jobs << base
21
+ end
22
+
23
+ def self.measured_jobs
24
+ @measured_jobs ||= []
25
+ end
12
26
  end
13
27
  end
14
28
  end
@@ -19,7 +19,7 @@ module Resque
19
19
  end
20
20
 
21
21
  # Increments the failed count when job is complete
22
- def around_perform_job_stats_duration(*payload)
22
+ def around_perform_job_stats_duration(*args)
23
23
  start = Time.now
24
24
  yield
25
25
  duration = Time.now - start
@@ -0,0 +1,33 @@
1
+ module Resque
2
+ module Plugins
3
+ module JobStats
4
+
5
+ # Extend your job with this module to track how many
6
+ # jobs are queued successfully
7
+ module Enqueued
8
+
9
+ # Sets the number of jobs queued
10
+ def jobs_enqueued=(int)
11
+ Resque.redis.set(jobs_enqueued_key,int)
12
+ end
13
+
14
+ # Returns the number of jobs enqueued
15
+ def jobs_enqueued
16
+ Resque.redis.get(jobs_enqueued_key).to_i
17
+ end
18
+
19
+ # Returns the key used for tracking jobs enqueued
20
+ def jobs_enqueued_key
21
+ "stats:jobs:#{self.name}:enqueued"
22
+ end
23
+
24
+ # Increments the enqueued count when job is queued
25
+ def after_enqueue_job_stats_enqueued(*args)
26
+ Resque.redis.incr(jobs_enqueued_key)
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+ end
33
+
@@ -2,7 +2,7 @@ module Resque
2
2
  module Plugins
3
3
  module JobStats
4
4
 
5
- # Extend your job with this module to track how many
5
+ # Extend your job with this module to track how many
6
6
  # jobs fail
7
7
  module Failed
8
8
 
@@ -13,7 +13,9 @@ module Resque
13
13
 
14
14
  # Returns the number of jobs failed
15
15
  def jobs_failed
16
- Resque.redis.get(jobs_failed_key).to_i
16
+ jobs_failed = Resque.redis.get(jobs_failed_key).to_i
17
+ return jobs_failed / 2 if Resque::VERSION == '1.20.0'
18
+ jobs_failed
17
19
  end
18
20
 
19
21
  # Returns the key used for tracking jobs failed
@@ -22,7 +24,7 @@ module Resque
22
24
  end
23
25
 
24
26
  # Increments the failed count when job is complete
25
- def on_failure_job_stats_failed(e,*payload)
27
+ def on_failure_job_stats_failed(e,*args)
26
28
  Resque.redis.incr(jobs_failed_key)
27
29
  end
28
30
 
@@ -2,7 +2,7 @@ module Resque
2
2
  module Plugins
3
3
  module JobStats
4
4
 
5
- # Extend your job with this module to track how many
5
+ # Extend your job with this module to track how many
6
6
  # jobs are performed successfully
7
7
  module Performed
8
8
 
@@ -22,7 +22,7 @@ module Resque
22
22
  end
23
23
 
24
24
  # Increments the performed count when job is complete
25
- def after_perform_job_stats_performed(*payload)
25
+ def after_perform_job_stats_performed(*args)
26
26
  Resque.redis.incr(jobs_performed_key)
27
27
  end
28
28
 
@@ -0,0 +1,43 @@
1
+ module Resque
2
+ module Plugins
3
+ module JobStats
4
+ # A class composed of a job class and the various job statistics
5
+ # collected for the given job.
6
+ class Statistic
7
+ include Comparable
8
+
9
+ # An array of the default statistics that will be displayed in the web tab
10
+ DEFAULT_STATS = [:jobs_enqueued, :jobs_performed, :jobs_failed, :job_rolling_avg, :longest_job]
11
+
12
+ attr_accessor *[:job_class].concat(DEFAULT_STATS)
13
+
14
+ class << self
15
+ # Find and load a Statistic for all resque jobs that are in the Resque::Plugins::JobStats.measured_jobs collection
16
+ def find_all(metrics)
17
+ Resque::Plugins::JobStats.measured_jobs.map{|j| new(j, metrics)}
18
+ end
19
+ end
20
+
21
+ # A series of metrics describing one job class.
22
+ def initialize(job_class, metrics)
23
+ self.job_class = job_class
24
+ self.load(metrics)
25
+ end
26
+
27
+ def load(metrics)
28
+ metrics.each do |metric|
29
+ self.send("#{metric}=", job_class.send(metric))
30
+ end
31
+ end
32
+
33
+ def name
34
+ self.job_class.name
35
+ end
36
+
37
+ def <=>(other)
38
+ self.name <=> other.name
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,95 @@
1
+ require 'time'
2
+
3
+ module Resque
4
+ module Plugins
5
+ module JobStats
6
+
7
+ # Extend your job with this module to track how many
8
+ # jobs are performed over a period of time
9
+ module Timeseries
10
+
11
+ module Common
12
+ # A timestamp rounded to the lowest minute
13
+ def timestamp
14
+ time = Time.now.utc
15
+ Time.at(time.to_i - time.sec).utc # to_i removes usecs
16
+ end
17
+
18
+ private
19
+
20
+ TIME_FORMAT = {:minutes => "%d:%H:%M", :hours => "%d:%H"}
21
+ FACTOR = {:minutes => 1, :hours => 60}
22
+
23
+ def range(sample_size, time_unit, end_time) # :nodoc:
24
+ (0..sample_size).map { |n| end_time - (n * 60 * FACTOR[time_unit])}
25
+ end
26
+
27
+ def timeseries_data(type, sample_size, time_unit) # :nodoc:
28
+ timeseries_range = range(sample_size, time_unit, timestamp)
29
+ timeseries_keys = timeseries_range.map { |time| jobs_timeseries_key(type, time, time_unit)}
30
+ timeseries_data = Resque.redis.mget(*(timeseries_keys))
31
+
32
+ return Hash[(0..sample_size).map { |i| [timeseries_range[i], timeseries_data[i].to_i]}]
33
+ end
34
+
35
+ def jobs_timeseries_key(type, key_time, time_unit) # :nodoc:
36
+ "#{prefix}:#{type}:#{key_time.strftime(TIME_FORMAT[time_unit])}"
37
+ end
38
+
39
+ def prefix # :nodoc:
40
+ "stats:jobs:#{self.name}:timeseries"
41
+ end
42
+
43
+ def incr_timeseries(type) # :nodoc:
44
+ increx(jobs_timeseries_key(type, timestamp, :minutes), (60 * 61)) # 1h + 1m for some buffer
45
+ increx(jobs_timeseries_key(type, timestamp, :hours), (60 * 60 * 25)) # 24h + 60m for some buffer
46
+ end
47
+
48
+ # Increments a key and sets its expiry time
49
+ def increx(key, ttl)
50
+ Resque.redis.incr(key)
51
+ Resque.redis.expire(key, ttl)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ module Resque::Plugins::JobStats::Timeseries::Enqueued
60
+ include Resque::Plugins::JobStats::Timeseries::Common
61
+
62
+ # Increments the enqueued count for the timestamp when job is queued
63
+ def after_enqueue_job_stats_timeseries(*args)
64
+ incr_timeseries(:enqueued)
65
+ end
66
+
67
+ # Hash of timeseries data over the last 60 minutes for queued jobs
68
+ def queued_per_minute
69
+ timeseries_data(:enqueued, 60, :minutes)
70
+ end
71
+
72
+ # Hash of timeseries data over the last 24 hours for queued jobs
73
+ def queued_per_hour
74
+ timeseries_data(:enqueued, 24, :hours)
75
+ end
76
+ end
77
+
78
+ module Resque::Plugins::JobStats::Timeseries::Performed
79
+ include Resque::Plugins::JobStats::Timeseries::Common
80
+
81
+ # Increments the performed count for the timestamp when job is complete
82
+ def after_perform_job_stats_timeseries(*args)
83
+ incr_timeseries(:performed)
84
+ end
85
+
86
+ # Hash of timeseries data over the last 60 minutes for completed jobs
87
+ def performed_per_minute
88
+ timeseries_data(:performed, 60, :minutes)
89
+ end
90
+
91
+ # Hash of timeseries data over the last 24 hours for completed jobs
92
+ def performed_per_hour
93
+ timeseries_data(:performed, 24, :hours)
94
+ end
95
+ end
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "resque-job-stats"
8
- s.version = "0.2.2"
8
+ s.version = "0.3.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["alanpeabody"]
12
- s.date = "2011-11-07"
12
+ s.date = "2012-06-22"
13
13
  s.description = "Tracks jobs performed, failed, and the duration of the last 100 jobs for each job type."
14
14
  s.email = "gapeabody@gmail.com"
15
15
  s.extra_rdoc_files = [
@@ -26,18 +26,24 @@ Gem::Specification.new do |s|
26
26
  "Rakefile",
27
27
  "VERSION",
28
28
  "lib/resque-job-stats.rb",
29
+ "lib/resque-job-stats/server.rb",
30
+ "lib/resque-job-stats/server/views/job_stats.erb",
29
31
  "lib/resque/plugins/job_stats.rb",
30
32
  "lib/resque/plugins/job_stats/duration.rb",
33
+ "lib/resque/plugins/job_stats/enqueued.rb",
31
34
  "lib/resque/plugins/job_stats/failed.rb",
32
35
  "lib/resque/plugins/job_stats/performed.rb",
36
+ "lib/resque/plugins/job_stats/statistic.rb",
37
+ "lib/resque/plugins/job_stats/timeseries.rb",
33
38
  "resque-job-stats.gemspec",
34
39
  "test/helper.rb",
35
- "test/test_job_stats.rb"
40
+ "test/test_job_stats.rb",
41
+ "test/test_server.rb"
36
42
  ]
37
43
  s.homepage = "http://github.com/alanpeabody/resque-job-stats"
38
44
  s.licenses = ["MIT"]
39
45
  s.require_paths = ["lib"]
40
- s.rubygems_version = "1.8.10"
46
+ s.rubygems_version = "1.8.17"
41
47
  s.summary = "Job-centric stats for Resque"
42
48
 
43
49
  if s.respond_to? :specification_version then
@@ -46,25 +52,28 @@ Gem::Specification.new do |s|
46
52
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
47
53
  s.add_runtime_dependency(%q<resque>, ["~> 1.17"])
48
54
  s.add_development_dependency(%q<minitest>, [">= 0"])
49
- s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
50
- s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
51
- s.add_development_dependency(%q<rcov>, [">= 0"])
55
+ s.add_development_dependency(%q<bundler>, ["~> 1.1.0"])
56
+ s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
52
57
  s.add_development_dependency(%q<mynyml-redgreen>, ["~> 0.7.1"])
58
+ s.add_development_dependency(%q<timecop>, [">= 0"])
59
+ s.add_development_dependency(%q<rack-test>, [">= 0"])
53
60
  else
54
61
  s.add_dependency(%q<resque>, ["~> 1.17"])
55
62
  s.add_dependency(%q<minitest>, [">= 0"])
56
- s.add_dependency(%q<bundler>, ["~> 1.0.0"])
57
- s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
58
- s.add_dependency(%q<rcov>, [">= 0"])
63
+ s.add_dependency(%q<bundler>, ["~> 1.1.0"])
64
+ s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
59
65
  s.add_dependency(%q<mynyml-redgreen>, ["~> 0.7.1"])
66
+ s.add_dependency(%q<timecop>, [">= 0"])
67
+ s.add_dependency(%q<rack-test>, [">= 0"])
60
68
  end
61
69
  else
62
70
  s.add_dependency(%q<resque>, ["~> 1.17"])
63
71
  s.add_dependency(%q<minitest>, [">= 0"])
64
- s.add_dependency(%q<bundler>, ["~> 1.0.0"])
65
- s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
66
- s.add_dependency(%q<rcov>, [">= 0"])
72
+ s.add_dependency(%q<bundler>, ["~> 1.1.0"])
73
+ s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
67
74
  s.add_dependency(%q<mynyml-redgreen>, ["~> 0.7.1"])
75
+ s.add_dependency(%q<timecop>, [">= 0"])
76
+ s.add_dependency(%q<rack-test>, [">= 0"])
68
77
  end
69
78
  end
70
79
 
data/test/helper.rb CHANGED
@@ -8,8 +8,11 @@ rescue Bundler::BundlerError => e
8
8
  exit e.status_code
9
9
  end
10
10
  require 'minitest/unit'
11
+ require 'minitest/mock'
12
+ require 'rack/test'
11
13
  require 'redgreen'
12
14
  require 'resque'
15
+ require 'timecop'
13
16
 
14
17
  Resque.redis = 'localhost:6379'
15
18
  Resque.redis.namespace = 'resque:job_stats'
@@ -17,7 +17,7 @@ class FailJob < BaseJob
17
17
  extend Resque::Plugins::JobStats::Failed
18
18
  @queue = :test
19
19
 
20
- def self.perform(*payload)
20
+ def self.perform(*args)
21
21
  raise 'fail'
22
22
  end
23
23
  end
@@ -31,6 +31,8 @@ end
31
31
  class TestResqueJobStats < MiniTest::Unit::TestCase
32
32
 
33
33
  def setup
34
+ # Ensure empty redis for each test
35
+ Resque.redis.flushdb
34
36
  @worker = Resque::Worker.new(:test)
35
37
  end
36
38
 
@@ -51,6 +53,16 @@ class TestResqueJobStats < MiniTest::Unit::TestCase
51
53
  assert_equal 3, SimpleJob.jobs_performed
52
54
  end
53
55
 
56
+ def test_jobs_enqueued
57
+ assert_equal 'stats:jobs:SimpleJob:enqueued', SimpleJob.jobs_enqueued_key
58
+ SimpleJob.jobs_enqueued = 0
59
+ 3.times do
60
+ Resque.enqueue(SimpleJob)
61
+ @worker.work(0)
62
+ end
63
+ assert_equal 3, SimpleJob.jobs_enqueued
64
+ end
65
+
54
66
  def test_jobs_failed
55
67
  assert_equal 'stats:jobs:FailJob:failed', FailJob.jobs_failed_key
56
68
  FailJob.jobs_failed = 0
@@ -97,4 +109,32 @@ class TestResqueJobStats < MiniTest::Unit::TestCase
97
109
  assert_in_delta 0.1, CustomDurJob.job_rolling_avg, 0.01
98
110
  end
99
111
 
112
+ def test_perform_timeseries
113
+ time = SimpleJob.timestamp
114
+ 3.times do
115
+ Resque.enqueue(SimpleJob)
116
+ @worker.work(0)
117
+ end
118
+ assert_equal 3, SimpleJob.performed_per_minute[time]
119
+ assert_equal 0, SimpleJob.performed_per_minute[(time - 60)]
120
+
121
+ assert_equal 3, SimpleJob.performed_per_hour[time]
122
+ assert_equal 0, SimpleJob.performed_per_hour[(time - 3600)]
123
+ end
124
+
125
+ def test_enqueue_timeseries
126
+ time = SimpleJob.timestamp
127
+ Timecop.freeze(time)
128
+ Resque.enqueue(SimpleJob,0)
129
+ Timecop.freeze(time + 60)
130
+ @worker.work(0)
131
+ assert_equal 1, SimpleJob.queued_per_minute[time]
132
+ assert_equal 0, SimpleJob.queued_per_minute[(time + 60)]
133
+ assert_equal 1, SimpleJob.performed_per_minute[(time + 60)]
134
+ Timecop.return
135
+ end
136
+
137
+ def test_measured_jobs
138
+ assert_equal [SimpleJob], Resque::Plugins::JobStats.measured_jobs
139
+ end
100
140
  end
@@ -0,0 +1,102 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
+ require 'resque-job-stats/server'
3
+
4
+ # A pretend job that has all of the statistics we want to display (i.e. extends
5
+ # Resque::Plugins::JobStats)
6
+ class AnyJobber
7
+ class << self
8
+ def jobs_enqueued
9
+ 111
10
+ end
11
+
12
+ def jobs_performed
13
+ 12345
14
+ end
15
+
16
+ def jobs_failed
17
+ 0
18
+ end
19
+
20
+ def job_rolling_avg
21
+ 0.3333232
22
+ end
23
+
24
+ def longest_job
25
+ 0.455555
26
+ end
27
+ end
28
+ end
29
+
30
+ class YetAnotherJobber < AnyJobber
31
+ end
32
+
33
+ class MyServer
34
+ def self.job_stats_to_display
35
+ [:jobs_enqueued]
36
+ end
37
+
38
+ include Resque::Plugins::JobStats::Server::Helpers
39
+ end
40
+
41
+ ENV['RACK_ENV'] = 'test'
42
+ class TestServer < MiniTest::Unit::TestCase
43
+ include Rack::Test::Methods
44
+
45
+ def setup
46
+ Resque::Server.job_stats_to_display = Resque::Plugins::JobStats::Statistic::DEFAULT_STATS
47
+ @server = MyServer.new
48
+ end
49
+
50
+ def app
51
+ Resque::Server
52
+ end
53
+
54
+ def test_job_stats
55
+ Resque::Plugins::JobStats.stub :measured_jobs, [AnyJobber] do
56
+ get '/job_stats'
57
+ assert_equal 200, last_response.status, last_response.body
58
+ assert last_response.body.include?("<td>AnyJobber</td>"), "job name was not found"
59
+ assert last_response.body.include?("<td>111</td>"), "jobs_enqueued was not found"
60
+ assert last_response.body.include?("<td>12345</td>"), "jobs_performed was not found"
61
+ assert last_response.body.include?("<td></td>"), "jobs_failed was not found"
62
+ assert last_response.body.include?("<td>0.33s</td>"), "job_rolling_avg was not found"
63
+ assert last_response.body.include?("<td>0.46s</td>"), "longest_job was not found"
64
+ end
65
+ end
66
+
67
+ def test_job_stats_filtered
68
+ Resque::Server.job_stats_to_display = [:longest_job]
69
+ Resque::Plugins::JobStats.stub :measured_jobs, [AnyJobber] do
70
+ get '/job_stats'
71
+ assert_equal 200, last_response.status, last_response.body
72
+ assert last_response.body.include?("<td>AnyJobber</td>"), "job name was not found"
73
+ assert !last_response.body.include?("<td>111</td>"), "jobs_enqueued was not found"
74
+ assert !last_response.body.include?("<td>12345</td>"), "jobs_performed was not found"
75
+ assert !last_response.body.include?("<td></td>"), "jobs_failed was not found"
76
+ assert !last_response.body.include?("<td>0.33s</td>"), "job_rolling_avg was not found"
77
+ assert last_response.body.include?("<td>0.46s</td>"), "longest_job was not found"
78
+ end
79
+ end
80
+
81
+ def test_stat_header
82
+ assert_equal "<th>Jobs enqueued</th>", @server.stat_header(:jobs_enqueued)
83
+ assert_equal nil, @server.stat_header(:FOOOOOOO)
84
+ end
85
+
86
+ def test_display_stat?
87
+ assert !@server.display_stat?(:jobs_barfing)
88
+ assert @server.display_stat?(:jobs_enqueued)
89
+ end
90
+
91
+ def test_job_sorting
92
+ Resque::Plugins::JobStats.stub :measured_jobs, [YetAnotherJobber, AnyJobber] do
93
+ get '/job_stats'
94
+ assert_equal 200, last_response.status, last_response.body
95
+ assert(last_response.body =~ /AnyJobber(.|\n)+YetAnotherJobber/, "AnyJobber should be found before YetAnotherJobber")
96
+ end
97
+ end
98
+
99
+ def test_tabs
100
+ assert app.tabs.include?("Job_Stats"), "The tab should be in resque's server"
101
+ end
102
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resque-job-stats
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-11-07 00:00:00.000000000Z
12
+ date: 2012-06-22 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: resque
16
- requirement: &16247440 !ruby/object:Gem::Requirement
16
+ requirement: &70220064841520 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '1.17'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *16247440
24
+ version_requirements: *70220064841520
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: minitest
27
- requirement: &16246940 !ruby/object:Gem::Requirement
27
+ requirement: &70220064841040 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,32 +32,43 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *16246940
35
+ version_requirements: *70220064841040
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: bundler
38
- requirement: &16246320 !ruby/object:Gem::Requirement
38
+ requirement: &70220064840560 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
42
42
  - !ruby/object:Gem::Version
43
- version: 1.0.0
43
+ version: 1.1.0
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *16246320
46
+ version_requirements: *70220064840560
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: jeweler
49
- requirement: &16245720 !ruby/object:Gem::Requirement
49
+ requirement: &70220064840080 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
53
53
  - !ruby/object:Gem::Version
54
- version: 1.6.4
54
+ version: 1.8.3
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *16245720
57
+ version_requirements: *70220064840080
58
58
  - !ruby/object:Gem::Dependency
59
- name: rcov
60
- requirement: &16245220 !ruby/object:Gem::Requirement
59
+ name: mynyml-redgreen
60
+ requirement: &70220064839600 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: 0.7.1
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70220064839600
69
+ - !ruby/object:Gem::Dependency
70
+ name: timecop
71
+ requirement: &70220064839120 !ruby/object:Gem::Requirement
61
72
  none: false
62
73
  requirements:
63
74
  - - ! '>='
@@ -65,18 +76,18 @@ dependencies:
65
76
  version: '0'
66
77
  type: :development
67
78
  prerelease: false
68
- version_requirements: *16245220
79
+ version_requirements: *70220064839120
69
80
  - !ruby/object:Gem::Dependency
70
- name: mynyml-redgreen
71
- requirement: &16244740 !ruby/object:Gem::Requirement
81
+ name: rack-test
82
+ requirement: &70220064838640 !ruby/object:Gem::Requirement
72
83
  none: false
73
84
  requirements:
74
- - - ~>
85
+ - - ! '>='
75
86
  - !ruby/object:Gem::Version
76
- version: 0.7.1
87
+ version: '0'
77
88
  type: :development
78
89
  prerelease: false
79
- version_requirements: *16244740
90
+ version_requirements: *70220064838640
80
91
  description: Tracks jobs performed, failed, and the duration of the last 100 jobs
81
92
  for each job type.
82
93
  email: gapeabody@gmail.com
@@ -95,13 +106,19 @@ files:
95
106
  - Rakefile
96
107
  - VERSION
97
108
  - lib/resque-job-stats.rb
109
+ - lib/resque-job-stats/server.rb
110
+ - lib/resque-job-stats/server/views/job_stats.erb
98
111
  - lib/resque/plugins/job_stats.rb
99
112
  - lib/resque/plugins/job_stats/duration.rb
113
+ - lib/resque/plugins/job_stats/enqueued.rb
100
114
  - lib/resque/plugins/job_stats/failed.rb
101
115
  - lib/resque/plugins/job_stats/performed.rb
116
+ - lib/resque/plugins/job_stats/statistic.rb
117
+ - lib/resque/plugins/job_stats/timeseries.rb
102
118
  - resque-job-stats.gemspec
103
119
  - test/helper.rb
104
120
  - test/test_job_stats.rb
121
+ - test/test_server.rb
105
122
  homepage: http://github.com/alanpeabody/resque-job-stats
106
123
  licenses:
107
124
  - MIT
@@ -117,7 +134,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
117
134
  version: '0'
118
135
  segments:
119
136
  - 0
120
- hash: 2761321648121208305
137
+ hash: 1776259179691476593
121
138
  required_rubygems_version: !ruby/object:Gem::Requirement
122
139
  none: false
123
140
  requirements:
@@ -126,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
126
143
  version: '0'
127
144
  requirements: []
128
145
  rubyforge_project:
129
- rubygems_version: 1.8.10
146
+ rubygems_version: 1.8.17
130
147
  signing_key:
131
148
  specification_version: 3
132
149
  summary: Job-centric stats for Resque