honeybadger 5.4.1 → 5.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c626b5316e989c412c5f9a99abdb0c678fd9d9c2f64962d838d884444fd31ff9
4
- data.tar.gz: 6cae1f48c30ee24af06e67f2051131fd31fcc6c88a04f7487bb01a07a4381948
3
+ metadata.gz: cb3f1cce4a9922e40fb7f08e37ccaa3f826aabd9d529b13a49fec50ba22990b0
4
+ data.tar.gz: 024676b627053cb84cec4bffbae7f35718d7dc38e81937a50fc4435c936a4301
5
5
  SHA512:
6
- metadata.gz: dd4b81cea301889471b61883309148588a6674ede84967f854dd324a71853edec14d918b590767c791b770e5f69476cee2137a620ed5fe3796b2ad01446e441b
7
- data.tar.gz: f32eff562df8eacfa51c0461537e8d0f066d174c5a100dc08d8c4fd8c7147338b5a2394b6ce07a01ffba00535a9189e66a1f665ce7b0858f2c525e9143fef57b
6
+ metadata.gz: 569ba0e6b76df2349b10a8d37417b13be728cb7abdc6aaec958fa76ed2d912498624758f1c18f085f9e003611832d81937a6427a4bbcd2f51f3532fa4fffe550
7
+ data.tar.gz: c5eb3f07437df88834e8ca6c35327f0af3cf1cf93b94cf8809212313ffa316d071c4ee32cdf4ef5ea89e98164cb01024992e086fd47386a1ad4b3a5fe9318bf5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Change Log
2
2
 
3
+ ## [5.5.0](https://github.com/honeybadger-io/honeybadger-ruby/compare/v5.4.1...v5.5.0) (2024-02-12)
4
+
5
+
6
+ ### Features
7
+
8
+ * implements honeybadger.event by synchronous log call ([#512](https://github.com/honeybadger-io/honeybadger-ruby/issues/512)) ([dbe7e3d](https://github.com/honeybadger-io/honeybadger-ruby/commit/dbe7e3dc20cbb432254b055b356826a42a76c609))
9
+
3
10
  ## [5.4.1](https://github.com/honeybadger-io/honeybadger-ruby/compare/v5.4.0...v5.4.1) (2023-12-22)
4
11
 
5
12
 
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2015 Honeybadger Industries LLC
1
+ Copyright (c) 2024 Honeybadger Industries LLC
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
@@ -7,6 +7,7 @@ require 'honeybadger/notice'
7
7
  require 'honeybadger/plugin'
8
8
  require 'honeybadger/logging'
9
9
  require 'honeybadger/worker'
10
+ require 'honeybadger/events_worker'
10
11
  require 'honeybadger/breadcrumbs'
11
12
 
12
13
  module Honeybadger
@@ -354,6 +355,7 @@ module Honeybadger
354
355
  yield
355
356
  ensure
356
357
  worker.flush
358
+ events_worker&.flush
357
359
  end
358
360
 
359
361
  # Stops the Honeybadger service.
@@ -362,9 +364,41 @@ module Honeybadger
362
364
  # Honeybadger.stop # => nil
363
365
  def stop(force = false)
364
366
  worker.shutdown(force)
367
+ events_worker&.shutdown(force)
365
368
  true
366
369
  end
367
370
 
371
+ # Sends event to events backend
372
+ #
373
+ # @example
374
+ # # With event type as first argument (recommended):
375
+ # Honeybadger.event("user_signed_up", user_id: 123)
376
+ #
377
+ # # With just a payload:
378
+ # Honeybadger.event(event_type: "user_signed_up", user_id: 123)
379
+ #
380
+ # @param event_name [String, Hash] a String describing the event or a Hash
381
+ # when the second argument is omitted.
382
+ # @param payload [Hash] Additional data to be sent with the event as keyword arguments
383
+ #
384
+ # @return [void]
385
+ def event(event_type, payload = {})
386
+ init_events_worker
387
+
388
+ ts = DateTime.now.new_offset(0).rfc3339
389
+ merged = {ts: ts}
390
+
391
+ if event_type.is_a?(String)
392
+ merged.merge!(event_type: event_type)
393
+ else
394
+ merged.merge!(Hash(event_type))
395
+ end
396
+
397
+ merged.merge!(Hash(payload))
398
+
399
+ events_worker.push(merged)
400
+ end
401
+
368
402
  # @api private
369
403
  attr_reader :config
370
404
 
@@ -437,7 +471,7 @@ module Honeybadger
437
471
  end
438
472
 
439
473
  # @api private
440
- attr_reader :worker
474
+ attr_reader :worker, :events_worker
441
475
 
442
476
  # @api private
443
477
  # @!method init!(...)
@@ -475,9 +509,15 @@ module Honeybadger
475
509
  end
476
510
 
477
511
  def init_worker
512
+ return if @worker
478
513
  @worker = Worker.new(config)
479
514
  end
480
515
 
516
+ def init_events_worker
517
+ return if @events_worker
518
+ @events_worker = EventsWorker.new(config)
519
+ end
520
+
481
521
  def with_error_handling
482
522
  yield
483
523
  rescue => ex
@@ -109,6 +109,16 @@ module Honeybadger
109
109
  notify(:deploys, payload)
110
110
  end
111
111
 
112
+ # Send event
113
+ # @example
114
+ # backend.event([{event_type: "email_received", ts: "2023-03-04T12:12:00+1:00", subject: 'Re: Aquisition' }})
115
+ #
116
+ # @param [Array] payload array of event hashes to send
117
+ # @raise NotImplementedError
118
+ def event(payload)
119
+ raise NotImplementedError, "must define #event on subclass"
120
+ end
121
+
112
122
  private
113
123
 
114
124
  attr_reader :config
@@ -17,6 +17,12 @@ module Honeybadger
17
17
  return Response.new(ENV['DEBUG_BACKEND_STATUS'].to_i, nil) if ENV['DEBUG_BACKEND_STATUS']
18
18
  super
19
19
  end
20
+
21
+ def event(payload)
22
+ logger.unknown("sending event to debug backend with event=#{payload.to_json}")
23
+ return Response.new(ENV['DEBUG_BACKEND_STATUS'].to_i, nil) if ENV['DEBUG_BACKEND_STATUS']
24
+ super
25
+ end
20
26
  end
21
27
  end
22
28
  end
@@ -24,6 +24,10 @@ module Honeybadger
24
24
  def check_in(id)
25
25
  StubbedResponse.new
26
26
  end
27
+
28
+ def event(payload)
29
+ StubbedResponse.new
30
+ end
27
31
  end
28
32
  end
29
33
  end
@@ -11,11 +11,10 @@ module Honeybadger
11
11
  class Server < Base
12
12
  ENDPOINTS = {
13
13
  notices: '/v1/notices'.freeze,
14
- deploys: '/v1/deploys'.freeze
14
+ deploys: '/v1/deploys'.freeze,
15
15
  }.freeze
16
-
17
16
  CHECK_IN_ENDPOINT = '/v1/check_in'.freeze
18
-
17
+ EVENTS_ENDPOINT = '/v1/events'.freeze
19
18
 
20
19
  HTTP_ERRORS = Util::HTTP::ERRORS
21
20
 
@@ -48,6 +47,18 @@ module Honeybadger
48
47
  Response.new(:error, nil, "HTTP Error: #{e.class}")
49
48
  end
50
49
 
50
+ # Send event
51
+ # @example
52
+ # backend.event([{event_type: "email_received", ts: "2023-03-04T12:12:00+1:00", subject: 'Re: Aquisition' }})
53
+ #
54
+ # @param [Array] payload array of event hashes to send
55
+ # @return [Response]
56
+ def event(payload)
57
+ Response.new(@http.post_newline_delimited(EVENTS_ENDPOINT, payload))
58
+ rescue *HTTP_ERRORS => e
59
+ Response.new(:error, nil, "HTTP Error: #{e.class}")
60
+ end
61
+
51
62
  private
52
63
 
53
64
  def payload_headers(payload)
@@ -91,6 +91,16 @@ module Honeybadger
91
91
  default: 100,
92
92
  type: Integer
93
93
  },
94
+ :'events.batch_size' => {
95
+ description: 'Send events batch if n events have accumulated',
96
+ default: 100,
97
+ type: Integer
98
+ },
99
+ :'events.timeout' => {
100
+ description: 'Timeout after which the events batch will be sent regardless (in milliseconds)',
101
+ default: 30_000,
102
+ type: Integer
103
+ },
94
104
  plugins: {
95
105
  description: 'An optional list of plugins to load. Default is to load all plugins.',
96
106
  default: nil,
@@ -224,6 +224,14 @@ module Honeybadger
224
224
  self[:max_queue_size]
225
225
  end
226
226
 
227
+ def events_batch_size
228
+ self[:'events.batch_size']
229
+ end
230
+
231
+ def events_timeout
232
+ self[:'events.timeout']
233
+ end
234
+
227
235
  def params_filters
228
236
  Array(self[:'request.filter_keys'])
229
237
  end
@@ -0,0 +1,319 @@
1
+ require 'forwardable'
2
+ require 'net/http'
3
+
4
+ require 'honeybadger/logging'
5
+
6
+ module Honeybadger
7
+ # A concurrent queue to notify the backend.
8
+ # @api private
9
+ class EventsWorker
10
+ extend Forwardable
11
+
12
+ include Honeybadger::Logging::Helper
13
+
14
+ # Sub-class thread so we have a named thread (useful for debugging in Thread.list).
15
+ class Thread < ::Thread; end
16
+
17
+ # Used to signal the worker to shutdown.
18
+ SHUTDOWN = :__hb_worker_shutdown!
19
+ FLUSH = :__hb_worker_flush!
20
+ CHECK_TIMEOUT = :__hb_worker_check_timeout!
21
+
22
+ # The base number for the exponential backoff formula when calculating the
23
+ # throttle interval. `1.05 ** throttle` will reach an interval of 2 minutes
24
+ # after around 100 429 responses from the server.
25
+ BASE_THROTTLE = 1.05
26
+
27
+ # TODO: These could be configurable?
28
+
29
+ def initialize(config)
30
+ @config = config
31
+ @throttle = 0
32
+ @throttle_interval = 0
33
+ @mutex = Mutex.new
34
+ @marker = ConditionVariable.new
35
+ @queue = Queue.new
36
+ @send_queue = Queue.new
37
+ @shutdown = false
38
+ @start_at = nil
39
+ @pid = Process.pid
40
+ @send_queue = []
41
+ @last_sent = nil
42
+ end
43
+
44
+ def push(msg)
45
+ return false unless start
46
+
47
+ if queue.size >= config.max_queue_size
48
+ warn { sprintf('Unable to send event; reached max queue size of %s.', queue.size) }
49
+ return false
50
+ end
51
+
52
+ queue.push(msg)
53
+ end
54
+
55
+ def send_now(msg)
56
+ handle_response(send_to_backend(msg))
57
+ end
58
+
59
+ def shutdown(force = false)
60
+ d { 'shutting down events worker' }
61
+
62
+ mutex.synchronize do
63
+ @shutdown = true
64
+ end
65
+
66
+ return true if force
67
+ return true unless thread&.alive?
68
+
69
+ if throttled?
70
+ warn { sprintf('Unable to send %s event(s) to Honeybadger (currently throttled)', queue.size) } unless queue.empty?
71
+ return true
72
+ end
73
+
74
+ info { sprintf('Waiting to send %s events(s) to Honeybadger', queue.size) } unless queue.empty?
75
+ queue.push(FLUSH)
76
+ queue.push(SHUTDOWN)
77
+ !!thread.join
78
+ ensure
79
+ queue.clear
80
+ kill!
81
+ end
82
+
83
+ # Blocks until queue is processed up to this point in time.
84
+ def flush
85
+ mutex.synchronize do
86
+ if thread && thread.alive?
87
+ queue.push(FLUSH)
88
+ queue.push(marker)
89
+ marker.wait(mutex)
90
+ end
91
+ end
92
+ end
93
+
94
+ def start
95
+ return false unless can_start?
96
+
97
+ mutex.synchronize do
98
+ @shutdown = false
99
+ @start_at = nil
100
+
101
+ return true if thread&.alive?
102
+
103
+ @pid = Process.pid
104
+ @thread = Thread.new { run }
105
+ @timeout_thread = Thread.new { schedule_timeout_check }
106
+ end
107
+
108
+ true
109
+ end
110
+
111
+ private
112
+
113
+ attr_reader :config, :queue, :pid, :mutex, :marker, :thread, :timeout_thread, :throttle,
114
+ :throttle_interval, :start_at, :send_queue, :last_sent
115
+
116
+ def_delegator :config, :backend
117
+
118
+ def shutdown?
119
+ mutex.synchronize { @shutdown }
120
+ end
121
+
122
+ def suspended?
123
+ mutex.synchronize { start_at && Time.now.to_i < start_at }
124
+ end
125
+
126
+ def can_start?
127
+ return false if shutdown?
128
+ return false if suspended?
129
+ true
130
+ end
131
+
132
+ def throttled?
133
+ mutex.synchronize { throttle > 0 }
134
+ end
135
+
136
+ def kill!
137
+ d { 'killing worker thread' }
138
+
139
+ if thread
140
+ Thread.kill(thread)
141
+ Thread.kill(timeout_thread)
142
+ thread.join # Allow ensure blocks to execute.
143
+ end
144
+
145
+ true
146
+ end
147
+
148
+ def suspend(interval)
149
+ mutex.synchronize do
150
+ @start_at = Time.now.to_i + interval
151
+ queue.clear
152
+ end
153
+
154
+ # Must be performed last since this may kill the current thread.
155
+ kill!
156
+ end
157
+
158
+ def schedule_timeout_check
159
+ loop do
160
+ sleep(config.events_timeout / 1000.0)
161
+ queue.push(CHECK_TIMEOUT)
162
+ end
163
+ end
164
+
165
+ def run
166
+ begin
167
+ d { 'worker started' }
168
+ mutex.synchronize do
169
+ @last_sent = Time.now
170
+ end
171
+ loop do
172
+ case msg = queue.pop
173
+ when SHUTDOWN then break
174
+ when CHECK_TIMEOUT then check_timeout
175
+ when FLUSH then flush_send_queue
176
+ when ConditionVariable then signal_marker(msg)
177
+ else work(msg)
178
+ end
179
+ end
180
+ ensure
181
+ d { 'stopping worker' }
182
+ end
183
+ rescue Exception => e
184
+ error {
185
+ msg = "Error in worker thread (shutting down) class=%s message=%s\n\t%s"
186
+ sprintf(msg, e.class, e.message.dump, Array(e.backtrace).join("\n\t"))
187
+ }
188
+ ensure
189
+ release_marker
190
+ end
191
+
192
+ def check_timeout
193
+ return if mutex.synchronize { send_queue.empty? }
194
+ ms_since = (Time.now.to_f - last_sent.to_f) * 1000.0
195
+ if ms_since >= config.events_timeout
196
+ send_batch
197
+ end
198
+ end
199
+
200
+ def enqueue_msg(msg)
201
+ mutex.synchronize do
202
+ @send_queue << msg
203
+ end
204
+ end
205
+
206
+ def send_batch
207
+ send_now(mutex.synchronize { send_queue })
208
+ mutex.synchronize do
209
+ @last_sent = Time.now
210
+ send_queue.clear
211
+ end
212
+ end
213
+
214
+ def check_and_send
215
+ return if mutex.synchronize { send_queue.empty? }
216
+ if mutex.synchronize { send_queue.length } >= config.events_batch_size
217
+ send_batch
218
+ end
219
+ end
220
+
221
+ def flush_send_queue
222
+ return if mutex.synchronize { send_queue.empty? }
223
+ send_batch
224
+ rescue StandardError => e
225
+ error {
226
+ msg = "Error in worker thread class=%s message=%s\n\t%s"
227
+ sprintf(msg, e.class, e.message.dump, Array(e.backtrace).join("\n\t"))
228
+ }
229
+ end
230
+
231
+ def work(msg)
232
+ enqueue_msg(msg)
233
+ check_and_send
234
+
235
+ if shutdown? && throttled?
236
+ warn { sprintf('Unable to send %s events(s) to Honeybadger (currently throttled)', queue.size) } if queue.size > 1
237
+ kill!
238
+ return
239
+ end
240
+
241
+ sleep(throttle_interval)
242
+ rescue StandardError => e
243
+ error {
244
+ msg = "Error in worker thread class=%s message=%s\n\t%s"
245
+ sprintf(msg, e.class, e.message.dump, Array(e.backtrace).join("\n\t"))
246
+ }
247
+ end
248
+
249
+
250
+ def send_to_backend(msg)
251
+ d { 'events_worker sending to backend' }
252
+ response = backend.event(msg)
253
+ response
254
+ end
255
+
256
+ def calc_throttle_interval
257
+ ((BASE_THROTTLE ** throttle) - 1).round(3)
258
+ end
259
+
260
+ def inc_throttle
261
+ mutex.synchronize do
262
+ @throttle += 1
263
+ @throttle_interval = calc_throttle_interval
264
+ throttle
265
+ end
266
+ end
267
+
268
+ def dec_throttle
269
+ mutex.synchronize do
270
+ return nil if throttle == 0
271
+ @throttle -= 1
272
+ @throttle_interval = calc_throttle_interval
273
+ throttle
274
+ end
275
+ end
276
+
277
+ def handle_response(response)
278
+ d { sprintf('events_worker response code=%s message=%s', response.code, response.message.to_s.dump) }
279
+
280
+ case response.code
281
+ when 429, 503
282
+ throttle = inc_throttle
283
+ warn { sprintf('Event send failed: project is sending too many events. code=%s throttle=%s interval=%s', response.code, throttle, throttle_interval) }
284
+ when 402
285
+ warn { sprintf('Event send failed: payment is required. code=%s', response.code) }
286
+ suspend(3600)
287
+ when 403
288
+ warn { sprintf('Event send failed: API key is invalid. code=%s', response.code) }
289
+ suspend(3600)
290
+ when 413
291
+ warn { sprintf('Event send failed: Payload is too large. code=%s', response.code) }
292
+ when 201
293
+ if throttle = dec_throttle
294
+ info { sprintf('Success ⚡ Event sent code=%s throttle=%s interval=%s', response.code, throttle, throttle_interval) }
295
+ else
296
+ info { sprintf('Success ⚡ Event sent code=%s', response.code) }
297
+ end
298
+ when :stubbed
299
+ info { sprintf('Success ⚡ Development mode is enabled; This event will be sent after app is deployed.') }
300
+ when :error
301
+ warn { sprintf('Event send failed: an unknown error occurred. code=%s error=%s', response.code, response.message.to_s.dump) }
302
+ else
303
+ warn { sprintf('Event send failed: unknown response from server. code=%s', response.code) }
304
+ end
305
+ end
306
+
307
+ # Release the marker. Important to perform during cleanup when shutting
308
+ # down, otherwise it could end up waiting indefinitely.
309
+ def release_marker
310
+ signal_marker(marker)
311
+ end
312
+
313
+ def signal_marker(marker)
314
+ mutex.synchronize do
315
+ marker.signal
316
+ end
317
+ end
318
+ end
319
+ end
@@ -38,6 +38,7 @@ module Honeybadger
38
38
  def_delegator :'Honeybadger::Agent.instance', :breadcrumbs
39
39
  def_delegator :'Honeybadger::Agent.instance', :clear!
40
40
  def_delegator :'Honeybadger::Agent.instance', :track_deployment
41
+ def_delegator :'Honeybadger::Agent.instance', :event
41
42
 
42
43
  # @!macro [attach] def_delegator
43
44
  # @!method $2(...)
@@ -49,6 +49,12 @@ module Honeybadger
49
49
  response
50
50
  end
51
51
 
52
+ def post_newline_delimited(endpoint, payload, headers = nil)
53
+ response = http_connection.post(endpoint, compress(payload.map(&:to_json).join("\n")), http_headers(headers))
54
+ debug { sprintf("http method=POST path=%s code=%d", endpoint.dump, response.code) }
55
+ response
56
+ end
57
+
52
58
  private
53
59
 
54
60
  attr_reader :config
@@ -1,4 +1,4 @@
1
1
  module Honeybadger
2
2
  # The current String Honeybadger version.
3
- VERSION = '5.4.1'.freeze
3
+ VERSION = '5.5.0'.freeze
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: honeybadger
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.4.1
4
+ version: 5.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Honeybadger Industries LLC
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-12-22 00:00:00.000000000 Z
11
+ date: 2024-02-12 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Make managing application errors a more pleasant experience.
14
14
  email:
@@ -55,6 +55,7 @@ files:
55
55
  - lib/honeybadger/const.rb
56
56
  - lib/honeybadger/context_manager.rb
57
57
  - lib/honeybadger/conversions.rb
58
+ - lib/honeybadger/events_worker.rb
58
59
  - lib/honeybadger/init/hanami.rb
59
60
  - lib/honeybadger/init/rails.rb
60
61
  - lib/honeybadger/init/rake.rb
@@ -161,7 +162,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
161
162
  - !ruby/object:Gem::Version
162
163
  version: '0'
163
164
  requirements: []
164
- rubygems_version: 3.4.10
165
+ rubygems_version: 3.4.19
165
166
  signing_key:
166
167
  specification_version: 4
167
168
  summary: Error reports you can be happy about.