rollbar 2.12.0 → 2.13.0

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.
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