aesop 1.1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/jwkoelewijn/aesop.png?branch=master)](https://travis-ci.org/jwkoelewijn/aesop)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/jwkoelewijn/aesop.png)](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
|