global_error_handler 0.3.3 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Rakefile +1 -2
- data/global_error_handler.gemspec +12 -10
- data/lib/global_error_handler/app_exception.rb +64 -56
- data/lib/global_error_handler/handler.rb +21 -19
- data/lib/global_error_handler/middleware.rb +14 -12
- data/lib/global_error_handler/parser.rb +41 -39
- data/lib/global_error_handler/rails.rb +15 -13
- data/lib/global_error_handler/redis.rb +152 -120
- data/lib/global_error_handler/redis_notification_subscriber.rb +57 -53
- data/lib/global_error_handler/server/views/index.html.haml +4 -4
- data/lib/global_error_handler/server.rb +27 -26
- data/lib/global_error_handler/version.rb +2 -2
- data/lib/global_error_handler.rb +4 -3
- data/lib/recipes/global_error_handler.rb +4 -4
- data/lib/tasks/global_error_handler.rake +13 -5
- metadata +36 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 08b88ca05ee6a8591095ccc87331c1bca8afaa71
|
4
|
+
data.tar.gz: 5242b40af3b6c0228fb80185cf948e987aef1263
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c6faa1b848bfc346376787cf7b212e90007174d14a07e4ff861dd206d5385ffb861600afb094ec329fad20a9340011dbedacd87d99cca2f14d34cb0184d3d379
|
7
|
+
data.tar.gz: 20e3123c510cd74ed829b477045cbe58c68402f4c427c2e999b1a2f13bd03782ca3c5cf69ef881d3aba52f3399526638cb0615633cc83ef2f8ca5579be1bba78
|
data/.gitignore
CHANGED
data/Rakefile
CHANGED
@@ -1,2 +1 @@
|
|
1
|
-
require
|
2
|
-
|
1
|
+
require 'bundler/gem_tasks'
|
@@ -4,23 +4,25 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'global_error_handler/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = 'global_error_handler'
|
8
8
|
spec.version = GlobalErrorHandler::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
-
spec.summary =
|
12
|
-
spec.description =
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
9
|
+
spec.authors = ['Andrii Rudenko']
|
10
|
+
spec.email = ['kolobock@gmail.com']
|
11
|
+
spec.summary = "Records application' exceptions into the separated redis database."
|
12
|
+
spec.description = 'On the middleware level catch an exception from Rails app and store in the separated Redis database.'
|
13
|
+
spec.homepage = 'https://github.com/kolobock/global_error_handler/'
|
14
|
+
spec.license = 'MIT'
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0")
|
17
17
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
|
20
|
-
spec.add_development_dependency
|
21
|
-
spec.add_development_dependency
|
22
|
-
spec.add_development_dependency
|
20
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
21
|
+
spec.add_development_dependency 'rake', '~> 10.3', '>= 10.3.2'
|
22
|
+
spec.add_development_dependency 'capistrano', '< 3.0'
|
23
23
|
|
24
24
|
spec.add_dependency 'resque', '~> 1.25', '>= 1.25.1'
|
25
25
|
spec.add_dependency 'haml', '~> 4.0', '>= 4.0.5', '< 4.1'
|
26
|
+
spec.add_runtime_dependency 'actionview', '>= 4.0.5', '< 4.3'
|
27
|
+
spec.add_dependency 'hashie', '>= 3.3'
|
26
28
|
end
|
@@ -1,72 +1,80 @@
|
|
1
|
-
|
2
|
-
class
|
3
|
-
|
4
|
-
start
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
1
|
+
module GlobalErrorHandler
|
2
|
+
class AppException #:nodoc:
|
3
|
+
class << self
|
4
|
+
def all(start, field = nil, filter = nil)
|
5
|
+
start ||= 0
|
6
|
+
if field && filter
|
7
|
+
keys = Redis.filter_exception_keys start, "error_#{field}", filter
|
8
|
+
else
|
9
|
+
keys = Redis.exception_keys start
|
10
|
+
end
|
11
|
+
Redis.find_all keys
|
9
12
|
end
|
10
|
-
GlobalErrorHandler::Redis.find_all keys
|
11
|
-
end
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
def count(field = nil, filter = nil)
|
15
|
+
if field && filter
|
16
|
+
Redis.filtered_exceptions_count("error_#{field}", filter)
|
17
|
+
else
|
18
|
+
Redis.exceptions_count
|
19
|
+
end
|
18
20
|
end
|
19
|
-
end
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
def find(id)
|
23
|
+
return if id.blank?
|
24
|
+
Redis.find exception_key(id)
|
25
|
+
end
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
def delete(id)
|
28
|
+
return if id.blank?
|
29
|
+
Redis.delete exception_key(id)
|
30
|
+
end
|
30
31
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
32
|
+
def delete_all(ids)
|
33
|
+
return if ids.blank?
|
34
|
+
keys = ids.map { |id| exception_key id }
|
35
|
+
Redis.delete_all keys
|
36
|
+
end
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
38
|
+
def truncate(filter = nil, opts = {})
|
39
|
+
if filter
|
40
|
+
field = opts.delete(:field)
|
41
|
+
total = opts.delete(:total) || 1000
|
42
|
+
size = 1000
|
43
|
+
(total / size.to_f).ceil.times do |iteration|
|
44
|
+
ids = filtered_ids_by field, filter, size, iteration
|
45
|
+
delete_all ids unless ids.blank?
|
46
|
+
end
|
47
|
+
else
|
48
|
+
Redis.truncate!
|
45
49
|
end
|
46
|
-
else
|
47
|
-
GlobalErrorHandler::Redis.truncate!
|
48
50
|
end
|
49
|
-
end
|
50
51
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
52
|
+
def filters_for(field)
|
53
|
+
keys = Redis.filter_keys_for "error_#{field}"
|
54
|
+
return [] if keys.blank?
|
55
|
+
keys.map do |key|
|
56
|
+
key =~ /^#{Redis::FILTER_KEY_PREFIX}:error_#{field}:(.*)/
|
57
|
+
Regexp.last_match(1)
|
58
|
+
end
|
57
59
|
end
|
58
|
-
end
|
59
60
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
61
|
+
def filtered_ids_by(field, str, len = 1000, start = 0)
|
62
|
+
keys = Redis.filter_exception_keys start, "error_#{field}", str, len
|
63
|
+
return [] if keys.blank?
|
64
|
+
keys.map do |key|
|
65
|
+
begin
|
66
|
+
key.split(':').last
|
67
|
+
rescue
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
end.compact
|
71
|
+
end
|
65
72
|
|
66
|
-
|
73
|
+
private
|
67
74
|
|
68
|
-
|
69
|
-
|
70
|
-
|
75
|
+
def exception_key(id)
|
76
|
+
Redis.exception_key(id)
|
77
|
+
end
|
78
|
+
end # class << self
|
71
79
|
end
|
72
80
|
end
|
@@ -1,25 +1,27 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
1
|
+
module GlobalErrorHandler
|
2
|
+
class Handler #:nodoc:
|
3
|
+
def initialize(env, exception)
|
4
|
+
@env = env
|
5
|
+
@exception = exception
|
6
|
+
@controller = @env['action_controller.instance']
|
7
|
+
@parsed_error = nil
|
8
|
+
end
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
def process_exception!
|
11
|
+
return if @env['global_error_handler.proceed_time']
|
12
|
+
@env['global_error_handler.proceed_time'] = Time.current.utc
|
13
|
+
parse_exception
|
14
|
+
store_exception
|
15
|
+
end
|
15
16
|
|
16
|
-
|
17
|
+
private
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
def parse_exception
|
20
|
+
@parsed_error = Parser.new(@env, @exception, @controller).parse
|
21
|
+
end
|
21
22
|
|
22
|
-
|
23
|
-
|
23
|
+
def store_exception
|
24
|
+
Redis.store(@parsed_error.info_hash)
|
25
|
+
end
|
24
26
|
end
|
25
27
|
end
|
@@ -1,15 +1,17 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
module GlobalErrorHandler
|
2
|
+
class Middleware #:nodoc:
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
end
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
7
|
+
def call(env)
|
8
|
+
exception = nil
|
9
|
+
status, headers, response = @app.call(env)
|
10
|
+
rescue StandardError => exception
|
11
|
+
GlobalErrorHandler::Handler.new(env, exception).process_exception!
|
12
|
+
ensure
|
13
|
+
fail exception if exception
|
14
|
+
[status, headers, response]
|
15
|
+
end
|
14
16
|
end
|
15
17
|
end
|
@@ -1,47 +1,49 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module GlobalErrorHandler
|
2
|
+
class Parser #:nodoc:
|
3
|
+
attr_reader :info_hash
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
5
|
+
def initialize(env, exception, controller)
|
6
|
+
@env = env
|
7
|
+
@exception = exception
|
8
|
+
@controller = controller
|
9
|
+
@request = ActionDispatch::Request.new(@env)
|
10
|
+
@info_hash = Hashie::Mash.new
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
13
|
+
def parse
|
14
|
+
info_hash.error_class = @exception.class.to_s.strip
|
15
|
+
info_hash.error_message = @exception.message.to_s.strip
|
16
|
+
info_hash.error_trace = form_backtrace @exception.backtrace
|
17
|
+
info_hash.request_method = @request.method
|
18
|
+
info_hash.request_params = @request.params.to_s
|
19
|
+
info_hash.target_url = @request.url
|
20
|
+
info_hash.referer_url = @request.referer
|
21
|
+
info_hash.user_agent = @request.user_agent
|
22
|
+
info_hash.user_info = user_info.to_s
|
23
|
+
info_hash.timestamp = Time.current.utc
|
24
|
+
self
|
25
|
+
end
|
25
26
|
|
26
|
-
|
27
|
+
private
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
29
|
+
def user_info
|
30
|
+
{
|
31
|
+
Orig_IP_Address: fetch_remote_ip,
|
32
|
+
IP_Address: @request.ip,
|
33
|
+
Remote_Address: @request.remote_addr
|
34
|
+
}
|
35
|
+
end
|
35
36
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
37
|
+
def fetch_remote_ip
|
38
|
+
@request.remote_ip
|
39
|
+
rescue => e
|
40
|
+
e.message
|
41
|
+
end
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
43
|
+
def form_backtrace(backtrace)
|
44
|
+
backtrace.join("\n")
|
45
|
+
rescue
|
46
|
+
''
|
47
|
+
end
|
46
48
|
end
|
47
49
|
end
|
@@ -1,19 +1,21 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module GlobalErrorHandler
|
2
|
+
class Railtie < Rails::Railtie #:nodoc:
|
3
|
+
railtie_name :global_error_handler
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
|
5
|
+
initializer 'global_error_handler.configure_rails_initialization' do
|
6
|
+
insert_middleware
|
7
|
+
end
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
rake_tasks do
|
10
|
+
load File.join(File.dirname(__FILE__), '..', 'tasks', 'global_error_handler.rake')
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
def insert_middleware
|
14
|
+
if defined? ActionDispatch::DebugExceptions
|
15
|
+
Rails.application.middleware.insert_after ActionDispatch::DebugExceptions, GlobalErrorHandler::Middleware
|
16
|
+
else
|
17
|
+
Rails.application.middleware.use GlobalErrorHandler::Middleware
|
18
|
+
end
|
17
19
|
end
|
18
20
|
end
|
19
21
|
end
|
@@ -1,154 +1,186 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
1
|
+
module GlobalErrorHandler
|
2
|
+
class Redis #:nodoc:
|
3
|
+
CURRENT_ID_KEY = 'global_error_handler:current_id'
|
4
|
+
EXCEPTIONS_REDIS_KEY = 'global_error_handler:exceptions'
|
5
|
+
EXCEPTION_KEY_PREFIX = 'global_error_handler:exception'
|
6
|
+
FILTER_KEY_PREFIX = 'global_error_handler:filter'
|
7
|
+
FILTER_FIELDS = %w(error_class error_message)
|
8
|
+
FILTER_MAX_CHARS = 60
|
9
|
+
REDIS_TTL = 4 * 7 * 24 * 60 * 60 # 4 weeks
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def store(info_hash)
|
13
|
+
return if info_hash.blank?
|
14
|
+
redis_key = exception_key(next_id!)
|
15
|
+
redis.hmset redis_key, info_hash.merge(id: current_id).to_a.flatten
|
16
|
+
redis.rpush EXCEPTIONS_REDIS_KEY, redis_key
|
17
|
+
FILTER_FIELDS.each do |field|
|
18
|
+
redis.rpush filter_key(field, build_filter_value(info_hash[field])), redis_key
|
19
|
+
end
|
20
|
+
redis.expire redis_key, REDIS_TTL
|
21
|
+
end
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
def initialize_redis_from_config
|
24
|
+
redis_config = YAML.load_file(File.join(current_root, 'config', 'redis.yml'))[current_env]
|
25
|
+
::Redis.new(redis_config['global_exception_handler'])
|
26
|
+
end
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
28
|
+
def redis
|
29
|
+
@redis ||= begin
|
30
|
+
$redis_global_exception_handler = initialize_redis_from_config unless $redis_global_exception_handler.is_a? ::Redis
|
31
|
+
$redis_global_exception_handler
|
32
|
+
end
|
33
|
+
end
|
33
34
|
|
34
|
-
|
35
|
-
|
36
|
-
|
35
|
+
def current_id
|
36
|
+
redis.get(CURRENT_ID_KEY)
|
37
|
+
end
|
37
38
|
|
38
|
-
|
39
|
-
|
40
|
-
|
39
|
+
# def sort(field, direction = 'ASC', page = 0, per_page = 1000)
|
40
|
+
# redis.sort(EXCEPTIONS_REDIS_KEY, by: "#{EXCEPTION_KEY_PREFIX}_*->#{field}_*", order: "#{direction}", limit: [page, per_page])
|
41
|
+
# end
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
def exceptions_count
|
44
|
+
redis.llen EXCEPTIONS_REDIS_KEY
|
45
|
+
end
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
|
47
|
+
def filtered_exceptions_count(field, filter)
|
48
|
+
redis.llen filter_key(field, filter)
|
49
|
+
end
|
49
50
|
|
50
|
-
|
51
|
-
|
52
|
-
|
51
|
+
def exception_keys(start = 0, per_page = 10)
|
52
|
+
redis.lrange EXCEPTIONS_REDIS_KEY, start.to_i, per_page.to_i + start.to_i - 1
|
53
|
+
end
|
53
54
|
|
54
|
-
|
55
|
-
|
56
|
-
|
55
|
+
def filter_exception_keys(start = 0, field = nil, filter = nil, per_page = 10)
|
56
|
+
redis.lrange filter_key(field, filter), start.to_i, per_page.to_i + start.to_i - 1
|
57
|
+
end
|
57
58
|
|
58
|
-
|
59
|
-
|
60
|
-
|
59
|
+
def filter_keys_for(field, filter = '')
|
60
|
+
redis.keys filter_key(field, "#{filter}*")
|
61
|
+
end
|
61
62
|
|
62
|
-
|
63
|
-
|
64
|
-
|
63
|
+
def find(key)
|
64
|
+
Hashie::Mash.new redis.hgetall(key)
|
65
|
+
end
|
65
66
|
|
66
|
-
|
67
|
-
|
68
|
-
|
67
|
+
def find_all(keys)
|
68
|
+
keys.map { |key| find(key) }.compact
|
69
|
+
end
|
69
70
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
71
|
+
def delete(key)
|
72
|
+
delete_dependencies key
|
73
|
+
redis.del key
|
74
|
+
end
|
74
75
|
|
75
|
-
|
76
|
-
|
77
|
-
|
76
|
+
def delete_all(keys)
|
77
|
+
keys.each do |key|
|
78
|
+
begin
|
79
|
+
delete(key)
|
80
|
+
rescue
|
81
|
+
next
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
78
85
|
|
79
|
-
|
80
|
-
|
81
|
-
|
86
|
+
def truncate!
|
87
|
+
redis.flushdb
|
88
|
+
end
|
82
89
|
|
83
|
-
|
84
|
-
|
85
|
-
|
90
|
+
def exception_key(id = current_id)
|
91
|
+
"#{EXCEPTION_KEY_PREFIX}:#{id}"
|
92
|
+
end
|
86
93
|
|
87
|
-
|
88
|
-
|
89
|
-
|
94
|
+
def filter_key(field, filter)
|
95
|
+
"#{FILTER_KEY_PREFIX}:#{field}:#{filter}"
|
96
|
+
end
|
90
97
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
98
|
+
def delete_dependencies(key)
|
99
|
+
redis.lrem EXCEPTIONS_REDIS_KEY, 1, key
|
100
|
+
clear_filters key
|
101
|
+
end
|
95
102
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
+
def clear_filters(key)
|
104
|
+
FILTER_FIELDS.each do |field|
|
105
|
+
retry_count = 0
|
106
|
+
field_value = build_filter_value(redis.hget key, field)
|
107
|
+
begin
|
108
|
+
filter_keys_for(field, field_value).each do |filter_key|
|
109
|
+
redis.lrem filter_key, 1, key
|
110
|
+
end
|
111
|
+
rescue
|
112
|
+
field_value = ''
|
113
|
+
retry if (retry_count += 1) < 2
|
103
114
|
end
|
104
|
-
rescue
|
105
|
-
field_value = ''
|
106
|
-
retry if (retry_count += 1) < 2
|
107
115
|
end
|
108
116
|
end
|
109
|
-
end
|
110
117
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
118
|
+
def cleanup_database_dependencies!
|
119
|
+
total_exceptions_count = exceptions_count
|
120
|
+
total_exception_keys_count = redis.keys(exception_key('*')).size
|
121
|
+
if total_exceptions_count > total_exception_keys_count
|
122
|
+
puts '==> Database dependency is broken. Need to fix it!'
|
123
|
+
start = 0
|
124
|
+
per_page = 500
|
125
|
+
exception_keys_to_be_cleaned_up = []
|
126
|
+
valid_chunks_count = 0
|
127
|
+
cleanup_count = exception_keys_to_be_cleaned_up.size
|
128
|
+
while total_exceptions_count > start
|
129
|
+
exception_keys(start, per_page).each do |redis_key|
|
130
|
+
exception_keys_to_be_cleaned_up.push redis_key unless redis.exists(redis_key)
|
131
|
+
end
|
132
|
+
if cleanup_count == (cleanup_count = exception_keys_to_be_cleaned_up.size)
|
133
|
+
valid_chunks_count += 1
|
134
|
+
end
|
135
|
+
break if valid_chunks_count > 3 # if three ranges in a row are consistent, treat database consistency and finish looping
|
136
|
+
start += per_page
|
124
137
|
end
|
125
|
-
|
126
|
-
|
138
|
+
|
139
|
+
puts "*** found #{exception_keys_to_be_cleaned_up.count} broken dependency keys."
|
140
|
+
exception_keys_to_be_cleaned_up.each do |redis_key|
|
141
|
+
begin
|
142
|
+
delete_dependencies(redis_key)
|
143
|
+
rescue
|
144
|
+
next
|
145
|
+
end
|
127
146
|
end
|
128
|
-
|
129
|
-
|
147
|
+
else
|
148
|
+
puts '==> Database dependency is OK. No need to fix it!'
|
130
149
|
end
|
150
|
+
end
|
131
151
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
else
|
137
|
-
puts "==> Database dependency is OK. No need to fix it!"
|
152
|
+
protected
|
153
|
+
|
154
|
+
def next_id!
|
155
|
+
redis.incr(CURRENT_ID_KEY)
|
138
156
|
end
|
139
|
-
end
|
140
157
|
|
141
|
-
|
158
|
+
private
|
142
159
|
|
143
|
-
|
144
|
-
|
145
|
-
|
160
|
+
def build_filter_value(txt)
|
161
|
+
str = begin
|
162
|
+
txt.split("\n").first
|
163
|
+
rescue
|
164
|
+
''
|
165
|
+
end
|
166
|
+
str[0...FILTER_MAX_CHARS]
|
167
|
+
end
|
146
168
|
|
147
|
-
|
169
|
+
def current_root
|
170
|
+
Rails.root
|
171
|
+
rescue
|
172
|
+
begin
|
173
|
+
settings.root
|
174
|
+
rescue
|
175
|
+
'.'
|
176
|
+
end
|
177
|
+
end
|
148
178
|
|
149
|
-
|
150
|
-
|
151
|
-
|
179
|
+
def current_env
|
180
|
+
Rails.env
|
181
|
+
rescue
|
182
|
+
ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
|
183
|
+
end
|
152
184
|
end
|
153
185
|
end
|
154
186
|
end
|
@@ -1,66 +1,70 @@
|
|
1
1
|
require 'global_error_handler/redis'
|
2
2
|
|
3
|
-
|
4
|
-
class
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
module GlobalErrorHandler
|
4
|
+
class RedisNotificationSubscriber #:nodoc:
|
5
|
+
class << self
|
6
|
+
def unsubscribe!
|
7
|
+
redis.unsubscribe(sub_channel)
|
8
|
+
rescue
|
9
|
+
nil
|
10
|
+
end
|
8
11
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
12
|
+
def subscribe!
|
13
|
+
check_redis_config
|
14
|
+
begin
|
15
|
+
fail SubscriptionError, "wont subscribe to ##{sub_channel}. Someone already listening to this channel" if subscribers_count > 0
|
16
|
+
redis.subscribe(sub_channel) do |on|
|
17
|
+
puts "*** ##{Process.pid}: Listeting for the notifications on ##{sub_channel}..."
|
18
|
+
on.message do |channel, key|
|
19
|
+
puts "**** ##{channel}: #{key}"
|
20
|
+
Redis.delete_dependencies(key) if key =~ /#{Redis.exception_key("\\d+")}/
|
21
|
+
end
|
22
|
+
on.subscribe do |channel, subscriptions|
|
23
|
+
puts "##{Process.pid}: Subscribed to ##{channel} (#{subscriptions} subscriptions)"
|
24
|
+
end
|
22
25
|
|
23
|
-
|
24
|
-
|
26
|
+
on.unsubscribe do |channel, subscriptions|
|
27
|
+
puts "##{Process.pid}: Unsubscribed from ##{channel} (#{subscriptions} subscriptions)"
|
28
|
+
end
|
25
29
|
end
|
30
|
+
rescue ::Redis::BaseConnectionError => error
|
31
|
+
puts "##{Process.pid}: #{error}, retrying in 1s"
|
32
|
+
sleep 1
|
33
|
+
retry
|
34
|
+
rescue SubscriptionError => error
|
35
|
+
puts "##{Process.pid}: #{error}, retrying in 1s"
|
36
|
+
sleep 1
|
37
|
+
retry
|
38
|
+
rescue Interrupt => error
|
39
|
+
puts "##{Process.pid}: unsubscribing..."
|
40
|
+
unsubscribe!
|
41
|
+
redis.quit
|
42
|
+
redis.client.reconnect
|
26
43
|
end
|
27
|
-
rescue Redis::BaseConnectionError => error
|
28
|
-
puts "##{Process.pid}: #{error}, retrying in 1s"
|
29
|
-
sleep 1
|
30
|
-
retry
|
31
|
-
rescue SubscriptionError => error
|
32
|
-
puts "##{Process.pid}: #{error}, retrying in 1s"
|
33
|
-
sleep 1
|
34
|
-
retry
|
35
|
-
rescue Interrupt => error
|
36
|
-
puts "##{Process.pid}: unsubscribing..."
|
37
|
-
unsubscribe!
|
38
|
-
redis.quit
|
39
|
-
redis.client.reconnect
|
40
44
|
end
|
41
|
-
end
|
42
45
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
+
def redis
|
47
|
+
@redis ||= Redis.initialize_redis_from_config
|
48
|
+
end
|
46
49
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
+
def sub_channel
|
51
|
+
@sub_channel ||= "__keyevent@#{redis.client.db}__:expired"
|
52
|
+
end
|
50
53
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
54
|
+
def check_redis_config
|
55
|
+
# x Expired events (events generated every time a key expires)
|
56
|
+
# g Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
|
57
|
+
# A Alias for g$lshzxe, so that the "AKE" string means all the events.
|
58
|
+
# E Keyevent events, published with __keyevent@<db>__ prefix.
|
59
|
+
### AE|gE|xE|AKE|gKE|xKE
|
60
|
+
redis.config('set', 'notify-keyspace-events', 'xE') unless redis.config('get', 'notify-keyspace-events').last =~ /[Agx]+.?E/
|
61
|
+
end
|
59
62
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
63
|
+
def subscribers_count
|
64
|
+
redis.publish sub_channel, "check subscribers count from ##{Process.pid}"
|
65
|
+
end
|
66
|
+
end # class << self
|
64
67
|
|
65
|
-
|
68
|
+
class SubscriptionError < StandardError; end
|
69
|
+
end
|
66
70
|
end
|
@@ -10,10 +10,10 @@
|
|
10
10
|
%li= link_to 'Truncate all', exceptions_path(nil, params[:filter_by], params[:filter]) + '/truncate',
|
11
11
|
method: :delete, class: 'js-link',
|
12
12
|
data: {confirm: 'Are you sure to truncate ALL the Exceptions?'}
|
13
|
-
%li.filter= select_tag 'filter_by_message', options_for_select(@all_messages, params[:filter_by].to_s.eql?('message') ? get_filter : nil),
|
14
|
-
class: 'filter-by', data: {field: 'message'}
|
15
|
-
%li.filter= select_tag 'filter_by_class', options_for_select(@all_classes, params[:filter_by].to_s.eql?('class') ? get_filter : nil),
|
16
|
-
class: 'filter-by', data: {field: 'class'}
|
13
|
+
%li.filter= select_tag 'filter_by_message', options_for_select(@all_messages, params[:filter_by].to_s.eql?('message') ? get_filter : nil),
|
14
|
+
prompt: 'Error Message Filter', class: 'filter-by', data: {field: 'message'}
|
15
|
+
%li.filter= select_tag 'filter_by_class', options_for_select(@all_classes, params[:filter_by].to_s.eql?('class') ? get_filter : nil),
|
16
|
+
prompt: 'Error Class Filter', class: 'filter-by', data: {field: 'class'}
|
17
17
|
|
18
18
|
%p.sub
|
19
19
|
Showing #{apps_start_at} to #{apps_end_at} of
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module GlobalErrorHandler
|
2
|
-
module Server
|
2
|
+
module Server #:nodoc:
|
3
3
|
GEH_VIEW_PATH = File.join(File.dirname(__FILE__), 'server', 'views')
|
4
4
|
GEH_PUBLIC_PATH = File.join(File.dirname(__FILE__), 'server', 'public')
|
5
5
|
|
@@ -18,7 +18,7 @@ module GlobalErrorHandler
|
|
18
18
|
end
|
19
19
|
|
20
20
|
app.get '/exceptions/:id' do
|
21
|
-
@app_exception =
|
21
|
+
@app_exception = AppException.find(params[:id])
|
22
22
|
show_view :show
|
23
23
|
end
|
24
24
|
|
@@ -31,33 +31,34 @@ module GlobalErrorHandler
|
|
31
31
|
end
|
32
32
|
|
33
33
|
app.delete '/exceptions/delete' do
|
34
|
-
|
34
|
+
AppException.delete_all(params[:app_exception_delete_ids])
|
35
35
|
redirect_to_exceptions
|
36
36
|
end
|
37
37
|
|
38
38
|
app.delete '/exceptions/:id' do
|
39
|
-
|
39
|
+
AppException.delete(params[:id])
|
40
40
|
redirect_to_exceptions
|
41
41
|
end
|
42
42
|
|
43
43
|
app.tabs << 'Exceptions'
|
44
44
|
|
45
|
+
require 'action_view'
|
45
46
|
app.helpers do
|
46
|
-
include ActionView::Helpers::TextHelper #link_to
|
47
|
-
include ActionView::Helpers::UrlHelper #simple_format
|
48
|
-
include ActionView::Helpers::FormHelper #select_tag check_box_tag
|
49
|
-
include ActionView::Helpers::FormOptionsHelper #options_for_select
|
50
|
-
include ActionView::Helpers::OutputSafetyHelper #delete via ajax
|
47
|
+
include ActionView::Helpers::TextHelper # link_to
|
48
|
+
include ActionView::Helpers::UrlHelper # simple_format
|
49
|
+
include ActionView::Helpers::FormHelper # select_tag check_box_tag
|
50
|
+
include ActionView::Helpers::FormOptionsHelper # options_for_select
|
51
|
+
include ActionView::Helpers::OutputSafetyHelper # delete via ajax
|
51
52
|
|
52
53
|
def prepare_and_show_index_action
|
53
|
-
@app_exceptions =
|
54
|
-
@all_classes =
|
55
|
-
@all_messages =
|
54
|
+
@app_exceptions = AppException.all(params[:start], params[:filter_by], fetch_filter)
|
55
|
+
@all_classes = AppException.filters_for('class')
|
56
|
+
@all_messages = AppException.filters_for('message')
|
56
57
|
show_view :index
|
57
58
|
end
|
58
59
|
|
59
60
|
def truncate_and_redirect_to_exceptions
|
60
|
-
|
61
|
+
AppException.truncate(fetch_filter, field: params[:filter_by], total: apps_size)
|
61
62
|
redirect exceptions_path
|
62
63
|
end
|
63
64
|
|
@@ -66,10 +67,10 @@ module GlobalErrorHandler
|
|
66
67
|
end
|
67
68
|
|
68
69
|
def show_view(filename = :index)
|
69
|
-
erb haml(
|
70
|
+
erb haml(File.read(File.join(GEH_VIEW_PATH, "#{filename}.html.haml")))
|
70
71
|
end
|
71
72
|
|
72
|
-
def geh_public_view(filename, dir='')
|
73
|
+
def geh_public_view(filename, dir = '')
|
73
74
|
file = File.join(GEH_PUBLIC_PATH, dir, filename)
|
74
75
|
begin
|
75
76
|
cache_control :public, max_age: 1800
|
@@ -80,13 +81,13 @@ module GlobalErrorHandler
|
|
80
81
|
end
|
81
82
|
|
82
83
|
def exceptions_path(start = nil, filter_by = nil, filter = nil)
|
83
|
-
path =
|
84
|
+
path = '/resque/exceptions'
|
84
85
|
path += "/filter/#{filter_by}/#{URI.escape(filter)}" if filter_by && filter
|
85
86
|
path += "?start=#{start}" if start
|
86
87
|
path
|
87
88
|
end
|
88
89
|
|
89
|
-
def exception_path(id, start=nil, filter_by = nil, filter = nil)
|
90
|
+
def exception_path(id, start = nil, filter_by = nil, filter = nil)
|
90
91
|
path = "/resque/exceptions/#{id}"
|
91
92
|
path_params = []
|
92
93
|
path_params.push "start=#{start}" if start
|
@@ -96,7 +97,7 @@ module GlobalErrorHandler
|
|
96
97
|
end
|
97
98
|
|
98
99
|
def apps_size
|
99
|
-
@apps_size ||=
|
100
|
+
@apps_size ||= AppException.count(params[:filter_by], fetch_filter).to_i
|
100
101
|
end
|
101
102
|
|
102
103
|
def apps_start_at
|
@@ -116,7 +117,7 @@ module GlobalErrorHandler
|
|
116
117
|
end
|
117
118
|
end
|
118
119
|
|
119
|
-
def each_app_exception
|
120
|
+
def each_app_exception
|
120
121
|
return unless block_given?
|
121
122
|
@app_exceptions.try(:each) do |app_exception|
|
122
123
|
yield app_exception
|
@@ -129,17 +130,17 @@ module GlobalErrorHandler
|
|
129
130
|
total = options[:total] || 0
|
130
131
|
return if total < per_page
|
131
132
|
|
132
|
-
markup =
|
133
|
+
markup = ''
|
133
134
|
if start - per_page >= 0
|
134
|
-
markup << link_to(raw(
|
135
|
+
markup << link_to(raw('« less'), exceptions_path(start - per_page), class: 'btn less')
|
135
136
|
elsif start > 0 && start < per_page
|
136
|
-
markup << link_to(raw(
|
137
|
+
markup << link_to(raw('« less'), exceptions_path(0), class: 'btn less')
|
137
138
|
end
|
138
139
|
|
139
140
|
markup << pages_markup(start, per_page, total)
|
140
141
|
|
141
142
|
if start + per_page < total
|
142
|
-
markup << link_to(raw(
|
143
|
+
markup << link_to(raw('more »'), exceptions_path(start + per_page), class: 'btn more')
|
143
144
|
end
|
144
145
|
markup
|
145
146
|
end
|
@@ -150,7 +151,7 @@ module GlobalErrorHandler
|
|
150
151
|
|
151
152
|
left_ind = start / per_page
|
152
153
|
markups = [left_ind.to_s]
|
153
|
-
while (left_ind -= 1) >= 0 && (start/per_page - left_ind <= max_side_links || pages_count < max_links)
|
154
|
+
while (left_ind -= 1) >= 0 && (start / per_page - left_ind <= max_side_links || pages_count < max_links)
|
154
155
|
markups.unshift link_to(left_ind, exceptions_path(left_ind * per_page, params[:filter_by], params[:filter]), class: 'btn pages')
|
155
156
|
end
|
156
157
|
right_ind = start / per_page
|
@@ -158,7 +159,7 @@ module GlobalErrorHandler
|
|
158
159
|
markups.unshift '...' if right_ind - max_side_links > 1
|
159
160
|
markups.unshift link_to(0, exceptions_path(0, params[:filter_by], params[:filter]), class: 'btn pages')
|
160
161
|
end
|
161
|
-
while (right_ind +=1) * per_page < total && (right_ind - start / per_page <= max_side_links || pages_count < max_links)
|
162
|
+
while (right_ind += 1) * per_page < total && (right_ind - start / per_page <= max_side_links || pages_count < max_links)
|
162
163
|
markups.push link_to(right_ind, exceptions_path(per_page * right_ind, params[:filter_by], params[:filter]), class: 'btn pages')
|
163
164
|
end
|
164
165
|
if pages_count >= max_links && pages_count >= right_ind
|
@@ -176,7 +177,7 @@ module GlobalErrorHandler
|
|
176
177
|
max_side_links * 2 + 1
|
177
178
|
end
|
178
179
|
|
179
|
-
def
|
180
|
+
def fetch_filter
|
180
181
|
URI.unescape(params[:filter]) if params[:filter]
|
181
182
|
end
|
182
183
|
end
|
@@ -1,3 +1,3 @@
|
|
1
|
-
module GlobalErrorHandler
|
2
|
-
VERSION =
|
1
|
+
module GlobalErrorHandler #:nodoc:
|
2
|
+
VERSION = '1.0.2'
|
3
3
|
end
|
data/lib/global_error_handler.rb
CHANGED
@@ -1,23 +1,24 @@
|
|
1
1
|
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
|
2
2
|
|
3
|
-
module GlobalErrorHandler
|
3
|
+
module GlobalErrorHandler #:nodoc:
|
4
4
|
end
|
5
5
|
|
6
6
|
require 'resque'
|
7
7
|
require 'resque/server'
|
8
8
|
require 'haml'
|
9
|
-
|
9
|
+
require 'hashie'
|
10
10
|
|
11
11
|
require 'global_error_handler/redis'
|
12
12
|
require 'global_error_handler/parser'
|
13
13
|
require 'global_error_handler/handler'
|
14
14
|
require 'global_error_handler/app_exception'
|
15
|
+
|
15
16
|
require 'global_error_handler/server'
|
16
17
|
|
17
18
|
require 'global_error_handler/middleware'
|
18
19
|
require 'global_error_handler/rails' if defined? Rails::Railtie
|
19
20
|
|
20
|
-
require
|
21
|
+
require 'global_error_handler/version'
|
21
22
|
|
22
23
|
require 'global_error_handler/redis_notification_subscriber'
|
23
24
|
|
@@ -1,17 +1,17 @@
|
|
1
1
|
module GlobalErrorHandler
|
2
|
-
class Capistrano
|
2
|
+
class Capistrano #:nodoc:
|
3
3
|
def self.load_into(config)
|
4
4
|
config.load do
|
5
5
|
namespace :global_error_handler do
|
6
6
|
desc 'Subscribe to expiration'
|
7
7
|
task :subscribe do
|
8
|
-
run %
|
9
|
-
run %
|
8
|
+
run %(cd #{current_path} && RAILS_ENV=#{rails_env} nohup rake global_error_handler:cleanup_database_dependencies >/dev/null 2>&1 & sleep 2)
|
9
|
+
run %(cd #{current_path} && RAILS_ENV=#{rails_env} nohup rake global_error_handler:subscribe_to_expired >/dev/null 2>&1 & sleep 3)
|
10
10
|
end
|
11
11
|
|
12
12
|
desc 'Unsubscribe from expiration'
|
13
13
|
task :unsubscribe do
|
14
|
-
run %
|
14
|
+
run %(cd #{current_path} && RAILS_ENV=#{rails_env} #{rake} global_error_handler:unsubscribe_from_expired)
|
15
15
|
end
|
16
16
|
|
17
17
|
desc 'Update Subscription to expiration'
|
@@ -1,18 +1,22 @@
|
|
1
1
|
namespace :global_error_handler do
|
2
2
|
desc 'Subscribe to expired keyevent notifications'
|
3
3
|
task subscribe_to_expired: :environment do
|
4
|
-
puts
|
5
|
-
File.open(pid_file, 'w'){|f| f.puts Process.pid}
|
4
|
+
puts('*** pid file exists!') || next if File.exist?(pid_file)
|
5
|
+
File.open(pid_file, 'w') { |f| f.puts Process.pid }
|
6
6
|
begin
|
7
7
|
GlobalErrorHandler::RedisNotificationSubscriber.subscribe!
|
8
8
|
ensure
|
9
|
-
|
9
|
+
begin
|
10
|
+
File.unlink pid_file
|
11
|
+
rescue
|
12
|
+
nil
|
13
|
+
end
|
10
14
|
end
|
11
15
|
end
|
12
16
|
|
13
17
|
desc 'Unsubscribe from expired keyevent notifications'
|
14
18
|
task unsubscribe_from_expired: :environment do
|
15
|
-
puts
|
19
|
+
puts('*** pid file does not exist!') || next unless File.exist?(pid_file)
|
16
20
|
process_id = File.read(pid_file).to_i
|
17
21
|
begin
|
18
22
|
Process.kill 0, process_id
|
@@ -25,7 +29,11 @@ namespace :global_error_handler do
|
|
25
29
|
rescue Errno::ESRCH
|
26
30
|
puts "** No such process ##{process_id}. Exiting..."
|
27
31
|
ensure
|
28
|
-
|
32
|
+
begin
|
33
|
+
File.unlink pid_file
|
34
|
+
rescue
|
35
|
+
nil
|
36
|
+
end
|
29
37
|
end
|
30
38
|
end
|
31
39
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: global_error_handler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrii Rudenko
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-11-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -104,6 +104,40 @@ dependencies:
|
|
104
104
|
- - "<"
|
105
105
|
- !ruby/object:Gem::Version
|
106
106
|
version: '4.1'
|
107
|
+
- !ruby/object:Gem::Dependency
|
108
|
+
name: actionview
|
109
|
+
requirement: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: 4.0.5
|
114
|
+
- - "<"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '4.3'
|
117
|
+
type: :runtime
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: 4.0.5
|
124
|
+
- - "<"
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '4.3'
|
127
|
+
- !ruby/object:Gem::Dependency
|
128
|
+
name: hashie
|
129
|
+
requirement: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '3.3'
|
134
|
+
type: :runtime
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '3.3'
|
107
141
|
description: On the middleware level catch an exception from Rails app and store in
|
108
142
|
the separated Redis database.
|
109
143
|
email:
|