honeybadger 2.7.2 → 3.0.0.beta1

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