libhoney 1.18.0 → 2.0.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.
@@ -36,9 +36,9 @@ module Libhoney
36
36
  @send_queue = Queue.new
37
37
  @threads = []
38
38
  @lock = Mutex.new
39
- # use a SizedQueue so the producer will block on adding to the batch_queue when @block_on_send is true
40
- @batch_queue = SizedQueue.new(@pending_work_capacity)
41
39
  @batch_thread = nil
40
+
41
+ setup_batch_queue
42
42
  end
43
43
 
44
44
  def add(event)
@@ -53,26 +53,6 @@ module Libhoney
53
53
  ensure_threads_running
54
54
  end
55
55
 
56
- def event_valid(event)
57
- invalid = []
58
- invalid.push('api host') if event.api_host.nil? || event.api_host.empty?
59
- invalid.push('write key') if event.writekey.nil? || event.writekey.empty?
60
- invalid.push('dataset') if event.dataset.nil? || event.dataset.empty?
61
-
62
- unless invalid.empty?
63
- e = StandardError.new("#{self.class.name}: nil or empty required fields (#{invalid.join(', ')})"\
64
- '. Will not attempt to send.')
65
- Response.new(error: e).tap do |error_response|
66
- error_response.metadata = event.metadata
67
- enqueue_response(error_response)
68
- end
69
-
70
- return false
71
- end
72
-
73
- true
74
- end
75
-
76
56
  def send_loop
77
57
  http_clients = build_http_clients
78
58
 
@@ -133,23 +113,28 @@ module Libhoney
133
113
  end
134
114
 
135
115
  def close(drain)
136
- # if drain is false, clear the remaining unprocessed events from the queue
137
- unless drain
138
- @batch_queue.clear
139
- @send_queue.clear
140
- end
116
+ @lock.synchronize do
117
+ # if drain is false, clear the remaining unprocessed events from the queue
118
+ if drain
119
+ warn "#{self.class.name} - close: draining events" if %w[debug trace].include?(ENV['LOG_LEVEL'])
120
+ else
121
+ warn "#{self.class.name} - close: deleting unsent events" if %w[debug trace].include?(ENV['LOG_LEVEL'])
122
+ @batch_queue.clear
123
+ @send_queue.clear
124
+ end
141
125
 
142
- @batch_queue.enq(nil)
143
- @batch_thread.join unless @batch_thread.nil?
126
+ @batch_queue.enq(nil)
127
+ @batch_thread&.join(1.0) # limit the amount of time we'll wait for the thread to end
144
128
 
145
- # send @threads.length number of nils so each thread will fall out of send_loop
146
- @threads.length.times { @send_queue << nil }
129
+ # send @threads.length number of nils so each thread will fall out of send_loop
130
+ @threads.length.times { @send_queue << nil }
147
131
 
148
- @threads.each(&:join)
149
- @threads = []
132
+ @threads.each(&:join)
133
+ @threads = []
134
+ end
150
135
 
151
136
  enqueue_response(nil)
152
-
137
+ warn "#{self.class.name} - close: close complete" if %w[debug trace].include?(ENV['LOG_LEVEL'])
153
138
  0
154
139
  end
155
140
 
@@ -161,25 +146,79 @@ module Libhoney
161
146
 
162
147
  loop do
163
148
  begin
149
+ # a timeout expiration waiting for an event
150
+ # 1. interrupts only when thread is in a blocking state (waiting for pop)
151
+ # 2. exception skips the break and is rescued
152
+ # 3. triggers the ensure to flush the current batch
153
+ # 3. begins the loop again with an updated next_send_time
164
154
  Thread.handle_interrupt(Timeout::Error => :on_blocking) do
155
+ # an event on the batch_queue
156
+ # 1. pops out and is truthy
157
+ # 2. gets included in the current batch
158
+ # 3. while waits for another event
165
159
  while (event = Timeout.timeout(@send_frequency) { @batch_queue.pop })
166
160
  key = [event.api_host, event.writekey, event.dataset]
167
161
  batched_events[key] << event
168
162
  end
169
163
  end
170
164
 
165
+ # a nil on the batch_queue
166
+ # 1. pops out and is falsy
167
+ # 2. ends the event-popping while do..end
168
+ # 3. breaks the loop
169
+ # 4. flushes the current batch
170
+ # 5. ends the batch_loop
171
171
  break
172
- rescue Exception
172
+ rescue Timeout::Error
173
+ # Timeout::Error happens when there is nothing to pop from the batch_queue.
174
+ # We rescue it here to avoid spamming the logs with "execution expired" errors.
175
+ rescue Exception => e
176
+ warn "#{self.class.name}: 💥 " + e.message if %w[debug trace].include?(ENV['LOG_LEVEL'])
177
+ warn e.backtrace.join("\n").to_s if ['trace'].include?(ENV['LOG_LEVEL'])
178
+
179
+ # regardless of the exception, figure out whether enough time has passed to
180
+ # send the current batched events, if so, send them and figure out the next send time
181
+ # before going back to the top of the loop
173
182
  ensure
174
183
  next_send_time = flush_batched_events(batched_events) if Time.now > next_send_time
175
184
  end
176
185
  end
177
186
 
187
+ # don't need to capture the next_send_time here because the batch_loop is exiting
188
+ # for some reason (probably transmission.close)
178
189
  flush_batched_events(batched_events)
179
190
  end
180
191
 
181
192
  private
182
193
 
194
+ def setup_batch_queue
195
+ # use a SizedQueue so the producer will block on adding to the batch_queue when @block_on_send is true
196
+ @batch_queue = SizedQueue.new(@pending_work_capacity)
197
+ end
198
+
199
+ REQUIRED_EVENT_FIELDS = %i[api_host writekey dataset].freeze
200
+
201
+ def event_valid(event)
202
+ missing_required_fields = REQUIRED_EVENT_FIELDS.select do |required_field|
203
+ event.public_send(required_field).nil? || event.public_send(required_field).empty?
204
+ end
205
+
206
+ if missing_required_fields.empty?
207
+ true
208
+ else
209
+ enqueue_response(
210
+ Response.new(
211
+ metadata: event.metadata,
212
+ error: StandardError.new(
213
+ "#{self.class.name}: nil or empty required fields (#{missing_required_fields.join(', ')})"\
214
+ '. Will not attempt to send.'
215
+ )
216
+ )
217
+ )
218
+ false
219
+ end
220
+ end
221
+
183
222
  ##
184
223
  # Enqueues a response to the responses queue suppressing ThreadError when
185
224
  # there is no space left on the queue and we are not blocking on response
@@ -190,15 +229,36 @@ module Libhoney
190
229
  end
191
230
 
192
231
  def process_response(http_response, before, batch)
193
- index = 0
194
- JSON.parse(http_response.body).each do |event|
195
- index += 1 while batch[index].nil? && index < batch.size
196
- break unless (batched_event = batch[index])
197
-
198
- Response.new(status_code: event['status']).tap do |response|
199
- response.duration = Time.now - before
200
- response.metadata = batched_event.metadata
201
- enqueue_response(response)
232
+ if http_response.status == 200
233
+ index = 0
234
+ JSON.parse(http_response.body).each do |event|
235
+ index += 1 while batch[index].nil? && index < batch.size
236
+ break unless (batched_event = batch[index])
237
+
238
+ enqueue_response(
239
+ Response.new(
240
+ status_code: event['status'],
241
+ duration: (Time.now - before),
242
+ metadata: batched_event.metadata
243
+ )
244
+ )
245
+ end
246
+ else
247
+ error = JSON.parse(http_response.body)['error']
248
+ if %w[debug trace].include?(ENV['LOG_LEVEL'])
249
+ warn "#{self.class.name}: error sending data to Honeycomb - #{http_response.status} #{error}"
250
+ end
251
+ batch.each do |batched_event|
252
+ next unless batched_event # skip nils enqueued from serialization errors
253
+
254
+ enqueue_response(
255
+ Response.new(
256
+ status_code: http_response.status, # single error from API applied to all events sent in batch
257
+ duration: (Time.now - before),
258
+ metadata: batched_event.metadata,
259
+ error: RuntimeError.new(error)
260
+ )
261
+ )
202
262
  end
203
263
  end
204
264
  end
@@ -234,14 +294,15 @@ module Libhoney
234
294
  end
235
295
 
236
296
  def build_user_agent(user_agent_addition)
237
- ua = "libhoney-rb/#{VERSION}"
238
- ua << " #{user_agent_addition}" if user_agent_addition
239
- ua
297
+ "libhoney-rb/#{VERSION}"
298
+ .concat(" #{user_agent_addition}")
299
+ .strip # remove trailing spaces if addition was empty
300
+ .concat(" Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM})")
240
301
  end
241
302
 
242
303
  def ensure_threads_running
243
304
  @lock.synchronize do
244
- @batch_thread = Thread.new { batch_loop } unless @batch_thread && @batch_thread.alive?
305
+ @batch_thread = Thread.new { batch_loop } unless @batch_thread&.alive?
245
306
  @threads.select!(&:alive?)
246
307
  @threads << Thread.new { send_loop } while @threads.length < @max_concurrent_batches
247
308
  end
@@ -1,3 +1,3 @@
1
1
  module Libhoney
2
- VERSION = '1.18.0'.freeze
2
+ VERSION = '2.0.0'.freeze
3
3
  end
data/libhoney.gemspec CHANGED
@@ -20,20 +20,22 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
21
  spec.require_paths = ['lib']
22
22
 
23
- spec.required_ruby_version = '>= 2.2.0'
23
+ spec.required_ruby_version = '>= 2.4.0'
24
24
 
25
25
  spec.add_development_dependency 'bump', '~> 0.5'
26
26
  spec.add_development_dependency 'bundler'
27
+ spec.add_development_dependency 'lockstep'
27
28
  spec.add_development_dependency 'minitest', '~> 5.0'
28
- spec.add_development_dependency 'rake', '~> 12.3'
29
- spec.add_development_dependency 'rubocop', '< 0.69'
29
+ spec.add_development_dependency 'minitest-reporters'
30
+ spec.add_development_dependency 'rake', '~> 13.0'
31
+ spec.add_development_dependency 'rubocop', '1.12.1'
30
32
  spec.add_development_dependency 'sinatra'
31
33
  spec.add_development_dependency 'sinatra-contrib'
32
- spec.add_development_dependency 'spy', '1.0.0'
34
+ spec.add_development_dependency 'spy', '~> 1.0'
33
35
  spec.add_development_dependency 'webmock', '~> 3.4'
34
36
  spec.add_development_dependency 'yard'
35
37
  spec.add_development_dependency 'yardstick', '~> 0.9'
36
38
  spec.add_dependency 'addressable', '~> 2.0'
37
39
  spec.add_dependency 'excon'
38
- spec.add_dependency 'http', '>= 2.0', '< 5.0'
40
+ spec.add_dependency 'http', '>= 2.0', '< 6.0'
39
41
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: libhoney
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.18.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Honeycomb.io Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-01-14 00:00:00.000000000 Z
11
+ date: 2021-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bump
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: lockstep
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: minitest
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -52,34 +66,48 @@ dependencies:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
68
  version: '5.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest-reporters
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: rake
57
85
  requirement: !ruby/object:Gem::Requirement
58
86
  requirements:
59
87
  - - "~>"
60
88
  - !ruby/object:Gem::Version
61
- version: '12.3'
89
+ version: '13.0'
62
90
  type: :development
63
91
  prerelease: false
64
92
  version_requirements: !ruby/object:Gem::Requirement
65
93
  requirements:
66
94
  - - "~>"
67
95
  - !ruby/object:Gem::Version
68
- version: '12.3'
96
+ version: '13.0'
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: rubocop
71
99
  requirement: !ruby/object:Gem::Requirement
72
100
  requirements:
73
- - - "<"
101
+ - - '='
74
102
  - !ruby/object:Gem::Version
75
- version: '0.69'
103
+ version: 1.12.1
76
104
  type: :development
77
105
  prerelease: false
78
106
  version_requirements: !ruby/object:Gem::Requirement
79
107
  requirements:
80
- - - "<"
108
+ - - '='
81
109
  - !ruby/object:Gem::Version
82
- version: '0.69'
110
+ version: 1.12.1
83
111
  - !ruby/object:Gem::Dependency
84
112
  name: sinatra
85
113
  requirement: !ruby/object:Gem::Requirement
@@ -112,16 +140,16 @@ dependencies:
112
140
  name: spy
113
141
  requirement: !ruby/object:Gem::Requirement
114
142
  requirements:
115
- - - '='
143
+ - - "~>"
116
144
  - !ruby/object:Gem::Version
117
- version: 1.0.0
145
+ version: '1.0'
118
146
  type: :development
119
147
  prerelease: false
120
148
  version_requirements: !ruby/object:Gem::Requirement
121
149
  requirements:
122
- - - '='
150
+ - - "~>"
123
151
  - !ruby/object:Gem::Version
124
- version: 1.0.0
152
+ version: '1.0'
125
153
  - !ruby/object:Gem::Dependency
126
154
  name: webmock
127
155
  requirement: !ruby/object:Gem::Requirement
@@ -201,7 +229,7 @@ dependencies:
201
229
  version: '2.0'
202
230
  - - "<"
203
231
  - !ruby/object:Gem::Version
204
- version: '5.0'
232
+ version: '6.0'
205
233
  type: :runtime
206
234
  prerelease: false
207
235
  version_requirements: !ruby/object:Gem::Requirement
@@ -211,7 +239,7 @@ dependencies:
211
239
  version: '2.0'
212
240
  - - "<"
213
241
  - !ruby/object:Gem::Version
214
- version: '5.0'
242
+ version: '6.0'
215
243
  description: Ruby gem for sending data to Honeycomb
216
244
  email: support@honeycomb.io
217
245
  executables: []
@@ -222,27 +250,46 @@ files:
222
250
  - ".circleci/setup-rubygems.sh"
223
251
  - ".editorconfig"
224
252
  - ".github/CODEOWNERS"
253
+ - ".github/ISSUE_TEMPLATE/bug_report.md"
254
+ - ".github/ISSUE_TEMPLATE/feature_request.md"
255
+ - ".github/ISSUE_TEMPLATE/question-discussion.md"
256
+ - ".github/ISSUE_TEMPLATE/security-vulnerability-report.md"
257
+ - ".github/PULL_REQUEST_TEMPLATE.md"
258
+ - ".github/dependabot.yml"
259
+ - ".github/workflows/add-to-project.yml"
260
+ - ".github/workflows/apply-labels.yml"
261
+ - ".github/workflows/stale.yml"
225
262
  - ".gitignore"
226
263
  - ".rubocop.yml"
227
264
  - ".rubocop_todo.yml"
228
265
  - CHANGELOG.md
266
+ - CODE_OF_CONDUCT.md
267
+ - CONTRIBUTING.md
229
268
  - CONTRIBUTORS
230
269
  - Gemfile
231
270
  - LICENSE
232
271
  - NOTICE
272
+ - OSSMETADATA
233
273
  - README.md
274
+ - RELEASING.md
234
275
  - Rakefile
276
+ - SECURITY.md
277
+ - SUPPORT.md
235
278
  - example/factorial.rb
236
279
  - lib/libhoney.rb
237
280
  - lib/libhoney/builder.rb
238
281
  - lib/libhoney/cleaner.rb
239
282
  - lib/libhoney/client.rb
240
283
  - lib/libhoney/event.rb
284
+ - lib/libhoney/experimental_transmission.rb
241
285
  - lib/libhoney/log_client.rb
242
286
  - lib/libhoney/log_transmission.rb
243
287
  - lib/libhoney/mock_transmission.rb
244
288
  - lib/libhoney/null_client.rb
245
289
  - lib/libhoney/null_transmission.rb
290
+ - lib/libhoney/queueing.rb
291
+ - lib/libhoney/queueing/LICENSE.txt
292
+ - lib/libhoney/queueing/sized_queue_with_timeout.rb
246
293
  - lib/libhoney/response.rb
247
294
  - lib/libhoney/test_client.rb
248
295
  - lib/libhoney/transmission.rb
@@ -260,14 +307,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
260
307
  requirements:
261
308
  - - ">="
262
309
  - !ruby/object:Gem::Version
263
- version: 2.2.0
310
+ version: 2.4.0
264
311
  required_rubygems_version: !ruby/object:Gem::Requirement
265
312
  requirements:
266
313
  - - ">="
267
314
  - !ruby/object:Gem::Version
268
315
  version: '0'
269
316
  requirements: []
270
- rubygems_version: 3.1.4
317
+ rubygems_version: 3.2.22
271
318
  signing_key:
272
319
  specification_version: 4
273
320
  summary: send data to Honeycomb