success_tracker 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +88 -0
- data/Rakefile +10 -0
- data/lib/success_tracker.rb +75 -0
- data/lib/success_tracker/version.rb +3 -0
- data/success_tracker.gemspec +22 -0
- data/test/success_tracker_test.rb +164 -0
- metadata +90 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 autohaus24 GmbH
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# SuccessTracker
|
2
|
+
|
3
|
+
Allows you to track success and failure of tasks and define thresholds for unexpected failure conditions. When the threshold is met, the failure method returns true (otherwise false). This can be used to define in code what to do when the failure condition is met. The failure method gets the name of a rule as a second parameter. This rule is used to define when a failure should be seen as unexpected. There are some rules defined as default (:percent\_10 which fails when there is at least a 10 percent failure rate with a minimum of 10 records and :sequence\_of\_5 which fails when there are at least 5 failures in a row).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'success_tracker'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install success_tracker
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
$success_tracker = SuccessTracker::Base.new(redis_connection)
|
23
|
+
$success_tracker.success('mytask')
|
24
|
+
if $success_tracker.failure('mytask', :percent_10)
|
25
|
+
puts "reached the threshold!"
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
You can define additional rules with your own code blocks or use the sequence_rule or ratio_rule class methods for this task. These code blocks get an array of the recorded notifications as parameters which can have between 0 and 100 elements all having either an empty string (failure) or a "1" (success).
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
$success_tracker = SuccessTracker::Base.new(redis_connection, :rules => {
|
33
|
+
:percent_50 => SuccessTracker::Base.ratio_rule(0.5)
|
34
|
+
})
|
35
|
+
$success_tracker.failure('mytask', :percent_50)
|
36
|
+
```
|
37
|
+
|
38
|
+
You can give a block to the failure method which is always yielded. In case the block raises an exception and the failure condition is not met, the exception is extended with the SuccessTracker::NonSignificantError module. This can then be used in rescue statements to define what should only happen if the handled exception was not triggered with a failure condition met.
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
$success_tracker = SuccessTracker::Base.new(redis_connection)
|
42
|
+
$success_tracker.failure('mytask', :percent_10) do
|
43
|
+
raise ArgumentError
|
44
|
+
end
|
45
|
+
|
46
|
+
# You can also use it to exclude these errors from Airbrake reporting.
|
47
|
+
Airbrake.configure do |config|
|
48
|
+
config.ignore_by_filter do |notice|
|
49
|
+
SuccessTracker::NonSignificantError === notice.exception
|
50
|
+
end
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
By default all StandardErrors are extended but in the options you can define an array with the exceptions which should be tagged.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
$success_tracker = SuccessTracker::Base.new(redis_connection)
|
58
|
+
$success_tracker.failure('mytask', :percent_10, :exceptions => [ MySpecialError ]) do
|
59
|
+
raise ArgumentError # will not be tagged because it is the wrong exception type
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
You can also define on_success and on_failure callbacks which run on success or failure and get the identifier given to the success or failure method as a parameter. Use this for example to track success and failure rates in StatsD.
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
# assuming you have statsd-client stored in $statsd
|
67
|
+
$success_tracker = SuccessTracker::Base.new(redis_connection, {
|
68
|
+
:on_success => lambda { |identifier| $statsd.increment("#{identifier}.success") },
|
69
|
+
:on_failure => lambda { |identifier| $statsd.increment("#{identifier}.failure") }
|
70
|
+
}
|
71
|
+
)
|
72
|
+
```
|
73
|
+
|
74
|
+
## Requirements
|
75
|
+
|
76
|
+
* Redis
|
77
|
+
|
78
|
+
## Contributing
|
79
|
+
|
80
|
+
1. Fork it
|
81
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
82
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
83
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
84
|
+
5. Create new Pull Request
|
85
|
+
|
86
|
+
## License
|
87
|
+
|
88
|
+
MIT License. Copyright 2013 autohaus24 GmbH
|
data/Rakefile
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require "success_tracker/version"
|
2
|
+
require 'redis'
|
3
|
+
|
4
|
+
module SuccessTracker
|
5
|
+
module NonSignificantError
|
6
|
+
end
|
7
|
+
|
8
|
+
class Base
|
9
|
+
attr_accessor :callbacks, :rules, :redis, :list_length
|
10
|
+
|
11
|
+
def initialize(redis, options={})
|
12
|
+
@redis = redis
|
13
|
+
|
14
|
+
@rules = {
|
15
|
+
:percent_10 => self.class.ratio_rule(0.1),
|
16
|
+
:sequence_of_5 => self.class.sequence_rule(5),
|
17
|
+
}.merge(options.delete(:rules) || {})
|
18
|
+
@callbacks = options.delete(:callbacks) || {}
|
19
|
+
raise ArgumentError unless options.empty?
|
20
|
+
|
21
|
+
@list_length = 100
|
22
|
+
end
|
23
|
+
|
24
|
+
def prefix(identifier); "success_tracker:#{identifier}" end
|
25
|
+
|
26
|
+
def success(identifier)
|
27
|
+
callbacks[:success].call(identifier) if callbacks[:success]
|
28
|
+
store(identifier, "1")
|
29
|
+
end
|
30
|
+
|
31
|
+
# +identifier+ is the key used for grouping success and failure cases together.
|
32
|
+
# +notify_rule+ is a symbol identifying the code block which is evaluated to know if the error is significant or not.
|
33
|
+
# + options+ is a hash which currently can only contain the list of exceptions which should be tagged with the NonSignificantError module
|
34
|
+
# the given block is always evaluated and the resulting errors are tagged with the NonSignificantError and reraised
|
35
|
+
def failure(identifier, notify_rule, options={})
|
36
|
+
callbacks[:failure].call(identifier) if callbacks[:failure]
|
37
|
+
store(identifier, nil)
|
38
|
+
|
39
|
+
redis.del(prefix(identifier)) if notify = rules[notify_rule].call(redis.lrange(prefix(identifier), 0,-1))
|
40
|
+
|
41
|
+
begin
|
42
|
+
yield if block_given?
|
43
|
+
rescue *(options[:exceptions] || [StandardError]) => error
|
44
|
+
error.extend(NonSignificantError) unless notify
|
45
|
+
raise
|
46
|
+
end
|
47
|
+
|
48
|
+
return notify
|
49
|
+
end
|
50
|
+
|
51
|
+
# yields the given code block and then marks success. In case a exception was triggered it marks a failure and reraises the exception (for the arguments see the #failure method)
|
52
|
+
def track(identifier, notify_rule, options={})
|
53
|
+
yield.tap { success(identifier) }
|
54
|
+
rescue => exception
|
55
|
+
failure(identifier, notify_rule, options) { raise exception }
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
# returns true if the failure ratio is higher than x (with a minimum of 10 records)
|
60
|
+
def self.ratio_rule(ratio=0.1, minimum=10)
|
61
|
+
lambda { |list| list.length >= minimum and list.select(&:empty?).length.to_f / list.length >= ratio }
|
62
|
+
end
|
63
|
+
|
64
|
+
# returns true if the last x elements have failed
|
65
|
+
def self.sequence_rule(elements=5)
|
66
|
+
lambda { |list| list.length >= elements && list[0..elements-1].reject(&:empty?).length == 0 }
|
67
|
+
end
|
68
|
+
|
69
|
+
protected
|
70
|
+
def store(identifier, value)
|
71
|
+
redis.lpush(prefix(identifier), value)
|
72
|
+
redis.ltrim(prefix(identifier), 0, @list_length - 1)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'success_tracker/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.homepage = "https://github.com/autohaus24/success_tracker"
|
8
|
+
gem.name = "success_tracker"
|
9
|
+
gem.version = SuccessTracker::VERSION
|
10
|
+
gem.authors = ["Michael Raidel"]
|
11
|
+
gem.email = ["m.raidel@autohaus24.de"]
|
12
|
+
gem.description = %q{SuccessTracker allows you to track success and failure of tasks}
|
13
|
+
gem.summary = %q{SuccessTracker allows you to track success and failure of tasks and define thresholds for unexpected failure conditions.}
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
gem.add_dependency "redis"
|
20
|
+
gem.add_development_dependency "shoulda"
|
21
|
+
gem.add_development_dependency "rake"
|
22
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'shoulda'
|
3
|
+
require 'success_tracker'
|
4
|
+
|
5
|
+
class SuccessTracker::BaseTest < Test::Unit::TestCase
|
6
|
+
def setup
|
7
|
+
@redis = Redis.new
|
8
|
+
@prefix = "success_tracker"
|
9
|
+
@key = "test_key"
|
10
|
+
end
|
11
|
+
|
12
|
+
def teardown
|
13
|
+
@redis.del("#{@prefix}:#{@key}")
|
14
|
+
end
|
15
|
+
|
16
|
+
should "store a success in the given redis key" do
|
17
|
+
success_tracker = SuccessTracker::Base.new(@redis)
|
18
|
+
success_tracker.success(@key)
|
19
|
+
|
20
|
+
assert_equal ["1"], @redis.lrange("#{@prefix}:#{@key}", 0, -1)
|
21
|
+
end
|
22
|
+
|
23
|
+
should "store an error in the given redis key" do
|
24
|
+
success_tracker = SuccessTracker::Base.new(@redis)
|
25
|
+
success_tracker.failure(@key, :percent_10)
|
26
|
+
|
27
|
+
assert_equal [""], @redis.lrange("#{@prefix}:#{@key}", 0, -1)
|
28
|
+
end
|
29
|
+
|
30
|
+
should "yield the success callback on success" do
|
31
|
+
success_tracker = SuccessTracker::Base.new(@redis, :callbacks => { :success => lambda { |identifier| @identifier = "success: #{identifier}" } })
|
32
|
+
success_tracker.success(@key)
|
33
|
+
|
34
|
+
assert_equal "success: #{@key}", @identifier
|
35
|
+
end
|
36
|
+
|
37
|
+
should "yield the failure callback on failure" do
|
38
|
+
success_tracker = SuccessTracker::Base.new(@redis, :callbacks => { :failure => lambda { |identifier| @identifier = "failure: #{identifier}" } })
|
39
|
+
success_tracker.failure(@key, :percent_10)
|
40
|
+
|
41
|
+
assert_equal "failure: #{@key}", @identifier
|
42
|
+
end
|
43
|
+
|
44
|
+
should "raise an ArgumentError when initializing with unknown options" do
|
45
|
+
assert_raise(ArgumentError) do
|
46
|
+
SuccessTracker::Base.new(@redis, :foo => "bar")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
should "allow a maximum number of records" do
|
51
|
+
success_tracker = SuccessTracker::Base.new(@redis)
|
52
|
+
105.times { success_tracker.success(@key) }
|
53
|
+
assert_equal 100, @redis.lrange("#{@prefix}:#{@key}", 0, -1).length
|
54
|
+
end
|
55
|
+
|
56
|
+
should "yield failure block" do
|
57
|
+
success_tracker = SuccessTracker::Base.new(@redis)
|
58
|
+
success_tracker.failure(@key, :percent_10) do
|
59
|
+
@output_from_block = true
|
60
|
+
end
|
61
|
+
|
62
|
+
assert @output_from_block
|
63
|
+
end
|
64
|
+
|
65
|
+
should "track success" do
|
66
|
+
success_tracker = SuccessTracker::Base.new(@redis)
|
67
|
+
success_tracker.track(@key, :percent_10) { "working block" }
|
68
|
+
assert_equal ["1"], @redis.lrange("#{@prefix}:#{@key}", 0, -1)
|
69
|
+
end
|
70
|
+
|
71
|
+
should "return result from block on #track" do
|
72
|
+
success_tracker = SuccessTracker::Base.new(@redis)
|
73
|
+
assert_equal "result", success_tracker.track(@key, :percent_10) { "result" }
|
74
|
+
end
|
75
|
+
|
76
|
+
should "track failure and reraise tagged exception" do
|
77
|
+
success_tracker = SuccessTracker::Base.new(@redis)
|
78
|
+
begin
|
79
|
+
success_tracker.track(@key, :percent_10) { raise ArgumentError }
|
80
|
+
flunk "should have raised an exception before"
|
81
|
+
rescue => exception
|
82
|
+
assert ArgumentError === exception, "exception should be an ArgumentError"
|
83
|
+
assert SuccessTracker::NonSignificantError === exception, "exception should be a NonSignificantError"
|
84
|
+
end
|
85
|
+
assert_equal [""], @redis.lrange("#{@prefix}:#{@key}", 0, -1)
|
86
|
+
end
|
87
|
+
|
88
|
+
context "ratio_rule" do
|
89
|
+
should "return false until threshold of x percent is reached" do
|
90
|
+
rule = SuccessTracker::Base.ratio_rule(0.1)
|
91
|
+
assert !rule.call(["1"] * 10 + [""] * 1), "should return false when threshold is not reached"
|
92
|
+
assert rule.call(["1"] * 9 + [""] * 1), "should return true when threshold is reached"
|
93
|
+
end
|
94
|
+
|
95
|
+
should "always return true before minimum" do
|
96
|
+
rule = SuccessTracker::Base.ratio_rule(0.1, 3)
|
97
|
+
assert !rule.call([""] * 2), "should return false when below minimum"
|
98
|
+
assert rule.call([""] * 3), "should return true when minimum is reached"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context "sequence_rule" do
|
103
|
+
should "return false until threshold of x percent is reached" do
|
104
|
+
rule = SuccessTracker::Base.sequence_rule(5)
|
105
|
+
assert !rule.call([""] * 4 + ["1"] * 5), "should return false before having x failures in a row"
|
106
|
+
assert rule.call([""] * 5 + ["1"] * 5), "should return true when having x failures in a row"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context "over threshold" do
|
111
|
+
setup do
|
112
|
+
@success_tracker = SuccessTracker::Base.new(@redis, :rules => { :never_below_threshold => lambda { |list| true } })
|
113
|
+
end
|
114
|
+
|
115
|
+
should "reraise errors from failure block not extended with module" do
|
116
|
+
begin
|
117
|
+
@success_tracker.failure(@key, :never_below_threshold) do
|
118
|
+
raise NoMethodError
|
119
|
+
end
|
120
|
+
flunk "should raise exception before"
|
121
|
+
rescue NoMethodError => exception
|
122
|
+
assert !(SuccessTracker::NonSignificantError === exception), "should not have the module when meeting the error condition"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
should "empty the list" do
|
127
|
+
@success_tracker.failure(@key, :never_below_threshold)
|
128
|
+
assert_equal [], @redis.lrange("#{@prefix}:#{@key}", 0, -1)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
context "below threshold" do
|
133
|
+
setup do
|
134
|
+
@success_tracker = SuccessTracker::Base.new(@redis, :rules => { :always_below_threshold => lambda { |list| false } })
|
135
|
+
end
|
136
|
+
|
137
|
+
should "reraise errors from failure block extended with module" do
|
138
|
+
begin
|
139
|
+
@success_tracker.failure(@key, :always_below_threshold) do
|
140
|
+
raise NoMethodError
|
141
|
+
end
|
142
|
+
flunk "should raise exception before"
|
143
|
+
rescue NoMethodError => exception
|
144
|
+
assert SuccessTracker::NonSignificantError === exception, "should have the module before meeting the error condition"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
should "not reraise errors from failure block extended with module" do
|
149
|
+
begin
|
150
|
+
@success_tracker.failure(@key, :always_below_threshold, :exceptions => [ ArgumentError ]) do
|
151
|
+
raise NoMethodError
|
152
|
+
end
|
153
|
+
flunk "should raise exception before"
|
154
|
+
rescue NoMethodError => exception
|
155
|
+
assert !(SuccessTracker::NonSignificantError === exception), "should not have the module for an NoMethodError"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
should "not empty the list" do
|
160
|
+
@success_tracker.failure(@key, :always_below_threshold)
|
161
|
+
assert_equal [""], @redis.lrange("#{@prefix}:#{@key}", 0, -1)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: success_tracker
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.5'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Michael Raidel
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-28 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: redis
|
16
|
+
requirement: &86238180 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *86238180
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: shoulda
|
27
|
+
requirement: &86237970 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *86237970
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rake
|
38
|
+
requirement: &86237730 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *86237730
|
47
|
+
description: SuccessTracker allows you to track success and failure of tasks
|
48
|
+
email:
|
49
|
+
- m.raidel@autohaus24.de
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- Gemfile
|
56
|
+
- LICENSE.txt
|
57
|
+
- README.md
|
58
|
+
- Rakefile
|
59
|
+
- lib/success_tracker.rb
|
60
|
+
- lib/success_tracker/version.rb
|
61
|
+
- success_tracker.gemspec
|
62
|
+
- test/success_tracker_test.rb
|
63
|
+
homepage: https://github.com/autohaus24/success_tracker
|
64
|
+
licenses: []
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ! '>='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ! '>='
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
requirements: []
|
82
|
+
rubyforge_project:
|
83
|
+
rubygems_version: 1.8.11
|
84
|
+
signing_key:
|
85
|
+
specification_version: 3
|
86
|
+
summary: SuccessTracker allows you to track success and failure of tasks and define
|
87
|
+
thresholds for unexpected failure conditions.
|
88
|
+
test_files:
|
89
|
+
- test/success_tracker_test.rb
|
90
|
+
has_rdoc:
|