aesop 1.1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.travis.yml +21 -0
- data/Gemfile +13 -0
- data/LICENSE +0 -0
- data/README.md +8 -0
- data/Rakefile +20 -0
- data/aesop.gemspec +29 -0
- data/config/init.rb +24 -0
- data/lib/aesop.rb +27 -0
- data/lib/aesop/aesop.rb +135 -0
- data/lib/aesop/bootloader.rb +76 -0
- data/lib/aesop/configuration.rb +13 -0
- data/lib/aesop/dispatcher.rb +49 -0
- data/lib/aesop/dispatchers/log_dispatcher.rb +7 -0
- data/lib/aesop/exceptions.rb +7 -0
- data/lib/aesop/logger.rb +41 -0
- data/lib/aesop/merb/merb_boot_loader.rb +8 -0
- data/lib/aesop/rails/middleware.rb +18 -0
- data/lib/aesop/rails/railtie.rb +18 -0
- data/lib/aesop/recipes.rb +20 -0
- data/lib/aesop/version.rb +10 -0
- data/recipes/aesop.rb +6 -0
- data/spec/aesop/aesop_spec.rb +276 -0
- data/spec/aesop/bootloader_spec.rb +138 -0
- data/spec/aesop/dispatcher_spec.rb +76 -0
- data/spec/aesop/dispatchers/log_dispatcher_spec.rb +19 -0
- data/spec/aesop/hooks_spec.rb +79 -0
- data/spec/aesop/logger_spec.rb +72 -0
- data/spec/aesop/merb/merb_boot_loader_spec.rb +25 -0
- data/spec/aesop/rails/middleware_spec.rb +13 -0
- data/spec/aesop/rails/railtie_spec.rb +62 -0
- data/spec/aesop/recipes_spec.rb +47 -0
- data/spec/spec_helper.rb +47 -0
- metadata +188 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NTIwZWNjYjljOWYyYzBlYWViYTYyZWNiNmYxMWFhNDNiNzViOTI0YQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
M2I2Y2Y5NWM1MDlmYjU0ODBhNzQ5Mzk2MmE3YTgwNjM0YWNjNzQwNA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YmI1MzIxMjRkNjMxMjFjNWM1ZThhYWRkZGJmNTY0ZTM5MTJlNmU5Yjc0YzUz
|
10
|
+
MTVhOTIyODcwMDcyNTVkNjc1NTMxZGI2ODhkYWU3ZmQ1MWM0YTM5ZGE0NmVi
|
11
|
+
NjcwM2JjMDIyNGQyNWM5ODQwN2RiMWZlYWVlZDI3OGQzNjAyNjQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MTkzZDJlNDJjNmRhNGQ1MTBiZjBmMTNlNDM5NzU4YzE0NTkwMmZhNTNhMTBj
|
14
|
+
NzIzYzg0MTE3ZTUxYzNlZmU1NjgxNzg1OTBmOWVjOTE4YTNhNWZmNzBjYzdj
|
15
|
+
MDlkNjVlYzY3Y2JmNTI5MTViNjg0MDcxMzlmMGEzOTU4YmFkMTA=
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
language: ruby
|
2
|
+
cache: bundler
|
3
|
+
services: redis
|
4
|
+
|
5
|
+
jdk:
|
6
|
+
- oraclejdk7
|
7
|
+
|
8
|
+
rvm:
|
9
|
+
- 1.8.7
|
10
|
+
- 1.9.3
|
11
|
+
- 2.0.0
|
12
|
+
- rbx-19mode
|
13
|
+
- jruby-19mode
|
14
|
+
- ruby-head
|
15
|
+
- jruby-head
|
16
|
+
|
17
|
+
matrix:
|
18
|
+
allow_failures:
|
19
|
+
- rvm: ruby-head
|
20
|
+
- rvm: jruby-head
|
21
|
+
- rvm: rbx-19mode
|
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in aesop.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem "configatron", :github => 'jwkoelewijn/configatron'
|
7
|
+
|
8
|
+
group :test do
|
9
|
+
gem "codeclimate-test-reporter", :require => false
|
10
|
+
gem "simplecov", :require => false
|
11
|
+
gem "capistrano-spec"
|
12
|
+
gem "capistrano", "~> 2.14.2"
|
13
|
+
end
|
data/LICENSE
ADDED
File without changes
|
data/README.md
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
aesop
|
2
|
+
=====
|
3
|
+
|
4
|
+
[](https://travis-ci.org/jwkoelewijn/aesop)
|
5
|
+
[](https://codeclimate.com/github/jwkoelewijn/aesop)
|
6
|
+
|
7
|
+
|
8
|
+
Like the boy who cried wolf, it is possible to receive so many error reports that you, as a developer/maintainer can get numb to new errors. With Aesop, errors will only be dispatched within a specified time after the last deployment and only if it did occur more often than a certain, configurable threshold.
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
|
11
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
|
12
|
+
|
13
|
+
require 'rspec/core/rake_task'
|
14
|
+
|
15
|
+
desc "Run specs"
|
16
|
+
RSpec::Core::RakeTask.new do |t|
|
17
|
+
t.pattern = 'spec/**/*_spec.rb'
|
18
|
+
end
|
19
|
+
|
20
|
+
task :default => :spec
|
data/aesop.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'aesop/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "aesop"
|
8
|
+
spec.version = Aesop::VERSION
|
9
|
+
spec.authors = ["J.W. Koelewijn"]
|
10
|
+
spec.email = ["jwkoelewijn@gmail.com"]
|
11
|
+
spec.description = %q{Check deployment time, write it to Redis and when exceptions are thrown, check if it should send notification}
|
12
|
+
spec.summary = %q{Manage exception notification}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
|
25
|
+
spec.add_dependency "redis"
|
26
|
+
spec.add_dependency "hiredis"
|
27
|
+
spec.add_dependency "configatron"
|
28
|
+
spec.add_dependency "log4r"
|
29
|
+
end
|
data/config/init.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'configatron'
|
2
|
+
|
3
|
+
configatron.redis do |redis|
|
4
|
+
redis.host = 'localhost'
|
5
|
+
redis.port = 6379
|
6
|
+
redis.password = ''
|
7
|
+
redis.database = 1
|
8
|
+
end
|
9
|
+
|
10
|
+
configatron.logger do |logger|
|
11
|
+
logger.name = 'Aesop'
|
12
|
+
logger.level = Aesop::Logger::INFO
|
13
|
+
logger.outputters = 'stdout'
|
14
|
+
end
|
15
|
+
|
16
|
+
configatron.deployment_key = 'aesop:deployment:timestamp'
|
17
|
+
configatron.deployment_file = 'DEPLOY_TIME'
|
18
|
+
configatron.exception_prefix = 'aesop:exceptions'
|
19
|
+
configatron.exception_count_threshold = 10
|
20
|
+
configatron.exception_time_threshold = 60*60
|
21
|
+
|
22
|
+
configatron.phonenumbers = []
|
23
|
+
configatron.dispatchers = [:log_dispatcher]
|
24
|
+
configatron.excluded_exceptions = []
|
data/lib/aesop.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require "aesop/version"
|
2
|
+
require "configatron"
|
3
|
+
require "aesop/configuration"
|
4
|
+
require "aesop/exceptions"
|
5
|
+
require "aesop/logger"
|
6
|
+
#Kernel.load File.join(File.dirname(__FILE__), '..', 'config', 'init.rb')
|
7
|
+
|
8
|
+
require "aesop/aesop"
|
9
|
+
require "aesop/bootloader"
|
10
|
+
require "aesop/dispatcher"
|
11
|
+
require "aesop/rails/middleware"
|
12
|
+
require "redis"
|
13
|
+
require "hiredis"
|
14
|
+
|
15
|
+
if defined?(Merb) && defined?(Merb::BootLoader)
|
16
|
+
require "aesop/merb/merb_boot_loader"
|
17
|
+
elsif defined? Rails
|
18
|
+
if Rails.respond_to?(:version) && Rails.version > '3'
|
19
|
+
require "aesop/rails/railtie"
|
20
|
+
else
|
21
|
+
# After verison 2.0 of Rails we can access the configuration directly.
|
22
|
+
# We need it to add dev mode routes after initialization finished.
|
23
|
+
Aesop::Aesop.instance.init
|
24
|
+
end
|
25
|
+
else
|
26
|
+
Aesop::Aesop.instance.init
|
27
|
+
end
|
data/lib/aesop/aesop.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
class Aesop::Aesop
|
4
|
+
include Singleton
|
5
|
+
include ::Aesop
|
6
|
+
|
7
|
+
def init
|
8
|
+
load_configuration
|
9
|
+
Aesop::Bootloader.new.boot
|
10
|
+
end
|
11
|
+
|
12
|
+
def load_configuration
|
13
|
+
config_file = if File.exist?("config/aesop.rb")
|
14
|
+
File.expand_path("config/aesop.rb")
|
15
|
+
else
|
16
|
+
File.expand_path(File.join( File.dirname(__FILE__), '..', '..', 'config', 'init.rb'))
|
17
|
+
end
|
18
|
+
load config_file
|
19
|
+
Aesop::Logger.debug("Loaded config in #{config_file}")
|
20
|
+
end
|
21
|
+
|
22
|
+
def catch_exceptions( exceptions )
|
23
|
+
if exceptions.is_a?(Array)
|
24
|
+
exceptions.each{ |e| catch_exception(e) }
|
25
|
+
else
|
26
|
+
raise IllegalArgumentException.new("#catch_exceptions should be called with an Array as argument, maybe use #catch_exception instead?")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def catch_exception(exception)
|
31
|
+
store_exception_occurrence(exception)
|
32
|
+
if should_dispatch?(exception)
|
33
|
+
dispatch_exception(exception)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def should_dispatch?(exception)
|
38
|
+
return false if is_excluded?(exception) || exception_already_dispatched?(exception)
|
39
|
+
return true if internal_exception?(exception)
|
40
|
+
current_amount = retrieve_exception_count(exception)
|
41
|
+
within_window?(exception) && (current_amount >= exception_count_threshold(exception))
|
42
|
+
end
|
43
|
+
|
44
|
+
def internal_exception?(exception)
|
45
|
+
parts = exception.class.name.split("::")
|
46
|
+
res = (parts.size > 1 && parts.first == "Aesop")
|
47
|
+
Aesop::Logger.debug("#{exception.class.to_s} is #{res ? "": "not "}an internal exception and will #{res ? "" : "not "}be dispatched right away")
|
48
|
+
res
|
49
|
+
end
|
50
|
+
|
51
|
+
def exception_count_threshold(exception)
|
52
|
+
configuration.exception_count_threshold
|
53
|
+
end
|
54
|
+
|
55
|
+
def record_exception_dispatch(exception)
|
56
|
+
redis.set( "#{exception_prefix}:#{exception.class.to_s}:dispatched", Time.now.to_i )
|
57
|
+
end
|
58
|
+
|
59
|
+
def dispatch_exception(exception)
|
60
|
+
record_exception_dispatch(exception)
|
61
|
+
Aesop::Dispatcher.instance.dispatch_exception(exception)
|
62
|
+
end
|
63
|
+
|
64
|
+
def exception_already_dispatched?(exception)
|
65
|
+
res = !redis.get( "#{exception_prefix}:#{exception.class.to_s}:dispatched" ).nil?
|
66
|
+
Aesop::Logger.debug("#{exception.class.to_s} has #{res ? "already" : "not yet"} been dispatched")
|
67
|
+
res
|
68
|
+
end
|
69
|
+
|
70
|
+
def retrieve_exception_count(exception)
|
71
|
+
res = redis.get( "#{exception_prefix}:#{exception.class.to_s}:count" ).to_i
|
72
|
+
Aesop::Logger.debug("This is occurrence number #{res} of #{exception.class.to_s}")
|
73
|
+
res
|
74
|
+
end
|
75
|
+
|
76
|
+
def exception_prefix
|
77
|
+
configuration.exception_prefix
|
78
|
+
end
|
79
|
+
|
80
|
+
def within_window?( exception )
|
81
|
+
res = if deployed_time = retrieve_deployment_time
|
82
|
+
(Time.now - deployed_time) < exception_time_threshold(exception)
|
83
|
+
else
|
84
|
+
false
|
85
|
+
end
|
86
|
+
Aesop::Logger.debug("#{exception.class.to_s} is#{res ? " " : " not "}within the window")
|
87
|
+
res
|
88
|
+
end
|
89
|
+
|
90
|
+
def is_excluded?( exception )
|
91
|
+
res = if (exceptions = configuration.excluded_exceptions)
|
92
|
+
exceptions.include?( exception.class )
|
93
|
+
else
|
94
|
+
false
|
95
|
+
end
|
96
|
+
Aesop::Logger.debug( "#{exception.class.to_s} is#{res ? " " : " not "}excluded")
|
97
|
+
res
|
98
|
+
end
|
99
|
+
|
100
|
+
def exception_time_threshold( exception )
|
101
|
+
configuration.exception_time_threshold
|
102
|
+
end
|
103
|
+
|
104
|
+
def retrieve_deployment_time
|
105
|
+
timestamp = redis.get( configuration.deployment_key ).to_i
|
106
|
+
Time.at(timestamp)
|
107
|
+
end
|
108
|
+
|
109
|
+
def store_exception_occurrence(exception)
|
110
|
+
redis.incr( "#{exception_prefix}:#{exception.class.to_s}:count" )
|
111
|
+
end
|
112
|
+
|
113
|
+
def redis_options
|
114
|
+
options = {
|
115
|
+
:host => configuration.redis.host,
|
116
|
+
:port => configuration.redis.port,
|
117
|
+
}
|
118
|
+
if (password = configuration.redis.password) && !password.empty?
|
119
|
+
options.merge!(:password => password)
|
120
|
+
end
|
121
|
+
options
|
122
|
+
end
|
123
|
+
|
124
|
+
def redis
|
125
|
+
if @redis.nil? || !@redis.connected?
|
126
|
+
begin
|
127
|
+
@redis = Redis.new(redis_options)
|
128
|
+
@redis.select( configuration.redis.database )
|
129
|
+
rescue => e
|
130
|
+
raise RedisConnectionException.new( e )
|
131
|
+
end
|
132
|
+
end
|
133
|
+
@redis
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
class Aesop::Bootloader
|
2
|
+
include ::Aesop
|
3
|
+
|
4
|
+
def boot
|
5
|
+
load_dispatchers
|
6
|
+
begin
|
7
|
+
if time = determine_latest_deploy_time
|
8
|
+
Aesop::Logger.info("Last deployment was at #{Time.at(time)}")
|
9
|
+
store_timestamp( time )
|
10
|
+
end
|
11
|
+
rescue => e
|
12
|
+
raise Aesop::BootloaderException.new(e)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def determine_latest_deploy_time
|
17
|
+
file_time = read_deploy_time
|
18
|
+
redis_time = read_current_timestamp
|
19
|
+
|
20
|
+
if file_time
|
21
|
+
if redis_time > 0
|
22
|
+
reset_exceptions if file_time > redis_time
|
23
|
+
[redis_time, file_time].max
|
24
|
+
else
|
25
|
+
reset_exceptions
|
26
|
+
file_time
|
27
|
+
end
|
28
|
+
else
|
29
|
+
redis_time ? redis_time : Time.now.to_i
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def read_deploy_time
|
34
|
+
if File.exists?(deployment_file)
|
35
|
+
File.open(deployment_file) do |file|
|
36
|
+
file.read.to_i
|
37
|
+
end
|
38
|
+
else
|
39
|
+
return nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def load_dispatchers
|
44
|
+
begin
|
45
|
+
current_dir = File.dirname(__FILE__)
|
46
|
+
Dir["#{current_dir}/dispatchers/**/*.rb"].each do |file|
|
47
|
+
require File.expand_path(file)
|
48
|
+
end
|
49
|
+
rescue => e
|
50
|
+
raise DispatcherLoadException.new(e)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def read_current_timestamp
|
55
|
+
redis.get( configuration.deployment_key ).to_i
|
56
|
+
end
|
57
|
+
|
58
|
+
def store_timestamp( time )
|
59
|
+
redis.set( configuration.deployment_key, time.to_i )
|
60
|
+
end
|
61
|
+
|
62
|
+
def deployment_file
|
63
|
+
self.configuration.deployment_file
|
64
|
+
end
|
65
|
+
|
66
|
+
def reset_exceptions
|
67
|
+
Aesop::Logger.debug("Resetting stored exception occurrences")
|
68
|
+
redis.keys( "#{configuration.exception_prefix}:*" ).each do |key|
|
69
|
+
redis.del key
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def redis
|
74
|
+
Aesop.instance.redis
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
module Aesop
|
3
|
+
module Dispatchers
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
class Aesop::Dispatcher
|
8
|
+
include Singleton
|
9
|
+
include ::Aesop
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
collect_dispatchers
|
13
|
+
end
|
14
|
+
|
15
|
+
def dispatch_exception(exception)
|
16
|
+
dispatchers.each do |dispatcher|
|
17
|
+
begin
|
18
|
+
Aesop::Logger.debug("#{dispatcher.class.to_s}: dispatching #{exception.class.to_s}")
|
19
|
+
dispatcher.dispatch_exception(exception)
|
20
|
+
rescue => e
|
21
|
+
Aesop::Logger.error( "Exception in #{dispatcher.class.to_s}: Exception: #{exception.class.to_s}. Trying to dispatch: #{e.class.to_s}: #{e.message}" )
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def instantiate_dispatcher( symbol )
|
27
|
+
Aesop::Logger.debug("Instantiating #{to_classname(symbol)}")
|
28
|
+
Aesop::Dispatchers.const_get( to_classname(symbol) ).new
|
29
|
+
end
|
30
|
+
|
31
|
+
def collect_dispatchers
|
32
|
+
configuration.dispatchers.each do |dispatch_symbol|
|
33
|
+
instance = instantiate_dispatcher( dispatch_symbol )
|
34
|
+
register_dispatcher(instance)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def register_dispatcher( dispatcher )
|
39
|
+
dispatchers << dispatcher
|
40
|
+
end
|
41
|
+
|
42
|
+
def dispatchers
|
43
|
+
@dispatchers ||= []
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_classname(symbol)
|
47
|
+
symbol.to_s.split(/[-_]/).map(&:capitalize).join
|
48
|
+
end
|
49
|
+
end
|