yogi_berra 0.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.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ Yogi Berra
2
+ ==========
3
+
4
+ "If the world were perfect, it wouldn't be." - Yogi Berra
5
+
6
+ Yogi Berra was the best catcher of all time.
7
+ This gem will catch all of your rails error in production or development and store them in mongodb if
8
+ it can. It uses the mongodb_ruby_driver in a non-blocking way.
9
+
10
+ Mongo::MongoClient.new(host, port, :w => 0)
11
+
12
+ The `:w => 0` option here makes requests to the mongodb server not wait for a response.
13
+ This will begin to fill a buffer when there is no connection. When the connection returns
14
+ the buffer will be entered into the database. There can be data loss if the buffer overflows.
15
+ There are some messages in the logs which will tell you if the connection is down.
16
+ This makes the catcher work when it does and not crash or slow when it doesn't.
17
+
18
+ Installation
19
+ ------------
20
+
21
+ add yogi_berra to your Gemfile:
22
+
23
+ gem 'yogi_berra'
24
+
25
+ Create a yogi.yml file in rails root config/ folder. Here is a sample:
26
+
27
+ defaults: &defaults
28
+ username: yogi
29
+ password: berra
30
+
31
+ development:
32
+ <<: *defaults
33
+ database: yogi_berra
34
+ host: localhost
35
+ port: 27017
36
+
37
+ Thanks
38
+ ------
39
+
40
+ To :
41
+ - Thoughtbot Airbrake:
42
+ https://github.com/airbrake/airbrake/tree/master/lib/airbrake
43
+ This gem is awesome and was the base for most of the code here.
44
+
45
+ - Exception Engine:
46
+ https://github.com/Consoci8/exception_engine
47
+ Which is an app which was the inital fork to start this code base.
48
+ Just took out the need for mongoid, and a rails app. Made it into a gem
49
+ that will later have a rails app to view the exceptions. Exception engine also
50
+ credits Thoughtbot for the Hoptoad::Notice part of the code. Hoptoad is now airbrake.
51
+
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'rubygems/package_task'
3
+ require 'rspec/core/rake_task'
4
+
5
+ spec = eval(File.read('yogi_berra_client.gemspec'))
6
+
7
+ Gem::PackageTask.new(spec) do |p|
8
+ p.gem_spec = spec
9
+ end
10
+
11
+ RSpec::Core::RakeTask.new
12
+ task :default => :spec
@@ -0,0 +1,45 @@
1
+ # Adapted from Airbrake code https://github.com/airbrake/airbrake/blob/master/lib/airbrake/rails/action_controller_catcher.rb
2
+ module YogiBerra
3
+ module ActionControllerCatcher
4
+ # Sets up an alias chain to catch exceptions for Rails 2
5
+ def self.included(base)
6
+ YogiBerra::Catcher.load_db_settings
7
+ if base.method_defined?(:rescue_action_in_public)
8
+ base.send(:alias_method, :rescue_action_in_public_without_yogi, :rescue_action_in_public)
9
+ base.send(:alias_method, :rescue_action_in_public, :rescue_action_in_public_with_yogi)
10
+ if YogiBerra::Catcher.settings
11
+ base.send(:alias_method, :rescue_action_locally_without_yogi, :rescue_action_locally)
12
+ base.send(:alias_method, :rescue_action_locally, :rescue_action_locally_with_yogi)
13
+ end
14
+ end
15
+ end
16
+
17
+ private
18
+ # Overrides the rescue_action method in ActionController::Base, but does not inhibit
19
+ # any custom processing that is defined with Rails 2's exception helpers.
20
+ def rescue_action_in_public_with_yogi(exception)
21
+ rescue_action_yogi(exception)
22
+ rescue_action_in_public_without_yogi(exception)
23
+ end
24
+
25
+ def rescue_action_locally_with_yogi(exception)
26
+ rescue_action_yogi(exception)
27
+ rescue_action_locally_without_yogi(exception)
28
+ end
29
+
30
+ def rescue_action_yogi(exception)
31
+ environment = {
32
+ :session => session,
33
+ :params => params,
34
+ :user_agent => request.headers['HTTP_USER_AGENT'],
35
+ :server_name => request.headers['SERVER_NAME'],
36
+ :server_port => request.headers['SERVER_PORT'],
37
+ :server_address => request.headers['SERVER_ADDR'],
38
+ :remote_address => request.remote_ip
39
+ }
40
+ error_id = YogiBerra.exceptionize(exception, environment, YogiBerra::Catcher.connection)
41
+ request.env['yogi_berra.error_id'] = error_id
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,100 @@
1
+ # Credits to Thoughtbot's Hoptoad::Backtrace
2
+ module YogiBerra
3
+ # Front end to parsing the backtrace for each notice
4
+ class Backtrace
5
+
6
+ # Handles backtrace parsing line by line
7
+ class Line
8
+
9
+ INPUT_FORMAT = %r{^([^:]+):(\d+)(?::in `([^']+)')?$}.freeze
10
+
11
+ # The file portion of the line (such as app/models/user.rb)
12
+ attr_reader :file
13
+
14
+ # The line number portion of the line
15
+ attr_reader :number
16
+
17
+ # The method of the line (such as index)
18
+ attr_reader :method
19
+
20
+ # Parses a single line of a given backtrace
21
+ # @param [String] unparsed_line The raw line from +caller+ or some backtrace
22
+ # @return [Line] The parsed backtrace line
23
+ def self.parse(unparsed_line)
24
+ _, file, number, method = unparsed_line.match(INPUT_FORMAT).to_a
25
+ new(file, number, method)
26
+ end
27
+
28
+ def initialize(file, number, method)
29
+ self.file = file
30
+ self.number = number
31
+ self.method = method
32
+ end
33
+
34
+ # Reconstructs the line in a readable fashion
35
+ def to_s
36
+ "#{file}:#{number}:in `#{method}'"
37
+ end
38
+
39
+ def ==(other)
40
+ to_s == other.to_s
41
+ end
42
+
43
+ def inspect
44
+ "<Line:#{to_s}>"
45
+ end
46
+
47
+ private
48
+
49
+ attr_writer :file, :number, :method
50
+ end
51
+
52
+ # holder for an Array of Backtrace::Line instances
53
+ attr_reader :lines
54
+
55
+ def self.parse(ruby_backtrace, opts = {})
56
+ ruby_lines = split_multiline_backtrace(ruby_backtrace)
57
+
58
+ filters = opts[:filters] || []
59
+ filtered_lines = ruby_lines.to_a.map do |line|
60
+ filters.inject(line) do |line, proc|
61
+ proc.call(line)
62
+ end
63
+ end.compact
64
+
65
+ lines = filtered_lines.collect do |unparsed_line|
66
+ Line.parse(unparsed_line)
67
+ end
68
+
69
+ instance = new(lines)
70
+ end
71
+
72
+ def initialize(lines)
73
+ self.lines = lines
74
+ end
75
+
76
+ def inspect
77
+ "<Backtrace: " + lines.collect { |line| line.inspect }.join(", ") + ">"
78
+ end
79
+
80
+ def ==(other)
81
+ if other.respond_to?(:lines)
82
+ lines == other.lines
83
+ else
84
+ false
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ attr_writer :lines
91
+
92
+ def self.split_multiline_backtrace(backtrace)
93
+ if backtrace.to_a.size == 1
94
+ backtrace.to_a.first.split(/\n\s*/)
95
+ else
96
+ backtrace
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,45 @@
1
+ require 'mongo'
2
+
3
+ module YogiBerra
4
+ class Catcher
5
+ cattr_accessor :settings, :mongo_client, :connection
6
+
7
+ class << self
8
+ def load_db_settings
9
+ begin
10
+ File.open("#{Rails.root}/config/yogi.yml", 'r') do |f|
11
+ yaml_file = YAML.load(f)
12
+ @@settings = yaml_file["#{Rails.env}"] if yaml_file
13
+ end
14
+ rescue
15
+ $stderr.puts "[YogiBerra Error] No such file: #{Rails.root}/config/yogi.yml"
16
+ end
17
+ end
18
+
19
+ def db_client(host, port)
20
+ # :w => 0 set the default write concern to 0, this allows writes to be non-blocking
21
+ # by not waiting for a response from mongodb
22
+ @@mongo_client = Mongo::MongoClient.new(host, port, :w => 0)
23
+ rescue
24
+ Rails.logger.error "[YogiBerra Error] Couldn't connect to the mongo database on host: #{host} port: #{port}."
25
+ nil
26
+ end
27
+
28
+ def quick_connection
29
+ settings = @@settings || load_db_settings
30
+ if settings
31
+ host = settings["host"]
32
+ port = settings["port"]
33
+ client = db_client(host, port)
34
+ if client
35
+ @@connection = client[settings["database"]]
36
+ else
37
+ Rails.logger.error "[YogiBerra Error] Couldn't connect to the mongo database on host: #{host} port: #{port}."
38
+ end
39
+ else
40
+ Rails.logger.error "[YogiBerra Error] Couldn't load the yogi.yml file."
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,34 @@
1
+ require 'mongo'
2
+
3
+ module YogiBerra
4
+ class Data
5
+ def self.store!(exception, environment, client)
6
+ data = parse_exception(exception)
7
+ if environment
8
+ session = environment.delete(:session)
9
+ data[:session] = parse_session(session) if session
10
+ data.merge!(environment)
11
+ end
12
+ client["caught_exceptions"].insert(data)
13
+ end
14
+
15
+ def self.parse_exception(notice)
16
+ data_hash = {
17
+ :error_class => notice.error_class,
18
+ :error_message => notice.error_message
19
+ }
20
+ if notice.backtrace.lines.any?
21
+ data_hash[:backtraces] = notice.backtrace.lines.collect(&:to_s)
22
+ end
23
+ data_hash[:created_at] = Time.now.utc
24
+ data_hash
25
+ end
26
+
27
+ def self.parse_session(session)
28
+ session.inject({}) do |result, element|
29
+ result[element.first] = element.last.respond_to?(:as_json) ? element.last.as_json(:except => ["password"]) : element.last
30
+ result
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ module YogiBerra
2
+ class Engine < Rails::Engine
3
+ config.app_middleware.use "YogiBerra::ExceptionMiddleware"
4
+ end
5
+ end
@@ -0,0 +1,41 @@
1
+ module YogiBerra
2
+ class ExceptionMiddleware
3
+ def initialize(app)
4
+ @app = app
5
+ YogiBerra::Catcher.quick_connection
6
+ end
7
+
8
+ def call(env)
9
+ begin
10
+ response = dup._call(env)
11
+ environment = {
12
+ :session => env['rack.session'],
13
+ :params => env['action_controller.request.path_parameters'].merge(env['rack.request.query_hash']),
14
+ :user_agent => env['HTTP_USER_AGENT'],
15
+ :server_name => env['SERVER_NAME'],
16
+ :server_port => env['SERVER_PORT'],
17
+ :server_address => env['SERVER_ADDR'],
18
+ :remote_address => env['REMOTE_ADDR']
19
+ }
20
+ rescue Exception => raised
21
+ YogiBerra.exceptionize(raised, environment, YogiBerra::Catcher.connection)
22
+ raise raised
23
+ end
24
+
25
+ if env['rack.exception']
26
+ YogiBerra.exceptionize(raised, environment, YogiBerra::Catcher.connection)
27
+ end
28
+ response
29
+ end
30
+
31
+ def _call(env)
32
+ @status, @headers, @response = @app.call(env)
33
+ [@status, @headers, self]
34
+ end
35
+
36
+ def each(&block)
37
+ @response.each(&block)
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,336 @@
1
+ require 'builder'
2
+
3
+ # Credits to Thoughtbot's Hoptoad::Notice
4
+ module YogiBerra
5
+ class Notice
6
+
7
+ # The exception that caused this notice, if any
8
+ attr_reader :exception
9
+
10
+ # The API key for the project to which this notice should be sent
11
+ attr_reader :api_key
12
+
13
+ # The backtrace from the given exception or hash.
14
+ attr_reader :backtrace
15
+
16
+ # The name of the class of error (such as RuntimeError)
17
+ attr_reader :error_class
18
+
19
+ # The name of the server environment (such as "production")
20
+ attr_reader :environment_name
21
+
22
+ # CGI variables such as HTTP_METHOD
23
+ attr_reader :cgi_data
24
+
25
+ # The message from the exception, or a general description of the error
26
+ attr_reader :error_message
27
+
28
+ # See Configuration#backtrace_filters
29
+ attr_reader :backtrace_filters
30
+
31
+ # See Configuration#params_filters
32
+ attr_reader :params_filters
33
+
34
+ # A hash of parameters from the query string or post body.
35
+ attr_reader :parameters
36
+ alias_method :params, :parameters
37
+
38
+ # The component (if any) which was used in this request (usually the controller)
39
+ attr_reader :component
40
+ alias_method :controller, :component
41
+
42
+ # The action (if any) that was called in this request
43
+ attr_reader :action
44
+
45
+ # A hash of session data from the request
46
+ attr_reader :session_data
47
+
48
+ # The path to the project that caused the error (usually RAILS_ROOT)
49
+ attr_reader :project_root
50
+
51
+ # The URL at which the error occurred (if any)
52
+ attr_reader :url
53
+
54
+ # See Configuration#ignore
55
+ attr_reader :ignore
56
+
57
+ # See Configuration#ignore_by_filters
58
+ attr_reader :ignore_by_filters
59
+
60
+ # The name of the notifier library sending this notice, such as "Hoptoad Notifier"
61
+ attr_reader :notifier_name
62
+
63
+ # The version number of the notifier library sending this notice, such as "2.1.3"
64
+ attr_reader :notifier_version
65
+
66
+ # A URL for more information about the notifier library sending this notice
67
+ attr_reader :notifier_url
68
+
69
+ def initialize(args)
70
+ self.args = args
71
+ self.exception = args[:exception]
72
+ self.api_key = args[:api_key]
73
+ self.project_root = args[:project_root]
74
+ self.url = args[:url] || rack_env(:url)
75
+
76
+ self.notifier_name = args[:notifier_name]
77
+ self.notifier_version = args[:notifier_version]
78
+ self.notifier_url = args[:notifier_url]
79
+
80
+ self.ignore = args[:ignore] || []
81
+ self.ignore_by_filters = args[:ignore_by_filters] || []
82
+ self.backtrace_filters = args[:backtrace_filters] || []
83
+ self.params_filters = args[:params_filters] || []
84
+ self.parameters = args[:parameters] ||
85
+ action_dispatch_params ||
86
+ rack_env(:params) ||
87
+ {}
88
+ self.component = args[:component] || args[:controller] || parameters['controller']
89
+ self.action = args[:action] || parameters['action']
90
+
91
+ self.environment_name = args[:environment_name]
92
+ self.cgi_data = args[:cgi_data] || args[:rack_env]
93
+ self.backtrace = Backtrace.parse(exception_attribute(:backtrace, caller), :filters => self.backtrace_filters)
94
+ self.error_class = exception_attribute(:error_class) {|exception| exception.class.name }
95
+ self.error_message = exception_attribute(:error_message, 'Notification') do |exception|
96
+ "#{exception.class.name}: #{exception.message}"
97
+ end
98
+
99
+ also_use_rack_params_filters
100
+ find_session_data
101
+ clean_params
102
+ clean_rack_request_data
103
+ end
104
+
105
+ # Converts the given notice to XML
106
+ def to_xml
107
+ builder = Builder::XmlMarkup.new
108
+ builder.instruct!
109
+ xml = builder.notice(:version => "1.0") do |notice|
110
+ notice.tag!("api-key", api_key)
111
+ notice.notifier do |notifier|
112
+ notifier.name(notifier_name)
113
+ notifier.version(notifier_version)
114
+ notifier.url(notifier_url)
115
+ end
116
+ notice.error do |error|
117
+ error.tag!('class', error_class)
118
+ error.message(error_message)
119
+ error.backtrace do |backtrace|
120
+ self.backtrace.lines.each do |line|
121
+ backtrace.line(:number => line.number,
122
+ :file => line.file,
123
+ :method => line.method)
124
+ end
125
+ end
126
+ end
127
+ if url ||
128
+ controller ||
129
+ action ||
130
+ !parameters.blank? ||
131
+ !cgi_data.blank? ||
132
+ !session_data.blank?
133
+ notice.request do |request|
134
+ request.url(url)
135
+ request.component(controller)
136
+ request.action(action)
137
+ unless parameters.nil? || parameters.empty?
138
+ request.params do |params|
139
+ xml_vars_for(params, parameters)
140
+ end
141
+ end
142
+ unless session_data.nil? || session_data.empty?
143
+ request.session do |session|
144
+ xml_vars_for(session, session_data)
145
+ end
146
+ end
147
+ unless cgi_data.nil? || cgi_data.empty?
148
+ request.tag!("cgi-data") do |cgi_datum|
149
+ xml_vars_for(cgi_datum, cgi_data)
150
+ end
151
+ end
152
+ end
153
+ end
154
+ notice.tag!("server-environment") do |env|
155
+ env.tag!("project-root", project_root)
156
+ env.tag!("environment-name", environment_name)
157
+ end
158
+ end
159
+ xml.to_s
160
+ end
161
+
162
+ # Determines if this notice should be ignored
163
+ def ignore?
164
+ ignored_class_names.include?(error_class) ||
165
+ ignore_by_filters.any? {|filter| filter.call(self) }
166
+ end
167
+
168
+ # Allows properties to be accessed using a hash-like syntax
169
+ #
170
+ # @example
171
+ # notice[:error_message]
172
+ # @param [String] method The given key for an attribute
173
+ # @return The attribute value, or self if given +:request+
174
+ def [](method)
175
+ case method
176
+ when :request
177
+ self
178
+ else
179
+ send(method)
180
+ end
181
+ end
182
+
183
+ private
184
+
185
+ attr_writer :exception, :api_key, :backtrace, :error_class, :error_message,
186
+ :backtrace_filters, :parameters, :params_filters,
187
+ :environment_filters, :session_data, :project_root, :url, :ignore,
188
+ :ignore_by_filters, :notifier_name, :notifier_url, :notifier_version,
189
+ :component, :action, :cgi_data, :environment_name
190
+
191
+ # Arguments given in the initializer
192
+ attr_accessor :args
193
+
194
+ # Gets a property named +attribute+ of an exception, either from an actual
195
+ # exception or a hash.
196
+ #
197
+ # If an exception is available, #from_exception will be used. Otherwise,
198
+ # a key named +attribute+ will be used from the #args.
199
+ #
200
+ # If no exception or hash key is available, +default+ will be used.
201
+ def exception_attribute(attribute, default = nil, &block)
202
+ (exception && from_exception(attribute, &block)) || args[attribute] || default
203
+ end
204
+
205
+ # Gets a property named +attribute+ from an exception.
206
+ #
207
+ # If a block is given, it will be used when getting the property from an
208
+ # exception. The block should accept and exception and return the value for
209
+ # the property.
210
+ #
211
+ # If no block is given, a method with the same name as +attribute+ will be
212
+ # invoked for the value.
213
+ def from_exception(attribute)
214
+ if block_given?
215
+ yield(exception)
216
+ else
217
+ exception.send(attribute)
218
+ end
219
+ end
220
+
221
+ # Removes non-serializable data from the given attribute.
222
+ # See #clean_unserializable_data
223
+ def clean_unserializable_data_from(attribute)
224
+ self.send(:"#{attribute}=", clean_unserializable_data(send(attribute)))
225
+ end
226
+
227
+ # Removes non-serializable data. Allowed data types are strings, arrays,
228
+ # and hashes. All other types are converted to strings.
229
+ # TODO: move this onto Hash
230
+ def clean_unserializable_data(data)
231
+ if data.respond_to?(:to_hash)
232
+ data.to_hash.inject({}) do |result, (key, value)|
233
+ result.merge(key => clean_unserializable_data(value))
234
+ end
235
+ elsif data.respond_to?(:to_ary)
236
+ data.collect do |value|
237
+ clean_unserializable_data(value)
238
+ end
239
+ else
240
+ data.to_s
241
+ end
242
+ end
243
+
244
+ # Replaces the contents of params that match params_filters.
245
+ # TODO: extract this to a different class
246
+ def clean_params
247
+ clean_unserializable_data_from(:parameters)
248
+ filter(parameters)
249
+ if cgi_data
250
+ clean_unserializable_data_from(:cgi_data)
251
+ filter(cgi_data)
252
+ end
253
+ if session_data
254
+ clean_unserializable_data_from(:session_data)
255
+ filter(session_data)
256
+ end
257
+ end
258
+
259
+ def clean_rack_request_data
260
+ if cgi_data
261
+ cgi_data.delete("rack.request.form_vars")
262
+ end
263
+ end
264
+
265
+ def filter(hash)
266
+ if params_filters
267
+ hash.each do |key, value|
268
+ if filter_key?(key)
269
+ hash[key] = "[FILTERED]"
270
+ elsif value.respond_to?(:to_hash)
271
+ filter(hash[key])
272
+ end
273
+ end
274
+ end
275
+ end
276
+
277
+ def filter_key?(key)
278
+ params_filters.any? do |filter|
279
+ key.to_s.include?(filter.to_s)
280
+ end
281
+ end
282
+
283
+ def find_session_data
284
+ self.session_data = args[:session_data] || args[:session] || rack_session || {}
285
+ self.session_data = session_data[:data] if session_data[:data]
286
+ end
287
+
288
+ # Converts the mixed class instances and class names into just names
289
+ # TODO: move this into Configuration or another class
290
+ def ignored_class_names
291
+ ignore.collect do |string_or_class|
292
+ if string_or_class.respond_to?(:name)
293
+ string_or_class.name
294
+ else
295
+ string_or_class
296
+ end
297
+ end
298
+ end
299
+
300
+ def xml_vars_for(builder, hash)
301
+ hash.each do |key, value|
302
+ if value.respond_to?(:to_hash)
303
+ builder.var(:key => key){|b| xml_vars_for(b, value.to_hash) }
304
+ else
305
+ builder.var(value.to_s, :key => key)
306
+ end
307
+ end
308
+ end
309
+
310
+ def rack_env(method)
311
+ rack_request.send(method) if rack_request
312
+ end
313
+
314
+ def rack_request
315
+ @rack_request ||= if args[:rack_env]
316
+ ::Rack::Request.new(args[:rack_env])
317
+ end
318
+ end
319
+
320
+ def action_dispatch_params
321
+ args[:rack_env]['action_dispatch.request.parameters'] if args[:rack_env]
322
+ end
323
+
324
+ def rack_session
325
+ args[:rack_env]['rack.session'] if args[:rack_env]
326
+ end
327
+
328
+ def also_use_rack_params_filters
329
+ if args[:rack_env]
330
+ @params_filters ||= []
331
+ @params_filters += rack_request.env["action_dispatch.parameter_filter"] || []
332
+ end
333
+ end
334
+
335
+ end
336
+ end