libhoney 1.16.0 → 1.19.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9034a56adcdd87d45a5d7602caa9add401b713cb52ba258d0383835b30304ef5
4
- data.tar.gz: 4db352186d89946e6121ace449f69d869175ce99cb3910084b35491bbbbc26a8
3
+ metadata.gz: 40d54151fc103bf8020f14ff8b7a9e6e0298366869e795ce3ddf0438af066e16
4
+ data.tar.gz: c2b834d39609a73bce6e5e6e19caf4d2499903498895d32fe22b55041db7e59b
5
5
  SHA512:
6
- metadata.gz: fce68faca7e1353f8d01bc2329fcb9d3157cdb38cedbdd75f451b2c2896d8119f2651b68eebcf8b5961b696c53f00f6a6a855d40fabfa9039d07e948a5a70683
7
- data.tar.gz: 3e9433a4e07bf24f0db174c87f2b8074cd51c1dacedfb5cf3bb403ed444f0403b81aa66ae9f3c2401f119a39edfebc53aba89246c3361719be7265cf0eabd5ca
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:
@@ -147,11 +149,11 @@ workflows:
147
149
  - test
148
150
  - publish_github:
149
151
  <<: *filters_publish
150
- context: Honeycomb Secrets
152
+ context: Honeycomb Secrets for Public Repos
151
153
  requires:
152
154
  - build_artifacts
153
155
  - publish_rubygems:
154
156
  <<: *filters_publish
155
- context: Honeycomb Secrets
157
+ context: Honeycomb Secrets for Public Repos
156
158
  requires:
157
159
  - build_artifacts
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,17 +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:
20
+ - lib/libhoney/transmission.rb # Should this remain so large?
19
21
  - test/*
20
22
 
21
23
  Metrics/MethodLength:
22
- Max: 40
24
+ Max: 45
23
25
  Exclude:
24
26
  - lib/libhoney/transmission.rb
25
27
  - test/*
data/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # libhoney-rb changelog
2
2
 
3
+ ## changes pending release
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
+
16
+ ## 1.18.0
17
+
18
+ ### Improvements
19
+
20
+ - replace HTTP client library to reduce external dependencies (#81)
21
+
22
+ ### Deprecations
23
+
24
+ - `Libhoney::Client.new(proxy_config: _)`: the `proxy_config` parameter for client
25
+ creation will no longer accept an Array in the next major version. The recommended
26
+ way to configure the client for operation behind forwarding web proxies is to set
27
+ http/https/no_proxy environment variables appropriately.
28
+
29
+ ## 1.17.0
30
+
31
+ ### Fixes:
32
+
33
+ - Allow Ruby 3.0.0 (removes overly-pessimistic exception) (#79)
34
+
35
+ ## 1.16.1
36
+
37
+ ### Fixes:
38
+
39
+ - Fix closing down the client when no threads have been started. (#74 & #76)
40
+
3
41
  ## 1.16.0
4
42
 
5
43
  ### Fixes:
@@ -1,6 +1,6 @@
1
+ require 'addressable/uri'
1
2
  require 'time'
2
3
  require 'json'
3
- require 'http'
4
4
  require 'forwardable'
5
5
 
6
6
  require 'libhoney/null_transmission'
@@ -46,11 +46,22 @@ 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
53
57
  # pending events exceeds 1000, this client will start dropping events.
58
+ # @param proxy_config [String, Array, nil] proxy connection information
59
+ # nil: (default, recommended) connection proxying will be determined from any http_proxy, https_proxy, and no_proxy environment
60
+ # variables set for the process.
61
+ # String: the value must be the URI for connecting to a forwarding web proxy. Must be parsable by stdlib URI.
62
+ # Array: (deprecated, removal in v2.0) the value must have one and at most four elements: e.g. ['host', port, 'username', 'password'].
63
+ # The assumption is that the TCP connection will be tunneled via HTTP, so the assumed scheme is 'http://'
64
+ # 'host' is required. 'port' is optional (default:80), unless a 'username' is included. 'password' is optional.
54
65
  # rubocop:disable Metrics/ParameterLists
55
66
  def initialize(writekey: nil,
56
67
  dataset: nil,
@@ -70,7 +81,7 @@ module Libhoney
70
81
  raise Exception, 'libhoney: max_concurrent_batches must be greater than 0' if max_concurrent_batches < 1
71
82
  raise Exception, 'libhoney: sample rate must be greater than 0' if sample_rate < 1
72
83
 
73
- unless Gem::Dependency.new('ruby', '~> 2.2').match?('ruby', RUBY_VERSION)
84
+ unless Gem::Dependency.new('ruby', '>= 2.2').match?('ruby', RUBY_VERSION)
74
85
  raise Exception, 'libhoney: Ruby versions < 2.2 are not supported'
75
86
  end
76
87
 
@@ -81,17 +92,6 @@ module Libhoney
81
92
  @builder.sample_rate = sample_rate
82
93
  @builder.api_host = api_host
83
94
 
84
- @transmission = transmission
85
- if !@transmission && !(writekey && dataset)
86
- # if no writekey or dataset are configured, and we didn't override the
87
- # transmission (e.g. to a MockTransmissionClient), that's almost
88
- # certainly a misconfiguration, even though it's possible to override
89
- # them on a per-event basis. So let's handle the misconfiguration
90
- # early rather than potentially throwing thousands of exceptions at runtime.
91
- warn "#{self.class.name}: no #{writekey ? 'dataset' : 'writekey'} configured, disabling sending events"
92
- @transmission = NullTransmissionClient.new
93
- end
94
-
95
95
  @user_agent_addition = user_agent_addition
96
96
 
97
97
  @block_on_send = block_on_send
@@ -101,8 +101,8 @@ module Libhoney
101
101
  @max_concurrent_batches = max_concurrent_batches
102
102
  @pending_work_capacity = pending_work_capacity
103
103
  @responses = SizedQueue.new(2 * @pending_work_capacity)
104
- @lock = Mutex.new
105
- @proxy_config = proxy_config
104
+ @proxy_config = parse_proxy_config(proxy_config)
105
+ @transmission = setup_transmission(transmission, writekey, dataset)
106
106
  end
107
107
 
108
108
  attr_reader :block_on_send, :block_on_responses, :max_batch_size,
@@ -190,22 +190,6 @@ module Libhoney
190
190
  # @param event [Event] the event to send to honeycomb
191
191
  # @api private
192
192
  def send_event(event)
193
- @lock.synchronize do
194
- transmission_client_params = {
195
- max_batch_size: @max_batch_size,
196
- send_frequency: @send_frequency,
197
- max_concurrent_batches: @max_concurrent_batches,
198
- pending_work_capacity: @pending_work_capacity,
199
- responses: @responses,
200
- block_on_send: @block_on_send,
201
- block_on_responses: @block_on_responses,
202
- user_agent_addition: @user_agent_addition,
203
- proxy_config: @proxy_config
204
- }
205
-
206
- @transmission ||= TransmissionClient.new(**transmission_client_params)
207
- end
208
-
209
193
  @transmission.add(event)
210
194
  end
211
195
 
@@ -224,5 +208,86 @@ module Libhoney
224
208
  def should_drop(sample_rate)
225
209
  rand(1..sample_rate) != 1
226
210
  end
211
+
212
+ private
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
+
265
+ # @api private
266
+ def parse_proxy_config(config)
267
+ case config
268
+ when nil then nil
269
+ when String
270
+ URI.parse(config)
271
+ when Array
272
+ warn <<-WARNING
273
+ DEPRECATION WARNING: #{self.class.name} the proxy_config parameter will require a String value, not an Array in libhoney 2.0.
274
+ To resolve:
275
+ + recommended: set http/https_proxy environment variables, which take precedence over any option set here, then remove proxy_config parameter from client initialization
276
+ + set proxy_config to a String containing the forwarding proxy URI (only used if http/https_proxy are not set)
277
+ WARNING
278
+ host, port, user, password = config
279
+
280
+ parsed_config = URI::HTTP.build(host: host, port: port).tap do |uri|
281
+ uri.userinfo = "#{user}:#{password}" if user
282
+ end
283
+ redacted_config = parsed_config.dup.tap do |uri|
284
+ uri.password = 'REDACTED' unless uri.password.nil? || uri.password.empty?
285
+ end
286
+ warn "The array config given has been assumed to mean: #{redacted_config}"
287
+ parsed_config
288
+ end
289
+ rescue URI::Error => e
290
+ warn "#{self.class.name}: unable to parse proxy_config. Detail: #{e.class}: #{e.message}"
291
+ end
227
292
  end
228
293
  end
@@ -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
@@ -1,7 +1,15 @@
1
- require 'http'
1
+ require 'http/response/status'
2
2
 
3
3
  module Libhoney
4
4
  class Response
5
+ # The response status from HTTP calls to a Honeycomb API endpoint.
6
+ #
7
+ # For most of the life of this client, this response object has been
8
+ # a pass-through to the underlying HTTP library's response object.
9
+ # This class in the Libhoney namespace now owns the interface for
10
+ # API responses.
11
+ class Status < HTTP::Response::Status; end
12
+
5
13
  attr_accessor :duration, :status_code, :metadata, :error
6
14
 
7
15
  def initialize(duration: 0,
@@ -9,7 +17,7 @@ module Libhoney
9
17
  metadata: nil,
10
18
  error: nil)
11
19
  @duration = duration
12
- @status_code = HTTP::Response::Status.new(status_code)
20
+ @status_code = Status.new(status_code)
13
21
  @metadata = metadata
14
22
  @error = error
15
23
  end
@@ -1,3 +1,5 @@
1
+ require 'addressable/uri'
2
+ require 'excon'
1
3
  require 'json'
2
4
  require 'timeout'
3
5
  require 'libhoney/response'
@@ -34,9 +36,9 @@ module Libhoney
34
36
  @send_queue = Queue.new
35
37
  @threads = []
36
38
  @lock = Mutex.new
37
- # use a SizedQueue so the producer will block on adding to the batch_queue when @block_on_send is true
38
- @batch_queue = SizedQueue.new(@pending_work_capacity)
39
39
  @batch_thread = nil
40
+
41
+ setup_batch_queue
40
42
  end
41
43
 
42
44
  def add(event)
@@ -51,26 +53,6 @@ module Libhoney
51
53
  ensure_threads_running
52
54
  end
53
55
 
54
- def event_valid(event)
55
- invalid = []
56
- invalid.push('api host') if event.api_host.nil? || event.api_host.empty?
57
- invalid.push('write key') if event.writekey.nil? || event.writekey.empty?
58
- invalid.push('dataset') if event.dataset.nil? || event.dataset.empty?
59
-
60
- unless invalid.empty?
61
- e = StandardError.new("#{self.class.name}: nil or empty required fields (#{invalid.join(', ')})"\
62
- '. Will not attempt to send.')
63
- Response.new(error: e).tap do |error_response|
64
- error_response.metadata = event.metadata
65
- enqueue_response(error_response)
66
- end
67
-
68
- return false
69
- end
70
-
71
- true
72
- end
73
-
74
56
  def send_loop
75
57
  http_clients = build_http_clients
76
58
 
@@ -93,7 +75,7 @@ module Libhoney
93
75
  }
94
76
 
95
77
  response = http.post(
96
- "/1/batch/#{Addressable::URI.escape(dataset)}",
78
+ path: "/1/batch/#{Addressable::URI.escape(dataset)}",
97
79
  body: body,
98
80
  headers: headers
99
81
  )
@@ -103,6 +85,8 @@ module Libhoney
103
85
  # because this is effectively the top-level exception handler for the
104
86
  # sender threads, and we don't want those threads to die (leaving
105
87
  # nothing consuming the queue).
88
+ warn "#{self.class.name}: 💥 " + e.message if %w[debug trace].include?(ENV['LOG_LEVEL'])
89
+ warn e.backtrace.join("\n").to_s if ['trace'].include?(ENV['LOG_LEVEL'])
106
90
  begin
107
91
  batch.each do |event|
108
92
  # nil events in the batch should already have had an error
@@ -129,23 +113,31 @@ module Libhoney
129
113
  end
130
114
 
131
115
  def close(drain)
132
- # if drain is false, clear the remaining unprocessed events from the queue
133
- unless drain
134
- @batch_queue.clear
135
- @send_queue.clear
136
- 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
137
125
 
138
- @batch_queue.enq(nil)
139
- @batch_thread.join
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
140
131
 
141
- # send @threads.length number of nils so each thread will fall out of send_loop
142
- @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 }
143
134
 
144
- @threads.each(&:join)
145
- @threads = []
135
+ @threads.each(&:join)
136
+ @threads = []
137
+ end
146
138
 
147
139
  enqueue_response(nil)
148
-
140
+ warn "#{self.class.name} - close: close complete" if %w[debug trace].include?(ENV['LOG_LEVEL'])
149
141
  0
150
142
  end
151
143
 
@@ -157,25 +149,76 @@ module Libhoney
157
149
 
158
150
  loop do
159
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
160
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
161
162
  while (event = Timeout.timeout(@send_frequency) { @batch_queue.pop })
162
163
  key = [event.api_host, event.writekey, event.dataset]
163
164
  batched_events[key] << event
164
165
  end
165
166
  end
166
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
167
174
  break
168
- 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
169
182
  ensure
170
183
  next_send_time = flush_batched_events(batched_events) if Time.now > next_send_time
171
184
  end
172
185
  end
173
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)
174
189
  flush_batched_events(batched_events)
175
190
  end
176
191
 
177
192
  private
178
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
+
179
222
  ##
180
223
  # Enqueues a response to the responses queue suppressing ThreadError when
181
224
  # there is no space left on the queue and we are not blocking on response
@@ -186,15 +229,36 @@ module Libhoney
186
229
  end
187
230
 
188
231
  def process_response(http_response, before, batch)
189
- index = 0
190
- http_response.parse.each do |event|
191
- index += 1 while batch[index].nil? && index < batch.size
192
- break unless (batched_event = batch[index])
193
-
194
- Response.new(status_code: event['status']).tap do |response|
195
- response.duration = Time.now - before
196
- response.metadata = batched_event.metadata
197
- 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
+ )
198
262
  end
199
263
  end
200
264
  end
@@ -256,14 +320,19 @@ module Libhoney
256
320
 
257
321
  def build_http_clients
258
322
  Hash.new do |h, api_host|
259
- client = HTTP.timeout(connect: @send_timeout, write: @send_timeout, read: @send_timeout)
260
- .persistent(api_host)
261
- .headers(
262
- 'User-Agent' => @user_agent,
263
- 'Content-Type' => 'application/json'
264
- )
265
-
266
- client = client.via(*@proxy_config) unless @proxy_config.nil?
323
+ client = ::Excon.new(
324
+ api_host,
325
+ persistent: true,
326
+ read_timeout: @send_timeout,
327
+ write_timeout: @send_timeout,
328
+ connect_timeout: @send_timeout,
329
+ proxy: @proxy_config,
330
+ headers: {
331
+ 'User-Agent' => @user_agent,
332
+ 'Content-Type' => 'application/json'
333
+ }
334
+ )
335
+
267
336
  h[api_host] = client
268
337
  end
269
338
  end
@@ -1,3 +1,3 @@
1
1
  module Libhoney
2
- VERSION = '1.16.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'
@@ -34,5 +36,6 @@ Gem::Specification.new do |spec|
34
36
  spec.add_development_dependency 'yard'
35
37
  spec.add_development_dependency 'yardstick', '~> 0.9'
36
38
  spec.add_dependency 'addressable', '~> 2.0'
39
+ spec.add_dependency 'excon'
37
40
  spec.add_dependency 'http', '>= 2.0', '< 5.0'
38
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.16.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: 2020-11-25 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
@@ -178,6 +206,20 @@ dependencies:
178
206
  - - "~>"
179
207
  - !ruby/object:Gem::Version
180
208
  version: '2.0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: excon
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :runtime
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
181
223
  - !ruby/object:Gem::Dependency
182
224
  name: http
183
225
  requirement: !ruby/object:Gem::Requirement
@@ -208,6 +250,9 @@ files:
208
250
  - ".circleci/setup-rubygems.sh"
209
251
  - ".editorconfig"
210
252
  - ".github/CODEOWNERS"
253
+ - ".github/dependabot.yml"
254
+ - ".github/workflows/add-to-project.yml"
255
+ - ".github/workflows/apply-labels.yml"
211
256
  - ".gitignore"
212
257
  - ".rubocop.yml"
213
258
  - ".rubocop_todo.yml"
@@ -224,11 +269,15 @@ files:
224
269
  - lib/libhoney/cleaner.rb
225
270
  - lib/libhoney/client.rb
226
271
  - lib/libhoney/event.rb
272
+ - lib/libhoney/experimental_transmission.rb
227
273
  - lib/libhoney/log_client.rb
228
274
  - lib/libhoney/log_transmission.rb
229
275
  - lib/libhoney/mock_transmission.rb
230
276
  - lib/libhoney/null_client.rb
231
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
232
281
  - lib/libhoney/response.rb
233
282
  - lib/libhoney/test_client.rb
234
283
  - lib/libhoney/transmission.rb
@@ -253,7 +302,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
253
302
  - !ruby/object:Gem::Version
254
303
  version: '0'
255
304
  requirements: []
256
- rubygems_version: 3.1.4
305
+ rubygems_version: 3.1.6
257
306
  signing_key:
258
307
  specification_version: 4
259
308
  summary: send data to Honeycomb