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 +6 -5
- data/README.rdoc +39 -4
- data/Rakefile +1 -9
- data/VERSION +1 -1
- data/lib/resque-job-stats/server.rb +68 -0
- data/lib/resque-job-stats/server/views/job_stats.erb +26 -0
- data/lib/resque/plugins/job_stats.rb +14 -0
- data/lib/resque/plugins/job_stats/duration.rb +1 -1
- data/lib/resque/plugins/job_stats/enqueued.rb +33 -0
- data/lib/resque/plugins/job_stats/failed.rb +5 -3
- data/lib/resque/plugins/job_stats/performed.rb +2 -2
- data/lib/resque/plugins/job_stats/statistic.rb +43 -0
- data/lib/resque/plugins/job_stats/timeseries.rb +95 -0
- data/resque-job-stats.gemspec +22 -13
- data/test/helper.rb +3 -0
- data/test/test_job_stats.rb +41 -1
- data/test/test_server.rb +102 -0
- metadata +39 -22
data/Gemfile
CHANGED
@@ -1,11 +1,12 @@
|
|
1
|
-
source
|
1
|
+
source 'http://rubygems.org'
|
2
2
|
|
3
3
|
gem 'resque', '~> 1.17'
|
4
4
|
|
5
5
|
group :development do
|
6
|
-
gem
|
7
|
-
gem
|
8
|
-
gem
|
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(*
|
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(*
|
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
|
-
*
|
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 '
|
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.
|
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
|
@@ -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,*
|
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(*
|
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
|
data/resque-job-stats.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "resque-job-stats"
|
8
|
-
s.version = "0.
|
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 = "
|
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.
|
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.
|
50
|
-
s.add_development_dependency(%q<jeweler>, ["~> 1.
|
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.
|
57
|
-
s.add_dependency(%q<jeweler>, ["~> 1.
|
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.
|
65
|
-
s.add_dependency(%q<jeweler>, ["~> 1.
|
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'
|
data/test/test_job_stats.rb
CHANGED
@@ -17,7 +17,7 @@ class FailJob < BaseJob
|
|
17
17
|
extend Resque::Plugins::JobStats::Failed
|
18
18
|
@queue = :test
|
19
19
|
|
20
|
-
def self.perform(*
|
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
|
data/test/test_server.rb
ADDED
@@ -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.
|
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:
|
12
|
+
date: 2012-06-22 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: resque
|
16
|
-
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: *
|
24
|
+
version_requirements: *70220064841520
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: minitest
|
27
|
-
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: *
|
35
|
+
version_requirements: *70220064841040
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: bundler
|
38
|
-
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.
|
43
|
+
version: 1.1.0
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70220064840560
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: jeweler
|
49
|
-
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.
|
54
|
+
version: 1.8.3
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70220064840080
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
|
-
name:
|
60
|
-
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: *
|
79
|
+
version_requirements: *70220064839120
|
69
80
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
71
|
-
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
|
87
|
+
version: '0'
|
77
88
|
type: :development
|
78
89
|
prerelease: false
|
79
|
-
version_requirements: *
|
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:
|
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.
|
146
|
+
rubygems_version: 1.8.17
|
130
147
|
signing_key:
|
131
148
|
specification_version: 3
|
132
149
|
summary: Job-centric stats for Resque
|