airbrake-ruby 4.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +7 -0
  2. data/lib/airbrake-ruby.rb +515 -0
  3. data/lib/airbrake-ruby/async_sender.rb +80 -0
  4. data/lib/airbrake-ruby/backtrace.rb +196 -0
  5. data/lib/airbrake-ruby/benchmark.rb +39 -0
  6. data/lib/airbrake-ruby/code_hunk.rb +51 -0
  7. data/lib/airbrake-ruby/config.rb +229 -0
  8. data/lib/airbrake-ruby/config/validator.rb +91 -0
  9. data/lib/airbrake-ruby/deploy_notifier.rb +36 -0
  10. data/lib/airbrake-ruby/file_cache.rb +54 -0
  11. data/lib/airbrake-ruby/filter_chain.rb +95 -0
  12. data/lib/airbrake-ruby/filters/context_filter.rb +29 -0
  13. data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
  14. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +46 -0
  15. data/lib/airbrake-ruby/filters/gem_root_filter.rb +33 -0
  16. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +92 -0
  17. data/lib/airbrake-ruby/filters/git_repository_filter.rb +64 -0
  18. data/lib/airbrake-ruby/filters/git_revision_filter.rb +66 -0
  19. data/lib/airbrake-ruby/filters/keys_blacklist.rb +49 -0
  20. data/lib/airbrake-ruby/filters/keys_filter.rb +140 -0
  21. data/lib/airbrake-ruby/filters/keys_whitelist.rb +48 -0
  22. data/lib/airbrake-ruby/filters/root_directory_filter.rb +28 -0
  23. data/lib/airbrake-ruby/filters/sql_filter.rb +128 -0
  24. data/lib/airbrake-ruby/filters/system_exit_filter.rb +23 -0
  25. data/lib/airbrake-ruby/filters/thread_filter.rb +92 -0
  26. data/lib/airbrake-ruby/hash_keyable.rb +37 -0
  27. data/lib/airbrake-ruby/ignorable.rb +44 -0
  28. data/lib/airbrake-ruby/inspectable.rb +39 -0
  29. data/lib/airbrake-ruby/loggable.rb +34 -0
  30. data/lib/airbrake-ruby/monotonic_time.rb +43 -0
  31. data/lib/airbrake-ruby/nested_exception.rb +38 -0
  32. data/lib/airbrake-ruby/notice.rb +162 -0
  33. data/lib/airbrake-ruby/notice_notifier.rb +134 -0
  34. data/lib/airbrake-ruby/performance_breakdown.rb +46 -0
  35. data/lib/airbrake-ruby/performance_notifier.rb +155 -0
  36. data/lib/airbrake-ruby/promise.rb +109 -0
  37. data/lib/airbrake-ruby/query.rb +54 -0
  38. data/lib/airbrake-ruby/request.rb +46 -0
  39. data/lib/airbrake-ruby/response.rb +74 -0
  40. data/lib/airbrake-ruby/stashable.rb +15 -0
  41. data/lib/airbrake-ruby/stat.rb +73 -0
  42. data/lib/airbrake-ruby/sync_sender.rb +113 -0
  43. data/lib/airbrake-ruby/tdigest.rb +393 -0
  44. data/lib/airbrake-ruby/thread_pool.rb +128 -0
  45. data/lib/airbrake-ruby/time_truncate.rb +17 -0
  46. data/lib/airbrake-ruby/timed_trace.rb +58 -0
  47. data/lib/airbrake-ruby/truncator.rb +115 -0
  48. data/lib/airbrake-ruby/version.rb +6 -0
  49. data/spec/airbrake_spec.rb +324 -0
  50. data/spec/async_sender_spec.rb +72 -0
  51. data/spec/backtrace_spec.rb +427 -0
  52. data/spec/benchmark_spec.rb +33 -0
  53. data/spec/code_hunk_spec.rb +115 -0
  54. data/spec/config/validator_spec.rb +184 -0
  55. data/spec/config_spec.rb +154 -0
  56. data/spec/deploy_notifier_spec.rb +48 -0
  57. data/spec/file_cache_spec.rb +34 -0
  58. data/spec/filter_chain_spec.rb +92 -0
  59. data/spec/filters/context_filter_spec.rb +23 -0
  60. data/spec/filters/dependency_filter_spec.rb +12 -0
  61. data/spec/filters/exception_attributes_filter_spec.rb +50 -0
  62. data/spec/filters/gem_root_filter_spec.rb +41 -0
  63. data/spec/filters/git_last_checkout_filter_spec.rb +46 -0
  64. data/spec/filters/git_repository_filter.rb +61 -0
  65. data/spec/filters/git_revision_filter_spec.rb +126 -0
  66. data/spec/filters/keys_blacklist_spec.rb +225 -0
  67. data/spec/filters/keys_whitelist_spec.rb +194 -0
  68. data/spec/filters/root_directory_filter_spec.rb +39 -0
  69. data/spec/filters/sql_filter_spec.rb +276 -0
  70. data/spec/filters/system_exit_filter_spec.rb +14 -0
  71. data/spec/filters/thread_filter_spec.rb +277 -0
  72. data/spec/fixtures/notroot.txt +7 -0
  73. data/spec/fixtures/project_root/code.rb +221 -0
  74. data/spec/fixtures/project_root/empty_file.rb +0 -0
  75. data/spec/fixtures/project_root/long_line.txt +1 -0
  76. data/spec/fixtures/project_root/short_file.rb +3 -0
  77. data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
  78. data/spec/helpers.rb +9 -0
  79. data/spec/ignorable_spec.rb +14 -0
  80. data/spec/inspectable_spec.rb +45 -0
  81. data/spec/monotonic_time_spec.rb +12 -0
  82. data/spec/nested_exception_spec.rb +73 -0
  83. data/spec/notice_notifier/options_spec.rb +259 -0
  84. data/spec/notice_notifier_spec.rb +356 -0
  85. data/spec/notice_spec.rb +296 -0
  86. data/spec/performance_breakdown_spec.rb +12 -0
  87. data/spec/performance_notifier_spec.rb +491 -0
  88. data/spec/promise_spec.rb +197 -0
  89. data/spec/query_spec.rb +11 -0
  90. data/spec/request_spec.rb +11 -0
  91. data/spec/response_spec.rb +88 -0
  92. data/spec/spec_helper.rb +100 -0
  93. data/spec/stashable_spec.rb +23 -0
  94. data/spec/stat_spec.rb +47 -0
  95. data/spec/sync_sender_spec.rb +133 -0
  96. data/spec/tdigest_spec.rb +230 -0
  97. data/spec/thread_pool_spec.rb +158 -0
  98. data/spec/time_truncate_spec.rb +13 -0
  99. data/spec/timed_trace_spec.rb +125 -0
  100. data/spec/truncator_spec.rb +238 -0
  101. metadata +216 -0
@@ -0,0 +1,80 @@
1
+ module Airbrake
2
+ # Responsible for sending notices to Airbrake asynchronously.
3
+ #
4
+ # @see SyncSender
5
+ # @api private
6
+ # @since v1.0.0
7
+ class AsyncSender
8
+ include Loggable
9
+
10
+ # @return [String]
11
+ WILL_NOT_DELIVER_MSG =
12
+ "%<log_label>s AsyncSender has reached its capacity of %<capacity>s " \
13
+ "and the following notice will not be delivered " \
14
+ "Error: %<type>s - %<message>s\nBacktrace: %<backtrace>s\n".freeze
15
+
16
+ def initialize(method = :post)
17
+ @config = Airbrake::Config.instance
18
+ @method = method
19
+ end
20
+
21
+ # Asynchronously sends a notice to Airbrake.
22
+ #
23
+ # @param [Airbrake::Notice] notice A notice that was generated by the
24
+ # library
25
+ # @return [Airbrake::Promise]
26
+ def send(notice, promise, endpoint = @config.endpoint)
27
+ unless thread_pool << [notice, promise, endpoint]
28
+ return will_not_deliver(notice, promise)
29
+ end
30
+
31
+ promise
32
+ end
33
+
34
+ # @return [void]
35
+ def close
36
+ thread_pool.close
37
+ end
38
+
39
+ # @return [Boolean]
40
+ def closed?
41
+ thread_pool.closed?
42
+ end
43
+
44
+ # @return [Boolean]
45
+ def has_workers?
46
+ thread_pool.has_workers?
47
+ end
48
+
49
+ private
50
+
51
+ def thread_pool
52
+ @thread_pool ||= begin
53
+ sender = SyncSender.new(@method)
54
+ ThreadPool.new(
55
+ worker_size: @config.workers,
56
+ queue_size: @config.queue_size,
57
+ block: proc { |args| sender.send(*args) }
58
+ )
59
+ end
60
+ end
61
+
62
+ def will_not_deliver(notice, promise)
63
+ error = notice[:errors].first
64
+
65
+ logger.error(
66
+ format(
67
+ WILL_NOT_DELIVER_MSG,
68
+ log_label: LOG_LABEL,
69
+ capacity: @config.queue_size,
70
+ type: error[:type],
71
+ message: error[:message],
72
+ backtrace: error[:backtrace].map do |line|
73
+ "#{line[:file]}:#{line[:line]} in `#{line[:function]}'"
74
+ end.join("\n")
75
+ )
76
+ )
77
+ promise.reject("AsyncSender has reached its capacity of #{@config.queue_size}")
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,196 @@
1
+ module Airbrake
2
+ # Represents a cross-Ruby backtrace from exceptions (including JRuby Java
3
+ # exceptions). Provides information about stack frames (such as line number,
4
+ # file and method) in convenient for Airbrake format.
5
+ #
6
+ # @example
7
+ # begin
8
+ # raise 'Oops!'
9
+ # rescue
10
+ # Backtrace.parse($!)
11
+ # end
12
+ #
13
+ # @api private
14
+ # @since v1.0.0
15
+ module Backtrace
16
+ module Patterns
17
+ # @return [Regexp] the pattern that matches standard Ruby stack frames,
18
+ # such as ./spec/notice_spec.rb:43:in `block (3 levels) in <top (required)>'
19
+ RUBY = %r{\A
20
+ (?<file>.+) # Matches './spec/notice_spec.rb'
21
+ :
22
+ (?<line>\d+) # Matches '43'
23
+ :in\s
24
+ `(?<function>.*)' # Matches "`block (3 levels) in <top (required)>'"
25
+ \z}x
26
+
27
+ # @return [Regexp] the pattern that matches JRuby Java stack frames, such
28
+ # as org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
29
+ JAVA = %r{\A
30
+ (?<function>.+) # Matches 'org.jruby.ast.NewlineNode.interpret'
31
+ \(
32
+ (?<file>
33
+ (?:uri:classloader:/.+(?=:)) # Matches '/META-INF/jruby.home/protocol.rb'
34
+ |
35
+ (?:uri_3a_classloader_3a_.+(?=:)) # Matches 'uri_3a_classloader_3a_/gems/...'
36
+ |
37
+ [^:]+ # Matches 'NewlineNode.java'
38
+ )
39
+ :?
40
+ (?<line>\d+)? # Matches '105'
41
+ \)
42
+ \z}x
43
+
44
+ # @return [Regexp] the pattern that tries to assume what a generic stack
45
+ # frame might look like, when exception's backtrace is set manually.
46
+ GENERIC = %r{\A
47
+ (?:from\s)?
48
+ (?<file>.+) # Matches '/foo/bar/baz.ext'
49
+ :
50
+ (?<line>\d+)? # Matches '43' or nothing
51
+ (?:
52
+ in\s`(?<function>.+)' # Matches "in `func'"
53
+ |
54
+ :in\s(?<function>.+) # Matches ":in func"
55
+ )? # ... or nothing
56
+ \z}x
57
+
58
+ # @return [Regexp] the pattern that matches exceptions from PL/SQL such as
59
+ # ORA-06512: at "STORE.LI_LICENSES_PACK", line 1945
60
+ # @note This is raised by https://github.com/kubo/ruby-oci8
61
+ OCI = /\A
62
+ (?:
63
+ ORA-\d{5}
64
+ :\sat\s
65
+ (?:"(?<function>.+)",\s)?
66
+ line\s(?<line>\d+)
67
+ |
68
+ #{GENERIC}
69
+ )
70
+ \z/x
71
+
72
+ # @return [Regexp] the pattern that matches CoffeeScript backtraces
73
+ # usually coming from Rails & ExecJS
74
+ EXECJS = /\A
75
+ (?:
76
+ # Matches 'compile ((execjs):6692:19)'
77
+ (?<function>.+)\s\((?<file>.+):(?<line>\d+):\d+\)
78
+ |
79
+ # Matches 'bootstrap_node.js:467:3'
80
+ (?<file>.+):(?<line>\d+):\d+(?<function>)
81
+ |
82
+ # Matches the Ruby part of the backtrace
83
+ #{RUBY}
84
+ )
85
+ \z/x
86
+ end
87
+
88
+ # @return [Integer] how many first frames should include code hunks
89
+ CODE_FRAME_LIMIT = 10
90
+
91
+ # Parses an exception's backtrace.
92
+ #
93
+ # @param [Exception] exception The exception, which contains a backtrace to
94
+ # parse
95
+ # @return [Array<Hash{Symbol=>String,Integer}>] the parsed backtrace
96
+ def self.parse(exception)
97
+ return [] if exception.backtrace.nil? || exception.backtrace.none?
98
+ parse_backtrace(exception)
99
+ end
100
+
101
+ # Checks whether the given exception was generated by JRuby's VM.
102
+ #
103
+ # @param [Exception] exception
104
+ # @return [Boolean]
105
+ def self.java_exception?(exception)
106
+ if defined?(Java::JavaLang::Throwable) &&
107
+ exception.is_a?(Java::JavaLang::Throwable)
108
+ return true
109
+ end
110
+
111
+ return false unless exception.respond_to?(:backtrace)
112
+
113
+ (Patterns::JAVA =~ exception.backtrace.first) != nil
114
+ end
115
+
116
+ class << self
117
+ include Loggable
118
+
119
+ private
120
+
121
+ def best_regexp_for(exception)
122
+ if java_exception?(exception)
123
+ Patterns::JAVA
124
+ elsif oci_exception?(exception)
125
+ Patterns::OCI
126
+ elsif execjs_exception?(exception)
127
+ Patterns::EXECJS
128
+ else
129
+ Patterns::RUBY
130
+ end
131
+ end
132
+
133
+ def oci_exception?(exception)
134
+ defined?(OCIError) && exception.is_a?(OCIError)
135
+ end
136
+
137
+ def execjs_exception?(exception)
138
+ return false unless defined?(ExecJS::RuntimeError)
139
+ return true if exception.is_a?(ExecJS::RuntimeError)
140
+ return true if exception.cause && exception.cause.is_a?(ExecJS::RuntimeError)
141
+
142
+ false
143
+ end
144
+
145
+ def stack_frame(regexp, stackframe)
146
+ if (match = match_frame(regexp, stackframe))
147
+ return {
148
+ file: match[:file],
149
+ line: (Integer(match[:line]) if match[:line]),
150
+ function: match[:function]
151
+ }
152
+ end
153
+
154
+ logger.error(
155
+ "can't parse '#{stackframe}' (please file an issue so we can fix " \
156
+ "it: https://github.com/airbrake/airbrake-ruby/issues/new)"
157
+ )
158
+ { file: nil, line: nil, function: stackframe }
159
+ end
160
+
161
+ def match_frame(regexp, stackframe)
162
+ match = regexp.match(stackframe)
163
+ return match if match
164
+
165
+ Patterns::GENERIC.match(stackframe)
166
+ end
167
+
168
+ def parse_backtrace(exception)
169
+ regexp = best_regexp_for(exception)
170
+ root_directory = Airbrake::Config.instance.root_directory.to_s
171
+
172
+ exception.backtrace.map.with_index do |stackframe, i|
173
+ frame = stack_frame(regexp, stackframe)
174
+ next(frame) if !Airbrake::Config.instance.code_hunks || frame[:file].nil?
175
+
176
+ if !root_directory.empty?
177
+ populate_code(frame) if frame_in_root?(frame, root_directory)
178
+ elsif i < CODE_FRAME_LIMIT
179
+ populate_code(frame)
180
+ end
181
+
182
+ frame
183
+ end
184
+ end
185
+
186
+ def populate_code(frame)
187
+ code = Airbrake::CodeHunk.new.get(frame[:file], frame[:line])
188
+ frame[:code] = code if code
189
+ end
190
+
191
+ def frame_in_root?(frame, root_directory)
192
+ frame[:file].start_with?(root_directory) && frame[:file] !~ %r{vendor/bundle}
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,39 @@
1
+ module Airbrake
2
+ # Benchmark benchmarks Ruby code.
3
+ #
4
+ # @since v4.2.4
5
+ # @api private
6
+ class Benchmark
7
+ # Measures monotonic time for the given operation.
8
+ #
9
+ # @yieldreturn [void]
10
+ def self.measure
11
+ benchmark = new
12
+
13
+ yield
14
+
15
+ benchmark.stop
16
+ benchmark.duration
17
+ end
18
+
19
+ # @return [Float]
20
+ attr_reader :duration
21
+
22
+ # @since v4.3.0
23
+ def initialize
24
+ @start = MonotonicTime.time_in_ms
25
+ @duration = 0.0
26
+ end
27
+
28
+ # Stops the benchmark and stores `duration`.
29
+ #
30
+ # @since v4.3.0
31
+ # @return [Boolean] true for the first invocation, false in all other cases
32
+ def stop
33
+ return false if @duration > 0.0
34
+
35
+ @duration = MonotonicTime.time_in_ms - @start
36
+ true
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,51 @@
1
+ module Airbrake
2
+ # Represents a small hunk of code consisting of a base line and a couple lines
3
+ # around it
4
+ # @api private
5
+ class CodeHunk
6
+ # @return [Integer] the maximum length of a line
7
+ MAX_LINE_LEN = 200
8
+
9
+ # @return [Integer] how many lines should be read around the base line
10
+ NLINES = 2
11
+
12
+ include Loggable
13
+
14
+ # @param [String] file The file to read
15
+ # @param [Integer] line The base line in the file
16
+ # @return [Hash{Integer=>String}, nil] lines of code around the base line
17
+ def get(file, line)
18
+ return unless File.exist?(file)
19
+ return unless line
20
+
21
+ lines = get_lines(file, [line - NLINES, 1].max, line + NLINES) || {}
22
+ return { 1 => '' } if lines.empty?
23
+
24
+ lines
25
+ end
26
+
27
+ private
28
+
29
+ def get_from_cache(file)
30
+ Airbrake::FileCache[file] ||= File.foreach(file)
31
+ rescue StandardError => ex
32
+ logger.error(
33
+ "#{self.class.name}: can't read code hunk for #{file}: #{ex}"
34
+ )
35
+ nil
36
+ end
37
+
38
+ def get_lines(file, start_line, end_line)
39
+ return unless (cached_file = get_from_cache(file))
40
+
41
+ lines = {}
42
+ cached_file.with_index(1) do |l, i|
43
+ next if i < start_line
44
+ break if i > end_line
45
+
46
+ lines[i] = l[0...MAX_LINE_LEN].rstrip
47
+ end
48
+ lines
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,229 @@
1
+ module Airbrake
2
+ # Represents the Airbrake config. A config contains all the options that you
3
+ # can use to configure an Airbrake instance.
4
+ #
5
+ # @api public
6
+ # @since v1.0.0
7
+ class Config
8
+ # @return [Integer] the project identificator. This value *must* be set.
9
+ # @api public
10
+ attr_accessor :project_id
11
+
12
+ # @return [String] the project key. This value *must* be set.
13
+ # @api public
14
+ attr_accessor :project_key
15
+
16
+ # @return [Hash] the proxy parameters such as (:host, :port, :user and
17
+ # :password)
18
+ # @api public
19
+ attr_accessor :proxy
20
+
21
+ # @return [Logger] the default logger used for debug output
22
+ # @api public
23
+ attr_reader :logger
24
+
25
+ # @return [String] the version of the user's application
26
+ # @api public
27
+ attr_accessor :app_version
28
+
29
+ # @return [Hash{String=>String}] arbitrary versions that your app wants to
30
+ # track
31
+ # @api public
32
+ # @since v2.10.0
33
+ attr_accessor :versions
34
+
35
+ # @return [Integer] the max number of notices that can be queued up
36
+ # @api public
37
+ attr_accessor :queue_size
38
+
39
+ # @return [Integer] the number of worker threads that process the notice
40
+ # queue
41
+ # @api public
42
+ attr_accessor :workers
43
+
44
+ # @return [String] the host, which provides the API endpoint to which
45
+ # exceptions should be sent
46
+ # @api public
47
+ attr_accessor :host
48
+
49
+ # @return [String, Pathname] the working directory of your project
50
+ # @api public
51
+ attr_accessor :root_directory
52
+
53
+ # @return [String, Symbol] the environment the application is running in
54
+ # @api public
55
+ attr_accessor :environment
56
+
57
+ # @return [Array<String,Symbol,Regexp>] the array of environments that
58
+ # forbids sending exceptions when the application is running in them.
59
+ # Other possible environments not listed in the array will allow sending
60
+ # occurring exceptions.
61
+ # @api public
62
+ attr_accessor :ignore_environments
63
+
64
+ # @return [Integer] The HTTP timeout in seconds.
65
+ # @api public
66
+ attr_accessor :timeout
67
+
68
+ # @return [Array<String, Symbol, Regexp>] the keys, which should be
69
+ # filtered
70
+ # @api public
71
+ # @since v1.2.0
72
+ attr_accessor :blacklist_keys
73
+
74
+ # @return [Array<String, Symbol, Regexp>] the keys, which shouldn't be
75
+ # filtered
76
+ # @api public
77
+ # @since v1.2.0
78
+ attr_accessor :whitelist_keys
79
+
80
+ # @return [Boolean] true if the library should attach code hunks to each
81
+ # frame in a backtrace, false otherwise
82
+ # @api public
83
+ # @since v2.5.0
84
+ attr_accessor :code_hunks
85
+
86
+ # @return [Boolean] true if the library should send route performance stats
87
+ # to Airbrake, false otherwise
88
+ # @api public
89
+ # @since v3.2.0
90
+ attr_accessor :performance_stats
91
+
92
+ # @return [Integer] how many seconds to wait before sending collected route
93
+ # stats
94
+ # @api private
95
+ # @since v3.2.0
96
+ attr_accessor :performance_stats_flush_period
97
+
98
+ # @return [Boolean] true if the library should send SQL stats to Airbrake,
99
+ # false otherwise
100
+ # @api public
101
+ # @since v4.6.0
102
+ attr_accessor :query_stats
103
+
104
+ class << self
105
+ # @return [Config]
106
+ attr_writer :instance
107
+
108
+ # @return [Config]
109
+ def instance
110
+ @instance ||= new
111
+ end
112
+ end
113
+
114
+ # @param [Hash{Symbol=>Object}] user_config the hash to be used to build the
115
+ # config
116
+ def initialize(user_config = {})
117
+ self.proxy = {}
118
+ self.queue_size = 100
119
+ self.workers = 1
120
+ self.code_hunks = true
121
+ self.logger = ::Logger.new(File::NULL)
122
+ self.project_id = user_config[:project_id]
123
+ self.project_key = user_config[:project_key]
124
+ self.host = 'https://api.airbrake.io'
125
+
126
+ self.ignore_environments = []
127
+
128
+ self.timeout = user_config[:timeout]
129
+
130
+ self.blacklist_keys = []
131
+ self.whitelist_keys = []
132
+
133
+ self.root_directory = File.realpath(
134
+ (defined?(Bundler) && Bundler.root) ||
135
+ Dir.pwd
136
+ )
137
+
138
+ self.versions = {}
139
+ self.performance_stats = true
140
+ self.performance_stats_flush_period = 15
141
+ self.query_stats = true
142
+
143
+ merge(user_config)
144
+ end
145
+
146
+ # The full URL to the Airbrake Notice API. Based on the +:host+ option.
147
+ # @return [URI] the endpoint address
148
+ def endpoint
149
+ @endpoint ||=
150
+ begin
151
+ self.host = ('https://' << host) if host !~ %r{\Ahttps?://}
152
+ api = "api/v3/projects/#{project_id}/notices"
153
+ URI.join(host, api)
154
+ end
155
+ end
156
+
157
+ # Sets the logger. Never allows to assign `nil` as the logger.
158
+ # @return [Logger] the logger
159
+ def logger=(logger)
160
+ @logger = logger || @logger
161
+ end
162
+
163
+ # Merges the given +config_hash+ with itself.
164
+ #
165
+ # @example
166
+ # config.merge(host: 'localhost:8080')
167
+ #
168
+ # @return [self] the merged config
169
+ def merge(config_hash)
170
+ config_hash.each_pair { |option, value| set_option(option, value) }
171
+ self
172
+ end
173
+
174
+ # @return [Boolean] true if the config meets the requirements, false
175
+ # otherwise
176
+ def valid?
177
+ validate.resolved?
178
+ end
179
+
180
+ # @return [Promise]
181
+ # @see Validator.validate
182
+ def validate
183
+ Validator.validate(self)
184
+ end
185
+
186
+ # @return [Promise]
187
+ # @see Validator.check_notify_ability
188
+ def check_notify_ability
189
+ Validator.check_notify_ability(self)
190
+ end
191
+
192
+ # @return [Boolean] true if the config ignores current environment, false
193
+ # otherwise
194
+ def ignored_environment?
195
+ check_notify_ability.rejected?
196
+ end
197
+
198
+ # @return [Promise] resolved promise if config is valid & can notify,
199
+ # rejected otherwise
200
+ def check_configuration
201
+ promise = validate
202
+ return promise if promise.rejected?
203
+
204
+ check_notify_ability
205
+ end
206
+
207
+ # @return [Promise] resolved promise if neither of the performance options
208
+ # reject it, false otherwise
209
+ def check_performance_options(resource)
210
+ promise = Airbrake::Promise.new
211
+
212
+ if !performance_stats
213
+ promise.reject("The Performance Stats feature is disabled")
214
+ elsif resource.is_a?(Airbrake::Query) && !query_stats
215
+ promise.reject("The Query Stats feature is disabled")
216
+ else
217
+ promise
218
+ end
219
+ end
220
+
221
+ private
222
+
223
+ def set_option(option, value)
224
+ __send__("#{option}=", value)
225
+ rescue NoMethodError
226
+ raise Airbrake::Error, "unknown option '#{option}'"
227
+ end
228
+ end
229
+ end