honeybadger 6.9.2 → 6.10.0
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 +4 -4
- data/CHANGELOG.md +18 -0
- data/lib/honeybadger/agent.rb +6 -1
- data/lib/honeybadger/backend/base.rb +36 -1
- data/lib/honeybadger/config/defaults.rb +1 -1
- data/lib/honeybadger/events_worker.rb +85 -79
- data/lib/honeybadger/init/rails.rb +8 -1
- data/lib/honeybadger/metrics_worker.rb +18 -13
- data/lib/honeybadger/rack/request_id.rb +27 -0
- data/lib/honeybadger/version.rb +1 -1
- data/lib/honeybadger/worker.rb +68 -67
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0a8b90189275576b1cb4bca4f7ee77db0fe316415a03239cd61ce1585deddb42
|
|
4
|
+
data.tar.gz: b5c7cdbcfd8a655009872a9b3e02f7aec6917684172fc5b270c8cf559b46f52f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 666daade7949f2d486498eda33d6f74fb411a1745081e212aeb838b1cb9dd72ad9125d3631b6d0a6fe4e225ab251ead2a7d5c4e41fa00ba093435914f671861e
|
|
7
|
+
data.tar.gz: 7c24c28bf5184bb79d7c1e87ce7ba1f36ffff14863b8cc50582cf8b01cc5e07a220ae51f0f4d41df384ffc88dafcce797ee03a54eaf0df3563e6dab9396e0070
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## [6.10.0](https://github.com/honeybadger-io/honeybadger-ruby/compare/v6.9.3...v6.10.0) (2026-10-20)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
* retry notices and events on short Retry-After instead of dropping them ([#847](https://github.com/honeybadger-io/honeybadger-ruby/issues/847)) ([bb948b3](https://github.com/honeybadger-io/honeybadger-ruby/commit/bb948b309f2dcc6c01c052f8ea3519c5a68187ac))
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
* Default `events.max_queue_size` has been reduced from 100,000 to 10,000; raise it if your app sends large bursts of events ([#847](https://github.com/honeybadger-io/honeybadger-ruby/issues/847))
|
|
14
|
+
|
|
15
|
+
## [6.9.3](https://github.com/honeybadger-io/honeybadger-ruby/compare/v6.9.2...v6.9.3) (2026-09-29)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* propagate the Rails request ID to notices and Insights events ([#848](https://github.com/honeybadger-io/honeybadger-ruby/issues/848)) ([23edb08](https://github.com/honeybadger-io/honeybadger-ruby/commit/23edb08f4fda3b3f66fb697c5ec5502f39aeb127))
|
|
21
|
+
|
|
4
22
|
## [6.9.2](https://github.com/honeybadger-io/honeybadger-ruby/compare/v6.9.1...v6.9.2) (2026-09-15)
|
|
5
23
|
|
|
6
24
|
|
data/lib/honeybadger/agent.rb
CHANGED
|
@@ -579,13 +579,18 @@ module Honeybadger
|
|
|
579
579
|
# @api private
|
|
580
580
|
def with_rack_env(rack_env, &block)
|
|
581
581
|
context_manager.set_rack_env(rack_env)
|
|
582
|
-
context_manager.set_request_id(
|
|
582
|
+
context_manager.set_request_id(SecureRandom.uuid)
|
|
583
583
|
yield
|
|
584
584
|
ensure
|
|
585
585
|
context_manager.set_rack_env(nil)
|
|
586
586
|
context_manager.set_request_id(nil)
|
|
587
587
|
end
|
|
588
588
|
|
|
589
|
+
# @api private
|
|
590
|
+
def set_request_id(request_id)
|
|
591
|
+
context_manager.set_request_id(request_id)
|
|
592
|
+
end
|
|
593
|
+
|
|
589
594
|
# @api private
|
|
590
595
|
attr_reader :worker, :events_worker, :metrics_worker
|
|
591
596
|
|
|
@@ -9,7 +9,10 @@ module Honeybadger
|
|
|
9
9
|
class Response
|
|
10
10
|
NOT_BLANK = /\S/
|
|
11
11
|
|
|
12
|
-
attr_reader :code, :body, :message, :error
|
|
12
|
+
attr_reader :code, :body, :message, :error, :retry_after, :limit_reason
|
|
13
|
+
|
|
14
|
+
# Longest Retry-After, in seconds, accepted for a single retry delay.
|
|
15
|
+
MAX_RETRY_AFTER = 60
|
|
13
16
|
|
|
14
17
|
FRIENDLY_ERRORS = {
|
|
15
18
|
429 => "Your project is currently sending too many errors.\nThis issue should resolve itself once error traffic is reduced.".freeze,
|
|
@@ -36,6 +39,9 @@ module Honeybadger
|
|
|
36
39
|
def initialize(*args)
|
|
37
40
|
if (response = args.first).is_a?(Net::HTTPResponse)
|
|
38
41
|
@code, @body, @message = response.code.to_i, response.body.to_s, response.message
|
|
42
|
+
@retry_after = parse_retry_after(response["Retry-After"])
|
|
43
|
+
# Why a 429 was sent, e.g. "overloaded".
|
|
44
|
+
@limit_reason = response["Honeybadger-Limit-Reason"]&.strip&.downcase
|
|
39
45
|
else
|
|
40
46
|
@code, @body, @message = args
|
|
41
47
|
end
|
|
@@ -48,6 +54,26 @@ module Honeybadger
|
|
|
48
54
|
@success
|
|
49
55
|
end
|
|
50
56
|
|
|
57
|
+
# The server is briefly unavailable (502/503/504, or 429 with
|
|
58
|
+
# Limit-Reason "overloaded") and Retry-After is absent or at most
|
|
59
|
+
# MAX_RETRY_AFTER. Hold the payload and retry it. Any other 429 means
|
|
60
|
+
# the server is rejecting data.
|
|
61
|
+
def retryable?
|
|
62
|
+
return false if retry_after && retry_after > MAX_RETRY_AFTER
|
|
63
|
+
[502, 503, 504].include?(code) || (code == 429 && limit_reason == "overloaded")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Seconds to drop queued data and reject new data, or nil if this isn't
|
|
67
|
+
# a rejection.
|
|
68
|
+
def reject_interval
|
|
69
|
+
return if retryable?
|
|
70
|
+
case code
|
|
71
|
+
when 429 then retry_after || 60
|
|
72
|
+
when 502, 503, 504 then retry_after
|
|
73
|
+
when 401, 403 then retry_after || 3600
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
51
77
|
def error_message
|
|
52
78
|
return message if code == :error
|
|
53
79
|
return error if code == 403 && NOT_BLANK.match?(error)
|
|
@@ -60,6 +86,15 @@ module Honeybadger
|
|
|
60
86
|
|
|
61
87
|
private
|
|
62
88
|
|
|
89
|
+
# Positive integer seconds only. Zero, negative, HTTP-date, and other
|
|
90
|
+
# values are treated as absent; Honeybadger only sends seconds.
|
|
91
|
+
def parse_retry_after(value)
|
|
92
|
+
return nil unless value
|
|
93
|
+
return nil unless /\A\d+\z/.match?(value.strip)
|
|
94
|
+
seconds = value.to_i
|
|
95
|
+
(seconds > 0) ? seconds : nil
|
|
96
|
+
end
|
|
97
|
+
|
|
63
98
|
def parse_error(body)
|
|
64
99
|
return unless NOT_BLANK.match?(body)
|
|
65
100
|
obj = JSON.parse(body)
|
|
@@ -16,25 +16,21 @@ module Honeybadger
|
|
|
16
16
|
|
|
17
17
|
# Used to signal the worker to shutdown.
|
|
18
18
|
SHUTDOWN = :__hb_worker_shutdown!
|
|
19
|
-
FLUSH = :__hb_worker_flush!
|
|
20
19
|
CHECK_TIMEOUT = :__hb_worker_check_timeout!
|
|
21
20
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
BASE_THROTTLE = 1.05
|
|
21
|
+
# Number of times a batch is resent while the server is unavailable
|
|
22
|
+
# (6 attempts in total). Each batch gets its own retries.
|
|
23
|
+
MAX_RETRIES = 5
|
|
26
24
|
|
|
27
25
|
# TODO: These could be configurable?
|
|
28
26
|
|
|
29
27
|
def initialize(config)
|
|
30
28
|
@config = config
|
|
31
|
-
@throttle = 0
|
|
32
|
-
@throttle_interval = 0
|
|
33
29
|
@mutex = Mutex.new
|
|
34
|
-
@
|
|
30
|
+
@flush_signal = ConditionVariable.new
|
|
35
31
|
@queue = Queue.new
|
|
36
|
-
@send_queue = Queue.new
|
|
37
32
|
@shutdown = false
|
|
33
|
+
@flushes = 0
|
|
38
34
|
@start_at = nil
|
|
39
35
|
@pid = Process.pid
|
|
40
36
|
@send_queue = []
|
|
@@ -67,13 +63,7 @@ module Honeybadger
|
|
|
67
63
|
return true if force
|
|
68
64
|
return true unless thread&.alive?
|
|
69
65
|
|
|
70
|
-
if throttled?
|
|
71
|
-
warn { sprintf("Unable to send %s event(s) to Honeybadger (currently throttled)", queue.size) } unless queue.empty?
|
|
72
|
-
return true
|
|
73
|
-
end
|
|
74
|
-
|
|
75
66
|
info { sprintf("Waiting to send %s events(s) to Honeybadger", queue.size) } unless queue.empty?
|
|
76
|
-
queue.push(FLUSH)
|
|
77
67
|
queue.push(SHUTDOWN)
|
|
78
68
|
!!thread.join
|
|
79
69
|
ensure
|
|
@@ -81,13 +71,18 @@ module Honeybadger
|
|
|
81
71
|
kill!
|
|
82
72
|
end
|
|
83
73
|
|
|
84
|
-
# Blocks until queue is processed up to this point in time.
|
|
74
|
+
# Blocks until queue is processed up to this point in time. Retries are
|
|
75
|
+
# skipped while a flush is pending, since the caller is waiting.
|
|
85
76
|
def flush
|
|
86
77
|
mutex.synchronize do
|
|
87
78
|
if thread&.alive?
|
|
88
|
-
|
|
89
|
-
queue.push(
|
|
90
|
-
|
|
79
|
+
@flushes += 1
|
|
80
|
+
queue.push(flush_signal)
|
|
81
|
+
begin
|
|
82
|
+
flush_signal.wait(mutex)
|
|
83
|
+
ensure
|
|
84
|
+
@flushes -= 1
|
|
85
|
+
end
|
|
91
86
|
end
|
|
92
87
|
end
|
|
93
88
|
end
|
|
@@ -111,8 +106,8 @@ module Honeybadger
|
|
|
111
106
|
|
|
112
107
|
private
|
|
113
108
|
|
|
114
|
-
attr_reader :config, :queue, :pid, :mutex, :
|
|
115
|
-
:
|
|
109
|
+
attr_reader :config, :queue, :pid, :mutex, :flush_signal, :thread, :timeout_thread,
|
|
110
|
+
:start_at, :send_queue, :last_sent
|
|
116
111
|
|
|
117
112
|
def_delegator :config, :backend
|
|
118
113
|
|
|
@@ -120,6 +115,10 @@ module Honeybadger
|
|
|
120
115
|
mutex.synchronize { @shutdown }
|
|
121
116
|
end
|
|
122
117
|
|
|
118
|
+
def flushing?
|
|
119
|
+
mutex.synchronize { @flushes > 0 }
|
|
120
|
+
end
|
|
121
|
+
|
|
123
122
|
def suspended?
|
|
124
123
|
mutex.synchronize { start_at && Time.now.to_i < start_at }
|
|
125
124
|
end
|
|
@@ -130,10 +129,6 @@ module Honeybadger
|
|
|
130
129
|
true
|
|
131
130
|
end
|
|
132
131
|
|
|
133
|
-
def throttled?
|
|
134
|
-
mutex.synchronize { throttle > 0 }
|
|
135
|
-
end
|
|
136
|
-
|
|
137
132
|
def kill!
|
|
138
133
|
d { "killing worker thread" }
|
|
139
134
|
|
|
@@ -150,10 +145,18 @@ module Honeybadger
|
|
|
150
145
|
true
|
|
151
146
|
end
|
|
152
147
|
|
|
148
|
+
# Drops queued and pending events and rejects new ones (push returns
|
|
149
|
+
# false) for +interval+ seconds. The thread restarts on the first push after that.
|
|
153
150
|
def suspend(interval)
|
|
151
|
+
warn { sprintf("Dropping queued events and rejecting new ones for %ss", interval) }
|
|
154
152
|
mutex.synchronize do
|
|
155
153
|
@start_at = Time.now.to_i + interval
|
|
156
154
|
queue.clear
|
|
155
|
+
send_queue.clear
|
|
156
|
+
if @dropped_events > 0
|
|
157
|
+
warn { sprintf("Dropped %s messages due to exceeding max queue size of %s", @dropped_events, config.events_max_queue_size) }
|
|
158
|
+
end
|
|
159
|
+
@dropped_events = 0
|
|
157
160
|
end
|
|
158
161
|
|
|
159
162
|
# Must be performed last since this may kill the current thread.
|
|
@@ -175,10 +178,13 @@ module Honeybadger
|
|
|
175
178
|
end
|
|
176
179
|
loop do
|
|
177
180
|
case msg = queue.pop
|
|
178
|
-
when SHUTDOWN
|
|
181
|
+
when SHUTDOWN
|
|
182
|
+
send_pending_batch
|
|
183
|
+
break
|
|
179
184
|
when CHECK_TIMEOUT then check_timeout
|
|
180
|
-
when
|
|
181
|
-
|
|
185
|
+
when flush_signal
|
|
186
|
+
send_pending_batch
|
|
187
|
+
signal_flush_done
|
|
182
188
|
else work(msg)
|
|
183
189
|
end
|
|
184
190
|
end
|
|
@@ -191,7 +197,7 @@ module Honeybadger
|
|
|
191
197
|
sprintf(msg, e.class, e.message.dump, Array(e.backtrace).join("\n\t"))
|
|
192
198
|
}
|
|
193
199
|
ensure
|
|
194
|
-
|
|
200
|
+
release_flush_waiters
|
|
195
201
|
end
|
|
196
202
|
|
|
197
203
|
def check_timeout
|
|
@@ -209,16 +215,35 @@ module Honeybadger
|
|
|
209
215
|
end
|
|
210
216
|
|
|
211
217
|
def send_batch
|
|
212
|
-
|
|
218
|
+
# batch is send_queue itself; clearing send_queue also clears batch.
|
|
219
|
+
batch = mutex.synchronize { send_queue }
|
|
220
|
+
response = send_to_backend(batch)
|
|
221
|
+
MAX_RETRIES.times do
|
|
222
|
+
break unless response.retryable?
|
|
223
|
+
break unless wait(response.retry_after || rand(5..15))
|
|
224
|
+
response = send_to_backend(batch)
|
|
225
|
+
end
|
|
226
|
+
handle_response(response)
|
|
227
|
+
if (interval = response.reject_interval)
|
|
228
|
+
suspend(interval) # clears the batch and terminates this thread
|
|
229
|
+
end
|
|
230
|
+
|
|
213
231
|
mutex.synchronize do
|
|
214
232
|
@last_sent = Time.now
|
|
215
|
-
debug { sprintf("
|
|
233
|
+
debug { sprintf("Finished attempting to send %s events", send_queue.length) }
|
|
216
234
|
send_queue.clear
|
|
217
235
|
if @dropped_events > 0
|
|
218
236
|
warn { sprintf("Dropped %s messages due to exceeding max queue size of %s", @dropped_events, config.events_max_queue_size) }
|
|
219
237
|
end
|
|
220
238
|
@dropped_events = 0
|
|
221
239
|
end
|
|
240
|
+
|
|
241
|
+
# Shutting down while the server is unavailable: stop without sending
|
|
242
|
+
# the rest of the queue.
|
|
243
|
+
if shutdown? && response.retryable?
|
|
244
|
+
warn { "Shutting down while the server is unavailable; dropping queued events" }
|
|
245
|
+
kill!
|
|
246
|
+
end
|
|
222
247
|
end
|
|
223
248
|
|
|
224
249
|
def check_and_send
|
|
@@ -228,7 +253,7 @@ module Honeybadger
|
|
|
228
253
|
end
|
|
229
254
|
end
|
|
230
255
|
|
|
231
|
-
def
|
|
256
|
+
def send_pending_batch
|
|
232
257
|
return if mutex.synchronize { send_queue.empty? }
|
|
233
258
|
send_batch
|
|
234
259
|
rescue => e
|
|
@@ -241,14 +266,6 @@ module Honeybadger
|
|
|
241
266
|
def work(msg)
|
|
242
267
|
enqueue_msg(msg)
|
|
243
268
|
check_and_send
|
|
244
|
-
|
|
245
|
-
if shutdown? && throttled?
|
|
246
|
-
warn { sprintf("Unable to send %s events(s) to Honeybadger (currently throttled)", queue.size) } if queue.size > 1
|
|
247
|
-
kill!
|
|
248
|
-
return
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
sleep(throttle_interval)
|
|
252
269
|
rescue => e
|
|
253
270
|
error {
|
|
254
271
|
msg = "Error in worker thread class=%s message=%s\n\t%s"
|
|
@@ -261,49 +278,33 @@ module Honeybadger
|
|
|
261
278
|
backend.event(msg)
|
|
262
279
|
end
|
|
263
280
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
@throttle_interval = calc_throttle_interval
|
|
272
|
-
throttle
|
|
273
|
-
end
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
def dec_throttle
|
|
277
|
-
mutex.synchronize do
|
|
278
|
-
return nil if throttle == 0
|
|
279
|
-
@throttle -= 1
|
|
280
|
-
@throttle_interval = calc_throttle_interval
|
|
281
|
-
throttle
|
|
281
|
+
# Sleeps in one-second steps so shutdown or a flush can interrupt.
|
|
282
|
+
# Returns false if one did, including during the final second; the
|
|
283
|
+
# batch in hand is then dropped.
|
|
284
|
+
def wait(seconds)
|
|
285
|
+
seconds.times do
|
|
286
|
+
return false if shutdown? || flushing?
|
|
287
|
+
sleep(1)
|
|
282
288
|
end
|
|
289
|
+
!(shutdown? || flushing?)
|
|
283
290
|
end
|
|
284
291
|
|
|
285
292
|
def handle_response(response)
|
|
286
293
|
d { sprintf("events_worker response code=%s message=%s", response.code, response.message.to_s.dump) }
|
|
287
294
|
|
|
288
295
|
case response.code
|
|
289
|
-
when 429, 503
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
when 403
|
|
296
|
+
when 429, 502, 503, 504
|
|
297
|
+
if response.retryable?
|
|
298
|
+
warn { sprintf("Insights Event send failed: server unavailable, dropping this batch. code=%s", response.code) }
|
|
299
|
+
else
|
|
300
|
+
warn { sprintf("Insights Event send failed: server is rejecting data. code=%s reason=%s", response.code, response.limit_reason) }
|
|
301
|
+
end
|
|
302
|
+
when 401, 403
|
|
297
303
|
warn { sprintf("Insights Event send failed: %s code=%s", response.error_message, response.code) }
|
|
298
|
-
suspend(3600)
|
|
299
304
|
when 413
|
|
300
305
|
warn { sprintf("Insights Event send failed: Payload is too large. code=%s", response.code) }
|
|
301
306
|
when 201
|
|
302
|
-
|
|
303
|
-
debug { sprintf("Success ⚡ Insights Event sent code=%s throttle=%s interval=%s", response.code, throttle, throttle_interval) }
|
|
304
|
-
else
|
|
305
|
-
debug { sprintf("Success ⚡ Insights Event sent code=%s", response.code) }
|
|
306
|
-
end
|
|
307
|
+
debug { sprintf("Success ⚡ Insights Event sent code=%s", response.code) }
|
|
307
308
|
when :stubbed
|
|
308
309
|
debug { "Success ⚡ Development mode is enabled; This event will be sent after app is deployed." }
|
|
309
310
|
when :error
|
|
@@ -313,17 +314,22 @@ module Honeybadger
|
|
|
313
314
|
end
|
|
314
315
|
end
|
|
315
316
|
|
|
316
|
-
#
|
|
317
|
-
# down, otherwise
|
|
318
|
-
def
|
|
319
|
-
|
|
317
|
+
# Wake every waiting flush. Important to perform during cleanup when
|
|
318
|
+
# shutting down, otherwise they could end up waiting indefinitely.
|
|
319
|
+
def release_flush_waiters
|
|
320
|
+
if mutex.owned?
|
|
321
|
+
flush_signal.broadcast
|
|
322
|
+
else
|
|
323
|
+
mutex.synchronize { flush_signal.broadcast }
|
|
324
|
+
end
|
|
320
325
|
end
|
|
321
326
|
|
|
322
|
-
|
|
327
|
+
# Wakes the flush waiting on this point in the queue.
|
|
328
|
+
def signal_flush_done
|
|
323
329
|
if mutex.owned?
|
|
324
|
-
|
|
330
|
+
flush_signal.signal
|
|
325
331
|
else
|
|
326
|
-
mutex.synchronize {
|
|
332
|
+
mutex.synchronize { flush_signal.signal }
|
|
327
333
|
end
|
|
328
334
|
end
|
|
329
335
|
end
|
|
@@ -2,6 +2,7 @@ require "rails"
|
|
|
2
2
|
require "yaml"
|
|
3
3
|
|
|
4
4
|
require "honeybadger/ruby"
|
|
5
|
+
require "honeybadger/rack/request_id"
|
|
5
6
|
|
|
6
7
|
module Honeybadger
|
|
7
8
|
module Init
|
|
@@ -14,8 +15,14 @@ module Honeybadger
|
|
|
14
15
|
initializer "honeybadger.install_middleware" do |app|
|
|
15
16
|
honeybadger_config = Honeybadger::Agent.instance.config
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
# ErrorNotifier also sets up the request context that Insights events
|
|
19
|
+
# read. Agent#notify does nothing when exceptions are disabled.
|
|
20
|
+
if honeybadger_config[:"exceptions.enabled"] || honeybadger_config[:"insights.enabled"]
|
|
18
21
|
app.config.middleware.insert(0, Honeybadger::Rack::ErrorNotifier)
|
|
22
|
+
app.config.middleware.insert_after(::ActionDispatch::RequestId, Honeybadger::Rack::RequestId)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
if honeybadger_config[:"exceptions.enabled"]
|
|
19
26
|
app.config.middleware.insert_before(Honeybadger::Rack::ErrorNotifier, Honeybadger::Rack::UserInformer) if honeybadger_config[:"user_informer.enabled"]
|
|
20
27
|
app.config.middleware.insert_before(Honeybadger::Rack::ErrorNotifier, Honeybadger::Rack::UserFeedback) if honeybadger_config[:"feedback.enabled"]
|
|
21
28
|
end
|
|
@@ -18,7 +18,7 @@ module Honeybadger
|
|
|
18
18
|
@config = config
|
|
19
19
|
@interval_seconds = 1
|
|
20
20
|
@mutex = Mutex.new
|
|
21
|
-
@
|
|
21
|
+
@flush_signal = ConditionVariable.new
|
|
22
22
|
@queue = Queue.new
|
|
23
23
|
@shutdown = false
|
|
24
24
|
@start_at = nil
|
|
@@ -60,8 +60,8 @@ module Honeybadger
|
|
|
60
60
|
def flush
|
|
61
61
|
mutex.synchronize do
|
|
62
62
|
if thread&.alive?
|
|
63
|
-
queue.push(
|
|
64
|
-
|
|
63
|
+
queue.push(flush_signal)
|
|
64
|
+
flush_signal.wait(mutex)
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
end
|
|
@@ -84,7 +84,7 @@ module Honeybadger
|
|
|
84
84
|
|
|
85
85
|
private
|
|
86
86
|
|
|
87
|
-
attr_reader :config, :queue, :pid, :mutex, :
|
|
87
|
+
attr_reader :config, :queue, :pid, :mutex, :flush_signal, :thread, :interval_seconds, :start_at
|
|
88
88
|
|
|
89
89
|
def shutdown?
|
|
90
90
|
mutex.synchronize { @shutdown }
|
|
@@ -127,7 +127,7 @@ module Honeybadger
|
|
|
127
127
|
loop do
|
|
128
128
|
case msg = queue.pop
|
|
129
129
|
when SHUTDOWN then break
|
|
130
|
-
when
|
|
130
|
+
when flush_signal then signal_flush_done
|
|
131
131
|
else work(msg)
|
|
132
132
|
end
|
|
133
133
|
end
|
|
@@ -140,7 +140,7 @@ module Honeybadger
|
|
|
140
140
|
sprintf(msg, e.class, e.message.dump, Array(e.backtrace).join("\n\t"))
|
|
141
141
|
}
|
|
142
142
|
ensure
|
|
143
|
-
|
|
143
|
+
release_flush_waiters
|
|
144
144
|
end
|
|
145
145
|
|
|
146
146
|
def work(msg)
|
|
@@ -160,17 +160,22 @@ module Honeybadger
|
|
|
160
160
|
sleep(interval_seconds)
|
|
161
161
|
end
|
|
162
162
|
|
|
163
|
-
#
|
|
164
|
-
# down, otherwise
|
|
165
|
-
def
|
|
166
|
-
|
|
163
|
+
# Wake every waiting flush. Important to perform during cleanup when
|
|
164
|
+
# shutting down, otherwise they could end up waiting indefinitely.
|
|
165
|
+
def release_flush_waiters
|
|
166
|
+
if mutex.owned?
|
|
167
|
+
flush_signal.broadcast
|
|
168
|
+
else
|
|
169
|
+
mutex.synchronize { flush_signal.broadcast }
|
|
170
|
+
end
|
|
167
171
|
end
|
|
168
172
|
|
|
169
|
-
|
|
173
|
+
# Wakes the flush waiting on this point in the queue.
|
|
174
|
+
def signal_flush_done
|
|
170
175
|
if mutex.owned?
|
|
171
|
-
|
|
176
|
+
flush_signal.signal
|
|
172
177
|
else
|
|
173
|
-
mutex.synchronize {
|
|
178
|
+
mutex.synchronize { flush_signal.signal }
|
|
174
179
|
end
|
|
175
180
|
end
|
|
176
181
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require "honeybadger/ruby"
|
|
2
|
+
|
|
3
|
+
module Honeybadger
|
|
4
|
+
module Rack
|
|
5
|
+
# @api private
|
|
6
|
+
#
|
|
7
|
+
# Copies the request ID assigned by ActionDispatch::RequestId into the
|
|
8
|
+
# Honeybadger context. Must sit after ActionDispatch::RequestId in the
|
|
9
|
+
# middleware stack.
|
|
10
|
+
#
|
|
11
|
+
# No ensure/cleanup here: ErrorNotifier owns the request lifecycle and
|
|
12
|
+
# clears state after the stack unwinds. Clearing here would nil the ID
|
|
13
|
+
# before ErrorNotifier builds the notice on exception.
|
|
14
|
+
class RequestId
|
|
15
|
+
def initialize(app, agent = nil)
|
|
16
|
+
@app = app
|
|
17
|
+
@agent = agent if agent.is_a?(Agent)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(env)
|
|
21
|
+
request_id = env["action_dispatch.request_id"]
|
|
22
|
+
(@agent || Honeybadger::Agent.instance).set_request_id(request_id) if request_id
|
|
23
|
+
@app.call(env)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/honeybadger/version.rb
CHANGED
data/lib/honeybadger/worker.rb
CHANGED
|
@@ -17,19 +17,17 @@ module Honeybadger
|
|
|
17
17
|
# Used to signal the worker to shutdown.
|
|
18
18
|
SHUTDOWN = :__hb_worker_shutdown!
|
|
19
19
|
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
BASE_THROTTLE = 1.05
|
|
20
|
+
# Number of times a notice is resent while the server is unavailable
|
|
21
|
+
# (6 attempts in total). Each notice gets its own retries.
|
|
22
|
+
MAX_RETRIES = 5
|
|
24
23
|
|
|
25
24
|
def initialize(config)
|
|
26
25
|
@config = config
|
|
27
|
-
@throttle = 0
|
|
28
|
-
@throttle_interval = 0
|
|
29
26
|
@mutex = Mutex.new
|
|
30
|
-
@
|
|
27
|
+
@flush_signal = ConditionVariable.new
|
|
31
28
|
@queue = Queue.new
|
|
32
29
|
@shutdown = false
|
|
30
|
+
@flushes = 0
|
|
33
31
|
@start_at = nil
|
|
34
32
|
@pid = Process.pid
|
|
35
33
|
end
|
|
@@ -45,6 +43,7 @@ module Honeybadger
|
|
|
45
43
|
queue.push(msg)
|
|
46
44
|
end
|
|
47
45
|
|
|
46
|
+
# Sends synchronously without retrying.
|
|
48
47
|
def send_now(msg)
|
|
49
48
|
response = notify_backend(msg)
|
|
50
49
|
run_after_notify_hooks(msg, response)
|
|
@@ -61,11 +60,6 @@ module Honeybadger
|
|
|
61
60
|
return true if force
|
|
62
61
|
return true unless thread&.alive?
|
|
63
62
|
|
|
64
|
-
if throttled?
|
|
65
|
-
warn { sprintf("Unable to report %s error(s) to Honeybadger (currently throttled)", queue.size) } unless queue.empty?
|
|
66
|
-
return true
|
|
67
|
-
end
|
|
68
|
-
|
|
69
63
|
info { sprintf("Waiting to report %s error(s) to Honeybadger", queue.size) } unless queue.empty?
|
|
70
64
|
|
|
71
65
|
queue.push(SHUTDOWN)
|
|
@@ -75,12 +69,18 @@ module Honeybadger
|
|
|
75
69
|
kill!
|
|
76
70
|
end
|
|
77
71
|
|
|
78
|
-
# Blocks until queue is processed up to this point in time.
|
|
72
|
+
# Blocks until queue is processed up to this point in time. Retries are
|
|
73
|
+
# skipped while a flush is pending, since the caller is waiting.
|
|
79
74
|
def flush
|
|
80
75
|
mutex.synchronize do
|
|
81
76
|
if thread&.alive?
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
@flushes += 1
|
|
78
|
+
queue.push(flush_signal)
|
|
79
|
+
begin
|
|
80
|
+
flush_signal.wait(mutex)
|
|
81
|
+
ensure
|
|
82
|
+
@flushes -= 1
|
|
83
|
+
end
|
|
84
84
|
end
|
|
85
85
|
end
|
|
86
86
|
end
|
|
@@ -103,8 +103,7 @@ module Honeybadger
|
|
|
103
103
|
|
|
104
104
|
private
|
|
105
105
|
|
|
106
|
-
attr_reader :config, :queue, :pid, :mutex, :
|
|
107
|
-
:throttle_interval, :start_at
|
|
106
|
+
attr_reader :config, :queue, :pid, :mutex, :flush_signal, :thread, :start_at
|
|
108
107
|
|
|
109
108
|
def_delegator :config, :backend
|
|
110
109
|
|
|
@@ -112,6 +111,10 @@ module Honeybadger
|
|
|
112
111
|
mutex.synchronize { @shutdown }
|
|
113
112
|
end
|
|
114
113
|
|
|
114
|
+
def flushing?
|
|
115
|
+
mutex.synchronize { @flushes > 0 }
|
|
116
|
+
end
|
|
117
|
+
|
|
115
118
|
def suspended?
|
|
116
119
|
mutex.synchronize { start_at && Time.now.to_i < start_at }
|
|
117
120
|
end
|
|
@@ -122,10 +125,6 @@ module Honeybadger
|
|
|
122
125
|
true
|
|
123
126
|
end
|
|
124
127
|
|
|
125
|
-
def throttled?
|
|
126
|
-
mutex.synchronize { throttle > 0 }
|
|
127
|
-
end
|
|
128
|
-
|
|
129
128
|
def kill!
|
|
130
129
|
d { "killing worker thread" }
|
|
131
130
|
|
|
@@ -137,7 +136,10 @@ module Honeybadger
|
|
|
137
136
|
true
|
|
138
137
|
end
|
|
139
138
|
|
|
139
|
+
# Drops queued notices and rejects new ones (push returns false) for
|
|
140
|
+
# +interval+ seconds. The thread restarts on the first push after that.
|
|
140
141
|
def suspend(interval)
|
|
142
|
+
warn { sprintf("Dropping queued errors and rejecting new ones for %ss", interval) }
|
|
141
143
|
mutex.synchronize do
|
|
142
144
|
@start_at = Time.now.to_i + interval
|
|
143
145
|
queue.clear
|
|
@@ -153,7 +155,7 @@ module Honeybadger
|
|
|
153
155
|
loop do
|
|
154
156
|
case msg = queue.pop
|
|
155
157
|
when SHUTDOWN then break
|
|
156
|
-
when
|
|
158
|
+
when flush_signal then signal_flush_done
|
|
157
159
|
else work(msg)
|
|
158
160
|
end
|
|
159
161
|
end
|
|
@@ -166,19 +168,28 @@ module Honeybadger
|
|
|
166
168
|
sprintf(msg, e.class, e.message.dump, Array(e.backtrace).join("\n\t"))
|
|
167
169
|
}
|
|
168
170
|
ensure
|
|
169
|
-
|
|
171
|
+
release_flush_waiters
|
|
170
172
|
end
|
|
171
173
|
|
|
172
174
|
def work(msg)
|
|
173
|
-
|
|
175
|
+
response = notify_backend(msg)
|
|
176
|
+
MAX_RETRIES.times do
|
|
177
|
+
break unless response.retryable?
|
|
178
|
+
break unless wait(response.retry_after || rand(5..15))
|
|
179
|
+
response = notify_backend(msg)
|
|
180
|
+
end
|
|
181
|
+
run_after_notify_hooks(msg, response)
|
|
182
|
+
handle_response(msg, response)
|
|
183
|
+
if (interval = response.reject_interval)
|
|
184
|
+
suspend(interval) # terminates this thread
|
|
185
|
+
end
|
|
174
186
|
|
|
175
|
-
|
|
176
|
-
|
|
187
|
+
# Shutting down while the server is unavailable: stop without sending
|
|
188
|
+
# the rest of the queue.
|
|
189
|
+
if shutdown? && response.retryable?
|
|
190
|
+
warn { "Shutting down while the server is unavailable; dropping queued errors" }
|
|
177
191
|
kill!
|
|
178
|
-
return
|
|
179
192
|
end
|
|
180
|
-
|
|
181
|
-
sleep(throttle_interval)
|
|
182
193
|
rescue => e
|
|
183
194
|
error {
|
|
184
195
|
msg = "Error in worker thread class=%s message=%s\n\t%s"
|
|
@@ -206,49 +217,34 @@ module Honeybadger
|
|
|
206
217
|
}
|
|
207
218
|
end
|
|
208
219
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
@throttle_interval = calc_throttle_interval
|
|
217
|
-
throttle
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def dec_throttle
|
|
222
|
-
mutex.synchronize do
|
|
223
|
-
return nil if throttle == 0
|
|
224
|
-
@throttle -= 1
|
|
225
|
-
@throttle_interval = calc_throttle_interval
|
|
226
|
-
throttle
|
|
220
|
+
# Sleeps in one-second steps so shutdown or a flush can interrupt.
|
|
221
|
+
# Returns false if one did, including during the final second; the
|
|
222
|
+
# payload in hand is then dropped.
|
|
223
|
+
def wait(seconds)
|
|
224
|
+
seconds.times do
|
|
225
|
+
return false if shutdown? || flushing?
|
|
226
|
+
sleep(1)
|
|
227
227
|
end
|
|
228
|
+
!(shutdown? || flushing?)
|
|
228
229
|
end
|
|
229
230
|
|
|
230
231
|
def handle_response(msg, response)
|
|
231
232
|
d { sprintf("worker response id=%s code=%s message=%s", msg.id, response.code, response.message.to_s.dump) }
|
|
232
233
|
|
|
233
234
|
case response.code
|
|
234
|
-
when 429, 503
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
when 403
|
|
235
|
+
when 429, 502, 503, 504
|
|
236
|
+
if response.retryable?
|
|
237
|
+
warn { sprintf("Error report failed: server unavailable, giving up on this error. id=%s code=%s", msg.id, response.code) }
|
|
238
|
+
else
|
|
239
|
+
warn { sprintf("Error report failed: server is rejecting data. id=%s code=%s reason=%s", msg.id, response.code, response.limit_reason) }
|
|
240
|
+
end
|
|
241
|
+
when 401, 403
|
|
241
242
|
warn { sprintf("Error report failed: %s id=%s code=%s", response.error_message, msg.id, response.code) }
|
|
242
|
-
suspend(3600)
|
|
243
243
|
when 413
|
|
244
244
|
warn { sprintf("Error report failed: Payload is too large. id=%s code=%s", msg.id, response.code) }
|
|
245
245
|
when 201
|
|
246
246
|
host = config.get(:"connection.ui_host")
|
|
247
|
-
|
|
248
|
-
info { sprintf("Success ⚡ https://%s/notice/%s id=%s code=%s throttle=%s interval=%s", host, msg.id, msg.id, response.code, throttle, throttle_interval) }
|
|
249
|
-
else
|
|
250
|
-
info { sprintf("Success ⚡ https://%s/notice/%s id=%s code=%s", host, msg.id, msg.id, response.code) }
|
|
251
|
-
end
|
|
247
|
+
info { sprintf("Success ⚡ https://%s/notice/%s id=%s code=%s", host, msg.id, msg.id, response.code) }
|
|
252
248
|
when :stubbed
|
|
253
249
|
debug { sprintf("Success ⚡ Development mode is enabled; this error will be reported if it occurs after you deploy your app. id=%s", msg.id) }
|
|
254
250
|
when :error
|
|
@@ -258,17 +254,22 @@ module Honeybadger
|
|
|
258
254
|
end
|
|
259
255
|
end
|
|
260
256
|
|
|
261
|
-
#
|
|
262
|
-
# down, otherwise
|
|
263
|
-
def
|
|
264
|
-
|
|
257
|
+
# Wake every waiting flush. Important to perform during cleanup when
|
|
258
|
+
# shutting down, otherwise they could end up waiting indefinitely.
|
|
259
|
+
def release_flush_waiters
|
|
260
|
+
if mutex.owned?
|
|
261
|
+
flush_signal.broadcast
|
|
262
|
+
else
|
|
263
|
+
mutex.synchronize { flush_signal.broadcast }
|
|
264
|
+
end
|
|
265
265
|
end
|
|
266
266
|
|
|
267
|
-
|
|
267
|
+
# Wakes the flush waiting on this point in the queue.
|
|
268
|
+
def signal_flush_done
|
|
268
269
|
if mutex.owned?
|
|
269
|
-
|
|
270
|
+
flush_signal.signal
|
|
270
271
|
else
|
|
271
|
-
mutex.synchronize {
|
|
272
|
+
mutex.synchronize { flush_signal.signal }
|
|
272
273
|
end
|
|
273
274
|
end
|
|
274
275
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: honeybadger
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 6.
|
|
4
|
+
version: 6.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Honeybadger Industries LLC
|
|
@@ -131,6 +131,7 @@ files:
|
|
|
131
131
|
- lib/honeybadger/plugins/thor.rb
|
|
132
132
|
- lib/honeybadger/plugins/warden.rb
|
|
133
133
|
- lib/honeybadger/rack/error_notifier.rb
|
|
134
|
+
- lib/honeybadger/rack/request_id.rb
|
|
134
135
|
- lib/honeybadger/rack/user_feedback.rb
|
|
135
136
|
- lib/honeybadger/rack/user_informer.rb
|
|
136
137
|
- lib/honeybadger/registry.rb
|
|
@@ -217,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
217
218
|
- !ruby/object:Gem::Version
|
|
218
219
|
version: "0"
|
|
219
220
|
requirements: []
|
|
220
|
-
rubygems_version: 4.1.0.
|
|
221
|
+
rubygems_version: 4.1.0.beta2
|
|
221
222
|
specification_version: 4
|
|
222
223
|
summary: "Full-stack error tracking, performance monitoring, logging, and more."
|
|
223
224
|
test_files: []
|