airbrake-ruby 1.0.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
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