airbrake-ruby 1.0.0.rc.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 92ce605f767bda511e878bfb2f3c693251573d79
4
+ data.tar.gz: 996a67010b00cd651a22921befd7c1d353d1eff2
5
+ SHA512:
6
+ metadata.gz: cef747483b2c451ee12df8221bf49da0cb8faf8ccf279929be286d2070f5c6993fefdf3e82b487ba2909599f40d4baacd94db39d2e95dc8b0caeb0ea93a5cc3c
7
+ data.tar.gz: 4b43779c5de8e1998760906e9816478ac71c697161d9054068b277e33bc5d831779b0b60638f3a389cb9e96482aebfa826b1083e08d64214b10d0a42854174a4
@@ -0,0 +1,292 @@
1
+ require 'net/https'
2
+ require 'logger'
3
+ require 'json'
4
+ require 'thread'
5
+ require 'set'
6
+ require 'English'
7
+
8
+ require 'airbrake-ruby/version'
9
+ require 'airbrake-ruby/config'
10
+ require 'airbrake-ruby/sync_sender'
11
+ require 'airbrake-ruby/async_sender'
12
+ require 'airbrake-ruby/response'
13
+ require 'airbrake-ruby/notice'
14
+ require 'airbrake-ruby/backtrace'
15
+ require 'airbrake-ruby/filter_chain'
16
+ require 'airbrake-ruby/payload_truncator'
17
+ require 'airbrake-ruby/filters'
18
+ require 'airbrake-ruby/filters/keys_filter'
19
+ require 'airbrake-ruby/filters/keys_whitelist'
20
+ require 'airbrake-ruby/filters/keys_blacklist'
21
+ require 'airbrake-ruby/notifier'
22
+
23
+ ##
24
+ # This module defines the Airbrake API. The user is meant to interact with
25
+ # Airbrake via its public class methods. Before using the library, you must to
26
+ # {configure} the default notifier.
27
+ #
28
+ # The module supports multiple notifiers, each of which can be configured
29
+ # differently. By default, every method is invoked in context of the default
30
+ # notifier. To use a different notifier, you need to {configure} it first and
31
+ # pass the notifier's name as the last argument of the method you're calling.
32
+ #
33
+ # You can have as many notifiers as you want, but they must have unique names.
34
+ #
35
+ # @example Configuring multiple notifiers and using them
36
+ # # Configure the default notifier.
37
+ # Airbrake.configure do |c|
38
+ # c.project_id = 113743
39
+ # c.project_key = 'fd04e13d806a90f96614ad8e529b2822'
40
+ # end
41
+ #
42
+ # # Configure a named notifier.
43
+ # Airbrake.configure(:my_other_project) do |c|
44
+ # c.project_id = 224854
45
+ # c.project_key = '91ac5e4a37496026c6837f63276ed2b6'
46
+ # end
47
+ #
48
+ # # Send an exception via the default notifier.
49
+ # Airbrake.notify('Oops!')
50
+ #
51
+ # # Send an exception via other configured notifier.
52
+ # params = {}
53
+ # Airbrake.notify('Oops', params, :my_other_project)
54
+ #
55
+ # @see Airbrake::Notifier
56
+ module Airbrake
57
+ ##
58
+ # The general error that this library uses when it wants to raise.
59
+ Error = Class.new(StandardError)
60
+
61
+ ##
62
+ # @return [String] the label to be prepended to the log output
63
+ LOG_LABEL = '**Airbrake:'.freeze
64
+
65
+ ##
66
+ # A Hash that holds all notifiers. The keys of the Hash are notifier
67
+ # names, the values are Airbrake::Notifier instances.
68
+ @notifiers = {}
69
+
70
+ class << self
71
+ ##
72
+ # Configures a new +notifier+ with the given name. If the name is not given,
73
+ # configures the default notifier.
74
+ #
75
+ # @example Configuring the default notifier
76
+ # Airbrake.configure do |c|
77
+ # c.project_id = 113743
78
+ # c.project_key = 'fd04e13d806a90f96614ad8e529b2822'
79
+ # end
80
+ #
81
+ # @example Configuring a named notifier
82
+ # # Configure a new Airbrake instance and
83
+ # # assign +:my_other_project+ as its name.
84
+ # Airbrake.configure(:my_other_project) do |c|
85
+ # c.project_id = 224854
86
+ # c.project_key = '91ac5e4a37496026c6837f63276ed2b6'
87
+ # end
88
+ #
89
+ # @param [Symbol] notifier the name to be associated with the notifier
90
+ # @yield [config] The configuration object
91
+ # @yieldparam config [Airbrake::Config]
92
+ # @return [void]
93
+ # @raise [Airbrake::Error] when trying to reconfigure already
94
+ # existing notifier
95
+ # @note There's no way to reconfigure a notifier
96
+ # @note There's no way to read config values outside of this library
97
+ def configure(notifier = :default)
98
+ yield config = Airbrake::Config.new
99
+
100
+ if @notifiers.key?(notifier)
101
+ raise Airbrake::Error,
102
+ "the '#{notifier}' notifier was already configured"
103
+ else
104
+ @notifiers[notifier] = Notifier.new(config)
105
+ end
106
+ end
107
+
108
+ # @!macro proxy_method
109
+ # @param [Symbol] notifier The name of the notifier
110
+ # @raise [Airbrake::Error] if +notifier+ doesn't exist
111
+ # @see Airbrake::Notifier#$0
112
+
113
+ ##
114
+ # Sends an exception to Airbrake asynchronously.
115
+ #
116
+ # @macro proxy_method
117
+ # @example Sending an exception
118
+ # Airbrake.notify(RuntimeError.new('Oops!'))
119
+ # @example Sending a string
120
+ # # Converted to RuntimeError.new('Oops!') internally
121
+ # Airbrake.notify('Oops!')
122
+ # @example Sending a Notice
123
+ # notice = airbrake.build_notice(RuntimeError.new('Oops!'))
124
+ # airbrake.notify(notice)
125
+ #
126
+ # @param [Exception, String, Airbrake::Notice] exception The exception to be
127
+ # sent to Airbrake
128
+ # @param [Hash] params The additional payload to be sent to Airbrake. Can
129
+ # contain any values. The provided values will be displayed in the Params
130
+ # tab in your project's dashboard
131
+ # @return [nil]
132
+ # @see .notify_sync
133
+ def notify(exception, params = {}, notifier = :default)
134
+ call_notifier(notifier, __method__, exception, params)
135
+ end
136
+
137
+ ##
138
+ # Sends an exception to Airbrake synchronously.
139
+ #
140
+ # @macro proxy_method
141
+ # @example
142
+ # Airbrake.notify_sync('App crashed!')
143
+ # #=> {"id"=>"123", "url"=>"https://airbrake.io/locate/321"}
144
+ #
145
+ # @return [Hash{String=>String}] the reponse from the server
146
+ # @see .notify
147
+ # @since v5.0.0
148
+ def notify_sync(exception, params = {}, notifier = :default)
149
+ call_notifier(notifier, __method__, exception, params)
150
+ end
151
+
152
+ ##
153
+ # Runs a callback before {.notify} or {.notify_sync} kicks in. This is
154
+ # useful if you want to ignore specific notices or filter the data the
155
+ # notice contains.
156
+ #
157
+ # @macro proxy_method
158
+ # @example Ignore all notices
159
+ # Airbrake.add_filter(&:ignore!)
160
+ # @example Ignore based on some condition
161
+ # Airbrake.add_filter do |notice|
162
+ # notice.ignore! if notice[:error_class] == 'StandardError'
163
+ # end
164
+ # @example Ignore with help of a class
165
+ # class MyFilter
166
+ # def call(notice)
167
+ # # ...
168
+ # end
169
+ # end
170
+ #
171
+ # Airbrake.add_filter(MyFilter.new)
172
+ #
173
+ # @param [#call] filter The filter object
174
+ # @yield [notice] The notice to filter
175
+ # @yieldparam [Airbrake::Notice]
176
+ # @yieldreturn [void]
177
+ # @return [void]
178
+ # @since v5.0.0
179
+ # @note Once a filter was added, there's no way to delete it
180
+ def add_filter(filter = nil, notifier = :default, &block)
181
+ call_notifier(notifier, __method__, filter, &block)
182
+ end
183
+
184
+ ##
185
+ # Specifies which keys should *not* be filtered. All other keys will be
186
+ # substituted with the +[Filtered]+ label.
187
+ #
188
+ # @macro proxy_method
189
+ # @example
190
+ # Airbrake.whitelist([:email, /user/i, 'account_id'])
191
+ #
192
+ # @param [Array<String, Symbol, Regexp>] keys The keys, which shouldn't be
193
+ # filtered
194
+ # @return [void]
195
+ # @since v5.0.0
196
+ # @see .blacklist_keys
197
+ def whitelist_keys(keys, notifier = :default)
198
+ call_notifier(notifier, __method__, keys)
199
+ end
200
+
201
+ ##
202
+ # Specifies which keys *should* be filtered. Such keys will be replaced with
203
+ # the +[Filtered]+ label.
204
+ #
205
+ # @macro proxy_method
206
+ # @example
207
+ # Airbrake.blacklist_keys([:email, /credit/i, 'password'])
208
+ #
209
+ # @param [Array<String, Symbol, Regexp>] keys The keys, which should be
210
+ # filtered
211
+ # @return [void]
212
+ # @since v5.0.0
213
+ # @see .whitelist_keys
214
+ def blacklist_keys(keys, notifier = :default)
215
+ call_notifier(notifier, __method__, keys)
216
+ end
217
+
218
+ ##
219
+ # Builds an Airbrake notice. This is useful, if you want to add or modify a
220
+ # value only for a specific notice. When you're done modifying the notice,
221
+ # send it with {.notify} or {.notify_sync}.
222
+ #
223
+ # @macro proxy_method
224
+ # @example
225
+ # notice = airbrake.build_notice('App crashed!')
226
+ # notice[:params][:username] = user.name
227
+ # airbrake.notify_sync(notice)
228
+ #
229
+ # @param [Exception] exception The exception on top of which the notice
230
+ # should be built
231
+ # @param [Hash] params The additional params attached to the notice
232
+ # @return [Airbrake::Notice] the notice built with help of the given
233
+ # arguments
234
+ # @since v5.0.0
235
+ def build_notice(exception, params = {}, notifier = :default)
236
+ call_notifier(notifier, __method__, exception, params)
237
+ end
238
+
239
+ ##
240
+ # Makes the notifier a no-op, which means you cannot use the {.notify} and
241
+ # {.notify_sync} methods anymore. It also stops the notifier's worker
242
+ # threads.
243
+ #
244
+ # @macro proxy_method
245
+ # @example
246
+ # Airbrake.close
247
+ # Airbrake.notify('App crashed!') #=> raises Airbrake::Error
248
+ #
249
+ # @return [void]
250
+ # @since v5.0.0
251
+ def close(notifier = :default)
252
+ call_notifier(notifier, __method__)
253
+ end
254
+
255
+ ##
256
+ # Pings the Airbrake Deploy API endpoint about the occurred deploy. This
257
+ # method is used by the airbrake gem for various integrations.
258
+ #
259
+ # @macro proxy_method
260
+ # @param [Hash{Symbol=>String}] deploy_params The params for the API
261
+ # @option deploy_params [Symbol] :environment
262
+ # @option deploy_params [Symbol] :username
263
+ # @option deploy_params [Symbol] :repository
264
+ # @option deploy_params [Symbol] :revision
265
+ # @option deploy_params [Symbol] :version
266
+ # @since v5.0.0
267
+ # @api private
268
+ def create_deploy(deploy_params, notifier = :default)
269
+ call_notifier(notifier, __method__, deploy_params)
270
+ end
271
+
272
+ private
273
+
274
+ ##
275
+ # Calls +method+ on +notifier+ with provided +args+.
276
+ #
277
+ # @raise [Airbrake::Error] if none of the notifiers exist
278
+ def call_notifier(notifier, method, *args)
279
+ if @notifiers.key?(notifier)
280
+ @notifiers[notifier].__send__(method, *args)
281
+ else
282
+ raise Airbrake::Error,
283
+ "the '#{notifier}' notifier isn't configured"
284
+ end
285
+ end
286
+ end
287
+ end
288
+
289
+ # Notify of unhandled exceptions, if there were any.
290
+ at_exit do
291
+ Airbrake.notify_sync($ERROR_INFO) if $ERROR_INFO
292
+ end
@@ -0,0 +1,90 @@
1
+ module Airbrake
2
+ ##
3
+ # Responsible for sending notices to Airbrake asynchronously. The class
4
+ # supports an unlimited number of worker threads and an unlimited queue size
5
+ # (both values are configurable).
6
+ #
7
+ # @see SyncSender
8
+ class AsyncSender
9
+ ##
10
+ # @param [Airbrake::Config] config
11
+ def initialize(config)
12
+ @config = config
13
+ @unsent = SizedQueue.new(config.queue_size)
14
+ @sender = SyncSender.new(config)
15
+ @closed = false
16
+ @workers = ThreadGroup.new
17
+
18
+ (0...config.workers).each { @workers.add(spawn_worker) }
19
+ @workers.enclose
20
+ end
21
+
22
+ ##
23
+ # Asynchronously sends a notice to Airbrake.
24
+ #
25
+ # @param [Airbrake::Notice] notice A notice that was generated by the
26
+ # library
27
+ # @return [nil]
28
+ def send(notice)
29
+ @unsent << notice
30
+ nil
31
+ end
32
+
33
+ ##
34
+ # Closes the instance making it a no-op (it shut downs all worker
35
+ # threads). Before closing, waits on all unsent notices to be sent.
36
+ #
37
+ # @return [void]
38
+ # @raise [Airbrake::Error] when invoked more than one time
39
+ def close
40
+ if closed?
41
+ raise Airbrake::Error, 'attempted to close already closed sender'
42
+ end
43
+
44
+ unless @unsent.empty?
45
+ msg = "#{LOG_LABEL} waiting to send #{@unsent.size} unsent notice(s)..."
46
+ @config.logger.debug(msg + ' (Ctrl-C to abort)')
47
+ end
48
+
49
+ @config.workers.times { @unsent << :stop }
50
+ @workers.list.each(&:join)
51
+ @closed = true
52
+
53
+ @config.logger.debug("#{LOG_LABEL} closed")
54
+ end
55
+
56
+ ##
57
+ # Checks whether the sender is closed and thus usable.
58
+ # @return [Boolean]
59
+ def closed?
60
+ @closed
61
+ end
62
+
63
+ ##
64
+ # Checks if an active sender has any workers. A sender doesn't have any
65
+ # workers only in two cases: when it was closed or when all workers
66
+ # crashed. An *active* sender doesn't have any workers only when something
67
+ # went wrong.
68
+ #
69
+ # Workers are expected to crash when you +fork+ the process the workers are
70
+ # living in. Another possible scenario is when you close the instance on
71
+ # +at_exit+, but some other +at_exit+ hook prevents the process from
72
+ # exiting.
73
+ #
74
+ # @return [Boolean] true if an instance wasn't closed, but has no workers
75
+ # @see https://goo.gl/oydz8h Example of at_exit that prevents exit
76
+ def has_workers?
77
+ !@closed && @workers.list.any?
78
+ end
79
+
80
+ private
81
+
82
+ def spawn_worker
83
+ Thread.new do
84
+ while (notice = @unsent.pop) != :stop
85
+ @sender.send(notice)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,75 @@
1
+ module Airbrake
2
+ ##
3
+ # Represents a cross-Ruby backtrace from exceptions (including JRuby Java
4
+ # exceptions). Provides information about stack frames (such as line number,
5
+ # file and method) in convenient for Airbrake format.
6
+ #
7
+ # @example
8
+ # begin
9
+ # raise 'Oops!'
10
+ # rescue
11
+ # Backtrace.parse($!)
12
+ # end
13
+ module Backtrace
14
+ ##
15
+ # @return [Regexp] the pattern that matches standard Ruby stack frames,
16
+ # such as ./spec/notice_spec.rb:43:in `block (3 levels) in <top (required)>'
17
+ STACKFRAME_REGEXP = %r{\A
18
+ (?<file>.+) # Matches './spec/notice_spec.rb'
19
+ :
20
+ (?<line>\d+) # Matches '43'
21
+ :in\s
22
+ `(?<function>.+)' # Matches "`block (3 levels) in <top (required)>'"
23
+ \z}x
24
+
25
+ ##
26
+ # @return [Regexp] the template that matches JRuby Java stack frames, such
27
+ # as org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
28
+ JAVA_STACKFRAME_REGEXP = /\A
29
+ (?<function>.+) # Matches 'org.jruby.ast.NewlineNode.interpret
30
+ \(
31
+ (?<file>[^:]+) # Matches 'NewlineNode.java'
32
+ :?
33
+ (?<line>\d+)? # Matches '105'
34
+ \)
35
+ \z/x
36
+
37
+ ##
38
+ # Parses an exception's backtrace.
39
+ #
40
+ # @param [Exception] exception The exception, which contains a backtrace to
41
+ # parse
42
+ # @return [Array<Hash{Symbol=>String,Integer}>] the parsed backtrace
43
+ def self.parse(exception)
44
+ regexp = if java_exception?(exception)
45
+ JAVA_STACKFRAME_REGEXP
46
+ else
47
+ STACKFRAME_REGEXP
48
+ end
49
+
50
+ (exception.backtrace || []).map do |stackframe|
51
+ stack_frame(regexp.match(stackframe))
52
+ end
53
+ end
54
+
55
+ ##
56
+ # Checks whether the given exception was generated by JRuby's VM.
57
+ #
58
+ # @param [Exception] exception
59
+ # @return [Boolean]
60
+ def self.java_exception?(exception)
61
+ defined?(Java::JavaLang::Throwable) &&
62
+ exception.is_a?(Java::JavaLang::Throwable)
63
+ end
64
+
65
+ class << self
66
+ private
67
+
68
+ def stack_frame(match)
69
+ { file: match[:file],
70
+ line: (Integer(match[:line]) if match[:line]),
71
+ function: match[:function] }
72
+ end
73
+ end
74
+ end
75
+ end