rollbar 2.12.0 → 2.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -6
  3. data/README.md +58 -8
  4. data/docs/configuration.md +12 -0
  5. data/gemfiles/rails30.gemfile +1 -0
  6. data/gemfiles/rails31.gemfile +1 -0
  7. data/gemfiles/rails32.gemfile +1 -0
  8. data/gemfiles/rails40.gemfile +3 -0
  9. data/gemfiles/rails41.gemfile +1 -0
  10. data/gemfiles/rails42.gemfile +7 -1
  11. data/gemfiles/rails50.gemfile +2 -1
  12. data/gemfiles/ruby_1_8_and_1_9_2.gemfile +3 -1
  13. data/lib/rollbar.rb +70 -654
  14. data/lib/rollbar/configuration.rb +32 -0
  15. data/lib/rollbar/item.rb +16 -6
  16. data/lib/rollbar/item/backtrace.rb +26 -17
  17. data/lib/rollbar/item/frame.rb +112 -0
  18. data/lib/rollbar/middleware/js.rb +39 -35
  19. data/lib/rollbar/middleware/rails/rollbar.rb +3 -3
  20. data/lib/rollbar/notifier.rb +645 -0
  21. data/lib/rollbar/plugins/delayed_job/job_data.rb +40 -21
  22. data/lib/rollbar/plugins/rails.rb +2 -2
  23. data/lib/rollbar/plugins/rake.rb +32 -6
  24. data/lib/rollbar/plugins/resque.rb +11 -0
  25. data/lib/rollbar/plugins/resque/failure.rb +39 -0
  26. data/lib/rollbar/plugins/validations.rb +10 -0
  27. data/lib/rollbar/request_data_extractor.rb +36 -18
  28. data/lib/rollbar/scrubbers/params.rb +2 -1
  29. data/lib/rollbar/truncation.rb +1 -1
  30. data/lib/rollbar/truncation/frames_strategy.rb +2 -1
  31. data/lib/rollbar/truncation/min_body_strategy.rb +2 -1
  32. data/lib/rollbar/truncation/strings_strategy.rb +1 -1
  33. data/lib/rollbar/version.rb +1 -1
  34. data/spec/controllers/home_controller_spec.rb +13 -24
  35. data/spec/delayed/backend/test.rb +1 -0
  36. data/spec/requests/home_spec.rb +1 -1
  37. data/spec/rollbar/configuration_spec.rb +22 -0
  38. data/spec/rollbar/item/backtrace_spec.rb +26 -0
  39. data/spec/rollbar/item/frame_spec.rb +267 -0
  40. data/spec/rollbar/item_spec.rb +27 -2
  41. data/spec/rollbar/middleware/js_spec.rb +23 -0
  42. data/spec/rollbar/middleware/sinatra_spec.rb +7 -7
  43. data/spec/rollbar/notifier_spec.rb +43 -0
  44. data/spec/rollbar/plugins/delayed_job/{job_data.rb → job_data_spec.rb} +15 -2
  45. data/spec/rollbar/plugins/rack_spec.rb +7 -7
  46. data/spec/rollbar/plugins/rake_spec.rb +1 -2
  47. data/spec/rollbar/plugins/resque/failure_spec.rb +36 -0
  48. data/spec/rollbar/request_data_extractor_spec.rb +103 -1
  49. data/spec/rollbar/truncation/min_body_strategy_spec.rb +1 -1
  50. data/spec/rollbar/truncation/strings_strategy_spec.rb +2 -2
  51. data/spec/rollbar_bc_spec.rb +4 -4
  52. data/spec/rollbar_spec.rb +99 -37
  53. data/spec/spec_helper.rb +2 -2
  54. data/spec/support/notifier_helpers.rb +2 -0
  55. metadata +16 -4
@@ -0,0 +1,645 @@
1
+ require 'rollbar'
2
+ require 'rollbar/lazy_store'
3
+ require 'rollbar/configuration'
4
+ require 'rollbar/util'
5
+ require 'rollbar/json'
6
+ require 'rollbar/exceptions'
7
+ require 'rollbar/language_support'
8
+ require 'rollbar/delay/girl_friday'
9
+ require 'rollbar/delay/thread'
10
+ require 'rollbar/logger_proxy'
11
+ require 'rollbar/item'
12
+
13
+ module Rollbar
14
+ class Notifier
15
+ attr_accessor :configuration
16
+ attr_accessor :last_report
17
+ attr_accessor :scope_object
18
+
19
+ @file_semaphore = Mutex.new
20
+
21
+ def initialize(parent_notifier = nil, payload_options = nil, scope = nil)
22
+ if parent_notifier
23
+ self.configuration = parent_notifier.configuration.clone
24
+ self.scope_object = parent_notifier.scope_object.clone
25
+
26
+ Rollbar::Util.deep_merge(scope_object, scope) if scope
27
+ else
28
+ self.configuration = ::Rollbar::Configuration.new
29
+ self.scope_object = ::Rollbar::LazyStore.new(scope)
30
+ end
31
+
32
+ Rollbar::Util.deep_merge(configuration.payload_options, payload_options) if payload_options
33
+ end
34
+
35
+ def reset!
36
+ self.scope_object = ::Rollbar::LazyStore.new({})
37
+ end
38
+
39
+ # Similar to configure below, but used only internally within the gem
40
+ # to configure it without initializing any of the third party hooks
41
+ def preconfigure
42
+ yield(configuration)
43
+ end
44
+
45
+ # Configures the notifier instance
46
+ def configure
47
+ configuration.enabled = true if configuration.enabled.nil?
48
+
49
+ yield(configuration)
50
+ end
51
+
52
+ def reconfigure
53
+ self.configuration = Configuration.new
54
+ configuration.enabled = true
55
+
56
+ yield(configuration)
57
+ end
58
+
59
+ def unconfigure
60
+ self.configuration = nil
61
+ end
62
+
63
+ def scope(scope_overrides = {}, config_overrides = {})
64
+ new_notifier = self.class.new(self, nil, scope_overrides)
65
+ new_notifier.configuration = configuration.merge(config_overrides)
66
+
67
+ new_notifier
68
+ end
69
+
70
+ def scope!(options = {}, config_overrides = {})
71
+ Rollbar::Util.deep_merge(scope_object, options)
72
+ configuration.merge!(config_overrides)
73
+
74
+ self
75
+ end
76
+
77
+ # Returns a new notifier with same configuration options
78
+ # but it sets Configuration#safely to true.
79
+ # We are using this flag to avoid having inifite loops
80
+ # when evaluating some custom user methods.
81
+ def safely
82
+ new_notifier = scope
83
+ new_notifier.configuration.safely = true
84
+
85
+ new_notifier
86
+ end
87
+
88
+ # Turns off reporting for the given block.
89
+ #
90
+ # @example
91
+ # Rollbar.silenced { raise }
92
+ #
93
+ # @yield Block which exceptions won't be reported.
94
+ def silenced
95
+ yield
96
+ rescue => e
97
+ e.instance_variable_set(:@_rollbar_do_not_report, true)
98
+ raise
99
+ end
100
+
101
+ # Sends a report to Rollbar.
102
+ #
103
+ # Accepts any number of arguments. The last String argument will become
104
+ # the message or description of the report. The last Exception argument
105
+ # will become the associated exception for the report. The last hash
106
+ # argument will be used as the extra data for the report.
107
+ #
108
+ # @example
109
+ # begin
110
+ # foo = bar
111
+ # rescue => e
112
+ # Rollbar.log(e)
113
+ # end
114
+ #
115
+ # @example
116
+ # Rollbar.log('This is a simple log message')
117
+ #
118
+ # @example
119
+ # Rollbar.log(e, 'This is a description of the exception')
120
+ #
121
+ def log(level, *args)
122
+ return 'disabled' unless configuration.enabled
123
+
124
+ message, exception, extra = extract_arguments(args)
125
+ use_exception_level_filters = extra && extra.delete(:use_exception_level_filters) == true
126
+
127
+ return 'ignored' if ignored?(exception, use_exception_level_filters)
128
+
129
+ begin
130
+ call_before_process(:level => level,
131
+ :exception => exception,
132
+ :message => message,
133
+ :extra => extra)
134
+ rescue Rollbar::Ignore
135
+ return 'ignored'
136
+ end
137
+
138
+ level = lookup_exception_level(level, exception,
139
+ use_exception_level_filters)
140
+
141
+ begin
142
+ report(level, message, exception, extra)
143
+ rescue Exception => e
144
+ report_internal_error(e)
145
+
146
+ 'error'
147
+ end
148
+ end
149
+
150
+ # See log() above
151
+ def debug(*args)
152
+ log('debug', *args)
153
+ end
154
+
155
+ # See log() above
156
+ def info(*args)
157
+ log('info', *args)
158
+ end
159
+
160
+ # See log() above
161
+ def warn(*args)
162
+ log('warning', *args)
163
+ end
164
+
165
+ # See log() above
166
+ def warning(*args)
167
+ log('warning', *args)
168
+ end
169
+
170
+ # See log() above
171
+ def error(*args)
172
+ log('error', *args)
173
+ end
174
+
175
+ # See log() above
176
+ def critical(*args)
177
+ log('critical', *args)
178
+ end
179
+
180
+ def process_item(item)
181
+ if configuration.write_to_file
182
+ if configuration.use_async
183
+ @file_semaphore.synchronize {
184
+ write_item(item)
185
+ }
186
+ else
187
+ write_item(item)
188
+ end
189
+ else
190
+ send_item(item)
191
+ end
192
+ rescue => e
193
+ log_error("[Rollbar] Error processing the item: #{e.class}, #{e.message}. Item: #{item.payload.inspect}")
194
+ raise e
195
+ end
196
+
197
+ # We will reraise exceptions in this method so async queues
198
+ # can retry the job or, in general, handle an error report some way.
199
+ #
200
+ # At same time that exception is silenced so we don't generate
201
+ # infinite reports. This example is what we want to avoid:
202
+ #
203
+ # 1. New exception in a the project is raised
204
+ # 2. That report enqueued to Sidekiq queue.
205
+ # 3. The Sidekiq job tries to send the report to our API
206
+ # 4. The report fails, for example cause a network failure,
207
+ # and a exception is raised
208
+ # 5. We report an internal error for that exception
209
+ # 6. We reraise the exception so Sidekiq job fails and
210
+ # Sidekiq can retry the job reporting the original exception
211
+ # 7. Because the job failed and Sidekiq can be managed by rollbar we'll
212
+ # report a new exception.
213
+ # 8. Go to point 2.
214
+ #
215
+ # We'll then push to Sidekiq queue indefinitely until the network failure
216
+ # is fixed.
217
+ #
218
+ # Using Rollbar.silenced we avoid the above behavior but Sidekiq
219
+ # will have a chance to retry the original job.
220
+ def process_from_async_handler(payload)
221
+ payload = Rollbar::JSON.load(payload) if payload.is_a?(String)
222
+
223
+ item = Item.build_with(payload,
224
+ :notifier => self,
225
+ :configuration => configuration,
226
+ :logger => logger)
227
+
228
+ Rollbar.silenced do
229
+ begin
230
+ process_item(item)
231
+ rescue => e
232
+ report_internal_error(e)
233
+
234
+ raise
235
+ end
236
+ end
237
+ end
238
+
239
+ def send_failsafe(message, exception, uuid = nil, host = nil)
240
+ exception_reason = failsafe_reason(message, exception)
241
+
242
+ log_error "[Rollbar] Sending failsafe response due to #{exception_reason}"
243
+
244
+ body = failsafe_body(exception_reason)
245
+
246
+ failsafe_data = {
247
+ :level => 'error',
248
+ :environment => configuration.environment.to_s,
249
+ :body => {
250
+ :message => {
251
+ :body => body
252
+ }
253
+ },
254
+ :notifier => {
255
+ :name => 'rollbar-gem',
256
+ :version => VERSION
257
+ },
258
+ :custom => {
259
+ :orig_uuid => uuid,
260
+ :orig_host => host
261
+ },
262
+ :internal => true,
263
+ :failsafe => true
264
+ }
265
+
266
+ failsafe_payload = {
267
+ 'access_token' => configuration.access_token,
268
+ 'data' => failsafe_data
269
+ }
270
+
271
+ begin
272
+ item = Item.build_with(failsafe_payload,
273
+ :notifier => self,
274
+ :configuration => configuration,
275
+ :logger => logger)
276
+ schedule_item(item)
277
+ rescue => e
278
+ log_error "[Rollbar] Error sending failsafe : #{e}"
279
+ end
280
+
281
+ failsafe_payload
282
+ end
283
+
284
+ ## Logging
285
+ %w(debug info warn error).each do |level|
286
+ define_method(:"log_#{level}") do |message|
287
+ logger.send(level, message)
288
+ end
289
+ end
290
+
291
+ private
292
+
293
+ def call_before_process(options)
294
+ options = {
295
+ :level => options[:level],
296
+ :scope => scope_object,
297
+ :exception => options[:exception],
298
+ :message => options[:message],
299
+ :extra => options[:extra]
300
+ }
301
+ handlers = configuration.before_process
302
+
303
+ handlers.each do |handler|
304
+ begin
305
+ handler.call(options)
306
+ rescue Rollbar::Ignore
307
+ raise
308
+ rescue => e
309
+ log_error("[Rollbar] Error calling the `before_process` hook: #{e}")
310
+
311
+ break
312
+ end
313
+ end
314
+ end
315
+
316
+ def extract_arguments(args)
317
+ message = nil
318
+ exception = nil
319
+ extra = nil
320
+
321
+ args.each do |arg|
322
+ if arg.is_a?(String)
323
+ message = arg
324
+ elsif arg.is_a?(Exception)
325
+ exception = arg
326
+ elsif arg.is_a?(Hash)
327
+ extra = arg
328
+ end
329
+ end
330
+
331
+ [message, exception, extra]
332
+ end
333
+
334
+ def lookup_exception_level(orig_level, exception, use_exception_level_filters)
335
+ return orig_level unless use_exception_level_filters
336
+
337
+ exception_level = filtered_level(exception)
338
+ return exception_level if exception_level
339
+
340
+ orig_level
341
+ end
342
+
343
+ def ignored?(exception, use_exception_level_filters = false)
344
+ return false unless exception
345
+ return true if use_exception_level_filters && filtered_level(exception) == 'ignore'
346
+ return true if exception.instance_variable_get(:@_rollbar_do_not_report)
347
+
348
+ false
349
+ end
350
+
351
+ def filtered_level(exception)
352
+ return unless exception
353
+
354
+ filter = configuration.exception_level_filters[exception.class.name]
355
+ if filter.respond_to?(:call)
356
+ filter.call(exception)
357
+ else
358
+ filter
359
+ end
360
+ end
361
+
362
+ def report(level, message, exception, extra)
363
+ unless message || exception || extra
364
+ log_error '[Rollbar] Tried to send a report with no message, exception or extra data.'
365
+
366
+ return 'error'
367
+ end
368
+
369
+ item = build_item(level, message, exception, extra)
370
+
371
+ return 'ignored' if item.ignored?
372
+
373
+ schedule_item(item)
374
+
375
+ data = item['data']
376
+ log_instance_link(data)
377
+ Rollbar.last_report = data
378
+
379
+ data
380
+ end
381
+
382
+ # Reports an internal error in the Rollbar library. This will be reported within the configured
383
+ # Rollbar project. We'll first attempt to provide a report including the exception traceback.
384
+ # If that fails, we'll fall back to a more static failsafe response.
385
+ def report_internal_error(exception)
386
+ log_error "[Rollbar] Reporting internal error encountered while sending data to Rollbar."
387
+
388
+ begin
389
+ item = build_item('error', nil, exception, {:internal => true})
390
+ rescue => e
391
+ send_failsafe("build_item in exception_data", e)
392
+ return
393
+ end
394
+
395
+ begin
396
+ process_item(item)
397
+ rescue => e
398
+ send_failsafe("error in process_item", e)
399
+ return
400
+ end
401
+
402
+ begin
403
+ log_instance_link(item['data'])
404
+ rescue => e
405
+ send_failsafe("error logging instance link", e)
406
+ return
407
+ end
408
+ end
409
+
410
+ ## Payload building functions
411
+
412
+ def build_item(level, message, exception, extra)
413
+ options = {
414
+ :level => level,
415
+ :message => message,
416
+ :exception => exception,
417
+ :extra => extra,
418
+ :configuration => configuration,
419
+ :logger => logger,
420
+ :scope => scope_object,
421
+ :notifier => self
422
+ }
423
+
424
+ item = Item.new(options)
425
+ item.build
426
+
427
+ item
428
+ end
429
+
430
+ ## Delivery functions
431
+
432
+ def send_item_using_eventmachine(item)
433
+ body = item.dump
434
+ return unless body
435
+
436
+ headers = { 'X-Rollbar-Access-Token' => item['access_token'] }
437
+ req = EventMachine::HttpRequest.new(configuration.endpoint).post(:body => body, :head => headers)
438
+
439
+ req.callback do
440
+ if req.response_header.status == 200
441
+ log_info '[Rollbar] Success'
442
+ else
443
+ log_warning "[Rollbar] Got unexpected status code from Rollbar.io api: #{req.response_header.status}"
444
+ log_info "[Rollbar] Response: #{req.response}"
445
+ end
446
+ end
447
+
448
+ req.errback do
449
+ log_warning "[Rollbar] Call to API failed, status code: #{req.response_header.status}"
450
+ log_info "[Rollbar] Error's response: #{req.response}"
451
+ end
452
+ end
453
+
454
+ def send_item(item)
455
+ log_info '[Rollbar] Sending item'
456
+
457
+ if configuration.use_eventmachine
458
+ send_item_using_eventmachine(item)
459
+ return
460
+ end
461
+
462
+ body = item.dump
463
+ return unless body
464
+
465
+ uri = URI.parse(configuration.endpoint)
466
+
467
+ handle_response(do_post(uri, body, item['access_token']))
468
+ end
469
+
470
+ def do_post(uri, body, access_token)
471
+ http = Net::HTTP.new(uri.host, uri.port)
472
+ http.open_timeout = configuration.open_timeout
473
+ http.read_timeout = configuration.request_timeout
474
+
475
+ if uri.scheme == 'https'
476
+ http.use_ssl = true
477
+ # This is needed to have 1.8.7 passing tests
478
+ http.ca_file = ENV['ROLLBAR_SSL_CERT_FILE'] if ENV.has_key?('ROLLBAR_SSL_CERT_FILE')
479
+ http.verify_mode = ssl_verify_mode
480
+ end
481
+
482
+ request = Net::HTTP::Post.new(uri.request_uri)
483
+
484
+ request.body = body
485
+ request.add_field('X-Rollbar-Access-Token', access_token)
486
+
487
+ handle_net_retries { http.request(request) }
488
+ end
489
+
490
+ def handle_net_retries
491
+ return yield if skip_retries?
492
+
493
+ retries = configuration.net_retries - 1
494
+
495
+ begin
496
+ yield
497
+ rescue *LanguageSupport.timeout_exceptions
498
+ raise if retries <= 0
499
+
500
+ retries -= 1
501
+
502
+ retry
503
+ end
504
+ end
505
+
506
+ def skip_retries?
507
+ Rollbar::LanguageSupport.ruby_18? || Rollbar::LanguageSupport.ruby_19?
508
+ end
509
+
510
+ def handle_response(response)
511
+ if response.code == '200'
512
+ log_info '[Rollbar] Success'
513
+ else
514
+ log_warning "[Rollbar] Got unexpected status code from Rollbar api: #{response.code}"
515
+ log_info "[Rollbar] Response: #{response.body}"
516
+ end
517
+ end
518
+
519
+ def ssl_verify_mode
520
+ if configuration.verify_ssl_peer
521
+ OpenSSL::SSL::VERIFY_PEER
522
+ else
523
+ OpenSSL::SSL::VERIFY_NONE
524
+ end
525
+ end
526
+
527
+ def write_item(item)
528
+ if configuration.use_async
529
+ @file_semaphore.synchronize {
530
+ do_write_item(item)
531
+ }
532
+ else
533
+ do_write_item(item)
534
+ end
535
+ end
536
+
537
+ def do_write_item(item)
538
+ log_info '[Rollbar] Writing item to file'
539
+
540
+ body = item.dump
541
+ return unless body
542
+
543
+ begin
544
+ unless @file
545
+ @file = File.open(configuration.filepath, "a")
546
+ end
547
+
548
+ @file.puts(body)
549
+ @file.flush
550
+ log_info "[Rollbar] Success"
551
+ rescue IOError => e
552
+ log_error "[Rollbar] Error opening/writing to file: #{e}"
553
+ end
554
+ end
555
+
556
+ def failsafe_reason(message, exception)
557
+ body = ''
558
+
559
+ if exception
560
+ begin
561
+ backtrace = exception.backtrace || []
562
+ nearest_frame = backtrace[0]
563
+
564
+ exception_info = exception.class.name
565
+ # #to_s and #message defaults to class.to_s. Add message only if add valuable info.
566
+ exception_info += %Q{: "#{exception.message}"} if exception.message != exception.class.to_s
567
+ exception_info += " in #{nearest_frame}" if nearest_frame
568
+
569
+ body += "#{exception_info}: #{message}"
570
+ rescue
571
+ end
572
+ else
573
+ begin
574
+ body += message.to_s
575
+ rescue
576
+ end
577
+ end
578
+
579
+ body
580
+ end
581
+
582
+ def failsafe_body(reason)
583
+ "Failsafe from rollbar-gem. #{reason}"
584
+ end
585
+
586
+ def schedule_item(item)
587
+ return unless item
588
+
589
+ log_info '[Rollbar] Scheduling item'
590
+
591
+ if configuration.use_async
592
+ process_async_item(item)
593
+ else
594
+ process_item(item)
595
+ end
596
+ end
597
+
598
+ def default_async_handler
599
+ return Rollbar::Delay::GirlFriday if defined?(GirlFriday)
600
+
601
+ Rollbar::Delay::Thread
602
+ end
603
+
604
+ def process_async_item(item)
605
+ configuration.async_handler ||= default_async_handler
606
+ configuration.async_handler.call(item.payload)
607
+ rescue => e
608
+ if configuration.failover_handlers.empty?
609
+ log_error '[Rollbar] Async handler failed, and there are no failover handlers configured. See the docs for "failover_handlers"'
610
+ return
611
+ end
612
+
613
+ async_failover(item)
614
+ end
615
+
616
+ def async_failover(item)
617
+ log_warning '[Rollbar] Primary async handler failed. Trying failovers...'
618
+
619
+ failover_handlers = configuration.failover_handlers
620
+
621
+ failover_handlers.each do |handler|
622
+ begin
623
+ handler.call(item.payload)
624
+ rescue
625
+ next unless handler == failover_handlers.last
626
+
627
+ log_error "[Rollbar] All failover handlers failed while processing item: #{Rollbar::JSON.dump(item.payload)}"
628
+ end
629
+ end
630
+ end
631
+
632
+ alias_method :log_warning, :log_warn
633
+
634
+ def log_instance_link(data)
635
+ return unless data[:uuid]
636
+
637
+ uuid_url = Util.uuid_rollbar_url(data, configuration)
638
+ log_info "[Rollbar] Details: #{uuid_url} (only available if report was successful)"
639
+ end
640
+
641
+ def logger
642
+ @logger ||= LoggerProxy.new(configuration.logger)
643
+ end
644
+ end
645
+ end