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