libhoney 1.18.0 → 1.19.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: 33a687ebf8ae87e69cf6a53b50c4e2cd926f9938af709635d42a209ec2e6c93e
4
- data.tar.gz: fd3f558a15f872c9f63fc6451722c3e9e21b7cba5ebe8d7816ec0cb08064318b
3
+ metadata.gz: 40d54151fc103bf8020f14ff8b7a9e6e0298366869e795ce3ddf0438af066e16
4
+ data.tar.gz: c2b834d39609a73bce6e5e6e19caf4d2499903498895d32fe22b55041db7e59b
5
5
  SHA512:
6
- metadata.gz: 36efc5d9734e125cd141e21dd60edbc6cf23350334bf431dead264c375dfd11b54bcf286f5509d5e97633ee4ce2d4fc689ebf5033d472237d5b6e7e2f2d0cb81
7
- data.tar.gz: 5d5147a6e9f3fc1055a94932943c5915c99f43ad550d4dd5f20955d2dad023c41a1939e6f8037b38856375a10dcdc51557b06e3d56b454296a1596c7dd62b74c
6
+ metadata.gz: 24d9d44cb0f1f5ad7549b75067dd6cbc2eb2264b8cad1c1c8cf523c19d955e41b91feb86d6cde74104885fc61ee4c4f5309b3ed46edfc5751483ba282d5e841e
7
+ data.tar.gz: 1b1690d32daf8f44a19761d2ccc26a5a099d1daec623ecd481776f34ea439506278c267ff8b319c0ccb380ec497ca1de4b6e9c2a7eee677a1584d9d1cfe1ec87
data/.circleci/config.yml CHANGED
@@ -97,6 +97,8 @@ jobs:
97
97
  - run:
98
98
  name: run tests
99
99
  command: bundle exec rake test
100
+ - store_test_results:
101
+ path: test/reports
100
102
 
101
103
  build_artifacts:
102
104
  executor:
data/.github/CODEOWNERS CHANGED
@@ -2,4 +2,4 @@
2
2
  # This file controls who is tagged for review for any given pull request.
3
3
 
4
4
  # For anything not explicitly taken by someone else:
5
- * @honeycombio/integrations-team @martin308
5
+ * @honeycombio/telemetry-team
@@ -0,0 +1,15 @@
1
+ # To get started with Dependabot version updates, you'll need to specify which
2
+ # package ecosystems to update and where the package manifests are located.
3
+ # Please see the documentation for all configuration options:
4
+ # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5
+
6
+ version: 2
7
+ updates:
8
+ - package-ecosystem: "bundler" # See documentation for possible values
9
+ directory: "/" # Location of package manifests
10
+ schedule:
11
+ interval: "weekly"
12
+ labels:
13
+ - "type: dependencies"
14
+ reviewers:
15
+ - "honeycombio/telemetry-team"
@@ -0,0 +1,14 @@
1
+ name: Apply project management flow
2
+ on:
3
+ issues:
4
+ types: [opened]
5
+ pull_request_target:
6
+ types: [opened]
7
+ jobs:
8
+ project-management:
9
+ runs-on: ubuntu-latest
10
+ name: Apply project management flow
11
+ steps:
12
+ - uses: honeycombio/oss-management-actions/projects@v1
13
+ with:
14
+ ghprojects-token: ${{ secrets.GHPROJECTS_TOKEN }}
@@ -0,0 +1,10 @@
1
+ name: Apply project labels
2
+ on: [issues, pull_request, label]
3
+ jobs:
4
+ apply-labels:
5
+ runs-on: ubuntu-latest
6
+ name: Apply common project labels
7
+ steps:
8
+ - uses: honeycombio/oss-management-actions/labels@v1
9
+ with:
10
+ github-token: ${{ secrets.GITHUB_TOKEN }}
data/.gitignore CHANGED
@@ -7,6 +7,7 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /spec/examples.txt
10
+ /test/reports/
10
11
  /test/tmp/
11
12
  /test/version_tmp/
12
13
  /tmp/
data/.rubocop.yml CHANGED
@@ -9,18 +9,19 @@ Style/Documentation:
9
9
  Lint/RescueException:
10
10
  Exclude:
11
11
  - 'lib/libhoney/transmission.rb'
12
+ - 'lib/libhoney/experimental_transmission.rb'
12
13
 
13
14
  Metrics/BlockLength:
14
15
  Max: 35
15
16
 
16
17
  Metrics/ClassLength:
17
- Max: 200
18
+ Max: 300
18
19
  Exclude:
19
20
  - lib/libhoney/transmission.rb # Should this remain so large?
20
21
  - test/*
21
22
 
22
23
  Metrics/MethodLength:
23
- Max: 40
24
+ Max: 45
24
25
  Exclude:
25
26
  - lib/libhoney/transmission.rb
26
27
  - test/*
data/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## changes pending release
4
4
 
5
+ ## 1.19.0
6
+
7
+ ### Improvements
8
+
9
+ - add a test_helper, Minitest reporters, & store test results in CI (#88)
10
+ - add experimental transmission with new sized-and-timed queue (#87)
11
+
12
+ ### Fixes
13
+
14
+ - Process single-error responses from the Batch API (#89)
15
+
5
16
  ## 1.18.0
6
17
 
7
18
  ### Improvements
@@ -46,7 +46,11 @@ module Libhoney
46
46
  # Honeycomb query engine will interpret it as representative of 10 events)
47
47
  # @param api_host [String] defaults to +API_HOST+, override to change the
48
48
  # destination for these Honeycomb events.
49
- # @param transmission [Object] transport used to actually send events. If nil (the default), will be lazily initialized with a {TransmissionClient} on first event send.
49
+ # @param transmission [Class, Object, nil] transport used to actually send events. If nil (the default),
50
+ # will be initialized with a {TransmissionClient}. If Class--for example, {MockTransmissionClient}--will attempt
51
+ # to create a new instance of that class with {TransmissionClient}'s usual parameters. If Object, checks
52
+ # that the instance passed in implements the Transmission interface (responds to :add and :close). If the
53
+ # check does not succeed, a no-op NullTransmission will be used instead.
50
54
  # @param block_on_send [Boolean] if more than pending_work_capacity events are written, block sending further events
51
55
  # @param block_on_responses [Boolean] if true, block if there is no thread reading from the response queue
52
56
  # @param pending_work_capacity [Fixnum] defaults to 1000. If the queue of
@@ -88,17 +92,6 @@ module Libhoney
88
92
  @builder.sample_rate = sample_rate
89
93
  @builder.api_host = api_host
90
94
 
91
- @transmission = transmission
92
- if !@transmission && !(writekey && dataset)
93
- # if no writekey or dataset are configured, and we didn't override the
94
- # transmission (e.g. to a MockTransmissionClient), that's almost
95
- # certainly a misconfiguration, even though it's possible to override
96
- # them on a per-event basis. So let's handle the misconfiguration
97
- # early rather than potentially throwing thousands of exceptions at runtime.
98
- warn "#{self.class.name}: no #{writekey ? 'dataset' : 'writekey'} configured, disabling sending events"
99
- @transmission = NullTransmissionClient.new
100
- end
101
-
102
95
  @user_agent_addition = user_agent_addition
103
96
 
104
97
  @block_on_send = block_on_send
@@ -108,8 +101,8 @@ module Libhoney
108
101
  @max_concurrent_batches = max_concurrent_batches
109
102
  @pending_work_capacity = pending_work_capacity
110
103
  @responses = SizedQueue.new(2 * @pending_work_capacity)
111
- @lock = Mutex.new
112
104
  @proxy_config = parse_proxy_config(proxy_config)
105
+ @transmission = setup_transmission(transmission, writekey, dataset)
113
106
  end
114
107
 
115
108
  attr_reader :block_on_send, :block_on_responses, :max_batch_size,
@@ -197,22 +190,6 @@ module Libhoney
197
190
  # @param event [Event] the event to send to honeycomb
198
191
  # @api private
199
192
  def send_event(event)
200
- @lock.synchronize do
201
- transmission_client_params = {
202
- max_batch_size: @max_batch_size,
203
- send_frequency: @send_frequency,
204
- max_concurrent_batches: @max_concurrent_batches,
205
- pending_work_capacity: @pending_work_capacity,
206
- responses: @responses,
207
- block_on_send: @block_on_send,
208
- block_on_responses: @block_on_responses,
209
- user_agent_addition: @user_agent_addition,
210
- proxy_config: @proxy_config
211
- }
212
-
213
- @transmission ||= TransmissionClient.new(**transmission_client_params)
214
- end
215
-
216
193
  @transmission.add(event)
217
194
  end
218
195
 
@@ -234,6 +211,57 @@ module Libhoney
234
211
 
235
212
  private
236
213
 
214
+ ##
215
+ # Parameters to pass to a transmission based on client config.
216
+ #
217
+ def transmission_client_params
218
+ {
219
+ max_batch_size: @max_batch_size,
220
+ send_frequency: @send_frequency,
221
+ max_concurrent_batches: @max_concurrent_batches,
222
+ pending_work_capacity: @pending_work_capacity,
223
+ responses: @responses,
224
+ block_on_send: @block_on_send,
225
+ block_on_responses: @block_on_responses,
226
+ user_agent_addition: @user_agent_addition,
227
+ proxy_config: @proxy_config
228
+ }
229
+ end
230
+
231
+ def setup_transmission(transmission, writekey, dataset)
232
+ # if a provided transmission can add and close, we'll assume the user
233
+ # has provided a working transmission with a customized configuration
234
+ return transmission if quacks_like_a_transmission?(transmission)
235
+
236
+ if !(writekey && dataset) # rubocop:disable Style/NegatedIf
237
+ # if no writekey or dataset are configured, and we didn't override the
238
+ # transmission (e.g. to a MockTransmissionClient), that's almost
239
+ # certainly a misconfiguration, even though it's possible to override
240
+ # them on a per-event basis. So let's handle the misconfiguration
241
+ # early rather than potentially throwing thousands of exceptions at runtime.
242
+ warn "#{self.class.name}: no #{writekey ? 'dataset' : 'writekey'} configured, disabling sending events"
243
+ return NullTransmissionClient.new
244
+ end
245
+
246
+ case transmission
247
+ when NilClass # the default value for new clients
248
+ return TransmissionClient.new(**transmission_client_params)
249
+ when Class
250
+ # if a class has been provided, attempt to instantiate it with parameters given to the client
251
+ t = transmission.new(**transmission_client_params)
252
+ if quacks_like_a_transmission?(t) # rubocop:disable Style/GuardClause
253
+ return t
254
+ else
255
+ warn "#{t.class.name}: does not appear to behave like a transmission, disabling sending events"
256
+ return NullTransmissionClient.new
257
+ end
258
+ end
259
+ end
260
+
261
+ def quacks_like_a_transmission?(transmission)
262
+ transmission.respond_to?(:add) && transmission.respond_to?(:close)
263
+ end
264
+
237
265
  # @api private
238
266
  def parse_proxy_config(config)
239
267
  case config
@@ -0,0 +1,109 @@
1
+ require 'libhoney/queueing'
2
+ require 'libhoney/transmission'
3
+
4
+ module Libhoney
5
+ ##
6
+ # An experimental variant of the standard {TransmissionClient} that uses
7
+ # a custom implementation of a sized queue whose pop/push methods support
8
+ # a timeout internally.
9
+ #
10
+ # @example Use this transmission with the Ruby Beeline
11
+ # require 'libhoney/experimental_transmission'
12
+ #
13
+ # Honeycomb.configure do |config|
14
+ # config.client = Libhoney::Client.new(
15
+ # writekey: ENV["HONEYCOMB_WRITE_KEY"],
16
+ # dataset: ENV.fetch("HONEYCOMB_DATASET", "awesome_sauce"),
17
+ # transmission: Libhoney::ExperimentalTransmissionClient
18
+ # )
19
+ # ...
20
+ # end
21
+ #
22
+ # @api private
23
+ #
24
+ class ExperimentalTransmissionClient < TransmissionClient
25
+ def add(event)
26
+ return unless event_valid(event)
27
+
28
+ begin
29
+ # if block_on_send is true, never timeout the wait to enqueue an event
30
+ # otherwise, timeout the wait immediately and if the queue is full, we'll
31
+ # have a ThreadError raised because we could not add to the queue.
32
+ timeout = @block_on_send ? :never : 0
33
+ @batch_queue.enq(event, timeout)
34
+ rescue Libhoney::Queueing::SizedQueueWithTimeout::PushTimedOut
35
+ # happens if the queue was full and block_on_send = false.
36
+ warn "#{self.class.name}: batch queue full, dropping event." if %w[debug trace].include?(ENV['LOG_LEVEL'])
37
+ end
38
+
39
+ ensure_threads_running
40
+ end
41
+
42
+ def batch_loop
43
+ next_send_time = Time.now + @send_frequency
44
+ batched_events = Hash.new do |h, key|
45
+ h[key] = []
46
+ end
47
+
48
+ loop do
49
+ begin
50
+ # an event on the batch_queue
51
+ # 1. pops out and is truthy
52
+ # 2. gets included in the current batch
53
+ # 3. while waits for another event
54
+ while (event = @batch_queue.pop(@send_frequency))
55
+ key = [event.api_host, event.writekey, event.dataset]
56
+ batched_events[key] << event
57
+ end
58
+
59
+ # a nil on the batch_queue
60
+ # 1. pops out and is falsy
61
+ # 2. ends the event-popping while do..end
62
+ # 3. breaks the loop
63
+ # 4. flushes the current batch
64
+ # 5. ends the batch_loop
65
+ break
66
+
67
+ # a timeout expiration waiting for an event
68
+ # 1. skips the break and is rescued
69
+ # 2. triggers the ensure to flush the current batch
70
+ # 3. begins the loop again with an updated next_send_time
71
+ rescue Libhoney::Queueing::SizedQueueWithTimeout::PopTimedOut => e
72
+ warn "#{self.class.name}: ⏱ " + e.message if %w[trace].include?(ENV['LOG_LEVEL'])
73
+
74
+ # any exception occurring in this loop should not take down the actual
75
+ # instrumented Ruby process, so handle here and log that there is trouble
76
+ rescue Exception => e
77
+ warn "#{self.class.name}: 💥 " + e.message if %w[debug trace].include?(ENV['LOG_LEVEL'])
78
+ warn e.backtrace.join("\n").to_s if ['trace'].include?(ENV['LOG_LEVEL'])
79
+
80
+ # regardless of the exception, figure out whether enough time has passed to
81
+ # send the current batched events, if so, send them and figure out the next send time
82
+ # before going back to the top of the loop
83
+ ensure
84
+ next_send_time = flush_batched_events(batched_events) if Time.now > next_send_time
85
+ end
86
+ end
87
+
88
+ # don't need to capture the next_send_time here because the batch_loop is exiting
89
+ # for some reason (probably transmission.close)
90
+ flush_batched_events(batched_events)
91
+ end
92
+
93
+ private
94
+
95
+ def setup_batch_queue
96
+ # override super()'s @batch_queue = SizedQueue.new(); use our SizedQueueWithTimeout:
97
+ # + block on adding events to the batch_queue when queue is full and @block_on_send is true
98
+ # + the queue knows how to limit size and how to time-out pushes and pops
99
+ @batch_queue = Libhoney::Queueing::SizedQueueWithTimeout.new(@pending_work_capacity)
100
+ warn "⚠️🐆 #{self.class.name} in use! It may drop data, consume all your memory, or cause skin irritation."
101
+ end
102
+
103
+ def build_user_agent(user_agent_addition)
104
+ ua = "libhoney-rb/#{VERSION} (exp-transmission)"
105
+ ua << " #{user_agent_addition}" if user_agent_addition
106
+ ua
107
+ end
108
+ end
109
+ end
@@ -7,7 +7,7 @@ module Libhoney
7
7
  # verify what events your instrumented code is sending. Use in
8
8
  # production is not recommended.
9
9
  class MockTransmissionClient
10
- def initialize
10
+ def initialize(**_)
11
11
  reset
12
12
  end
13
13
 
@@ -4,6 +4,8 @@ module Libhoney
4
4
  #
5
5
  # @api private
6
6
  class NullTransmissionClient
7
+ def initialize(**_); end
8
+
7
9
  def add(event); end
8
10
 
9
11
  def close(drain); end
@@ -0,0 +1 @@
1
+ require 'libhoney/queueing/sized_queue_with_timeout'
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2021 Honeycomb
2
+ Copyright (c) 2013 Avdi Grimm
3
+
4
+ MIT License
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,176 @@
1
+ ##
2
+ # SizedQueueWithTimeout is copyright and licensed per the LICENSE.txt in
3
+ # its containing subdirectory of this codebase.
4
+ #
5
+ module Libhoney
6
+ module Queueing
7
+ ##
8
+ # A queue implementation with optional size limit and optional timeouts on pop and push
9
+ # operations. Heavily influenced / liberally mimicking Avdi Grimm's
10
+ # {Tapas::Queue}[https://github.com/avdi/tapas-queue].
11
+ #
12
+ class SizedQueueWithTimeout
13
+ class PushTimedOut < ThreadError; end
14
+ class PopTimedOut < ThreadError; end
15
+
16
+ ##
17
+ # @param max_size [Integer, Float::INFINITY] the size limit for this queue
18
+ # @param options [Hash] optional dependencies to inject, primarily for testing
19
+ # @option options [QLock, Mutex] :lock the lock for synchronizing queue state change
20
+ # @option options [QCondition] :space_available_condition the condition variable
21
+ # to wait/signal on for space being available in the queue; when provided, must
22
+ # be accompanied by an +:item_available_condition+ and the shared +:lock+
23
+ # @option options [QCondition] :item_available_condition the condition variable
24
+ # to wait/signal on for an item being added to the queue; when provided, must
25
+ # be accompanied by an +:space_available_condition+ and the shared +:lock+
26
+ def initialize(max_size = Float::INFINITY, options = {})
27
+ @items = []
28
+ @max_size = max_size
29
+ @lock = options.fetch(:lock) { QLock.new }
30
+ @space_available = options.fetch(:space_available_condition) { QCondition.new(@lock) }
31
+ @item_available = options.fetch(:item_available_condition) { QCondition.new(@lock) }
32
+ end
33
+
34
+ ##
35
+ # Push something onto the queue.
36
+ #
37
+ # @param obj [Object] the thing to add to the queue
38
+ # @param timeout [Numeric, :never] how long in seconds to wait for the queue to have space available or
39
+ # +:never+ to wait "forever"
40
+ # @param timeout_policy [#call] defaults to +-> { raise PushTimedOut }+ - a lambda/Proc/callable, what to do
41
+ # when the timeout expires
42
+ #
43
+ # @raise {PushTimedOut}
44
+ def push(obj, timeout = :never, &timeout_policy)
45
+ timeout_policy ||= -> { raise PushTimedOut }
46
+
47
+ wait_for_condition(@space_available, -> { !full? }, timeout, timeout_policy) do
48
+ @items.push(obj)
49
+ @item_available.signal
50
+ end
51
+ end
52
+ alias enq push
53
+ alias << push
54
+
55
+ ##
56
+ # Pop something off the queue.
57
+ #
58
+ # @param timeout [Numeric, :never] how long in seconds to wait for the queue to have an item available or
59
+ # +:never+ to wait "forever"
60
+ # @param timeout_policy [#call] defaults to +-> { raise PopTimedOut }+ - a lambda/Proc/callable, what to do
61
+ # when the timeout expires
62
+ #
63
+ # @return [Object]
64
+ # @raise {PopTimedOut}
65
+ def pop(timeout = :never, &timeout_policy)
66
+ timeout_policy ||= -> { raise PopTimedOut }
67
+
68
+ wait_for_condition(@item_available, -> { !empty? }, timeout, timeout_policy) do
69
+ item = @items.shift
70
+ @space_available.signal unless full?
71
+ item
72
+ end
73
+ end
74
+ alias deq pop
75
+ alias shift pop
76
+
77
+ ##
78
+ # Removes all objects from the queue. They are cast into the abyss never to be seen again.
79
+ #
80
+ def clear
81
+ @lock.synchronize do
82
+ @items = []
83
+ @space_available.signal unless full?
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ ##
90
+ # Whether the queue is at capacity. Must be called with the queue's lock
91
+ # or the answer won't matter if you try to change state based on it.
92
+ #
93
+ # @return [true/false]
94
+ # @api private
95
+ def full?
96
+ @max_size <= @items.size
97
+ end
98
+
99
+ ##
100
+ # Whether the queue is empty. Must be called with the queue's lock or the
101
+ # answer won't matter if you try to change state based on it.
102
+ #
103
+ # @return [true/false]
104
+ # @api private
105
+ def empty?
106
+ @items.empty?
107
+ end
108
+
109
+ # a generic conditional variable wait with a timeout loop
110
+ #
111
+ # @param condition [#wait] a condition variable to wait upon.
112
+ # @param condition_predicate [#call] a callable (i.e. lambda or proc) that returns true/false to act
113
+ # as a state tester (i.e. "is the queue currently empty?") to check on whether to keep waiting;
114
+ # used to handle spurious wake ups occurring before the timeout has elapsed
115
+ # @param timeout [:never, Numeric] the amount of time in (seconds?) to wait, or :never to wait forever
116
+ # @param timeout_policy [#call] a callable, what to do when a timeout occurs? Return a default? Raise an
117
+ # exception? You decide.
118
+ def wait_for_condition(condition, condition_predicate, timeout = :never, timeout_policy = -> { nil })
119
+ deadline = timeout == :never ? :never : trustworthy_current_time + timeout
120
+ @lock.synchronize do
121
+ loop do
122
+ time_remaining = timeout == :never ? nil : deadline - trustworthy_current_time
123
+
124
+ if !condition_predicate.call && time_remaining.to_f >= 0 # rubocop:disable Style/IfUnlessModifier
125
+ condition.wait(time_remaining)
126
+ end
127
+
128
+ if condition_predicate.call # rubocop:disable Style/GuardClause
129
+ return yield
130
+ elsif deadline == :never || deadline > trustworthy_current_time
131
+ next
132
+ else
133
+ return timeout_policy.call
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ # Within the context of the current process, return time from a
140
+ # monotonically increasing clock because for timeouts we care about
141
+ # elapsed time within the process, not human time.
142
+ #
143
+ # @return [Numeric]
144
+ def trustworthy_current_time
145
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
146
+ end
147
+ end
148
+
149
+ class QCondition
150
+ def initialize(lock)
151
+ @lock = lock
152
+ @cv = ConditionVariable.new
153
+ end
154
+
155
+ def wait(timeout = nil)
156
+ @cv.wait(@lock.mutex, timeout)
157
+ end
158
+
159
+ def signal
160
+ @cv.signal
161
+ end
162
+ end
163
+
164
+ class QLock
165
+ attr_reader :mutex
166
+
167
+ def initialize
168
+ @mutex = Mutex.new
169
+ end
170
+
171
+ def synchronize(&block)
172
+ @mutex.synchronize(&block)
173
+ end
174
+ end
175
+ end
176
+ end
@@ -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,31 @@ 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
+ if @batch_thread.nil?
128
+ else
129
+ @batch_thread.join(1.0) # limit the amount of time we'll wait for the thread to end
130
+ end
144
131
 
145
- # send @threads.length number of nils so each thread will fall out of send_loop
146
- @threads.length.times { @send_queue << nil }
132
+ # send @threads.length number of nils so each thread will fall out of send_loop
133
+ @threads.length.times { @send_queue << nil }
147
134
 
148
- @threads.each(&:join)
149
- @threads = []
135
+ @threads.each(&:join)
136
+ @threads = []
137
+ end
150
138
 
151
139
  enqueue_response(nil)
152
-
140
+ warn "#{self.class.name} - close: close complete" if %w[debug trace].include?(ENV['LOG_LEVEL'])
153
141
  0
154
142
  end
155
143
 
@@ -161,25 +149,76 @@ module Libhoney
161
149
 
162
150
  loop do
163
151
  begin
152
+ # a timeout expiration waiting for an event
153
+ # 1. interrupts only when thread is in a blocking state (waiting for pop)
154
+ # 2. exception skips the break and is rescued
155
+ # 3. triggers the ensure to flush the current batch
156
+ # 3. begins the loop again with an updated next_send_time
164
157
  Thread.handle_interrupt(Timeout::Error => :on_blocking) do
158
+ # an event on the batch_queue
159
+ # 1. pops out and is truthy
160
+ # 2. gets included in the current batch
161
+ # 3. while waits for another event
165
162
  while (event = Timeout.timeout(@send_frequency) { @batch_queue.pop })
166
163
  key = [event.api_host, event.writekey, event.dataset]
167
164
  batched_events[key] << event
168
165
  end
169
166
  end
170
167
 
168
+ # a nil on the batch_queue
169
+ # 1. pops out and is falsy
170
+ # 2. ends the event-popping while do..end
171
+ # 3. breaks the loop
172
+ # 4. flushes the current batch
173
+ # 5. ends the batch_loop
171
174
  break
172
- rescue Exception
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
@@ -1,3 +1,3 @@
1
1
  module Libhoney
2
- VERSION = '1.18.0'.freeze
2
+ VERSION = '1.19.0'.freeze
3
3
  end
data/libhoney.gemspec CHANGED
@@ -24,7 +24,9 @@ Gem::Specification.new do |spec|
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'
29
+ spec.add_development_dependency 'minitest-reporters'
28
30
  spec.add_development_dependency 'rake', '~> 12.3'
29
31
  spec.add_development_dependency 'rubocop', '< 0.69'
30
32
  spec.add_development_dependency 'sinatra'
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: 1.19.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-07-23 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,6 +66,20 @@ 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
@@ -222,6 +250,9 @@ files:
222
250
  - ".circleci/setup-rubygems.sh"
223
251
  - ".editorconfig"
224
252
  - ".github/CODEOWNERS"
253
+ - ".github/dependabot.yml"
254
+ - ".github/workflows/add-to-project.yml"
255
+ - ".github/workflows/apply-labels.yml"
225
256
  - ".gitignore"
226
257
  - ".rubocop.yml"
227
258
  - ".rubocop_todo.yml"
@@ -238,11 +269,15 @@ files:
238
269
  - lib/libhoney/cleaner.rb
239
270
  - lib/libhoney/client.rb
240
271
  - lib/libhoney/event.rb
272
+ - lib/libhoney/experimental_transmission.rb
241
273
  - lib/libhoney/log_client.rb
242
274
  - lib/libhoney/log_transmission.rb
243
275
  - lib/libhoney/mock_transmission.rb
244
276
  - lib/libhoney/null_client.rb
245
277
  - lib/libhoney/null_transmission.rb
278
+ - lib/libhoney/queueing.rb
279
+ - lib/libhoney/queueing/LICENSE.txt
280
+ - lib/libhoney/queueing/sized_queue_with_timeout.rb
246
281
  - lib/libhoney/response.rb
247
282
  - lib/libhoney/test_client.rb
248
283
  - lib/libhoney/transmission.rb
@@ -267,7 +302,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
267
302
  - !ruby/object:Gem::Version
268
303
  version: '0'
269
304
  requirements: []
270
- rubygems_version: 3.1.4
305
+ rubygems_version: 3.1.6
271
306
  signing_key:
272
307
  specification_version: 4
273
308
  summary: send data to Honeycomb