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 +7 -0
- data/lib/airbrake-ruby.rb +292 -0
- data/lib/airbrake-ruby/async_sender.rb +90 -0
- data/lib/airbrake-ruby/backtrace.rb +75 -0
- data/lib/airbrake-ruby/config.rb +120 -0
- data/lib/airbrake-ruby/filter_chain.rb +86 -0
- data/lib/airbrake-ruby/filters.rb +10 -0
- data/lib/airbrake-ruby/filters/keys_blacklist.rb +37 -0
- data/lib/airbrake-ruby/filters/keys_filter.rb +65 -0
- data/lib/airbrake-ruby/filters/keys_whitelist.rb +37 -0
- data/lib/airbrake-ruby/notice.rb +207 -0
- data/lib/airbrake-ruby/notifier.rb +145 -0
- data/lib/airbrake-ruby/payload_truncator.rb +141 -0
- data/lib/airbrake-ruby/response.rb +53 -0
- data/lib/airbrake-ruby/sync_sender.rb +76 -0
- data/lib/airbrake-ruby/version.rb +7 -0
- data/spec/airbrake_spec.rb +177 -0
- data/spec/async_sender_spec.rb +121 -0
- data/spec/backtrace_spec.rb +77 -0
- data/spec/config_spec.rb +67 -0
- data/spec/filter_chain_spec.rb +157 -0
- data/spec/notice_spec.rb +190 -0
- data/spec/notifier_spec.rb +690 -0
- data/spec/notifier_spec/options_spec.rb +217 -0
- data/spec/payload_truncator_spec.rb +458 -0
- data/spec/spec_helper.rb +98 -0
- metadata +158 -0
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
|