global_error_handler 0.3.3 → 1.0.2
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 +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:
|