honeybadger 2.7.2 → 3.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -3
  3. data/README.md +8 -15
  4. data/lib/honeybadger.rb +9 -232
  5. data/lib/honeybadger/agent.rb +292 -134
  6. data/lib/honeybadger/backend.rb +6 -6
  7. data/lib/honeybadger/backend/base.rb +11 -0
  8. data/lib/honeybadger/backend/server.rb +2 -14
  9. data/lib/honeybadger/cli.rb +0 -2
  10. data/lib/honeybadger/cli/deploy.rb +42 -0
  11. data/lib/honeybadger/cli/exec.rb +138 -0
  12. data/lib/honeybadger/cli/heroku.rb +1 -22
  13. data/lib/honeybadger/cli/install.rb +74 -0
  14. data/lib/honeybadger/cli/main.rb +138 -153
  15. data/lib/honeybadger/cli/notify.rb +66 -0
  16. data/lib/honeybadger/cli/test.rb +266 -0
  17. data/lib/honeybadger/config.rb +178 -162
  18. data/lib/honeybadger/config/defaults.rb +5 -5
  19. data/lib/honeybadger/config/env.rb +8 -6
  20. data/lib/honeybadger/config/ruby.rb +100 -0
  21. data/lib/honeybadger/config/yaml.rb +18 -19
  22. data/lib/honeybadger/const.rb +3 -16
  23. data/lib/honeybadger/context_manager.rb +50 -0
  24. data/lib/honeybadger/init/rails.rb +9 -21
  25. data/lib/honeybadger/init/rake.rb +2 -0
  26. data/lib/honeybadger/init/ruby.rb +9 -0
  27. data/lib/honeybadger/init/sinatra.rb +13 -6
  28. data/lib/honeybadger/notice.rb +29 -14
  29. data/lib/honeybadger/plugins/delayed_job/plugin.rb +4 -5
  30. data/lib/honeybadger/plugins/passenger.rb +1 -2
  31. data/lib/honeybadger/plugins/rails.rb +0 -28
  32. data/lib/honeybadger/plugins/resque.rb +2 -5
  33. data/lib/honeybadger/plugins/shoryuken.rb +2 -2
  34. data/lib/honeybadger/plugins/sidekiq.rb +2 -2
  35. data/lib/honeybadger/plugins/sucker_punch.rb +1 -0
  36. data/lib/honeybadger/plugins/thor.rb +2 -2
  37. data/lib/honeybadger/plugins/warden.rb +1 -0
  38. data/lib/honeybadger/rack/error_notifier.rb +11 -9
  39. data/lib/honeybadger/rack/user_feedback.rb +6 -4
  40. data/lib/honeybadger/rack/user_informer.rb +6 -4
  41. data/lib/honeybadger/ruby.rb +2 -0
  42. data/lib/honeybadger/singleton.rb +26 -0
  43. data/lib/honeybadger/util/http.rb +12 -0
  44. data/lib/honeybadger/util/request_hash.rb +71 -0
  45. data/lib/honeybadger/util/sanitizer.rb +101 -64
  46. data/lib/honeybadger/version.rb +1 -1
  47. data/lib/honeybadger/worker.rb +246 -0
  48. metadata +17 -13
  49. data/lib/honeybadger/agent/batch.rb +0 -50
  50. data/lib/honeybadger/agent/null_worker.rb +0 -26
  51. data/lib/honeybadger/agent/worker.rb +0 -243
  52. data/lib/honeybadger/cli/helpers.rb +0 -160
  53. data/lib/honeybadger/config/callbacks.rb +0 -70
  54. data/lib/honeybadger/plugins/unicorn.rb +0 -27
  55. data/lib/honeybadger/rack/metrics_reporter.rb +0 -16
  56. data/lib/honeybadger/rack/request_hash.rb +0 -55
@@ -0,0 +1,66 @@
1
+ require 'digest'
2
+ require 'forwardable'
3
+ require 'honeybadger/cli/main'
4
+ require 'honeybadger/util/http'
5
+ require 'honeybadger/util/stats'
6
+
7
+ module Honeybadger
8
+ module CLI
9
+ class Notify
10
+ extend Forwardable
11
+
12
+ def initialize(options, args, config)
13
+ @options = options
14
+ @args = args
15
+ @config = config
16
+ @shell = ::Thor::Base.shell.new
17
+ end
18
+
19
+ def run
20
+ payload = {
21
+ api_key: config.get(:api_key),
22
+ notifier: NOTIFIER,
23
+ error: {
24
+ class: options['class'],
25
+ message: options['message']
26
+ },
27
+ request: {},
28
+ server: {
29
+ project_root: Dir.pwd,
30
+ environment_name: config.get(:env),
31
+ time: Time.now,
32
+ stats: Util::Stats.all
33
+ }
34
+ }
35
+
36
+ payload[:error][:fingerprint] = Digest::SHA1.hexdigest(options['fingerprint']) if option?('fingerprint')
37
+ payload[:error][:tags] = options['tags'].to_s.strip.split(',').map(&:strip) if option?('tags')
38
+
39
+ payload[:request][:component] = options['component'] if option?('component')
40
+ payload[:request][:action] = options['action'] if option?('action')
41
+ payload[:request][:url] = options['url'] if option?('url')
42
+
43
+ payload.delete(:request) if payload[:request].empty?
44
+
45
+ http = Util::HTTP.new(config)
46
+ result = http.post('/v1/notices', payload)
47
+ if result.code == '201'
48
+ say("Error notification complete.", :green)
49
+ else
50
+ say("Invalid response from server: #{result.code}", :red)
51
+ exit(1)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :options, :args, :config
58
+
59
+ def_delegator :@shell, :say
60
+
61
+ def option?(key)
62
+ options.has_key?(key) && options[key] != key
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,266 @@
1
+ require 'erb'
2
+ require 'forwardable'
3
+ require 'honeybadger/cli/main'
4
+ require 'pathname'
5
+
6
+ module Honeybadger
7
+ module CLI
8
+ class Test
9
+ extend Forwardable
10
+
11
+ TEST_EXCEPTION = begin
12
+ exception_name = ENV['EXCEPTION'] || 'HoneybadgerTestingException'
13
+ Object.const_get(exception_name)
14
+ rescue
15
+ Object.const_set(exception_name, Class.new(Exception))
16
+ end.new('Testing honeybadger via "honeybadger test". If you can see this, it works.')
17
+
18
+ class TestBackend
19
+ def initialize(backend)
20
+ @backend = backend
21
+ end
22
+
23
+ def self.callings
24
+ @callings ||= Hash.new {|h,k| h[k] = [] }
25
+ end
26
+
27
+ def notify(feature, payload)
28
+ response = @backend.notify(feature, payload)
29
+ self.class.callings[feature] << [payload, response]
30
+ response
31
+ end
32
+ end
33
+
34
+ def initialize(options)
35
+ @options = options
36
+ @shell = ::Thor::Base.shell.new
37
+ end
38
+
39
+ def run
40
+ require 'honeybadger/ruby'
41
+
42
+ begin
43
+ require File.join(Dir.pwd, 'config', 'application.rb')
44
+ raise LoadError unless defined?(::Rails)
45
+ require 'honeybadger/init/rails'
46
+ Rails.application.initialize!
47
+ say("Detected Rails #{Rails::VERSION::STRING}")
48
+ rescue LoadError
49
+ require 'honeybadger/init/ruby'
50
+ end
51
+
52
+ if Honeybadger.config.get(:api_key).to_s =~ BLANK
53
+ say("Unable to send test: Honeybadger API key is missing.", :red)
54
+ exit(1)
55
+ end
56
+
57
+ Honeybadger.config.set(:report_data, !options[:dry_run])
58
+ test_backend = TestBackend.new(Honeybadger.config.backend)
59
+ Honeybadger.config.backend = test_backend
60
+
61
+ at_exit do
62
+ Honeybadger.flush
63
+
64
+ if calling = TestBackend.callings[:notices].find {|c| c[0].exception.eql?(TEST_EXCEPTION) }
65
+ notice, response = *calling
66
+
67
+ if response.code != 201
68
+ host = Honeybadger.config.get(:'connection.host')
69
+ say(<<-MSG, :red)
70
+ !! --- Honeybadger test failed ------------------------------------------------ !!
71
+
72
+ The error notifier is installed, but we encountered an error:
73
+
74
+ #{response.code.kind_of?(Integer) ? "(#{response.code}) " : nil}#{response.error_message}
75
+
76
+ To fix this issue, please try the following:
77
+
78
+ - Make sure the gem is configured properly.
79
+ - Retry executing this command a few times.
80
+ - Make sure you can connect to #{host} (`ping #{host}`).
81
+ - Email support@honeybadger.io for help. Include as much debug info as you
82
+ can for a faster resolution!
83
+
84
+ !! --- End -------------------------------------------------------------------- !!
85
+ MSG
86
+ exit(1)
87
+ end
88
+
89
+ say(generate_success_message(response), :green)
90
+
91
+ exit(0)
92
+ end
93
+
94
+ say(<<-MSG, :red)
95
+ !! --- Honeybadger test failed ------------------------------------------------ !!
96
+
97
+ Error: The test exception was not reported; the application may not be
98
+ configured properly.
99
+
100
+ This is usually caused by one of the following issues:
101
+
102
+ - There was a problem loading your application. Check your logs to see if a
103
+ different exception is being raised.
104
+ - The exception is being rescued before it reaches our Rack middleware. If
105
+ you're using `rescue` or `rescue_from` you may need to notify Honeybadger
106
+ manually: `Honeybadger.notify(exception)`.
107
+ - The honeybadger gem is misconfigured. Check the settings in your
108
+ honeybadger.yml file.
109
+ MSG
110
+
111
+ notices = TestBackend.callings[:notices].map(&:first)
112
+ unless notices.empty?
113
+ say("\nThe following errors were reported:", :red)
114
+ notices.each {|n| say("\n - #{n.error_class}: #{n.error_message}", :red) }
115
+ end
116
+
117
+ say("\nSee https://git.io/vXCYp for more troubleshooting help.\n\n", :red)
118
+ say("!! --- End -------------------------------------------------------------------- !!", :red)
119
+
120
+ exit(1)
121
+ end
122
+
123
+ run_test
124
+ end
125
+
126
+ private
127
+
128
+ attr_reader :options
129
+
130
+ def_delegator :@shell, :say
131
+
132
+ def run_test
133
+ if defined?(::Rails.application)
134
+ run_rails_test
135
+ else
136
+ run_standalone_test
137
+ end
138
+ end
139
+
140
+ def test_exception_class
141
+ exception_name = ENV['EXCEPTION'] || 'HoneybadgerTestingException'
142
+ Object.const_get(exception_name)
143
+ rescue
144
+ Object.const_set(exception_name, Class.new(Exception))
145
+ end
146
+
147
+ def run_standalone_test
148
+ Honeybadger.notify(TEST_EXCEPTION)
149
+ end
150
+
151
+ def run_rails_test
152
+ # Suppress error logging in Rails' exception handling middleware. Rails 3.0
153
+ # uses ActionDispatch::ShowExceptions to rescue/show exceptions, but does
154
+ # not log anything but application trace. Rails 3.2 now falls back to
155
+ # logging the framework trace (moved to ActionDispatch::DebugExceptions),
156
+ # which caused cluttered output while running the test task.
157
+ defined?(::ActionDispatch::DebugExceptions) and
158
+ ::ActionDispatch::DebugExceptions.class_eval { def logger(*args) ; @logger ||= Logger.new('/dev/null') ; end }
159
+ defined?(::ActionDispatch::ShowExceptions) and
160
+ ::ActionDispatch::ShowExceptions.class_eval { def logger(*args) ; @logger ||= Logger.new('/dev/null') ; end }
161
+
162
+ # Detect and disable the better_errors gem
163
+ if defined?(::BetterErrors::Middleware)
164
+ say('Better Errors detected: temporarily disabling middleware.', :yellow)
165
+ ::BetterErrors::Middleware.class_eval { def call(env) @app.call(env); end }
166
+ end
167
+
168
+ begin
169
+ require './app/controllers/application_controller'
170
+ rescue LoadError
171
+ nil
172
+ end
173
+
174
+ unless defined?(::ApplicationController)
175
+ say('Error: No ApplicationController found.', :red)
176
+ return false
177
+ end
178
+
179
+ eval(<<-CONTROLLER)
180
+ class Honeybadger::TestController < ApplicationController
181
+ # This is to bypass any filters that may prevent access to the action.
182
+ if respond_to?(:prepend_before_action)
183
+ prepend_before_action :test_honeybadger
184
+ else
185
+ prepend_before_filter :test_honeybadger
186
+ end
187
+ def test_honeybadger
188
+ puts "Raising '#{Honeybadger::CLI::Test::TEST_EXCEPTION.class.name}' to simulate application failure."
189
+ raise Honeybadger::CLI::Test::TEST_EXCEPTION
190
+ end
191
+ # Ensure we actually have an action to go to.
192
+ def verify; end
193
+ end
194
+ CONTROLLER
195
+
196
+ ::Rails.application.routes.tap do |r|
197
+ # RouteSet#disable_clear_and_finalize prevents existing routes from
198
+ # being cleared. We'll set it back to the original value when we're
199
+ # done so not to mess with Rails state.
200
+ d = r.disable_clear_and_finalize
201
+ begin
202
+ r.disable_clear_and_finalize = true
203
+ r.clear!
204
+ r.draw do
205
+ match 'verify' => 'honeybadger/test#verify', :as => 'verify', :via => :get
206
+ end
207
+ ::Rails.application.routes_reloader.paths.each{ |path| load(path) }
208
+ ::ActiveSupport.on_load(:action_controller) { r.finalize! }
209
+ ensure
210
+ r.disable_clear_and_finalize = d
211
+ end
212
+ end
213
+
214
+ ssl = defined?(::Rails.configuration.force_ssl) && ::Rails.configuration.force_ssl
215
+ env = ::Rack::MockRequest.env_for("http#{ ssl ? 's' : nil }://www.example.com/verify", 'REMOTE_ADDR' => '127.0.0.1')
216
+
217
+ ::Rails.application.call(env)
218
+ end
219
+
220
+ def generate_success_message(response)
221
+ notice_id = JSON.parse(response.body)['id']
222
+ notice_url = "https://app.honeybadger.io/notice/#{notice_id}"
223
+
224
+ unless options[:install]
225
+ return "⚡ Success: #{notice_url}"
226
+ end
227
+
228
+ <<-MSG
229
+ ⚡ --- Honeybadger is installed! -----------------------------------------------
230
+
231
+ Good news: You're one deploy away from seeing all of your exceptions in
232
+ Honeybadger. For now, we've generated a test exception for you:
233
+
234
+ #{notice_url}
235
+
236
+ Optional steps:
237
+
238
+ - Show a feedback form on your error page:
239
+ http://docs.honeybadger.io/gem-feedback
240
+ - Show a UUID or link to Honeybadger on your error page:
241
+ http://docs.honeybadger.io/gem-informer
242
+ - Track deployments (if you're using Capistrano, we already did this):
243
+ http://docs.honeybadger.io/gem-deploys
244
+
245
+ If you ever need help:
246
+
247
+ - Read the gem troubleshooting guide: https://git.io/vXCYp
248
+ - Check out our documentation: http://docs.honeybadger.io/
249
+ - Email the founders: support@honeybadger.io
250
+
251
+ Most people don't realize that Honeybadger is a small, bootstrapped company. We
252
+ really couldn't do this without you. Thank you for allowing us to do what we
253
+ love: making developers awesome.
254
+
255
+ Happy 'badgering!
256
+
257
+ Sincerely,
258
+ Ben, Josh and Starr
259
+ https://www.honeybadger.io/about/
260
+
261
+ ⚡ --- End --------------------------------------------------------------------
262
+ MSG
263
+ end
264
+ end
265
+ end
266
+ end
@@ -10,7 +10,6 @@ require 'honeybadger/backend'
10
10
  require 'honeybadger/config/defaults'
11
11
  require 'honeybadger/util/http'
12
12
  require 'honeybadger/logging'
13
- require 'honeybadger/rack/request_hash'
14
13
 
15
14
  module Honeybadger
16
15
  class Config
@@ -20,9 +19,11 @@ module Honeybadger
20
19
 
21
20
  class ConfigError < StandardError; end
22
21
 
23
- autoload :Callbacks, 'honeybadger/config/callbacks'
22
+ # Config subclasses have circular dependencies, so they must be loaded
23
+ # after constants are defined.
24
24
  autoload :Env, 'honeybadger/config/env'
25
25
  autoload :Yaml, 'honeybadger/config/yaml'
26
+ autoload :Ruby, 'honeybadger/config/ruby'
26
27
 
27
28
  KEY_REPLACEMENT = Regexp.new('[^a-z\d_]', Regexp::IGNORECASE).freeze
28
29
 
@@ -30,70 +31,100 @@ module Honeybadger
30
31
 
31
32
  NOT_BLANK = Regexp.new('\S').freeze
32
33
 
33
- FEATURES = [:notices, :local_variables].freeze
34
-
35
- MERGE_DEFAULT = [:'exceptions.ignore'].freeze
36
-
37
- OVERRIDE = {
38
- :'exceptions.ignore' => :'exceptions.ignore_only'
39
- }.freeze
40
-
41
- DEFAULT_REQUEST_HASH = {}.freeze
42
-
43
34
  def initialize(opts = {})
44
- l = opts.delete(:logger)
45
-
46
- @values = opts
47
-
48
- priority = {}
49
- priority.update(opts)
50
- load_config_from_disk {|yml| priority.update(yml) }
51
- priority.update(Env.new(ENV))
52
- update(merge_defaults!(priority))
35
+ @ruby = opts.freeze
36
+ @env = {}.freeze
37
+ @yaml = {}.freeze
38
+ @framework = {}.freeze
39
+ end
40
+
41
+ attr_accessor :ruby, :env, :yaml, :framework
42
+
43
+ def init!(opts = {}, env = ENV)
44
+ self.framework = opts.freeze
45
+ self.env = Env.new(env).freeze
46
+ load_config_from_disk {|yaml| self.yaml = yaml.freeze }
47
+ init_logging!
48
+ init_backend!
49
+ logger.info(sprintf('Initializing Honeybadger Error Tracker for Ruby. Ship it! version=%s framework=%s', Honeybadger::VERSION, detected_framework))
50
+ logger.warn('Entering development mode: data will not be reported.') if dev? && backend.kind_of?(Backend::Null)
51
+ if valid? && !ping
52
+ logger.warn('Failed to connect to Honeybadger service -- please verify that api.honeybadger.io is reachable (connection will be retried).')
53
+ end
54
+ self
55
+ end
53
56
 
54
- @logger = Logging::ConfigLogger.new(self, build_logger(l))
55
- Logging::BootLogger.instance.flush(@logger)
57
+ def configure
58
+ ruby_config = Ruby.new(self)
59
+ yield(ruby_config)
60
+ self.ruby = ruby.merge(ruby_config).freeze
61
+ @logging = @backend = nil
62
+ self
63
+ end
56
64
 
57
- @features = Hash[FEATURES.map{|f| [f, true] }]
65
+ def backtrace_filter
66
+ self[:backtrace_filter] = Proc.new if block_given?
67
+ self[:backtrace_filter]
58
68
  end
59
69
 
60
- def_delegators :@values, :update
70
+ def exception_filter
71
+ self[:exception_filter] = Proc.new if block_given?
72
+ self[:exception_filter]
73
+ end
61
74
 
62
- attr_reader :features
75
+ def exception_fingerprint
76
+ self[:exception_fingerprint] = Proc.new if block_given?
77
+ self[:exception_fingerprint]
78
+ end
63
79
 
64
80
  def get(key)
65
- key = key.to_sym
66
- if OVERRIDE.has_key?(key) && @values.has_key?(OVERRIDE[key])
67
- @values[OVERRIDE[key]]
68
- elsif @values.has_key?(key)
69
- @values[key]
70
- else
71
- DEFAULTS[key]
81
+ [:@ruby, :@env, :@yaml, :@framework].each do |var|
82
+ source = instance_variable_get(var)
83
+ if source.has_key?(key)
84
+ return source[key]
85
+ end
72
86
  end
87
+
88
+ DEFAULTS[key]
73
89
  end
74
90
  alias [] :get
75
91
 
76
92
  def set(key, value)
77
- @values[key] = value
93
+ self.ruby = ruby.merge(key => value).freeze
94
+ @logging = @backend = nil
78
95
  end
79
96
  alias []= :set
80
97
 
81
98
  def to_hash(defaults = false)
82
- hash = defaults ? DEFAULTS.merge(@values) : @values
99
+ hash = [:@ruby, :@env, :@yaml, :@framework].reverse.reduce({}) do |a,e|
100
+ a.merge!(instance_variable_get(e))
101
+ end
102
+
103
+ hash = DEFAULTS.merge(hash) if defaults
104
+
83
105
  undotify_keys(hash.select {|k,v| DEFAULTS.has_key?(k) })
84
106
  end
85
- alias :to_h :to_hash
107
+ alias to_h to_hash
86
108
 
87
- def feature?(feature)
88
- !!features[feature.to_sym]
89
- end
109
+ # Internal Helpers
90
110
 
91
111
  def logger
92
- @logger || Logging::BootLogger.instance
112
+ init_logging! unless @logger
113
+ @logger
93
114
  end
94
115
 
95
116
  def backend
96
- Backend.for((self[:backend] || default_backend).to_sym).new(self)
117
+ init_backend! unless @backend
118
+ @backend
119
+ end
120
+
121
+ def backend=(backend)
122
+ set(:backend, backend)
123
+ @backend = nil
124
+ end
125
+
126
+ def disabled?
127
+ !!self[:disabled]
97
128
  end
98
129
 
99
130
  def dev?
@@ -106,14 +137,6 @@ module Honeybadger
106
137
  !self[:env] || !dev?
107
138
  end
108
139
 
109
- def default_backend
110
- if public?
111
- :server
112
- else
113
- :null
114
- end
115
- end
116
-
117
140
  def valid?
118
141
  self[:api_key].to_s =~ /\S/
119
142
  end
@@ -127,28 +150,12 @@ module Honeybadger
127
150
  !!self[:'logging.debug']
128
151
  end
129
152
 
130
- # Internal: Optional path to honeybadger.log log file. If nil, STDOUT will be used
131
- # instead.
132
- #
133
- # Returns the Pathname log path if a log path was specified.
134
- def log_path
135
- if self[:'logging.path'] && self[:'logging.path'] != 'STDOUT'
136
- locate_absolute_path(self[:'logging.path'], self[:root])
137
- end
138
- end
139
-
140
- # Internal: Path to honeybadger.yml configuration file; this should be the
141
- # root directory if no path was specified.
142
- #
143
- # Returns the Pathname configuration path.
144
- def config_path
145
- config_paths.first
146
- end
153
+ def ignored_classes
154
+ ignore_only = get(:'exceptions.ignore_only')
155
+ return ignore_only if ignore_only
156
+ return DEFAULTS[:'exceptions.ignore'] unless ignore = get(:'exceptions.ignore')
147
157
 
148
- def config_paths
149
- Array(ENV['HONEYBADGER_CONFIG_PATH'] || get(:'config.path')).map do |c|
150
- locate_absolute_path(c, self[:root])
151
- end
158
+ DEFAULTS[:'exceptions.ignore'] | Array(ignore)
152
159
  end
153
160
 
154
161
  def ca_bundle_path
@@ -183,32 +190,12 @@ module Honeybadger
183
190
  end
184
191
  end
185
192
 
186
- def request
187
- Thread.current[:__honeybadger_request]
188
- end
189
-
190
- def with_request(request, &block)
191
- Thread.current[:__honeybadger_request] = request
192
- yield
193
- ensure
194
- Thread.current[:__honeybadger_request] = nil
195
- end
196
-
197
193
  def max_queue_size
198
194
  self[:max_queue_size]
199
195
  end
200
196
 
201
- def request_hash
202
- return DEFAULT_REQUEST_HASH unless request
203
- Rack::RequestHash.new(request)
204
- end
205
-
206
197
  def params_filters
207
- self[:'request.filter_keys'] + rails_params_filters
208
- end
209
-
210
- def rails_params_filters
211
- request && request.env['action_dispatch.parameter_filter'] or []
198
+ Array(self[:'request.filter_keys'])
212
199
  end
213
200
 
214
201
  def excluded_request_keys
@@ -220,23 +207,6 @@ module Honeybadger
220
207
  end
221
208
  end
222
209
 
223
- def write
224
- path = config_path
225
-
226
- if path.exist?
227
- raise ConfigError, "The configuration file #{path} already exists."
228
- elsif !path.dirname.writable?
229
- raise ConfigError, "The configuration path #{path.dirname} is not writable."
230
- end
231
-
232
- File.open(path, 'w+') do |file|
233
- file.write(<<-CONFIG)
234
- ---
235
- api_key: '#{self[:api_key]}'
236
- CONFIG
237
- end
238
- end
239
-
240
210
  def log_level(key = :'logging.level')
241
211
  case self[key].to_s
242
212
  when /\A(0|debug)\z/i then Logger::DEBUG
@@ -249,21 +219,33 @@ api_key: '#{self[:api_key]}'
249
219
  end
250
220
 
251
221
  def load_plugin?(name)
252
- return false if includes_token?(self[:'plugins.skip'], name)
222
+ return false if includes_token?(self[:'skipped_plugins'], name)
253
223
  return true unless self[:plugins].kind_of?(Array)
254
224
  includes_token?(self[:plugins], name)
255
225
  end
256
226
 
227
+ # Internal: Match the project root.
228
+ #
229
+ # Returns Regexp matching the project root in a file string.
230
+ def root_regexp
231
+ return @root_regexp if @root_regexp
232
+ return nil if @no_root
233
+
234
+ root = get(:root).to_s
235
+ @no_root = true and return nil unless root =~ NOT_BLANK
236
+
237
+ @root_regexp = Regexp.new("^#{ Regexp.escape(root) }")
238
+ end
239
+
257
240
  def ping
258
241
  if result = send_ping
259
- @features = symbolize_keys(result['features']) if result['features']
260
242
  return true
261
243
  end
262
244
 
263
245
  false
264
246
  end
265
247
 
266
- def framework
248
+ def detected_framework
267
249
  if self[:framework] =~ NOT_BLANK
268
250
  self[:framework].to_sym
269
251
  elsif defined?(::Rails::VERSION) && ::Rails::VERSION::STRING > '3.0'
@@ -278,7 +260,7 @@ api_key: '#{self[:api_key]}'
278
260
  end
279
261
 
280
262
  def framework_name
281
- case framework
263
+ case detected_framework
282
264
  when :rails then "Rails #{::Rails::VERSION::STRING}"
283
265
  when :sinatra then "Sinatra #{::Sinatra::VERSION}"
284
266
  when :rack then "Rack #{::Rack.release}"
@@ -287,20 +269,92 @@ api_key: '#{self[:api_key]}'
287
269
  end
288
270
  end
289
271
 
290
- # Internal: Match the project root.
272
+ private
273
+
274
+ # Internal: Optional path to honeybadger.log log file.
291
275
  #
292
- # Returns Regexp matching the project root in a file string.
293
- def root_regexp
294
- return @root_regexp if @root_regexp
295
- return nil if @no_root
276
+ # Returns the Pathname log path if a log path was specified.
277
+ def log_path
278
+ return if log_stdout?
279
+ return if !self[:'logging.path']
280
+ locate_absolute_path(self[:'logging.path'], self[:root])
281
+ end
296
282
 
297
- root = get(:root).to_s
298
- @no_root = true and return nil unless root =~ NOT_BLANK
283
+ # Internal: Path to honeybadger.yml configuration file; this should be the
284
+ # root directory if no path was specified.
285
+ #
286
+ # Returns the Pathname configuration path.
287
+ def config_path
288
+ config_paths.first
289
+ end
299
290
 
300
- @root_regexp = Regexp.new("^#{ Regexp.escape(root) }")
291
+ def config_paths
292
+ Array(ENV['HONEYBADGER_CONFIG_PATH'] || get(:'config.path')).map do |c|
293
+ locate_absolute_path(c, self[:root])
294
+ end
301
295
  end
302
296
 
303
- private
297
+ def default_backend
298
+ return Backend::Server.new(self) if public?
299
+ Backend::Null.new(self)
300
+ end
301
+
302
+ def init_backend!
303
+ if self[:backend].is_a?(String) || self[:backend].is_a?(Symbol)
304
+ @backend = Backend.for(self[:backend].to_sym).new(self)
305
+ return
306
+ end
307
+
308
+ if ruby[:backend].respond_to?(:notify)
309
+ @backend = ruby[:backend]
310
+ return
311
+ end
312
+
313
+ if ruby[:backend]
314
+ logger.warn(sprintf('Unknown backend: %p; default will be used. Backend must respond to #notify', self[:backend]))
315
+ end
316
+
317
+ @backend = default_backend
318
+ end
319
+
320
+ def build_stdout_logger
321
+ logger = Logger.new($stdout)
322
+ logger.formatter = lambda do |severity, datetime, progname, msg|
323
+ "#{msg}\n"
324
+ end
325
+ logger.level = log_level
326
+ Logging::FormattedLogger.new(logger)
327
+ end
328
+
329
+ def build_file_logger(path)
330
+ Logger.new(path).tap do |logger|
331
+ logger.level = log_level
332
+ logger.formatter = Logger::Formatter.new
333
+ end
334
+ end
335
+
336
+ def log_stdout?
337
+ self[:'logging.path'] && self[:'logging.path'].to_s.downcase == 'stdout'
338
+ end
339
+
340
+ def build_logger
341
+ return ruby[:logger] if ruby[:logger]
342
+
343
+ return build_stdout_logger if log_stdout?
344
+
345
+ if path = log_path
346
+ FileUtils.mkdir_p(path.dirname) unless path.dirname.writable?
347
+ return build_file_logger(path)
348
+ end
349
+
350
+ return framework[:logger] if framework[:logger]
351
+
352
+ Logger.new('/dev/null')
353
+ end
354
+
355
+ def init_logging!
356
+ @logger = Logging::ConfigLogger.new(self, build_logger)
357
+ end
304
358
 
305
359
  # Internal: Does collection include the String value or Symbol value?
306
360
  #
@@ -349,25 +403,6 @@ api_key: '#{self[:api_key]}'
349
403
  end
350
404
  end
351
405
 
352
- def build_logger(default = nil)
353
- if path = log_path
354
- FileUtils.mkdir_p(path.dirname) unless path.dirname.writable?
355
- Logger.new(path).tap do |logger|
356
- logger.level = log_level
357
- logger.formatter = Logger::Formatter.new
358
- end
359
- elsif self[:'logging.path'] != 'STDOUT' && default
360
- default
361
- else
362
- logger = Logger.new($stdout)
363
- logger.level = log_level
364
- logger.formatter = lambda do |severity, datetime, progname, msg|
365
- "#{msg}\n"
366
- end
367
- Logging::FormattedLogger.new(logger)
368
- end
369
- end
370
-
371
406
  def load_config_from_disk
372
407
  if (path = config_paths.find(&:exist?)) && path.file?
373
408
  Yaml.new(path, self[:env]).tap do |yml|
@@ -375,11 +410,11 @@ api_key: '#{self[:api_key]}'
375
410
  end
376
411
  end
377
412
  rescue ConfigError => e
378
- error("error while loading config from disk: #{e}")
413
+ error("Error loading config from disk: #{e}")
379
414
  nil
380
415
  rescue StandardError => e
381
416
  error {
382
- msg = "error while loading config from disk class=%s message=%s\n\t%s"
417
+ msg = "Error loading config from disk. class=%s message=%s\n\t%s"
383
418
  sprintf(msg, e.class, e.message.dump, Array(e.backtrace).join("\n\t"))
384
419
  }
385
420
  nil
@@ -392,29 +427,10 @@ api_key: '#{self[:api_key]}'
392
427
  new_hash[$1] ||= {}
393
428
  new_hash[$1] = undotify_keys(new_hash[$1].merge({$2 => v}))
394
429
  else
395
- new_hash[k] = v
430
+ new_hash[k.to_s] = v
396
431
  end
397
432
  end
398
433
  end
399
434
  end
400
-
401
- def symbolize_keys(hash)
402
- Hash[hash.map {|k,v| [k.to_sym, v] }]
403
- end
404
-
405
- # Internal: Merges supplied config options with defaults.
406
- #
407
- # config - The Hash config options to merge.
408
- #
409
- # Returns the updated Hash config with merged values.
410
- def merge_defaults!(config)
411
- MERGE_DEFAULT.each do |option|
412
- if config[option].kind_of?(Array)
413
- config[option] = (DEFAULTS[option] | config[option])
414
- end
415
- end
416
-
417
- config
418
- end
419
435
  end
420
436
  end