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.
@@ -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=
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ DEPLOY_TIME
19
+ .DS_STORE
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --colour
2
+ --backtrace
3
+ --format documentation
@@ -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
@@ -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.
@@ -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
@@ -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
@@ -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 = []
@@ -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
@@ -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,13 @@
1
+ module Aesop
2
+ def configuration
3
+ configatron
4
+ end
5
+
6
+ def self.configuration
7
+ if block_given?
8
+ yield configatron
9
+ else
10
+ configatron
11
+ end
12
+ end
13
+ 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